Home > front end >  What is the best way to check multiple has_many associations?
What is the best way to check multiple has_many associations?

Time:07-30

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
  • Related