Using Ruby on Rails 3.2.13 and Squeel I have the following models:
class Group < ActiveRecord::Base
has_and_belongs_to_many :users
end
class User < ActiveRecord::Base
has_and_belongs_to_many :groups
has_many :characters, :dependent => :destroy
end
class Character < ActiveRecord::Base
belongs_to :user
end
Characters have a boolean attribute :public
.
Within the Character model, I want to retrieve all characters that are visible to the current user, as determined by the following conditions:
The result has to be an ActiveRecord::Relation
.
Matching the first two conditions is simple enough:
def self.own_or_public user_to_check
where{
(user_id == user_to_check.id) |
(public)
}
end
For the third condition the following query yields the correct results, but is probably not the best way to do it:
def self.shares_group_with user_to_check
user_groups = Group.joins{users}.where{users.id == user_to_check.id}
joins{user.groups}.
where{
user.groups.id.in(user_groups.select(id))
}.uniq
end
Furthermore, I cannot find a way to concatenate the two results yielding an ActiveRecord::Relation
containing the results from both queries (merge
yields elements that match both queries, and +
returns an Array
instead of an ActiveRecord::Relation
).
Any help on how to handle this in one single Squeel query is much appreciated.
Let's try restructuring you problem a little and replace the has_and_belongs_to_many
associations with has_many, through
associations, and we will add another has_many, through
association on the Character
model as follows:
class Membership < ActiveRecord::Base
belongs_to :user
belongs_to :group
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :users, through: :memberships
end
class User < ActiveRecord::Base
has_many :memberships
has_many :groups, through: :memberships
has_many :characters
end
class Character < ActiveRecord::Base
belongs_to :user
has_many :groups, through: :user
end
The Membership
model is the representation of the relationship between a User
and a Group
- essentially the join table that is hidden when using has_and_belongs_to_many
. I prefer to see the relationship (especially if it is important).
We also have an association on the Character
model to the Group
s associated with the user. This is helpful when we try to join our scopes.
Fleshing the Character
model out, let's add the following:
sifter :by_user do |user|
user_id == user.id
end
sifter :public do
public
end
Using the sifters as our building blocks, we can add the following to get the visible characters (as you defined it) with:
def self.get_visible(user)
Character.uniq.joins{groups.outer}.where{(sift :public)|(sift :by_user, user)|(groups.id.in(user.groups))}
end
This method takes an instance of User
and finds the following Character
s:
And then we only take the distinct list of characters from those sets.
From rails console:
irb(main):053:0> Character.get_visible(User.find(4))
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 4]]
Group Load (0.7ms) SELECT "groups".* FROM "groups" INNER JOIN "memberships" ON "groups"."id" = "memberships"."group_id" WHERE "memberships"."user_id" = 4
Character Load (0.9ms) SELECT DISTINCT "characters".* FROM "characters" LEFT OUTER JOIN "users" ON "users"."id" = "characters"."user_id" LEFT OUTER JOIN "memberships" ON "memberships"."user_id" = "users"."id" LEFT OUTER JOIN "groups" ON "groups"."id" = "memberships"."group_id" WHERE ((("characters"."public" OR "characters"."user_id" = 4) OR "groups"."id" IN (2)))
[
[0] #<Character:0x00000005a16b48> {
:id => 4,
:user_id => 4,
:name => "Testiculies",
:created_at => Tue, 13 Aug 2013 14:35:50 UTC +00:00,
:updated_at => Tue, 13 Aug 2013 14:35:50 UTC +00:00,
:public => nil
},
[1] #<Character:0x00000005d9db40> {
:id => 1,
:user_id => 1,
:name => "conan",
:created_at => Mon, 12 Aug 2013 20:18:52 UTC +00:00,
:updated_at => Tue, 13 Aug 2013 12:53:42 UTC +00:00,
:public => true
}
]
To find all characters a particular User
has, add an instance method to the User
model:
def get_visible_characters
Character.get_visible(self)
end
I think that will get you where you want to go.