What is the best way to check multiple has_many associations? - ruby-on-rails

I want to check if any of a set of has_many associations of a Ruby Class has, at least, one item.
Currently the method in_use? is written this way:
class Venue < ApplicationRecord
has_many :destination_day_items
has_many :user_bookmarks
has_many :attachments
has_many :notes
has_many :notifications
has_many :expenses
def in_use?
destination_day_items.any? ||
user_bookmarks.any? ||
attachments.any? ||
notes.any? ||
notifications.any? ||
expenses.any?
end
end
I think that adding a local variable may prevent a lot of repetead calls if this method is called a few times.
def in_use?
#in_use ||= destination_day_items.any? ||
user_bookmarks.any? ||
(...)
#in_use
end
But I still feel it's not the best approach.
My question is: Does anyone know a better idea on how to implement this using "The RoR Way"?

At first this was intendend to check if a Venue is associated to any other of these classes (Bookmarks, Notes, Attachments...) and validate if it's safe to delete it
You can use option :restrict_with_error. This option causes an error to be added to the owner if there is an associated object
class Venue < ApplicationRecord
has_many :destination_day_items, dependent: :restrict_with_error
has_many :user_bookmarks, dependent: :restrict_with_error
has_many :attachments, dependent: :restrict_with_error
has_many :notes, dependent: :restrict_with_error
has_many :notifications, dependent: :restrict_with_error
has_many :expenses, dependent: :restrict_with_error
end
Let's assume venue has some destination day item and we try to destroy it
venue.destroy
Output will something like this:
BEGIN
DestinationDayItem Exists? SELECT 1 AS one FROM "destination_day_items" WHERE "destination_day_items"."venue_id" = $1 LIMIT $2 [["venue_id", 1], ["LIMIT", 1]]
ROLLBACK
=> false
venue.errors[:base]
# => ["Cannot delete record because dependent destination day item exist"]
So you can show this message to the user if you need
Another option -- dependent: :restrict_with_exception. It causes an ActiveRecord::DeleteRestrictionError exception to be raised if there is an associated record
These options work when you apply destroy method and don't work when delete

As they say, there's only two hard problems in computer science: "Cache invalidation, naming things, and off-by-one errors". The cached result can fall out of date if any relationships are added or removed during the life of the object.
Also, associations are cached, so there isn't much use in caching the result. destination_day_items will only query the database once during the life of the object. However, the cache is not always properly invalidated if the relationships change.
You could add a counter_cache to all the associations, then it only has to query the cache number, but see above.
Instead of a query for each association, you can increase performance by doing it in a single query.
def in_use?
left_joins(:destination_day_items)
.left_joins(:user_bookmarks)
...
.where.not(destination_day_items: { venue_id: nil })
.or( UserBookmarks.where.not(venue_id: nil )
.or( ... )
.exists?
end
The upside is this query will always get the correct result. The downside is it will always run the query. You could cache the result, but see above.
The real advantage is this can be turned into a scope to search for all in-use venues efficiently.
scope :left_join_all_associations(
left_joins(:destination_day_items)
.left_joins(:user_bookmarks)
...
)
scope :in_use, -> {
left_join_all_associations
.where.not(destination_day_items: { venue_id: nil })
.or( UserBookmarks.where.not(venue_id: nil )
.or( ... )
}
And you can query for those which are not in use with a left excluding join.
scope :not_in_use, -> {
left_join_all_associations
.where(
destination_day_items: { venue_id: nil },
user_bookmarks: { venue_id; nil },
...
)
}
I'm not 100% sure I got the Rails queries right, so here's a SQL demonstration.

def in_use?
# define a relations array
relations = Venue.reflect_on_all_associations(:has_many).map(&:name).map(&:to_s)
# check condition
relations.any? { |association| public_send(association).any? }
end

Related

Rails 6.1: Create a scope containing .first or .last through a has_many relation

In Rails 6.1, assuming two models:
PeriodicJob: has_many :executions
Execution with a field state that is either succeeded or failed
I want to run the query:
Give me all PeriodicJobs which last (highest ID) execution has state succeed.
A potential solution would be raw SQL with a subquery, as pointed out in this other Stackoverflow question: Ransack searching for instances with specific value in last of has_many associations
However, this seems overly complicated code for such a simple question in English. Given the power of Rails, I'd have expected to see something like:
PeriodicJob.joins(:executions).where(cool_trick_im_yet_unaware_of_to_get_last_ordered_by_id_execution: { state: 'succeeded' }
Does such a thing exist in Rails and how would it be applied to this example?
One way of optimizing this for reading would be to setup a separate foreign key column and association as a "shortcut" to the latest execution:
class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
def change
add_reference :latest_execution, :execution
end
end
class PeriodicJob < ApplicationRecord
has_many :executions,
after_add: :set_latest_execution
belongs_to :latest_execution,
optional: true,
class_name: 'Execution'
private
def set_latest_execution(execution)
update_attribute(:latest_execution_id, execution.id)
end
end
This lets you do PeriodicJob.eager_load(:latest_execution) and avoid both a N+1 query and loading all the records off the executions table. This is especially important if you have a lot of executions per peroidic job.
The cost is that it requires an extra write query every time an execution is created.
If you want to limit this to just the latest success/failure you could add two columns:
class AddLatestExecutionToProducts < ActiveRecord::Migration[6.0]
def change
add_reference :latest_successful_execution, :execution
add_reference :latest_failed_execution, :execution
end
end
class Execution ​< ApplicationRecord
enum state: {
​succeeded: 'succeeded',
​failed: 'failed'
​ }
end
class PeriodicJob < ApplicationRecord
has_many :executions,
after_add: :set_latest_execution
belongs_to :latest_successful_execution,
optional: true,
class_name: 'Execution'
belongs_to :latest_failed_execution,
optional: true,
class_name: 'Execution'
private
def set_latest_execution(execution)
if execution.succeeded?
update_attribute(:latest_successful_execution_id, execution.id)
else
update_attribute(:latest_failed_execution_id, execution.id)
end
end
end
If you are using regular numeric ids, you can add a condition to the relationship. This will set a custom relationship on the model that will return the latest execution that is succeeded. THe has one macro will limit the 'collection' to one.
has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(id: :desc) }, class_name: 'Execution'
If you're using uuid, I would have set a default scope on those mdoels to order by created at asc to ensure the first one is always the oldest, ie the first one created. So then you could reorder to desc to get latest one
has_one :current_succeeded_execution, -> { where(state: "succeeded").reorder(created_at: :desc) }, class_name: 'Execution'

ActiveRecord - nested includes

I'm trying to perform the following query in Rails 5 in a way that it doesn't trigger N+1 queries when I access each events.contact:
events = #company.recipients_events
.where(contacts: { user_id: user_id })
I tried some combinations of .includes, .references and .eager_loading, but none of them worked. Some of them returned an SQL error, and other ones returned a nil object when I access events.contact.
Here's a brief version of my associations:
class Company
has_many :recipients
has_many :recipients_events, through: :recipients, source: :events
end
class Recipient
belongs_to :contact
has_many :events, as: :eventable
end
class Event
belongs_to :eventable, polymorphic: true
end
class Contact
has_many :recipients
end
What would be the correct way to achieve what I need?
If you already know user_id when you load #company, I'd do something like this:
#company = Company.where(whatever)
.includes(recipients: [:recipients_events, :contact])
.where(contacts: { user_id: user_id })
.take
events = #company.recipients_events
OR, if not:
events = Company.where(whatever)
.includes(recipients: [:recipients_events, :contact])
.where(contacts: { user_id: user_id })
.take
.recipients_events
The ActiveRecord query planner will determine what it thinks is the best way to get that data. It might be 1 query per table without the where, but when you chain includes().where() you will probably get 2 queries both with left outer joins on them.

delete_all does not work while each do delete does

Intended functionality - delete all linked asset_items when an asset_line_item is deleted. (without using destroy, destroy_all). I am using postgres
With the following model:
class AssetLineItem < PurchaseLineItem
has_many :asset_items
...
after_destroy :destroy_cleanup
private
def destroy_cleanup
asset_items.delete_all
end
end
This results in the asset_items remaining, however all of their asset_line_item columns are set to null.
def destroy_cleanup
asset_items.each do |asset_item|
asset_item.delete
end
end
replacing delete_all with the loop above however has the intended result of delete all associated asset_items.
Although I have working code, I'm curious what could cause delete_all to act in this way?
Calling just delete_all on association just nullifies the reference. It is the same as delete_all(:nullify):
pry(main)> Booking.last.passengers.delete_all
Booking Load (0.6ms) SELECT `bookings`.* FROM `bookings` ORDER BY `bookings`.`id` DESC LIMIT 1
SQL (2.8ms) UPDATE `passengers` SET `passengers`.`booking_id` = NULL WHERE `passengers`.`booking_id` = 157
=> nil
You need to call delete_all(:delete_all) to actually delete associated records.
Here is docs.
Or to get desired effect you can add following line to your AssetLineItem model:
has_many :asset_items, dependent: :destroy
as lakhvir kumar mentioned.
Also your destroy_cleanup callback could be refactored to:
def destroy_cleanup
asset_items.map(&:delete)
end
Here is some good links to the topic:
delete_all vs destroy_all?
Rails :dependent => :destroy VS :dependent => :delete_all
use has_many :asset_items dependent: :destroy

accept_nested_attributes_for in a many-to-many relationship

I have tried to find a solution for this but most of the literature around involves how to create the form rather than how to save the stuff in the DB. The problem I am having is that the accepts_nested_attributes_for seems to work ok when saving modifications to existing DB entities, but fails when trying to create a new object tree.
Some background. My classes are as follows:
class UserGroup < ActiveRecord::Base
has_many :permissions
has_many :users
accepts_nested_attributes_for :users
accepts_nested_attributes_for :permissions
end
class User < ActiveRecord::Base
has_many :user_permissions
has_many :permissions, :through => :user_permissions
belongs_to :user_group
accepts_nested_attributes_for :user_permissions
end
class Permission < ActiveRecord::Base
has_many :user_permissions
has_many :users, :through => :user_permissions
belongs_to :user_group
end
class UserPermission < ActiveRecord::Base
belongs_to :user
belongs_to :permission
validates_associated :user
validates_associated :permission
validates_numericality_of :threshold_value
validates_presence_of :threshold_value
default_scope order("permission_id ASC")
end
The permission seem strange but each of them has a threshold_value which is different for each user, that's why it is needed like this.
Anyway, as I said, when I PUT an update, for example to the threshold values, everything works ok. This is the controller code (UserGroupController, I am posting whole user groups rather than one user at a time):
def update
#ug = UserGroup.find(params[:id])
#ug.update_attributes!(params[:user_group])
respond_with #ug
end
A typical data coming in would be:
{"user_group":
{"id":3,
"permissions":[
{"id":14,"name":"Perm1"},
{"id":15,"name":"Perm2"}],
"users":[
{"id":7,"name":"Tallmaris",
"user_permissions":[
{"id":1,"permission_id":14,"threshold_value":"0.1"},
{"id":2,"permission_id":15,"threshold_value":0.3}]
},
{"name":"New User",
"user_permissions":[
{"permission_id":14,"threshold_value":0.4},
{"permission_id":15,"threshold_value":0.2}]
}]
}
}
As you can see, the "New User" has no ID and his permission records have no ID either, because I want everything to be created. The "Tallmaris" user works ok and the changed values are updated no problem (I can see the UPDATE sql getting run by the server); on the contrary, the new user gives me this nasty log:
[...]
User Exists (0.4ms) SELECT 1 AS one FROM "users" WHERE "users"."name" = 'New User' LIMIT 1
ModelSector Load (8.7ms) SELECT "user_permissions".* FROM "user_permissions" WHERE (user_id = ) ORDER BY permission_id ASC
PG::Error: ERROR: syntax error at or near ")"
The error is obviously the (user_id = ) with nothing, since of course the user does not exists, there are no user_permissions set already and I wanted them to be created on the spot.
Thanks to looking around to this other question I realised it was a problem with the validation on the user.
Basically I was validating that the threshold_values summed up within certain constraints but to do that I was probably doing something wrong and Rails was loading data from the DB, which was ok for existing values but of course there was nothing for new values.
I fixed that and now it's working. I'll leave this here just as a reminder that often a problem in one spot has solutions coming from other places. :)

How do I create/maintain a valid reference to a particular object in an ActiveRecord association?

Using ActiveRecord, I have an object, Client, that zero or more Users (i.e. via a has_many association). Client also has a 'primary_contact' attribute that can be manually set, but always has to point to one of the associated users. I.e. primary_contact can only be blank if there are no associated users.
What's the best way to implement Client such that:
a) The first time a user is added to a client, primary_contact is set to point to that user?
b) The primary_contact is always guaranteed to be in the users association, unless all of the users are deleted? (This has two parts: when setting a new primary_contact or removing a user from the association)
In other words, how can I designate and reassign the title of "primary contact" to one of a given client's users? I've tinkered around with numerous filters and validations, but I just can't get it right. Any help would be appreciated.
UPDATE: Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
My solution is to do everything in the join model. I think this works correctly on the client transitions to or from zero associations, always guaranteeing a primary contact is designated if there is any existing association. I'd be interested to hear anyone's feedback.
I'm new here, so cannot comment on François below. I can only edit my own entry. His solution presumes user to client is one to many, whereas my solution presumes many to many. I was thinking the user model represented an "agent" or "rep" perhaps, and would surely manage multiple clients. The question is ambiguous in this regard.
class User < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :clients, :through => :user_client
end
class UserClient < ActiveRecord::Base
belongs_to :user
belongs_to :client
# user_client join table contains :primary column
after_create :init_primary
before_destroy :preserve_primary
def init_primary
# first association for a client is always primary
if self.client.user_clients.length == 1
self.primary = true
self.save
end
end
def preserve_primary
if self.primary
#unless this is the last association, make soemone else primary
unless self.client.user_clients.length == 1
# there's gotta be a more concise way...
if self.client.user_clients[0].equal? self
self.client.user_clients[1].primary = true
else
self.client.user_clients[0].primary = true
end
end
end
end
end
class Client < ActiveRecord::Base
has_many :user_clients, :dependent => true
has_many :users, :through => :user_client
end
Though I'm sure there are a myriad of solutions, I ended up having User inform Client when it is being deleted and then using a before_save call in Client to validate (and set, if necessary) its primary_contact. This call is triggered by User just before it is deleted. This doesn't catch all of the edge cases when updating associations, but it's good enough for what I need.
I would do this using a boolean attribute on users. #has_one can be used to find the first model that has this boolean set to true.
class Client < AR::B
has_many :users, :dependent => :destroy
has_one :primary_contact, :class_name => "User",
:conditions => {:primary_contact => true},
:dependent => :destroy
end
class User < AR::B
belongs_to :client
after_save :ensure_only_primary
before_create :ensure_at_least_one_primary
after_destroy :select_another_primary
private
# We always want one primary contact, so find another one when I'm being
# deleted
def select_another_primary
return unless primary_contact?
u = self.client.users.first
u.update_attribute(:primary_contact, true) if u
end
def ensure_at_least_one_primary
return if self.client.users.count(:primary_contact).nonzero?
self.primary_contact = true
end
# We want only 1 primary contact, so if I am the primary contact, all other
# ones have to be secondary
def ensure_only_primary
return unless primary_contact?
self.client.users.update_all(["primary_contact = ?", false], ["id <> ?", self.id])
end
end

Resources