Rails Activerecord query through three associations? - ruby-on-rails

Company has_many :agents, Agent belongs_to :company.
Agent has_many :comments, Comment belongs_to both :agent and :business.
Business has_many :comments.
What ActiveRecord query finds all the businesses with a comment written by an agent of the company?
I can get all the comments for all the agents of a company:
#company.agents.joins(:comments)
And using that query I can pluck all the business_ids that are commented on by company agents:
#company.agents.joins(:comments).pluck(:business_id)
(So if nothing else works I could get the desired list of businesses using a second query into Business given an array of IDs.)
However I cannot seem to extend the association chain in a single query to includes the Businesses, eg, a query that finds the Business record for each business commented upon by company agents, eg something like:
#company.agents.joins(:comments).joins(:business) # Can't join 'Agent' to association named 'businesses'
EDIT:
Tried #company.agents.joins(:comments => :business) as suggested by Jon in the comments. That works if doing a .count() or a .pluck().
If need to also query by Business fields, that can be done with:
#company.agents.joins(:comments => :business).where(:businesses => {:account_status => :active})

You're adding the second join query onto the agents scope. You'll need to use something like the following:
#company.agents.joins(:comments => :business)
If you need to add conditions against the business you can do so as follows:
#company.agents.joins(:comments => :business).where(:businesses => {:account_status => :active})
This way it correctly chains the join queries for you and adds the conditions to the appropriate tables.

Related

Issue with polymorphic ActiveRecord query

I have three models with the following associations:
User has_many :owns, has_many :owned_books, :through => :owns, source: :book
Book has_many :owns
Own belongs_to :user, :counter_cache => true, belongs_to :book
I also have a page that tracks the top users by owns with the following query:
User.all.order('owns_count desc').limit(25)
I would now like to add a new page which can track top users by owns as above, but with a condition:
Book.where(:publisher => "Publisher #1")
What would be the most efficient way to do this?
I'm interesting if there is something special for this case, but my shot would be the following.
First, I don't see how polymorphic association can be applied here. You have just one object (user) that book can belong to. As I understand, polymorphic is for connecting book to several dif. objects (e.g. to User, Library, Shelf, etc.) (edit - initial text of question mentioned polymorphic associations, now it doesn't)
Second, I don't believe there is a way to cache counters here, as long as "Publisher #1" is a varying input parameter, and not a set of few pre-defined and known publishers (few constants).
Third, I would assume that amount of books by single Publisher is relatively limited. So even if you have millions of books in your table, amount of books per publisher should be hundreds maximum.
Then you can first query for all Publisher's books ids, e.g.
book_ids = Book.where(:publisher => "Publisher #1").pluck(:id)
And then query in owns table for top users ids:
Owns.select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order(total_owns: :desc).limit(25)
Disclaimer - I didn't try the statement in rails console, as I don't have your objects defined. I'm basing on group call in ActiveRecord docs
Edit. In order to make things more efficient, you can try the following:
0) Just in case, ensure you have indexes on Owns table for both foreign keys.
1) Use pluck for the second query as well not to create Own objects, although should not be a big difference because of limit(25). Something like this:
users_ids = Owns.where(book_id: book_ids).group(:user_id).order("count(*) DESC").limit(25).pluck("user_id")
See this question for reference.
2) Load all result users in one subsequent query and not N queries for each user
top_users = User.where(:id => users_ids)
3) Try joining User table in the first order:
owns_res = Owns.includes(:user).select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order("total_owns DESC").limit(25)
And then use owns_res.first.user

Search for model by multiple join record ids associated to model by has_many in rails

I have a product model setup like the following:
class Product < ActiveRecord::Base
has_many :product_atts, :dependent => :destroy
has_many :atts, :through => :product_atts
has_many :variants, :class_name => "Product", :foreign_key => "parent_id", :dependent => :destroy
end
And I want to search for products that have associations with multiple attributes.
I thought maybe this would work:
Product.joins(:product_atts).where(parent_id: params[:product_id]).where(product_atts: {att_id: [5,7]})
But this does not seem to do what I am looking for. This does where ID or ID.
So I tried the following:
Product.joins(:product_atts).where(parent_id: 3).where(product_atts: {att_id: 5}).where(product_atts: {att_id: 7})
But this doesn't work either, it returns 0 results.
So my question is how do I look for a model by passing in attributes of multiple join models of the same model type?
SOLUTION:
att_ids = params[:att_ids] #This is an array of attribute ids
product = Product.find(params[:product_id]) #This is the parent product
scope = att_ids.reduce(product.variants) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE product_id=products.id AND att_id=?)', att_id)
end
product_variant = scope.first
This is a seemingly-simple request made actually pretty tricky by how SQL works. Joins are always just joining rows together, and your WHERE clauses are only going to be looking at one row at a time (hence why your expectations are not working like you expect -- it's not possible for one row to have two values for the same column.
There are a bunch of ways to solve this when dealing with raw SQL, but in Rails, I've found the simplest (not most efficient) way is to embed subqueries using the EXISTS keyword. Wrapping that up in a solution which handles arbitrary number of desired att_ids, you get:
scope = att_ids_to_find.reduce(Product) do |relation, att_id|
relation.where('EXISTS (SELECT 1 FROM product_atts WHERE parent_id=products.id AND att_id=?)', att_id)
end
products = scope.all
If you're not familiar with reduce, what's going on is it's taking Product, then adding one additional where clause for each att_id. The end result is something like Product.where(...).where(...).where(...), but you don't need to worry about that too much. This solution also works well when mixed with scopes and other joins.

Rails mongoid has_one queries

In User model there is has_one relation to Professional. In the professional model I have one Array field named industries.
I need to take all values where professional industries in "IT"
I tried User.where(:"professional.industries".in => ["IT"])
But Its not working. Any sugestions..??
In order for your query to work you should use
class User
embeds_one :professional
end
If you are sure that Professional should be a separate collection you could use something like:
uids = Professional.where(:"industries".in => ["IT"]).distinct(:user_id)
users = User.where(:_id.in => uids)

Ruby on Rails Associations

I am starting to create my sites in Ruby on Rails these days instead of PHP.
I have picked up the language easily but still not 100% confident with associations :)
I have this situation:
User Model
has_and_belongs_to_many :roles
Roles Model
has_and_belongs_to_many :users
Journal Model
has_and_belongs_to_many :roles
So I have a roles_users table and a journals_roles table
I can access the user roles like so:
user = User.find(1)
User.roles
This gives me the roles assigned to the user, I can then access the journal model like so:
journals = user.roles.first.journals
This gets me the journals associated with the user based on the roles. I want to be able to access the journals like so user.journals
In my user model I have tried this:
def journals
self.roles.collect { |role| role.journals }.flatten
end
This gets me the journals in a flatten array but unfortunately I am unable to access anything associated with journals in this case, e.g in the journals model it has:
has_many :items
When I try to access user.journals.items it does not work as it is a flatten array which I am trying to access the has_many association.
Is it possible to get the user.journals another way other than the way I have shown above with the collect method?
Hope you guys understand what I mean, if not let me know and ill try to explain it better.
Cheers
Eef
If you want to have user.journals you should write query by hand. As far as I know Rails does has_many :through associations (habtm is a kind of has_many :through) one level deep. You can use has_many with finder_sql.
user.journals.items in your example doesn't work, becouse journals is an array and it doesn't have items method associated. So, you need to select one journal and then call items:
user.journals.first.items
I would also modify your journals method:
def journals
self.roles(:include => :journals).collect { |role| role.journals }.flatten.uniq
end
uniq removes duplicates and :inlcude => :journals should improve sql queries.
Similar question https://stackoverflow.com/questions/2802539/ruby-on-rails-join-table-associations
You can use Journal.scoped to create scope with conditions you need. As you have many-to-many association for journals-roles, you need to access joining table either with separate query or with inner select:
def journals
Journal.scoped(:conditions => ["journals.id in (Select journal_id from journals_roles where role_id in (?))", role_ids])
end
Then you can use user.journals.all(:include => :items) etc

select many through many... kind of

This is my first post on Stack, so please bear with me if I breach any protocol.
I'm working on a project in Rails (2.1.2), and I have a relations scenario that looks like this:
event [has_many] days
People (in different categories) can sign up for event days, giving the following binding results:
category [has_many] customers [has_many] orders [has_many] days
[belongs_to] event
Now, I'd like to have the total number of 'events' for one customer, or for all customers in a certain category, and I'm stuck. AFAIK, there's no way of performing a simple 'find' through an array of objects, correct? So, what would be the solution; nested loops, and a collect method to get the 'events' from the 'days' in orders?
Please let me know if I'm unclear.
Thanks for your help!
I would personally do this using a MySQL statement. I don't know for sure, but I think it is a lot faster then the other examples (using the rails provided association methods).
That means that in the Customer model you could do something like:
(Note that I'm assuming you are using the default association keys: 'model_name_id')
class Customer
def events
Event.find_by_sql("SELECT DISTINCT e.* FROM events e, days d, orders o, customers c WHERE c.id=o.customer_id AND o.id=d.order_id AND e.id=d.event_id")
end
end
That will return all the events associated with the user, and no duplicated (the 'DISTINCT' keyword makes sure of that). You will, as with the example above, lose information about what days exactly the user signed up for. If you need that information, please say so.
Also, I haven't included an example for your Category model, because I assumed you could adapt my example yourself. If not, just let me know.
EDIT:
I just read you just want to count the events. That can be done even faster (or at least, less memory intensive) using the count statement. To use that, just use the following function:
def event_count
Event.count_by_sql(SELECT DISTINCT COUNT(e.*) FROM ... ... ...
end
Your models probably look like this:
class Category
has_many :customers
class Customer
has_many :orders
has_many :days, :through => :orders # I added this
belongs_to :category
class Order
has_many :days
belongs_to :customer
class Day
belongs_to :event
belongs_to :order
class Event
has_many :days
With this you can count events for customer:
events = customer.days.count(:group => 'event_id')
It will return OrderedHash like this:
#<OrderedHash {1=>5, 2=>13, 3=>0}>
You can get events count by:
events[event_id]
events[1] # => 5
events[2] # => 13
etc.
If you want total number of uniq events:
events.size # => 3
In case of counting events for all customers in category I'd do something like this:
events = {}
category.customers.each {|c| events.merge!(c.days.count(:group => 'event_id') ) }
events.size # => 9 - it should be total number of events
But in this case you lose information how many times each event appeared.
I didn't test this, so there could be some mistakes.

Resources