Rails active record query through multiple tables - sql

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)

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))

How to write a Rails SQL query for finding an object where all children have an equal value

I've been reading this, but can't make sense of writing it into a Rails scope :
find all parent records where all child records have a given value (but not just some child records)
I have a Course, Section, and Quiz, object :
class Course < ActiveRecord::Base
has_many :course_members
has_many :members, through: :course_members
has_many :sections
has_many :quizzes, through: :sections
end
class Quiz < ActiveRecord::Base
belongs_to :member
belongs_to :section
end
class Section < ActiveRecord::Base
belongs_to :course
has_many :quizzes
end
I'd like to find all courses of a member, where all quizzes related to that course have the attribute completed = true.
So in my Member class, I'd ideally like to write something like :
has_many :completed_courses, -> {
joins(:courses, :quizzes, :sections)
# .select( 'CASE WHEN quizzes.completed = true then 1 end') ??? maybe ???
}, class_name: 'Course'
Haha! But barring that being too complicated. I've been trying to write this simply in the Course would also be fine.
class Member < ActiveRecord::Base
has_many :courses, through: :course_members
has_many :course_members
has_many :completed_courses,
-> { joins(:quizzes).where.not(quizzes: {completed: [false, nil]}) },
through: :course_members,
source: :course
end
If your completed boolean column is NOT NULL, then change [false, nil] above to just simply false
Usage Example
irb(main):002:0> Member.first.completed_courses
Member Load (0.2ms) SELECT "members".* FROM "members" ORDER BY "members"."id" ASC LIMIT 1
Course Load (0.1ms) SELECT "courses".* FROM "courses" INNER JOIN "sections" ON "sections"."course_id" = "courses"."id" INNER JOIN "quizzes" ON "quizzes"."section_id" = "sections"."id" INNER JOIN "course_members" ON "courses"."id" = "course_members"."course_id" WHERE (NOT (("quizzes"."completed" = 'f' OR "quizzes"."completed" IS NULL))) AND "course_members"."member_id" = ? [["member_id", 1]]

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.

rails alternative name for association one to many

Simply i have table
[product_categories]
name
and
[products]
category_id
name
if i use in product model
class Product < ActiveRecord::Base
attr_accessible :...
belongs_to :product_category
end
class ProductCategory < ActiveRecord::Base
attr_accessible :...
set_table_name "product_categories"
has_many :products
end
i can fire
product = Product.new
product.product_category
but it is possible to rename that product_category for association for example
product.category
?
If you don't need to refer to the category with product.product_category and want to use only product.category do:
class Product < ActiveRecord::Base
attr_accessible :...
belongs_to :category, class_name: 'ProductCategory'
end

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.