How to merge (link) 2 Relations on different tables in Rails 4 - ruby-on-rails

Given 2 ActiveRecord relations that generate following SQL:
relation a = SELECT comments.* FROM comments INNER JOIN attachments ON attachments.comment_id = comments.id WHERE attachment.name ILIKE '%foo%
relation b = SELECT attachments.* FROM attachments INNER JOIN users ON attachments.user_id = users.id WHERE users.other_conditions
This worked in Rails/ActiveRecord 3:
puts a.merge(b).to_sql # Rails 3
> "SELECT comments.* FROM comments INNER JOIN attachments ON attachments.comment_id = comments.id INNER JOIN users ON attachments.user_id = users.id WHERE attachment.name ILIKE '%foo% AND users.other_conditions"
I think it worked because the merge was ignoring any non-existing associations on the queries.
But Rails 4 is much more pedantic and fails with:
puts a.merge(b).to_sql # Rails 4
> ActiveRecord::ConfigurationError: Association named 'user' was not found on Comment; perhaps you misspelled it?
So the question is how can I literally merge the 2 relations without Rails being worried about the correctness (my specs take responsibility for that)?

Can you describe your models and their relations a little more?
For me it worked like this:
class User
has_many :facebook_friends
end
class FacebookFriend
belongs_to :user
end
a = User.where("users.first_name LIKE '%Sandy%'")
b = FacebookFriend.where("facebook_friends.last_name LIKE '%John%'")
a.merge(b)
=> User Load (0.5ms) SELECT users.* FROM users WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
=> Mysql2::Error: Unknown column 'facebook_friends.last_name' in 'where clause': SELECT users.* FROM users WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
a.joins(:facebook_friends).merge(b)
=> User Load (0.6ms) SELECT users.* FROM users INNER JOIN facebook_friends ON facebook_friends.user_uid = users.uid WHERE (users.first_name LIKE '%Sandy%') AND (facebook_friends.last_name LIKE '%John%')
=> []

The amazing scuttle.io transforms your sql as follows:
Comment.select(Comment.arel_table[Arel.star]).where(
Attachment.arel_table[:name].and(User.arel_table[:other_conditions])
).joins(
Comment.arel_table.join(Attachment.arel_table).on(
Attachment.arel_table[:comment_id].eq(Comment.arel_table[:id])
).join_sources
).joins(
Comment.arel_table.join(User.arel_table).on(
Attachment.arel_table[:user_id].eq(User.arel_table[:id])
).join_sources
)

Related

How to join on the same table multiple times?

I'm querying for the mutual friends of a given two users. The query below should do the trick for the most part and the friendship table should be self-evident, containing a user_id and friend_id.
SELECT `users`.* FROM `users`
INNER JOIN `friendships` `a` ON `users`.`id` = `a`.`friend_id`
INNER JOIN `friendships` `b` ON `users`.`id` = `b`.`friend_id`
WHERE `a`.`user_id` = 1 AND `b`.`user_id` = 2
What's got me confused is how to write this semantic ActiveRecord. With ActiveRecord you can join on an association, but only once. So how do you go about writing this as plainly as possible in ActiveRecord?
I do it with string arguments to joins:
User.
joins("INNER JOIN friendships a ON users.id = a.friend_id").
joins("INNER JOIN friendships b ON users.id = b.friend_id").
where("a.user_id" => 1, "b.user_id" => 2)
I'm not aware of a higher-level way to do such a join with Active Record.
Firstly, you should have proper Model Relationship between User & Friendship.
user model:
has_many :friendships
friendship model:
belongs_to :user
with that:
you can get activerecords in your iteration like:
users = User.all
users.each do |user|
user.friendships
.....
end
Or, by specific user, like:
user = User.first
user.friendships #returns association
or
User.first.friendships
for 2 specific users (given), you can do like:
User.where(id: [1,2]) #array of user id, then get the friendship record as above.
Hope this helps!

retrieve reverse multiple records of rails association

I have two models product and category.
I am able to make successful queries like Category.products etc.
Product.rb
belongs_to :category
Category.rb
has_many :products
Now I want to retrieve only those categories that has at least one existing product.
I tried like this :
#categories = Category.where(Category.products.present?)
# returned error undefined method `products' also changing to product didn't work.
Getting your comment that you need Categories with products and that the product property with_operator to be true, you can do that query in "rails style" using joins and merge:
#categories = Category.joins(:products).merge(Product.where(with_operator: true)).uniq
Which will generate the following SQL:
SELECT DISTINCT "categories".* FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id" WHERE "products"."with_operator" = 't'
You could also use the rails 4 syntax, as pointed by #yukke:
Category.joins(:products).where(products: { with_operator: true }).uniq
All you need is inner join. It will skip those categories, that has no products. And to add a condition on joined table you can use rails 4 where's syntax:
#categories = Category.joins(:products).where(products: { with_operator: true }).uniq
It will produce next sql query:
SELECT DISTINCT "categories".*
FROM "categories" INNER JOIN "products" ON "products"."category_id" = "categories"."id"
WHERE "products"."with_operator" = 't'

ActiveRecord where at least x records of association exist

Let's say users have comments and I want all users with three comments or more.
User.joins(:comments) will get me any user that has
one or more comments. What is the nicest way to get Users with at least three comments?
A nicer way might be to write that subquery using the API:
subquery = Comment.select("user_id").
group(:user_id).
having("COUNT(*) >= 3").to_sql
User.where("id IN (#{subquery})")
SQL like this:
SELECT users.* FROM users
WHERE EXISTS (SELECT id FROM comments WHERE user_id = users.id GROUP BY user_id HAVING COUNT(*) >= 3)
Which in ActiveRecord notation translates to:
User.where('EXISTS (SELECT id FROM comments WHERE user_id = users.id GROUP BY user_id HAVING COUNT(*) >= 3)')

How do I get Rails ActiveRecord to generate optimized SQL?

Let's say that I have 4 models which are related in the following ways:
Schedule has foreign key to Project
Schedule has foreign key to User
Project has foreign key to Client
In my Schedule#index view I want the most optimized SQL so that I can display links to the Schedule's associated Project, Client, and User. So, I should not pull all of the columns for the Project, Client, and User; only their IDs and Name.
If I were to manually write the SQL it might look like this:
select
s.id,
s.schedule_name,
s.schedule_type,
s.project_id,
p.name project_name,
p.client_id client_id,
c.name client_name,
s.user_id,
u.login user_login,
s.created_at,
s.updated_at,
s.data_count
from
Users u inner join
Clients c inner join
Schedules s inner join
Projects p
on p.id = s.project_id
on c.id = p.client_id
on u.id = s.user_id
order by
s.created_at desc
My question is: What would the ActiveRecord code look like to get Rails 3 to generate that SQL? For example, somthing like:
#schedules = Schedule. # ?
I already have the associations setup in the models (i.e. has_many / belongs_to).
I think this will build (or at least help) you get what you're looking for:
Schedule.select("schedules.id, schedules.schedule_name, projects.name as project_name").joins(:user, :project=>:client).order("schedules.created_at DESC")
should yield:
SELECT schedules.id, schedules.schedule_name, projects.name as project_name FROM `schedules` INNER JOIN `users` ON `users`.`id` = `schedules`.`user_id` INNER JOIN `projects` ON `projects`.`id` = `schedules`.`project_id` INNER JOIN `clients` ON `clients`.`id` = `projects`.`client_id`
The main problem I see in your approach is that you're looking for schedule objects but basing your initial "FROM" clause on "User" and your associations given are also on Schedule, so I built this solution based on the plain assumption that you want schedules!
I also didn't include all of your selects to save some typing, but you get the idea. You will simply have to add each one qualified with its full table name.

Converting SQL to Rails 3 ActiveRecord

I am looking for the most Rails-ish way to retrieve a distinct list of categories for available songs on a given album.
Here it is in SQL for album_id = 1
-- Using subselects
select * from categories where id in (
select distinct category_id
from categorizations
where song_id in (select song_id from album_songs
where album_id = 1 and available = 't')
)
order by name asc;
-- Using joins
select distinct c.* from categories c
inner join categorizations cz on c.id = cz.category_id
left join album_songs a on cz.song_id = a.song_id
where a.album_id = 1 and a.available = 't'
order by c.name asc;
My working (albeit naive!) attempts to port this to ActiveRecord
## attempting to do it like subselects (although they're not really
## subselects, it executes them individually -- from what i've read
## ActiveRecord won't do subselects?)
Category.where('id IN (?)',
Categorization.select('DISTINCT category_id').where('song_id IN (?)',
Album.find(1).songs.available.map(&:song_id)
).map(&:category_id)
).order('name ASC')
## joins - although at this point it's pretty much all sql
## as i couldn't find a way to do the left join in pure AR
## i'm also duplicating my AlbumSongs.available scope -- is
## that scope reusable here? (outside the AlbumSongs model?)
Category.select('DISTINCT categories.*')
.joins(:categorizations,
'LEFT OUTER JOIN album_songs ON categorizations.song_id = album_songs.song_id')
.where('album_songs.album_id = ? and available', 1)
I am going with the final one but it seems like I might as well just write it in SQL?
Is there any way to improve this to be more Rails-ish?
Well, it would certainly help if you post your model set up. But assuming that:
* song has_many :categories, :through => categorizations
* an album does not have a huge amount of songs on it
Why not just do:
Album.includes({:songs => {:categorizations => :categories}}).find(1).songs.collect {|s| s.category}.flatten.uniq

Resources