Rails 3: has_many through query - ruby-on-rails-3

I have these three Active Record models:
class Event < ActiveRecord::Base
has_many :event_categories, inverse_of: :event
has_many :categories, through: :event_categories
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
I think the relations are good.
If I want to know what Events have a Category, for example id=5.. I do:
Category.find(5).events
But, if I want to know all Events for more than one category, for example:
Category.where(:id => [3,5]).events
It isn't working. Any ideas?

Please note, when you do has_many :events in a model, Active Record defines a method of name events for that class.
When you do Category.find(5).events, you get events associated with one object (i.e. Category.find(5)) , however Category.where(:id => [3,5]) returns an array of Category objects, so you can't use events function on an array, Only way to get events for all search results is iterate over them and access them individually, something like following:
all_events = Category.where(:id => [3,5]).inject([]) {|res,cat| res << cat.events}
Above code will do one query per iteration, to avoid this, we can include events, in the first query itself, like following, which will provide result in only one query:
all_events = Category.includes(:events).where(:id => [3,5]).inject([]) {|res,cat| res << cat.events}

Related

Filtering an association by missing records in Rails

In a Rails application I'm working on, I've got a few different models associated thusly (condensed for clarity):
group.rb
class Group < ApplicationRecord
has_many :members, class_name: 'GroupMember'
has_many :newsletters
end
group_member.rb
class GroupMember < ApplicationRecord
belongs_to :group,
has_many :subscriptions, inverse_of: :group_member, class_name: 'Newsletter::Subscriber'
scope :subscribed_to, ->(newsletter_id) { joins(:subscriptions).merge(Newsletter::Subscriber.where(["newsletter_id = ?", newsletter_id])) }
scope :not_subscribed_to, ->(newsletter_id) { where.missing(:subscriptions) }
end
newsletter.rb
class Newsletter < ApplicationRecord
acts_as_tenant :group
has_many :subscribers, inverse_of: :newsletter
end
newsletter/subscriber.rb
class Newsletter::Subscriber < ApplicationRecord
acts_as_tenant :group
belongs_to :newsletter, inverse_of: :subscribers
belongs_to :group_member, class_name: 'GroupMember', inverse_of: :subscriptions
end
Given the above associated models, here's the framework I'm working within:
Each Group has n Group Members and n Newsletters.
Each Group Member can have multiple Newsletter Subscriptions (one per newsletter in the group)
What I'm trying to do (unsuccessfully, so far), is find out which members in a group are NOT subscribed to a specific newsletter that is associated with the group.
I can find out the members that DO have a subscription using the following scope on the GroupMember object:
scope :subscribed_to, ->(newsletter_id) { joins(:subscriptions).merge(Newsletter::Subscriber.where(["newsletter_id = ?", newsletter_id])) }
That allows me to query, for instance, current_group.members.subscribed_to(current_group.newsletters.first.id).
However, I'm not sure how to negate that and get the the opposite of that set of members. That is, members NOT subscribed to that specific newsletter. The :not_subscribed_to scope I currently have defined isn't cutting it because it doesn't take into account which newsletter I'm referring to.
Given the variable newsletter_id.
One alternative is to use WHERE NOT EXISTS(...) with a subquery:
Member
.where(
'NOT EXISTS(
SELECT 1 FROM "subscriptions"
WHERE "subscriptions"."member_id" = "members"."id"
AND "subscriptions"."newsletter_id" = ?
)', newsletter_id
)
Translated into Arel:
Member.where(
Subscription.select('1')
.where(
Subscription.arel_table[:member_id].eq(Member.arel_table[:id])
).where(
newsletter_id: newsletter_id
).arel.exists.not
)
Or group, count and having:
Member.group(:id)
.left_joins(:subscriptions)
.where(subscriptions: { newsletter_id: newsletter_id })
.having(Subscription.arel_table[:id].count.eq(0))

Rails query records based on attribute of a certain association

I have a three tier model relationship defined like this:
class User < ActiveRecord::Base
has_many :orders
scope :with_key, ->(key) { joins(:orders).merge( Order.with_key(key) ) }
end
class Order < ActiveRecord::Base
belongs_to :user
has_many :tags
scope :with_key, ->(key) { joins(:tags).merge( Tag.with_key(key) ) }
end
class Tag < ActiveRecord::Base
belongs_to :order
attr_accessible :key
scope :with_key, ->(key) { where(key: key) }
end
Basically a user has many orders and each order has many tags. Tags have a field for key. The scope with_key allows me to merge-chain several joins as to allow
User.with_key("some_key") #=> All users whose orders have a tag with "some_key" as key
This is not what I need, however.
How do I find all users whose last order's tags have "some_key" as the key?

Rails - eager load has_many :through with condition on association

Let's say I have an Item model and Category model with has_many :through association:
class Item < ActiveRecord::Base
has_many :category_items
has_many :categories, through: category_items
end
class Category < ActiveRecord::Base
has_many :category_items
has_many :items, through: category_items
end
class CategoryItems < ActiveRecord::Base
belongs_to :category
belongs_to :items
end
now, I want to have a scope on items that will get all items that are in specific status (assume it has status attribute) for specific category. for example: get all items with status "in stock" and which belongs to category with id = 3, something like:
scope :in_stock_for_category, ->(category) { where(status: SOME_ENUMERATED_VALUE) ....
i'm missing the last part of the query to limit the result set to the specific category.
Thanks!
Since you don't have a category_id column in your items table, you need to join either category_items or cateogeries in your scope before you can specify a particular category's ID condition.
class Item < ActiveRecord::Base
scope :in_stock_for_category, -> do |category|
joins(:category_items).
where(category_items: {category_id: category.id}).
where(items: {status: SOME_ENUMERATED_VALUE}).
group("items.id") # grouping might be unnecessary since you're adding the where condition for the category's id
end
end
That will work. Or if you want to join categories, do the following:
class Item < ActiveRecord::Base
scope :in_stock_for_category, -> do |category|
joins(:categories).
where(categories: {id: category.id}).
where(items: {status: SOME_ENUMERATED_VALUE}).
group("items.id") # grouping might be unnecessary since you're adding the where condition for the category's id
end
end
If you already have a category however, it might be useful to create a has_many relationship for a items that have a certain status. Something like the following:
class Category < ActiveRecord::Base
has_many :in_stock_items, -> do
where(items: {status: SOME_ENUMERATED_VALUE})
end, through: :category_items, source: :item
end
Also, if you have a status scope in Item (something like scope :in_stock, -> { where(status: SOME_ENUMERATED_VALUE) }), you can most likely change the above has_many relationship to the following:
class Category < ActiveRecord::Base
has_many :in_stock_items, -> do
merge(Item.in_stock) # http://apidock.com/rails/ActiveRecord/SpawnMethods/merge
end, through: :category_items, source: :item
end
That should tidy things up.

Why are individual SELECT queries running when an all-encompassing SELECT already ran? (Rails/ActiveRecord)

I have the following code (note the includes and the .each):
subscribers = []
mailgroup.mailgroup_members.opted_to_receive_email.includes(:roster_contact, :roster_info).each { |m|
subscribers << { :EmailAddress => m.roster_contact.member_email,
:Name => m.roster_contact.member_name,
:CustomFields => [ { :Key => 'gender',
:Value => m.roster_info.gender.present? ? m.roster_info.gender : 'X'
} ]
} if m.roster_contact.member_email.present?
}
subscribers
Correspondingly, I see the following in my logs (i.e. select * from ROSTER_INFO ... IN (...)):
SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` IN ('1450', '1000', '1111')
Yet immediately after that there are select * from ROSTER_INFO for each ID already specified in the IN list of the previous query:
RosterInfo Load (84.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1450' LIMIT 1
RosterInfo Load (59.2ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1000' LIMIT 1
RosterInfo Load (56.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1111' LIMIT 1
If select * had already been done on ROSTER_INFO on all IDs of interest (IN (...)), why is another select * being done again for each of the same IDs? Doesn't ActiveRecord already know all the ROSTER_INFO columns for each ID?
(Meanwhile, there are no individual queries for ROSTER_CONTACT, yet if I remove :roster_contact from the includes method, then ROSTER_INFO is not queried again, but ROSTER_CONTACT is.)
RosterInfo model (abridged)
class RosterInfo < ActiveRecord::Base
self.primary_key = 'ID'
end
RosterContact model (abridged)
class RosterContact < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'rosterID'
has_many :mailgroups, through: :mailgroup_members
has_one :roster_info, foreign_key: 'ID' # can use this line
#belongs_to :roster_info, foreign_key: 'ID' # or this with no difference
def member_name # I added this method to this
roster_info.member_name # question only *after* having
end # figured out the problem.
end
RosterWeb model (abridged)
class RosterWeb < ActiveRecord::Base
self.primary_key = 'ID'
end
Mailgroup model (abridged)
class Mailgroup < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'mailCatID'
has_one :mailing_list, foreign_key: :legacy_id
end
MailgroupMember model (abridged)
class MailgroupMember < ActiveRecord::Base
self.primary_key = 'ID'
belongs_to :mailgroup, foreign_key: 'mailCatID'
belongs_to :roster_contact, foreign_key: 'rosterID'
belongs_to :roster_info, foreign_key: 'rosterID'
belongs_to :roster_web, foreign_key: 'rosterID'
scope :opted_to_receive_email, joins(:roster_web).where('ROSTER_WEB.receiveEmail=?', 1)
end
The issue turned out to be related to m.roster_contact.member_name -- unfortunately I made member_name a method of roster_contact that itself (indirectly) queried roster_info.member_name. I resolved this by changing the line
:Name => m.roster_contact.member_name,
to directly query roster_info as follows
:Name => m.roster_info.member_name,
I am sorry for the trouble!
I'm going to stick my neck out and say that this is probably an in-flight optimization by your query engine. The 'IN' is typically used to compare large sets of keys, the most efficient way of resolving three keys (assuming ID is the key) would be to retrieve each row by key, as has happened.
class RosterInfo < ActiveRecord::Base
has_one :roster_contact, foreign_key: 'ID'
end
class RosterContact < ActiveRecord::Base
has_one :roster_info, foreign_key: 'ID'
end
I don't know what is the premise for having bi-directional has_one, but I suspect it will turn out badly. Probably change one of them to belongs_to. Do the same for the other bi-directional has_one associations.
Another thing is that you are using 'ID' for the foreign_key column, where the usual practice is roster_contact_id or whichever class you are referencing.
Edit:
On closer examination, RosterInfo, RosterContact, RosterWeb look like separate tables for what should be a single record since they are all having the same set of mutual has_one associations. This is something that should be addressed on the schema level, but right now you should be able to drop the has_one associations from one of the three models to solve your immediate problem.

ActiveRecord query all the first items of a unique has-many association

I am having problems to create a Rails ActiveRecord query that retrieves the first Item by unique Activity considering a creation time internal. I also need the values available in ItemStat that is why the includes.
The current method implementation is working, but it is poor and needs optimization.
This is my analogue model:
Activity:
class Activity < ActiveRecord::Base
has_many :items
end
Item:
class Item < ActiveRecord::Base
belongs_to :activity
has_one :item_stat
end
ItemStat:
class ItemStat < ActiveRecord::Base
belongs_to :item
end
Current working method (activities_id are all activities available by an user):
def self.first_items_by_unique_activity(activities_id, time_begin, time_end)
items = Item.includes(:item_stat).where(:activity_id => activities_id, :created_at => time_begin..time_end)
#make the first item unique by activity
uniques = {}
items.each do |item|
identifier = item.activity_id
uniques[identifier] = item if uniques[identifier].nil?
end
uniques.values
end
Thanks any help!