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
greatnessattribute.
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:
- The most simple solution would be to just iterate over
Cat.alland 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. - Using
has_onewith awhere()condition did not work as this cannot be used directly in the main query'sjoins.
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:
Arelis 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
Areland the ORM at the same time, likeCats.where(..).with_toys_by(4).limit(5), seems not to be supported, at least ifArelis not only used for awhere-clause.
Is there a "clean" solution to my problem using the offical ORM directly?
Or do I need to fall back to Arel?