Rails - eager load has_many :through with condition on association - sql

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.

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 3: has_many through query

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}

Rails active record query through multiple tables

I am trying to query for questions based on subject or category. I have a Category model which has many Subjects, and a Subjects model which has many Questions. How do I select 50 questions where the subject_id = x or category_id = y? I'm not sure if I need to change my model associations then query or use a query with the current associations. Here are the models (stripped of some excess code):
category.rb
class Category < ActiveRecord::Base
has_many :subjects, class_name: "Subject",
foreign_key: "category_id"
has_many :questions, through: :subjects
end
subject.rb
class Subject < ActiveRecord::Base
belongs_to :category
has_many :questions, class_name: "Question",
foreign_key: "subject_id"
end
question.rb
class Question < ActiveRecord::Base
belongs_to :subject
end
The most success I've had is with "Question.joins(:subject).group(category_id:1)", which only returns the last question with an associated category. Any suggestions? Thanks!
So you could make a scope
class Question < ActiveRecord::Base
scope :q_or_c ->(q, c){ where('category_id = ? OR question_id = ?', q, c) }
...
end
and then call it with
Question.q_or_c(question_id, category_id)

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!