How to left-join the first previous/next instance of a has_many reference based on a parameter

48 views Asked by At

Running Rails 7 on Ruby 3.2 with PostgreSQL 15.1.

First, I'd like to give an abstract example:

Cats:

id name
1 Ricewind
2 Tiffany

Toys:

ID cat_id greatness
1 1 1
2 1 5
3 2 2

The problem I want to solve is:

Based on an not-saved-yet toy and its greatness, I want to get a list of all cats and their toys which are just below or greater than the one I want to create.

If given a new toy with greatness=4, the result should be:

cat_id toy_prev.greatness toy_next.greatness
1 1 5
2 2 NULL

Requirements:

  • Both toys should be "left-joined" as they do not need to be existing.
  • Only one row per cat
  • I would need full access to the models, including the cat and both toys, not only the greatness attribute.

The pseudo-code I like to use later would be sth like:

Cats.with_toys_by(4).each do |cat|
  [cat.id, cat.toy_prev.greatness, cat.toy_next.greatness]
end

Things, I've tried so far:

  1. The most simple solution would be to just iterate over Cat.all and within this loop, call @cat.toys.where("greatness < ?", 4).order(greatness: :desc).first() to get the left side and @cat.toys.where("greatness > ?", 4).order(greatness: :asc).first() to get the right side. this is very unperformant as this triggers N+2 queries.
  2. Using has_one with a where() condition did not work as this cannot be used directly in the main query's joins.

Things, I'm thinking about:

If I would write it in plain SQL, I would left-join the Toy table two times in both directions:

SELECT cats.*, t1.* FROM cats
  -- makes sure the requirement matches
  LEFT JOIN toys t1 ON (cats.id = t1.cat_id AND t1.greatness < 4)
  -- filter out ones which would have toys with a greater greatness, a.k.a. get the first one
  LEFT JOIN toys t2 ON (cats.id = t1.cat_id AND t1.greatness < t2.greatness)
  -- same for the other side
  LEFT JOIN toys t3 ON (cats.id = t3.cat_id AND t3.greatness > 4)
  LEFT JOIN toys t4 ON (cats.id = t4.cat_id AND t3.greatness > t4.greatness)
WHERE
  t2.id IS NULL AND t4.id IS NULL

t1 would be toy_prev, the one with the lower greatness - and t3 would be toy_next.

Dispite using Arel would be an option, I did not used it yet and I don't really like the idea for the following reasons:

  • Arel is a private API and is discouraged to be used publicly.
  • It seems to give a hard time when trying to map between the hashes returned and the model-instances. Further filtering or using Arel and the ORM at the same time, like Cats.where(..).with_toys_by(4).limit(5), seems not to be supported, at least if Arel is not only used for a where-clause.

Is there a "clean" solution to my problem using the offical ORM directly? Or do I need to fall back to Arel?

0

There are 0 answers