How do I write a UNION chain with ActiveRelation? - ruby-on-rails

I need to be able to chain an arbitrary number of sub-selects with UNION using ActiveRelation.
I'm a little confused by the ARel implementation of this, since it seems to assume UNION is a binary operation.
However:
( select_statement_a ) UNION ( select_statement_b ) UNION ( select_statement_c )
is valid SQL. Is this possible without doing nasty string-substitution?

You can do a bit better than what Adam Lassek has proposed though he is on the right track. I've just solved a similar problem trying to get a friends list from a social network model. Friends can be aquired automatically in various ways but I would like to have an ActiveRelation friendly query method that can handle further chaining. So I have
class User
has_many :events_as_owner, :class_name => "Event", :inverse_of => :owner, :foreign_key => :owner_id, :dependent => :destroy
has_many :events_as_guest, :through => :invitations, :source => :event
def friends
friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
User.where do
(id.in friends_as_guests.select{id}
) |
(id.in friends_as_hosts.select{id}
)
end
end
end
which takes advantage of Squeels subquery support. Generated SQL is
SELECT "users".*
FROM "users"
WHERE (( "users"."id" IN (SELECT "users"."id"
FROM "users"
INNER JOIN "invitations"
ON "invitations"."user_id" = "users"."id"
INNER JOIN "events"
ON "events"."id" = "invitations"."event_id"
WHERE "events"."owner_id" = 87)
OR "users"."id" IN (SELECT "users"."id"
FROM "users"
INNER JOIN "events"
ON "events"."owner_id" = "users"."id"
INNER JOIN "invitations"
ON "invitations"."user_id" =
"users"."id"
WHERE "invitations"."user_id" = 87) ))
An alternative pattern where you need a variable number of components is demonstrated with a slight modification to the above code
def friends
friends_as_guests = User.joins{events_as_guest}.where{events_as_guest.owner_id==my{id}}
friends_as_hosts = User.joins{events_as_owner}.joins{invitations}.where{invitations.user_id==my{id}}
components = [friends_as_guests, friends_as_hosts]
User.where do
components = components.map { |c| id.in c.select{id} }
components.inject do |s, i|
s | i
end
end
end
And here is a rough guess as to the solution for the OP's exact question
class Shift < ActiveRecord::Base
def self.limit_per_day(options = {})
options[:start] ||= Date.today
options[:stop] ||= Date.today.next_month
options[:per_day] ||= 5
queries = (options[:start]..options[:stop]).map do |day|
where{|s| s.scheduled_start >= day}.
where{|s| s.scheduled_start < day.tomorrow}.
limit(options[:per_day])
end
where do
queries.map { |c| id.in c.select{id} }.inject do |s, i|
s | i
end
end
end
end

Because of the way the ARel visitor was generating the unions, I kept getting SQL errors while using Arel::Nodes::Union. Looks like old-fashioned string interpolation was the only way to get this working.
I have a Shift model, and I want to get a collection of shifts for a given date range, limited to five shifts per day. This is a class method on the Shift model:
def limit_per_day(options = {})
options[:start] ||= Date.today
options[:stop] ||= Date.today.next_month
options[:per_day] ||= 5
queries = (options[:start]..options[:stop]).map do |day|
select{id}.
where{|s| s.scheduled_start >= day}.
where{|s| s.scheduled_start < day.tomorrow}.
limit(options[:per_day])
end.map{|q| "( #{ q.to_sql } )" }
where %{"shifts"."id" in ( #{queries.join(' UNION ')} )}
end
(I am using Squeel in addition to ActiveRecord)
Having to resort to string-interpolation is annoying, but at least the user-provided parameters are being sanitized correctly. I would of course appreciate suggestions to make this cleaner.

I like Squeel. But don't use it. So I came to this solution (Arel 4.0.2)
def build_union(left, right)
if right.length > 1
Arel::Nodes::UnionAll.new(left, build_union(right[0], right[1..-1]))
else
Arel::Nodes::UnionAll.new(left, right[0])
end
end
managers = [select_manager_1, select_manager_2, select_manager_3]
build_union(managers[0], managers[1..-1]).to_sql
# => ( (SELECT table1.* from table1)
# UNION ALL
# ( (SELECT table2.* from table2)
# UNION ALL
# (SELECT table3.* from table3) ) )

There's a way to make this work using arel:
tc=TestColumn.arel_table
return TestColumn.where(tc[:id]
.in(TestColumn.select(:id)
.where(:attr1=>true)
.union(TestColumn.select(:id)
.select(:id)
.where(:attr2=>true))))

Related

activerecord joins multiple time the same table

I am doing a simple search looking like this (with 8 different params, but I will copy 2 just for the exemple)
if params[:old] == "on"
#events = #events.joins(:services).where(services: { name: "old" })
end
if params[:big] == "on"
#events = #events.joins(:services).where(services: { name: "big" })
end
The query works fine when I have only one params "on" and returns my events having the service in the param.
BUT if I have two params "on", even if my event has both services, it's not working anymore.
I've tested it in the console, and I can see that if I do a .joins(:services).where(services: { name: "big" }) on the element that was already joined before, it returns nothing.
I don't understand why.
The first #events (when one param) returns an active record relation with a few events inside.
Why can't I do another .joins on it?
I really don't understand what's wrong in this query and why it becomes empty as soon as it is joined twice.
Thanks a lot
The code you're using will translate to:
SELECT "events".* FROM "events" INNER JOIN "services" ON "services"."event_id" = "events"."id" WHERE "services"."name" = ? AND "services"."name" = ? LIMIT ? [["name", "big"], ["name", "old"], ["LIMIT", 11]]
This is why it returns zero record.
Here is the solution I can think of at the moment, not sure if it's the ideal, but yes it works and has been tested.
# event.rb
class Event < ApplicationRecord
has_many :services
has_many :old_services, -> { where(name: 'old') }, class_name: 'Service'
has_many :big_services, -> { where(name: 'big') }, class_name: 'Service'
end
# service.rb
class Service < ApplicationRecord
belongs_to :event
end
And your search method can be written this way:
if params[:old] == "on"
#events = #events.joins(:old_services)
end
if params[:big] == "on"
#events = #events.joins(:big_services)
end
#events = #events.distinct
# SELECT DISTINCT "events".* FROM "events" INNER JOIN "services" ON "services"."event_id" = "events"."id" AND "services"."name" = ? INNER JOIN "services" "old_services_events" ON "old_services_events"."event_id" = "events"."id" AND "old_services_events"."name" = ? LIMIT ? [["name", "big"], ["name", "old"], ["LIMIT", 11]]

Write and Activerecord join query

I'm writing an activerecord join query but It doesn't work.
I have these two classes
class User
belongs_to :store, required: true
end
class Store < ActiveRecord::Base
has_many :users, dependent: :nullify
has_one :manager, -> { where role: User.roles[:manager] }, class_name: 'User'
end
I need to get all the stores with a manager and all the stores without a manager.
I write these two queries
Store.includes(:users).where('users.role <> ?', User.roles[:manager]).references(:users).count
Store.includes(:users).where('users.role = ?', User.roles[:manager]).references(:users).count
and the result is
2.2.1 :294 > Store.includes(:users).where('users.role <> ?', User.roles[:manager]).references(:users).count
(6.6ms) SELECT COUNT(DISTINCT "stores"."id") FROM "stores" LEFT OUTER JOIN "users" ON "users"."store_id" = "stores"."id" WHERE (users.role <> 1)
=> 201
2.2.1 :295 > Store.includes(:users).where('users.role = ?', User.roles[:manager]).references(:users).count
(4.0ms) SELECT COUNT(DISTINCT "stores"."id") FROM "stores" LEFT OUTER JOIN "users" ON "users"."store_id" = "stores"."id" WHERE (users.role = 1)
=> 217
Now I know that I have 219 stores, and using
with_manager = 0
without_manager = 0
Store.all.each do |s|
if s.manager.present?
with_manager = with_manager +1
else
without_manager = without_manager +1
end
end
I know also that I have 217 stores with manager and 2 store without manager. One query is working, the second (stores without manager) fails.
So I must fix the query, but I cannot understand how can I fix it...
Usually I use the following thing for, getting al stores with a manager:
Store.joins(:manager)
Joins will use a inner join and not a left join that is includes case.
In the opposite case, that's tricky, but I do in this way:
Store.includes(:manager).where(users: { id: nil })
It's a left join with manager and getting all stores without a user included.

Rails: How to get objects with at least one child?

After googling, browsing SO and reading, there doesn't seem to be a Rails-style way to efficiently get only those Parent objects which have at least one Child object (through a has_many :children relation). In plain SQL:
SELECT *
FROM parents
WHERE EXISTS (
SELECT 1
FROM children
WHERE parent_id = parents.id)
The closest I've come is
Parent.all.reject { |parent| parent.children.empty? }
(based on another answer), but it's really inefficient because it runs a separate query for each Parent.
Parent.joins(:children).uniq.all
As of Rails 5.1, uniq is deprecated and distinct should be used instead.
Parent.joins(:children).distinct
This is a follow-up on Chris Bailey's answer. .all is removed as well from the original answer as it doesn't add anything.
The accepted answer (Parent.joins(:children).uniq) generates SQL using DISTINCT but it can be slow query. For better performance, you should write SQL using EXISTS:
Parent.where<<-SQL
EXISTS (SELECT * FROM children c WHERE c.parent_id = parents.id)
SQL
EXISTS is much faster than DISTINCT. For example, here is a post model which has comments and likes:
class Post < ApplicationRecord
has_many :comments
has_many :likes
end
class Comment < ApplicationRecord
belongs_to :post
end
class Like < ApplicationRecord
belongs_to :post
end
In database there are 100 posts and each post has 50 comments and 50 likes. Only one post has no comments and likes:
# Create posts with comments and likes
100.times do |i|
post = Post.create!(title: "Post #{i}")
50.times do |j|
post.comments.create!(content: "Comment #{j} for #{post.title}")
post.likes.create!(user_name: "User #{j} for #{post.title}")
end
end
# Create a post without comment and like
Post.create!(title: 'Hidden post')
If you want to get posts which have at least one comment and like, you might write like this:
# NOTE: uniq method will be removed in Rails 5.1
Post.joins(:comments, :likes).distinct
The query above generates SQL like this:
SELECT DISTINCT "posts".*
FROM "posts"
INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "likes" ON "likes"."post_id" = "posts"."id"
But this SQL generates 250000 rows(100 posts * 50 comments * 50 likes) and then filters out duplicated rows, so it could be slow.
In this case you should write like this:
Post.where <<-SQL
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id)
AND
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
SQL
This query generates SQL like this:
SELECT "posts".*
FROM "posts"
WHERE (
EXISTS (SELECT * FROM comments c WHERE c.post_id = posts.id)
AND
EXISTS (SELECT * FROM likes l WHERE l.post_id = posts.id)
)
This query does not generate useless duplicated rows, so it could be faster.
Here is benchmark:
user system total real
Uniq: 0.010000 0.000000 0.010000 ( 0.074396)
Exists: 0.000000 0.000000 0.000000 ( 0.003711)
It shows EXISTS is 20.047661 times faster than DISTINCT.
I pushed the sample application in GitHub, so you can confirm the difference by yourself:
https://github.com/JunichiIto/exists-query-sandbox
I have just modified this solution for your need.
Parent.joins("left join childrens on childrends.parent_id = parents.id").where("childrents.parent_id is not null")
You just want an inner join with a distinct qualifier
SELECT DISTINCT(*)
FROM parents
JOIN children
ON children.parent_id = parents.id
This can be done in standard active record as
Parent.joins(:children).uniq
However if you want the more complex result of find all parents with no children
you need an outer join
Parent.joins("LEFT OUTER JOIN children on children.parent_id = parent.id").
where(:children => { :id => nil })
which is a solution which sux for many reasons. I recommend Ernie Millers squeel library which will allow you to do
Parent.joins{children.outer}.where{children.id == nil}
try including the children with #includes()
Parent.includes(:children).all.reject { |parent| parent.children.empty? }
This will make 2 queries:
SELECT * FROM parents;
SELECT * FROM children WHERE parent_id IN (5, 6, 8, ...);
[UPDATE]
The above solution is usefull when you need to have the Child objects loaded.
But children.empty? can also use a counter cache1,2 to determine the amount of children.
For this to work you need to add a new column to the parents table:
# a new migration
def up
change_table :parents do |t|
t.integer :children_count, :default => 0
end
Parent.reset_column_information
Parent.all.each do |p|
Parent.update_counters p.id, :children_count => p.children.length
end
end
def down
change_table :parents do |t|
t.remove :children_count
end
end
Now change your Child model:
class Child
belongs_to :parent, :counter_cache => true
end
At this point you can use size and empty? without touching the children table:
Parent.all.reject { |parent| parent.children.empty? }
Note that length doesn't use the counter cache whereas size and empty? do.

Rails eager loading seems to be querying wrong

I'm attempting to eager load in my Rails 3 app. I've narrowed it down to a very basic sample, and instead of generating the one query I'm expecting, it's generating 4.
First, here's a simple breakdown of my models.
class Profile < ActiveRecord::Base
belongs_to :gender
def to_param
self.name
end
end
class Gender < ActiveRecord::Base
has_many :profiles, :dependent => :nullify
end
I then has a ProfilesController::show action, where's I'm querying for the model.
def ProfilesController < ApplicationController
before_filter :find_profile, :only => [:show]
def show
end
private
def find_profile
#profile = Profile.find_by_username(params[:id], :include => :gender)
raise ActiveRecord::RecordNotFound, "Page not found" unless #profile
end
end
When I look at the queries this generates, it shows the following:
SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`username` = 'matt' LIMIT 1
SELECT `genders`.* FROM `genders` WHERE (`genders`.`id` = 1)
What I expected to see is a single query:
SELECT `profiles`.*, `genders`.* FROM `profiles` LEFT JOIN `genders` ON `profiles`.gender_id = `genders`.id WHERE `profiles`.`username` = 'matt' LIMIT 1
Anyone know what I'm doing wrong here? Everything I've found on eager loading makes it sound like this should work.
Edit: After trying joins, as recommended by sled, I'm still seeing the same results.
The code:
#profile = Profile.joins(:gender).where(:username => params[:id]).limit(1).first
The query:
SELECT `profiles`.* FROM `profiles` INNER JOIN `genders` ON `genders`.`id` = `profiles`.`gender_id` WHERE `profiles`.`username` = 'matt' LIMIT 1
Again, you can see no genders data is being retrieved, and so a second query to genders is being made.
I even tried adding a select, to no avail:
#profile = Profile.joins(:gender).select('profiles.*, genders.*').where(:username => params[:id]).limit(1).first
which correctly resulted in:
SELECT profiles.*, genders.* FROM `profiles` INNER JOIN `genders` ON `genders`.`id` = `profiles`.`gender_id` WHERE `profiles`.`username` = 'matt' LIMIT 1
...but it still performed a second query on genders later when accessing #profile.gender's attributes.
Edit 2: I also tried creating a scope that includes both select and joins in order to get all the fields I require, (similar to the custom left join method sled demonstrated). It looks like this:
class Profile < ActiveRecord::Base
# ...
ALL_ATTRIBUTES = [:photo, :city, :gender, :relationship_status, :physique, :children,
:diet, :drink, :smoke, :drug, :education, :income, :job, :politic, :religion, :zodiac]
scope :with_attributes,
select((ALL_ATTRIBUTES.collect { |a| "`#{reflect_on_association(a).table_name}`.*" } + ["`#{table_name}`.*"]).join(', ')).
joins(ALL_ATTRIBUTES.collect { |a|
assoc = reflect_on_association(a)
"LEFT JOIN `#{assoc.table_name}` ON `#{table_name}`.#{assoc.primary_key_name} = `#{assoc.table_name}`.#{assoc.active_record_primary_key}"
}.join(' '))
# ...
end
This generates the following query, which appears correct:
SELECT `photos`.*, `cities`.*, `profile_genders`.*, `profile_relationship_statuses`.*, `profile_physiques`.*, `profile_children`.*, `profile_diets`.*, `profile_drinks`.*, `profile_smokes`.*, `profile_drugs`.*, `profile_educations`.*, `profile_incomes`.*, `profile_jobs`.*, `profile_politics`.*, `profile_religions`.*, `profile_zodiacs`.*, `profiles`.* FROM `profiles` LEFT JOIN `photos` ON `profiles`.photo_id = `photos`.id LEFT JOIN `cities` ON `profiles`.city_id = `cities`.id LEFT JOIN `profile_genders` ON `profiles`.gender_id = `profile_genders`.id LEFT JOIN `profile_relationship_statuses` ON `profiles`.relationship_status_id = `profile_relationship_statuses`.id LEFT JOIN `profile_physiques` ON `profiles`.physique_id = `profile_physiques`.id LEFT JOIN `profile_children` ON `profiles`.children_id = `profile_children`.id LEFT JOIN `profile_diets` ON `profiles`.diet_id = `profile_diets`.id LEFT JOIN `profile_drinks` ON `profiles`.drink_id = `profile_drinks`.id LEFT JOIN `profile_smokes` ON `profiles`.smoke_id = `profile_smokes`.id LEFT JOIN `profile_drugs` ON `profiles`.drug_id = `profile_drugs`.id LEFT JOIN `profile_educations` ON `profiles`.education_id = `profile_educations`.id LEFT JOIN `profile_incomes` ON `profiles`.income_id = `profile_incomes`.id LEFT JOIN `profile_jobs` ON `profiles`.job_id = `profile_jobs`.id LEFT JOIN `profile_politics` ON `profiles`.politic_id = `profile_politics`.id LEFT JOIN `profile_religions` ON `profiles`.religion_id = `profile_religions`.id LEFT JOIN `profile_zodiacs` ON `profiles`.zodiac_id = `profile_zodiacs`.id WHERE `profiles`.`username` = 'matt' LIMIT 1
Unfortunately, it doesn't seem that calls to relationship attributes (e.g.: #profile.gender.name) are using the data that was returned in the original SELECT. Instead, I see a flood of queries following this first one:
Profile::Gender Load (0.2ms) SELECT `profile_genders`.* FROM `profile_genders` WHERE `profile_genders`.`id` = 1 LIMIT 1
Profile::Gender Load (0.4ms) SELECT `profile_genders`.* FROM `profile_genders` INNER JOIN `profile_attractions` ON `profile_genders`.id = `profile_attractions`.gender_id WHERE ((`profile_attractions`.profile_id = 2))
City Load (0.4ms) SELECT `cities`.* FROM `cities` WHERE `cities`.`id` = 1 LIMIT 1
Country Load (0.3ms) SELECT `countries`.* FROM `countries` WHERE `countries`.`id` = 228 ORDER BY FIELD(code, 'US') DESC, name ASC LIMIT 1
Profile Load (0.4ms) SELECT `profiles`.* FROM `profiles` WHERE `profiles`.`id` = 2 LIMIT 1
Profile::Language Load (0.4ms) SELECT `profile_languages`.* FROM `profile_languages` INNER JOIN `profile_profiles_languages` ON `profile_languages`.id = `profile_profiles_languages`.language_id WHERE ((`profile_profiles_languages`.profile_id = 2))
SQL (0.3ms) SELECT COUNT(*) FROM `profile_ethnicities` INNER JOIN `profile_profiles_ethnicities` ON `profile_ethnicities`.id = `profile_profiles_ethnicities`.ethnicity_id WHERE ((`profile_profiles_ethnicities`.profile_id = 2))
Profile::Religion Load (0.5ms) SELECT `profile_religions`.* FROM `profile_religions` WHERE `profile_religions`.`id` = 2 LIMIT 1
Profile::Politic Load (0.2ms) SELECT `profile_politics`.* FROM `profile_politics` WHERE `profile_politics`.`id` = 3 LIMIT 1
your example is fine and it will end up in two queries because that's how eager loading is implemented in rails. It becomes handy if you have many associated records. You can read more about it here
What you probably want is a simple join:
#profile = Profile.joins(:gender).where(:username => params[:id])
Edit
If the profile consists of many pieces there are multiple approaches here:
Custom left joins - maybe there is a plugin out there which does the job otherwise I'd suggest to do something like:
class Profile < ActiveRecord::Base
# .... code .....
def self.with_dependencies
attr_joins = []
attr_selects = []
attr_selects << "`profiles`.*"
attr_selects << "`genders`.*"
attr_selects << "`colors`.*"
attr_joins << "LEFT JOIN `genders` ON `gender`.`id` = `profiles`.gender_id"
attr_joins << "LEFT JOIN `colors` ON `colors`.`id` = `profiles`.color_id"
prep_model = select(attr_selects.join(','))
attr_joins.each do |c_join|
prep_model = prep_model.joins(c_join)
end
return prep_model
end
end
Now you could do something like:
#profile = Profile.with_dependencies.where(:username => params[:id])
Another solution is to use the :include => [:gender, :color] it may be some queries more but it's the cleaner "rails way". If you run into performance issues you may want to rethink your DB Schema but do you have really such a heavy load?
A friend of mine wrote a nice little solution for this simple 1:n relations (like genders) it's called simple_enum
After working with sled's suggestions, I finally came up with this solution. I'm sure it could be made cleaner with a plugin, but here's what I've got for now:
class Profile < ActiveRecord::Base
ALL_ATTRIBUTES = [:photo, :city, :gender, :relationship_status, :physique, :children,
:diet, :drink, :smoke, :drug, :education, :income, :job, :politic, :religion, :zodiac]
scope :with_attributes,
includes(ALL_ATTRIBUTES).
select((ALL_ATTRIBUTES.collect { |a| "`#{reflect_on_association(a).table_name}`.*" } + ["`#{table_name}`.*"]).join(', '))
end
The two main points are:
A call to includes, which passes the symbols of the relationships I want
A call to select that makes sure to retrieve all columns for the related tables. Note that I call reflect_on_association so that I don't have to hard-code the related tables' names, letting the Rails models do the work for me.
I can now call:
Profile.with_attributes.where(:username => params[:id]).limit(1).first
Going to mark sled's answer as correct since it's his help (answers + comments combined) that led me here, even though this is the code I'm ultimately using.

ActiveRecord find with association details

Suppose I am doing matchmaking of users and games.
I have models containing users and games.
class Game < ActiveRecord::Base
has_and_belongs_to_many :users
class User < ActiveRecord::Base
has_and_belongs_to_many :games
Games can have many users, users can be playing many games. Because of HASBM I have a table called games_users too.
I want to search and find games that are waiting for players, which do not also contain the username of the player (i.e. I don't want to add the same player to a game twice...)
I want something like this:
#game = Game.find_by_status(Status::WAITING_USERS, :condition => "game.users.doesnt_contain('username=player')
But I'm not sure how to do it?
Update:
Using jdl's solution, I got the code to run, but get items that I tried to exclude returned in the results. Here's my test code:
logger.debug "Excluding user: #{#user.id}"
games = Game.excluding_user(#user)
if (games != nil && games.count > 0)
#game = Game.find(games[0].id)
games[0].users.each {
|u|
logger.debug "returned game user: #{u.id}"
}
end
(the above code also begs 2 questions.... - how do I get a result of just one game instead of an array, and how to I get a non-readonly version of it; that's why I do the second Game.find...)
And here's the output in the log:
Excluding user: 2
Game Load (0.3ms) SELECT `games`.* FROM `games` left outer join games_users gu on gu.game_id = games.id WHERE (gu.game_id is null or gu.user_id != 2)
Game Columns (1.0ms) SHOW FIELDS FROM `games`
SQL (0.2ms) SELECT count(*) AS count_all FROM `games` left outer join games_users gu on gu.game_id = games.id WHERE (gu.game_id is null or gu.user_id != 2)
Game Load (0.1ms) SELECT * FROM `games` WHERE (`games`.`id` = 3)
games_users Columns (6.8ms) SHOW FIELDS FROM `games_users`
User Load (0.9ms) SELECT * FROM `users` INNER JOIN `games_users` ON `users`.id = `games_users`.user_id WHERE (`games_users`.game_id = 3 )
returned game user: 1
returned game user: 2
It might be easier in a two step process.
Step 1 get the list of games the user is involved in:
games_playing = user.games.for_status('playing')
Step 2 get a list of open games for the player:
open_games = Game.for_status('waiting').not_including(games_playing)
Where you have an additional named scope in the Game class:
named_scope :not_including, lambda {|g| { :conditions => ["id not in (?) ", g] }}
Named scopes are your friend here.
For example:
class Game < ActiveRecord::Base
has_and_belongs_to_many :users
named_scope :for_status, lambda {|s| {:conditions => {:status => s}}}
named_scope :excluding_user, lambda {|u| {:conditions => ["gu.game_id is null or gu.game_id not in (select game_id from games_users where user_id = ?) ", u.id], :joins => "left outer join games_users gu on gu.game_id = games.id", :group => "games.id" }}
end
This will let you do things like the following:
user = User.first # Or whoever.
games_in_progress = Game.for_status("playing")
games_in_progress_for_others = Game.excluding_user(user).for_status("playing")
# etc...
Also, since you say that you're new to Rails, you might not realize that these named scopes will also work when you're traversing associations. For example:
user = User.first
users_games_in_waiting = user.games.for_status("waiting")
You could execute your query the way you want and use the slice method to remove the user from the results (but this is not very good performance wise if the user is added to a lot of games already)
sort of like this
a = [ "a", "b", "c" ]
a.slice!(1) #=> "b"
a #=> ["a", "c"]
Or write a custom sql query (using find_by_sql and use != user_id to exclude that from the query.
I am not sure if there is a "pure" Rails way to do it in the find itself without using a custom query.
edit:
You could do something like this, pretty "Railsy",
#game = Game.find_by_status(Status::WAITING_USERS, :conditions => ["id NOT IN #{user_id}"])
Or for multiple users, an array
#game = Game.find_by_status(Status::WAITING_USERS, :conditions => ["id NOT IN (?)", [1,2,3]])
Let me know if that works out for you :-)

Resources