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"?
CodePudding user response:
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
CodePudding user response:
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.
CodePudding user response:
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