Complex JOIN with ActiveRecord and Rails 3 - sql

I have the following models:
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Post < ActiveRecord::Base
belongs_to :group
end
I have to find all posts that belong to groups where user is a member. I have made it with this method:
#post = Post
.joins(:group => {:memberships => :user})
.where(:memberships => {:user_id => current_user.id})
but it produces unefficient SQL:
SELECT "posts".* FROM "posts"
INNER JOIN "groups" ON "groups"."id" = "posts"."group_id"
INNER JOIN "memberships" ON "memberships"."group_id" = "groups"."id"
INNER JOIN "users" ON "users"."id" = "memberships"."user_id"
WHERE "memberships"."user_id" = 1
I want to make a query like this:
SELECT posts.* FROM posts
INNER JOIN memberships ON memberships.group_id = posts.group_id
WHERE memberships.user_id = 1
How can I do this without using raw SQL?

You can get closer without changing your model at all, by removing the unused join from your call:
Post.joins(group: :memberships).where(memberships: { user_id: 1 })
compiles to SQL
SELECT "posts".* FROM "posts"
INNER JOIN "groups" ON "groups"."id" = "posts"."group_id"
INNER JOIN "memberships" ON "memberships"."group_id" = "groups"."id"
WHERE ("memberships"."user_id" = 1)

something like this should work for you, although it requires mixing in a little raw SQL
Post
.joins("INNER JOIN memberships ON memberships.group_id = posts.group_id")
.where(:memberships => {:user_id => current_user.id})

Related

ActiveRecord map count of whole collection, then that query to SQL

How can I write this query in SQL?
a = Brand.find(1).publications.map(&:component_id)
Hash[a.group_by(&:itself).map {|k, v| [Component.find(k).name, v.size] }]
=> {"title one"=>1, "something"=>1, "continue"=>1}
Obviously I can't call .to_sql on Ennumerable. I've written this so far, but it seems to be counting something other than the number of occurrences:
SELECT c.name, c.id, COUNT(p.component_id)
FROM publications p
INNER JOIN components c
ON c.id = p.component_id
INNER JOIN brands_components bc
ON bc.brand_id IN (1)
GROUP BY 1, 2
This gets the wrong numbers (i.e., too many are identical):
name id count
------------------------------
something 2026 114
another name 3028 1,140
another new one 2409 2,850
world class 264 6,612
top up 3370 114
The model associations look like this:
class Brand < ActiveRecord::Base
has_many :documents
has_many :publications, through: :users
has_many :users
end
class User < ActiveRecord::Base
has_many :documents, dependent: :destroy
has_many :publications, through: :documents, dependent: :destroy
end
class Document < ActiveRecord::Base
belongs_to :user
belongs_to :brand
has_many :components, through: :publications
has_many :publications, dependent: :destroy
end
class Publication < ActiveRecord::Base
belongs_to :document
belongs_to :component
end
class Component < ActiveRecord::Base
has_many :publications
has_many :documents, through: :publications
end
I want to return the count of occurrences of component_id on Publication filtered by Brand in SQL.
I've solved my issue:
SELECT c.name, COUNT(p.component_id)
FROM publications p
INNER JOIN components c
ON c.id = p.component_id
INNER JOIN brands_components bc
ON c.id = bc.component_id
AND bc.brand_id IN (1)
GROUP BY 1
I had forgotten to make this match: c.id = bc.component_id. Might help someone.

Ar multiple joins

I have the following models:
class Distributor < ActiveRecord::Base
has_many :products
end
class Producer < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
has_one :favorite
belongs_to :producer
belongs_to :distributor
end
class Favorite < ActiveRecord::Base
belongs_to :product
end
class User < ActiveRecord::Base
has_many :favorites
end
I would like to build a AR expression is analog of sql query:
select *
from `favorites`
inner join `products` on `products`.`id` = `favorites`.`product_id`
inner join `producers` on `producers`.`id` = `products`.`producer_id`
inner join `distributors` on `distributors`.`id` = `products`.`distributor_id`
where `favorites`.`user_id` = 1
You can use a nested set of joins methods like this:
Favorite.joins(:product => [:producer , :distributor]).where("favorites.user_id = 1")
Note that i am using the => notation, but you can use the ruby 1.9+ one too.

rails nested has_many with foreign scope join misses to join table: sql error

A self running template with all models to test by yourself is available in this github gist - run it and it triggers the error.
To visualize it, the structure looks like this:
Colors
|n|
|:|
|1| -----
Houses n:n Conditions
|n| -----
|:|
|1|
People
Starting from the blank db I create some test data (console commands, return values omitted to keep it clear):
irb(main):001:0> Condition.create(condition: :damaged)
irb(main):002:0> house = House.create(conditions: [Condition.first])
irb(main):003:0> person = Person.create
irb(main):004:0> house.person = person
irb(main):005:0> house.save
So now I have some test data. Let's retrieve the person's houses (which are only the damaged ones by definition):
irb(main):006:0> person.damaged_houses
House Load (0.2ms)
SELECT "houses".* FROM "houses"
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
[["person_id", 1]]
=> #<ActiveRecord::Associations::CollectionProxy [#<House id: 1, person_id: 1>]>
All good, the damaged house is returned and the sql joined the conditions table correctly. Now I want to get all colors of the person, which is defined as all colors of the houses, where the houses are still only the damaged ones. This should return an empty collection (since no colors are in the db yet).
irb(main):007:0> person.damaged_colors
Color Load (0.4ms)
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
[["person_id", 1]]
SQLite3::SQLException: no such column: conditions.condition:
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
It's clear from the sql string that the join table conditions is missing and therefore conditions.condition is not available. If I see it correctly, simply this string from the query before is missing:
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
So the query should be:
SELECT "colors".* FROM "colors"
INNER JOIN "houses" ON "colors"."house_id" = "houses"."id"
INNER JOIN "conditions_houses" ON "conditions_houses"."house_id" = "houses"."id"
INNER JOIN "conditions" ON "conditions"."id" = "conditions_houses"."condition_id"
WHERE "houses"."person_id" = ? AND "conditions"."condition" = 'damaged'
Is this a rails bug or am I doing it wrong? Why is the join conditions missing?
The code:
class Color < ActiveRecord::Base
belongs_to :house
end
class Condition < ActiveRecord::Base
has_and_belongs_to_many :houses
end
class House < ActiveRecord::Base
has_many :colors
belongs_to :person
has_and_belongs_to_many :conditions
scope :damaged, -> { joins(:conditions).where(:'conditions.condition' => 'damaged') }
end
class Person < ActiveRecord::Base
has_many :damaged_houses, -> { damaged }, :class_name => "House"
has_many :damaged_colors, through: :damaged_houses, :source => :colors
end
So I was able to get your gist to run by modifying the following:
has_many :damaged_colors, through: :damaged_houses, :source => :colors
to
has_many :damaged_colors, -> { joins({house: :conditions}).where(:'conditions.condition' => 'damaged') }, through: :damaged_houses, :source => :colors
It does appear to be an ActiveRecord bug on HABTM associations as alluded to in the comments... Hope this helps in the mean time...
Funny thing was, I converted your gist to a hm:hm and had the same issue...

Multiple joins with AND where conditions

I have a simple tagging system of items with the following structure:
class Item < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :item
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
end
I'd like to add a scope to the Item class to be able to get all the items with a given set of tags (all tags in the set included)
So I tried the following scope:
scope :tag_context, lambda { |context| (context.empty? ? all :
joins(:taggings).where(:taggings => {tag_id => context.collect(&:id)})
)
}
where context is an Array of Tag objects.
The point is that this scope yield the following sql:
SELECT items.* FROM items INNER JOIN taggings ON taggings.item_id = items.id
WHERE taggings.tag_id IN (1,2)
assuming context contains tag 1 and 2.
I would like to get items that are tagged by tag 1 AND tag 2.
So I assume, something like:
SELECT items.* FROM items INNER JOIN taggings as t1 ON t1.item_id = items.id
INNER JOIN taggings as t2 ON t2.item_id = items.id
WHERE t1.tag_id = 1 AND t2.tag_id = 2
How should I proceed to translate it in a Rails scope?
I need a scope in order to be able to chain the various scopes on the Item class.
Thanks for your help!
You can try building the scope dynamically like this (and you don't need an .empty? check with inject):
scope :tag_context, lambda { |context|
context.collect(&:id).inject(self) do |m, id|
m.joins("INNER JOIN taggings as t#{id} ON t#{id}.item_id = items.id").
where("t#{id}.tag_id = ?", id)
end
}

Ruby ActiveRecord multiple joins through associations

I'd like to convert
SELECT `users`.* FROM `users`
INNER JOIN `memberships`
ON `memberships`.`user_id` = `users`.`id`
INNER JOIN `roles`
ON `roles`.`id` = `memberships`.`role_id`
WHERE `memberships`.`group_id` = 'NUCC' AND (expiration > '2012-07-02')
ORDER BY `roles`.`rank` DESC
Into an ActiveRecord association.
Groups have many members (class User) through memberships. Each membership has a role (role_id) which maps to another table (roles) and subsequently an AR model (Role). Each role has a rank (integer) associated with it.
I'd like to simply sort the members of a group by the memberships-roles-rank.
Untested, probably has typos, but...
class User < ActiveRecord::Base
has_many :memberships
has_many :roles, :through => :memberships, :uniq => true
end
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class Role < ActiveRecord::Base
has_many :memberships
has_many :users, :through => :memberships, :uniq => true
end
And then, to sort the users by roles.rank:
q = User.joins(:memberships => :users)
q = q.where(:memberships.group_id => 'NUCC')
q = q.where("expressionn > '2012-07-02'")
q = q.order("roles.rank DESC")
puts q.to_sql
AREL lets you join things up like that pretty easily. For instance, you can keep that going with even further INNER JOINS with syntax similar to:
User.joins(:memberships => { :users => :someothermodel })
Just remember to replicate that structure whenever you need to reference something through the JOIN, or just write your own SQL fragment..