I'm trying to show only brand instances which the current user has not tagged, even if other users have tagged the same brand already. Something like:
Controller
This is my controller code and even though it should be working, it currently returns all brand instances.
@brand = current_user.brands.includes(:taggings).where( [ "taggings.id IS NULL OR taggings.tagger_id != ?", current_user.id ] ).order("RANDOM()").first
Schema (including my join model for good measure)
create_table "brand_users", :force => true do |t|
t.integer "brand_id"
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "taggings", :force => true do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
t.integer "tagger_id"
t.string "tagger_type"
t.string "context"
t.datetime "created_at"
end
add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_co开发者_运维百科ntext"
create_table "tags", :force => true do |t|
t.string "name"
end
end
So if you're using the acts-as-taggable-on gem and have the following models:
class User < ActiveRecord::Base
acts_as_tagger
has_many :brand_users
has_many :brands, :through => :brand_users
end
So you also have the tables in your schema like:
create_table "users", :force => true do |t|
t.string "name"
end
create_table "brands", :force => true do |t|
t.string "name"
end
Then the following SQL query should hopefully do what you want (?):
SELECT brands.*
FROM brands
WHERE brands.id NOT IN (
SELECT brands.id
FROM brands
INNER JOIN brand_users ON brand_users.brand_id = brands.id
INNER JOIN taggings ON (taggings.tagger_id = brand_users.user_id AND taggings.tagger_type = 'User')
WHERE brand_users.user_id = 1 AND taggings.taggable_id = brand_users.brand_id
)
To translate this into Rails ORM, I can't get any closer without hard coding the whole sub-select SQL string, something like:
class Brand < ActiveRecord::Base
has_many :brand_users
has_many :users, :through => :brand_users
scope :has_not_been_tagged_by_user, lambda {|user| where("brands.id NOT IN (SELECT brands.id
FROM brands
INNER JOIN brand_users ON brand_users.brand_id = brands.id
INNER JOIN taggings ON (taggings.tagger_id = brand_users.user_id AND taggings.tagger_type = 'User')
WHERE brand_users.user_id = ? AND taggings.taggable_id = brand_users.brand_id)", user.id) }
end
(I know you could do this and then use ruby's .map(&:id).join(',') but if this is a large app I think you loose a lot of performance by taking this out of the database, converting it into a string of integers and feeding it back in (as I understand it).)
Then in your controller I think you'd do something like:
@brand = current_user.brands.has_not_been_tagged_by_user(current_user)
As an aside, I think this would actually then execute an SQL like below (is that right?):
SELECT brands.*
FROM users
INNER JOIN brand_users ON brand_users.user_id = users.id
INNER JOIN brands ON brands.id = brand_users.brand_id
WHERE brands.id NOT IN (
SELECT brands.id
FROM brands
INNER JOIN brand_users ON brand_users.brand_id = brands.id
INNER JOIN taggings ON (taggings.tagger_id = brand_users.user_id AND taggings.tagger_type = 'User')
WHERE brand_users.user_id = 1 AND taggings.taggable_id = brand_users.brand_id
) AND users.id = 1
As far as I know there is no SELECT * FROM x WHERE * IS NULL
so a procedure would probably be the only way. Just create a procedure in your DB and call it from the code, you don't have to put the SQL in the code.
You can see an example of such a procedure here.
精彩评论