简体   繁体   中英

Rails Postgres query to exclude any results that contain one of three records on join

This is a hard problem to describe but I have Rails query where I join another table and I want to exclude any results where the join table contain one of three conditions.

I have a Device model that relates to a CarUserRole model/record. In that CarUserRole record it will contain one of three :role - "owner", "monitor", "driver". I want to return any results where there is no related CarUserRole record where role: "owner" . How would I do that?

This was my first attempt -

Device.joins(:car_user_roles).where('car_user_roles.role = ? OR car_user_roles.role = ? AND car_user_roles.role != ?', 'monitor', 'driver', 'owner') 

Here is the sql -

"SELECT \"cars\".* FROM \"cars\" INNER JOIN \"car_user_roles\" ON \"car_user_roles\".\"car_id\" = \"cars\".\"id\" WHERE (car_user_roles.role = 'monitor' OR car_user_roles.role = 'driver' AND car_user_roles.role != 'owner')"

Update

I should mention that a device sometimes has multiple CarUserRole records. A device can have an "owner" and a "driver" CarUserRole. I should also note that they can only have one owner.

Anwser

I ended up going with @Reub's solution via our chat -

where(CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not)

Since the car_user_roles table can have multiple records with the same car_id, an inner join can result in the join table having multiple rows for each row in the cars table. So, for a car that has 3 records in the car_user_roles table (monitor, owner and driver), there will be 3 records in the join table (each record having a different role). Your query will filter out the row where the role is owner, but it will match the other two, resulting in that car being returned as a result of your query even though it has a record with role as 'owner'.

Lets first try to form an sql query for the result that you want. We can then convert this into a Rails query.

SELECT * FROM cars WHERE NOT EXISTS (SELECT id FROM car_user_roles WHERE role='owner' AND car_id = cars.id);

The above is sufficient if you want devices which do not have any car_user_role with role as 'owner'. But this can also give you devices which have no corresponding record in car_user_roles. If you want to ensure that the device has at least one record in car_user_roles, you can add the following to the above query.

AND EXISTS (SELECT id FROM car_user_roles WHERE role IN ('monitor', 'driver') AND car_id = cars.id);

Now, we need to convert this into a Rails query.

Device.where(
     CarUserRole.where("car_user_roles.car_id = cars.id").where(role: 'owner').exists.not
).where(
     CarUserRole.where("car_user_roles.car_id = cars.id").where(role: ['monitor', 'driver']).exists
).all

You could also try the following if your Rails version supports exists? :

Device.joins(:car_user_roles).exists?(role: ['monitor', 'driver']).exists?(role: 'owner').not.select('cars.*').distinct
  • Select the distinct cars

     SELECT DISTINCT (cars.*) FROM cars 
  • Use a LEFT JOIN to pull in the car_user_roles

     LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id 
  • Select only the cars that DO NOT contain an 'owner' car_user_role

     WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner') 
  • Select only the cars that DO contain either a 'driver' or 'monitor' car_user_role

     AND (car_user_roles.role IN ('driver','monitor')) 

Put it all together:

SELECT DISTINCT (cars.*) FROM cars LEFT JOIN car_user_roles ON cars.id = car_user_roles.car_id WHERE NOT EXISTS(SELECT NULL FROM car_user_roles WHERE cars.id = car_user_roles.car_id AND car_user_roles.role = 'owner') AND (car_user_roles.role IN ('driver','monitor'));

Edit:

  • Execute the query directly from Rails and return only the found object IDs

     ActiveRecord::Base.connection.execute(sql).collect { |x| x['id'] } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM