How do I get all the users who do not have a car?

class User < ActiveRecord::Base
  has_one :car
end

class Car < ActiveRecord::Base
  belongs_to :user
end

I was doing the following:

all.select {|user| not user.car }

That worked perfect until my database of users and cars got too big and now I get strange errors, especially when I try and sort the result. I need to do the filtering in the query and the ordering as well as part of the query.

UPDATE: What I did was the following:

where('id not in (?)', Car.pluck(:user_id)).order('first_name, last_name, middle_name')

It's fairly slow as Rails has to grab all the user_ids from the cars table and then issue a giant query. I know I can do a sub-query in SQL, but there must be a better Rails/ActiveRecord way.

UPDATE 2: I now have a noticeably more efficient query:

includes(:car).where(cars: {id: nil})

The answer I accepted below has joins with a SQL string instead of includes. I don't know if includes is more inefficient because it stores the nil data in Ruby objects whereas joins might not? I like not using strings...

有帮助吗?

解决方案

One way is to use a left join from the users table to the cars table and only take user entries that don't have any corresponding values in the cars table, this looks like:

User.select('users.*').joins('LEFT JOIN cars ON users.id = cars.user_id').where('cars.id IS NULL')

其他提示

Most of the work that needs to be done here is SQL. Try this:

User.joins("LEFT OUTER JOIN cars ON users.id = cars.user_id").where("cars.id IS NULL")

It is incredibly inefficient to do this with ruby, as you appear to be trying to do.

You can throw an order on there too:

User.
  joins("LEFT OUTER JOIN cars ON users.id = cars.user_id").
  where("cars.id IS NULL").
  order(:first_name, :last_name, :middle_name)

You can make this a scope on your User model so you only have one place to deal with it:

class User < ActiveRecord::Base
  has_one :car

  def self.without_cars
    joins("LEFT OUTER JOIN cars ON users.id = cars.user_id").
    where("cars.id IS NULL").
    order(:first_name, :last_name, :middle_name)
  end
end

This way you can do:

User.without_cars

In your controller or another method, or even chain the scope:

User.without_cars.where("users.birthday > ?", 18.years.ago)

to find users without cars that are under 18 years old (arbitrary example, but you get the idea). My point is, this kind of thing should always be made into a scope, so it can be chained with other scopes :) Arel is awesome that way.

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top