Active records complex relationship query - ruby-on-rails

I have the following scenario:
User->HABTM->businesses
Suppliers->HABTM->businesses
Suppliers->HAS_MANY->Payments
I am having real trouble working out how to get all the payments for a user through the HABTM relationships that describe the business->supplier and User->business relationship.
I am after all payments that belong to the user through the supplier business relationship.
I can do this with SQL very easily but am having trouble doing it the rails way.
It's similar to a post on getting all the comments that belong to a user via a post model.
Any help would be appreciated.
Is it even possible?
I am doing this at the moment:
has_many :payments,:finder_sql => Proc.new {
%Q{
SELECT DISTINCT *
FROM payments
INNER JOIN businesses_users ON businesses_users.user_id
INNER JOIN businesses_suppliers ON businesses_suppliers.business_id
WHERE payments.supplier_id = businesses_suppliers.supplier_id AND businesses_users.user_id = #{id}
ORDER BY payments.created_at
}}
This lets me do user.payments

Kinda pressed for time, but wanted to chime in. First, your models seem to suggest that there might be an IS A relationship between User and Supplier, in which case you could employ polymorphic associations. If that's not the case, I'd look into the includes option within the activerecord query interface. In that manner, you can basically force AR to eager load the relationships down the chain. An example might look like:
all_users_and_their_pmts = User.includes(:businesses => {:suppliers => :payments })
An alternative, but highly inefficient, way to do this would be:
user_record.businesses.map {|b| b.suppliers.map {|s| s.payments}}.flatten
which would give you an array of payments. Using raw sql like you have above will be far more efficient than this, since activerecord can't chain the calls within map{}. I think :include would be a more idiomatic way for you to go, but your solution isn't horrible.

Related

ruby on rails manys' many

I am wondering how to do this without double each loop.
Assume that I have a user model and order model, and user has_many orders.
Now I have a users array which class is User::ActiveRecord_Relation
How can I get the orders of these users in one line?
Actually, the best way to do that is :
users.includes(:orders).map(&:orders)
Cause you eager load the orders of the users (only 2 sql queries)
Or
Order.where(user_id: users.pluck(:id))
is quite good too in term of performance
If you've got a many-to-many association and you need to quickly load in all the associated orders you will need to be careful to avoid the so-called "N plus 1" load that can result from the most obvious approach:
orders = users.collect(&:orders).flatten
This will iterate over each user and run a query like SELECT * FROM orders WHERE user_id=? without any grouping.
What you really want is this:
orders = Order.where(user_id: users.collect(&:id))
That should find all orders from all users in a single query.
An answer just come up my mind after I asked....
users.map {|user| user.orders}

Rails Data Modelling

In my company, we are trying to cache some data that we are querying from an API. We are using Rails. Two of my models are 'Query' and 'Response'. I want to create a one-to-many relationship between Query and Response, wherein, one query can have many responses.
I thought this is the right way to do it.
Query = [query]
Response = [query_id, response_detail_1, response_detail_2]
Then, in the Models, I did the following Data Associations:
class Query < ActiveRecord::Base
has_many :response
end
class Response < ActiveRecord::Base
belongs_to :query
end
So, canonically, whenever I want to find all the responses for a given query, I would do -
"_id" = Query.where(:query => "given query").id
Response.where(:query_id => "_id")
But my boss made me use an Array column in the Query model, remove the Data Associations between the models and put the id of each response record in that array column in the Query model. So, now the Query model looks like
Query = [query_id, [response_id_1, response_id_2, response_id_3,...]]
I just want to know what are the merits and demerits of doing it both ways and which is the right way to do it.
If the relationship is really a one-to-many relationship, the "standard" approach is what you originally suggested, or using a junction table. You're losing out on referential integrity that you could get with a FK by using the array. Postgres almost had FK constraints on array columns, but from what I researched it looks like it's not currently in the roadmap:
http://blog.2ndquadrant.com/postgresql-9-3-development-array-element-foreign-keys/
You might get some performance advantages out of the array approach if you consider it like a denormalization/caching assist. See this answer for some info on that, but it still recommends using a junction table:
https://stackoverflow.com/a/17012344/4280232. This answer and the comments also offer some thoughts on the array performance vs the join performance:
https://stackoverflow.com/a/13840557/4280232
Another advantage of using the array is that arrays will preserve order, so if order is important you could get some benefits there:
https://stackoverflow.com/a/2489805/4280232
But even then, you could put the order directly on the responses table (assuming they're unique to each query) or you could put it on a join table.
So, in sum, you might get some performance advantages out of the array foreign keys, and they might help with ordering, but you won't be able to enforce FK constraints on them (as of the time of this writing). Unless there's a special situation going on here, it's probably better to stick with the "FK column on the child table" approach, as that is considerably more common.
Granted, that all applies mainly to SQL databases, which I notice now you didn't specify in your question. If you're using NoSQL there may be other conventions for this.

How to make a simple join with query in rails

I am really new to Rails, but I had some experience with sql, so right now I am really struggling with doing a simple thing in rails syntax.
So, there are two tables:
class WorkshopMetadata < ActiveRecord::Base
attr_accessible :uuid, :action
belongs_to :workshop
end
class Workshop < ActiveRecord::Base
has_many :workshop_metadatas
end
And the query I want to do is:
SELECT workshops.*
FROM workshops LEFT JOIN
(SELECT workshop_metadatas.workshop_id as id, workshop_metadatas.uuid
FROM workshop_metadatas WHERE uuid = 'smth') as metadatas
WHERE uuid IS NULL
I know that to do left join you have to use includes, but how do I include query, not the table? I am completely baffled by this.
Thank you!
P.S. And while we are at it, are there any good and comprehensive docs for rails? The one that are listing all the available arguments for includes method, for example.
I'd recommend reading through the ActiveRecord query interface guide - it's a bit verbose, but it's got a lot of great pointers! For the exhaustive API reference I'd normally point you to http://api.rubyonrails.org/, but it's pretty bare for ActiveRecord::QueryMethods :( The guide is the best bet...
Let me rephrase your query to make sure I have it correct: You want to select all workshops that do not have a metadata row with a uuid of smth?
Thankfully, rails lets you drop down to query fragments, so you should be able to do it via:
Workshop.joins(
'LEFT JOIN workshop_metadatas ON workshop_metadatas.workshop_id = workshops.id'
).where('uuid IS NULL')
(Isn't that expressing the same thing w/o the subquery? If not: you should be able to pass your subquery into the joins call)

Translate SQL to Rails queries

How can I write this SQL statement the 'Rails way'?
SELECT users.*, count(invitations.id) AS invitations_count
FROM users
LEFT OUTER JOIN invitations ON invitations.sender_id = users.id
GROUP BY users.id
HAVING count(invitations.sender_id) < 5 AND users.email_address NOTNULL
ORDER BY invitations_count
Should I be using squeel gem for these kinds of queries?
This is available if you want to just transfer the SQL:
User.find_by_sql("...")
However, if you follow rails conventions with your naming (invitations.user_id), and store the invitation count on users (and update it when you add invitations) rather than doing a join to get it each time, you could do this:
On users:
scope :emailable, where('users.email_address IS NOT NULL')
scope :low_invitations, where('users.invitation_count < 5')
Then to query users with under 5 invites and an email address, ordered by no of invites:
#users = User.emailable.low_invitations.order('invitation_count asc')
Then access the invitations for a user with something like:
#user.invitations
#user.invitations.count
etc
For the above, you would have to add an invitation_count col to users, change sender_id to user_id, and add some scopes to the user model. You could also probably use joins to get a count without having a denormalised invitation_count.
If you are going to use rails it's far easier to go with these conventions than against them, and you might find it worthwhile setting up a small experimental app and playing with relations, plus reading the associations guide:
http://guides.rubyonrails.org/association_basics.html
You need to define proper models for Invitations and Users, then connect them via n*1 or n*n relationships (via belongs_to, has_many, etc). Then you'll be able to write code that will generate this or similar query "under the hood".
When using Rails you have to stop thinking SQL and start thinking Models, Views, Controllers.

How to find all items not related to another model - Rails 3

I have a fairly complicated lookup i'm trying to do in Rails and I'm not entirely sure how hoping someone can help.
I have two models, User and Place.
A user is related to Place twice. Once for visited_places and once for planned_places. Its a many to many relationship but using has_many :through. Here's the relationship from User.
has_many :visited_places
has_many :visited, :class_name=>"Place", :through=>:visited_places, :source=>:place
has_many :planned_places
has_many :planned, :class_name=>"Place", :through=>:planned_places, :source=>:place
In place the relationship is also defined. Here's the definition there
has_many :visited_users, :class_name=>"User", :through=>:visited_places
has_many :planned_users, :class_name=>"User", :through=>:planned_places
I'm trying to write a find on Place that returns all places in the database that aren't related to a User through either visited or planned. Right now I'm accomplishing this by simply querying all Places and then subtracting visited and planned from the results but I want to add in pagination and I'm worried this could complicate that. Here's my current code.
all_places = Place.find(:all)
all_places = all_places - user.visited - user.planned
Anyone know how i can accomplish this in just a call to Place.find. Also this is a Rails 3 app so if any of the active record improvements make this easier they are an option.
How about something like:
unvisited_places = Place.find(:all, :conditions => "id NOT IN(#{visited_places.map(&:place_id)})")
That's the general idea -- it can be made more efficient and convenient depending on your final needs.
You don't show it but if I am right in assuming that the VisitedPlace and PlannedPlace models have a belongs_to :user relationships then those tables have a user_id secondary key, right?
So in that case I would think it would be most efficient to do this in the database in which case you are looking for a select across a table join of places, visited_places and planned_places where users.id is not in either of visited_places or planned_places
in sql:
select * from places where id not in
(
(select place_id from visited_places where user_id = ?)
union
(select place_id from planned_places where user_id=?)
)
If that query works, you can use as follows:
Places.find_by_sql(...the complete sql query ...)
I would not know how to write such a query, with an exclusion, in Rails 3 otherwise.
I ran into a similar desire recently... I wanted to get all Model1s that weren't associated with a Model2. Using Rails 4.1, here's what I did:
Model1.where.not(id: Model2.select(:user_id).uniq)
This creates a nested SELECT, like #nathanvda suggested, effectively letting the database do all the work. Example SQL produced is:
SELECT "model1s".* FROM "model1s" WHERE ("model1s"."id" NOT IN (SELECT DISTINCT "model2s"."model1_id" FROM "model2s"))

Resources