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
}
Related
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]]
I have a Product class that has_many Gender through Connection class instances. I want to query to find products that have both end_a and end_b present. The current class method works with 2 caveats:
Fails to return correctly if searching where end_a and end_b are the same. Instead should search if product has 2 instances, not just one of object.
Returns an Array when I want an ActiveRecord_Relation.
The class method .query is below, any feedback or ideas are appreciated.
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
def self.query(end_a, end_b)
search_base = active.joins(:connections)
end_a_search = search_base.where(connections: { gender_id: end_a } )
end_a_search & search_base.where(connections: { gender_id: end_b } )
end
end
ps: Once this is figured out will likely move this to a scope for Product
class Product < ActiveRecord::Base
has_many :connections, dependent: :destroy, as: :connectionable
has_many :genders, through: :connections
scope :with_genders, -> (end_a, end_b) {
relation = joins('INNER JOIN connections c1 ON c1.connectionable_id = products.id AND c1.connectionable_type = \'Product\'')
.joins('INNER JOIN connections c2 ON c1.connectionable_id = c2.connectionable_id AND c2.connectionable_type = \'Product\'')
.where(c1: {gender_id: end_a}, c2: {gender_id: end_b})
.group('products.id')
end_a == end_b ? relation.having('COUNT(products.id) > 1') : relation
}
end
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...
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..
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})