customize sql for has_many relation - ruby-on-rails

I have two Tables:
Locations, which is self-refential:
int id,
string name,
int location_id
and Nodes:
int id,
string name,
int location_id
The Relations are:
class Node
belongs_to :location
end
class Location
has_many :nodes
end
That works, but i want not only the direct associated Nodes for an Location but also that Nodes, wich are associated to any Child of the Location. I have an Select Statement with some CTE, which archives exactly this:
with sublocations (name, id, lvl) as
(
select
l.name,
l.id,
1 as lvl
from locations l
where l.id = 10003
union all
select
sl.name,
sl.id,
lvl + 1 as lvl
from sublocations inner join locations sl
on (sublocations.id = sl.location_id)
)
select
sl.name as location,
sl.id as location_code,
n.name
from sublocations sl join nodes n on n.LOCATION_ID = sl.ID;
But how can i bring this in the has_many Relation?
Thanks, Jan

Related

How to join with multiple instances of a model when using a join table with active record?

This is for Rails 5 / Active Record 5 using PostgreSQL.
Let's say I have two models: Product and Widget. A Product has_many widgets and a Widget has_many products via a join table called products_widgets.
I want to write a query that finds all products that are associated with both the widget with id=37 and the widget with id=42.
I actually have a list of ids, but if I can write the above query, I can solve this problem in general.
Note that the easier version of this query is to find all widgets that are associated with either the widget with id=37 or the widget with id=42, which you could write as follows:
Product.joins(:products_widgets).where(products_widgets: {widget_id: [37, 42]})
But that isn't what I need.
As a starter: in pure SQL, you could phrase the query with exists conditions:
select p.*
from product p
where
exists (
select 1
from products_widgets pw
where pw.product_id = p.product_id and pw.widget_id = 37
)
and exists (
select 1
from products_widgets pw
where pw.product_id = p.product_id and pw.widget_id = 42
)
In active Record, we can try and use the raw subqueries directly in where conditions:
product
.where('exists(select 1 from products_widgets where product_id = product.product_id and widget_id = ?)', 37)
.where('exists(select 1 from products_widgets where product_id = product.product_id and widget_id = ?)', 42)
I think that using .arel.exist might also work:
product
.where(products_widgets.where('product_id = product.product_id and widget_id = ?', 37).arel.exists)
.where(products_widgets.where('product_id = product.product_id and widget_id = ?', 42).arel.exists)

How to I find relations with the exact same set of has_many relationships using ActiveRecord in Ruby on Rails?

I have the following Active Record Models:
class House < ActiveRecord::Base
has_many :features
end
class Feature < ActiveRecord::Base
has_many :houses
end
How can I write a query that returns all houses that have the exact same set of many-to-many relations? So If a house has features 1,2,3, I would like exactly them while excluding:
houses that only have a subset of the features (example, 1 & 2) houses that have that set but include others (so if they have features 1,2,3 & 4 they should be excluded as well).
How can I accomplish this?
If you are starting with a given House or given set of Features then:
The following should work
Find the Houses that meet the following criteria:
the Houses are not in the list of houses that have features other than the features provided
the Houses total count of features is equal to the number of features provided
features = [1,2,3]
House.where(
id: House.select(:id)
.joins(:features)
.where.not(
id: House.select(:id)
.joins(:features)
.where.not(features: {id: features})
)
.group(:id)
.having(Feature.arel_attribute(:id).count.eq(features.size))
)
SQL akin to: (assuming your join table is named houses_features)
SELECT
houses.*
FROM
houses
WHERE
houses.id IN (
SELECT
houses.id
FROM
houses
INNER JOIN houses_features ON houses.id = houses_features.house_id
INNER JOIN features ON features.id = houses_features.feature_id
WHERE
houses.id NOT IN (
SELECT
houses.id
FROM
houses
INNER JOIN houses_features ON houses.id = houses_features.house_id
INNER JOIN features ON features.id = houses_features.feature_id
WHERE
features.id NOT IN (1,2,3)
)
GROUP BY
houses.id
HAVING
COUNT(features.id) = 3
)

Select records all of whose records exist in another join table

In the following book club example with associations:
class User
has_and_belongs_to_many :clubs
has_and_belongs_to_many :books
end
class Club
has_and_belongs_to_many :users
has_and_belongs_to_many :books
end
class Book
has_and_belongs_to_many :users
has_and_belongs_to_many :clubs
end
given a specific club record:
club = Club.find(params[:id])
how can I find all the users in the club who have all books in array of books?
club.users.where_has_all_books(books)
In PostgreSQL it can be done with a single query. (Maybe in MySQL too, I'm just not sure.)
So, some basic assumptions first. 3 tables: clubs, users and books, every table has id as a primary key. 3 join tables, books_clubs, books_users, clubs_users, each table contains pairs of ids (for books_clubs it will be [book_id, club_id]), and those pairs are unique within that table. Quite reasonable conditions IMO.
Building a query:
First, let's get ids of books from given club:
SELECT book_id
FROM books_clubs
WHERE club_id = 1
ORDER BY book_id
Then get users from given club, and group them by user.id:
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = 1
GROUP BY CU.user_id
Join these two queries by adding having to 2nd query:
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(##1##)
where ##1## is the 1st query.
What's going on here: Function array_agg from the left part creates a sorted list (of array type) of book_ids. These are books of user. ARRAY(##1##) from the right part returns the sorted list of books of the club. And operator #> checks if 1st array contains all elements of the 2nd (ie if user has all books of the club).
Since 1st query needs to be performed only once, it can be moved to WITH clause.
Your complete query:
WITH club_book_ids AS (
SELECT book_id
FROM books_clubs
WHERE club_id = :club_id
ORDER BY book_id
)
SELECT CU.user_id
FROM clubs_users CU
JOIN users U ON U.id = CU.user_id
JOIN books_users BU ON BU.user_id = CU.user_id
WHERE CU.club_id = :club_id
GROUP BY CU.user_id
HAVING array_agg(BU.book_id ORDER BY BU.book_id) #> ARRAY(SELECT * FROM club_book_ids);
It can be verified in this sandbox: https://www.db-fiddle.com/f/cdPtRfT2uSGp4DSDywST92/5
Wrap it to find_by_sql and that's it.
Some notes:
ordering by book_id is not necessary; #> operator works with unordered arrays too. I just have a suspicion that comparison of ordered array is faster.
JOIN users U ON U.id = CU.user_id in 2nd query is only necessary for fetching user properties; in case of fetching user ids only it can be removed
It appears to work by grouping and counting.
club.users.joins(:books).where(books: { id: club.books.pluck(:id) }).group('users.id').having('count(*) = ?', club.books.count)
If anyone knows how to run the query without intermediate queries that would be great and I will accept the answer.
This looks like a situation where you'd make two queries, one to get all the ids you need, the other select perform a WHERE IN.

Respect negative conditions for advanced collection associations

I was trying to use this functionality introduced in #645 with conditional 2nd degree has_many ... through relationships with little success.
In my case:
a Course has_many :user_assigned_content_skills, -> { where(source: 'user') }, class_name: "ContentSkill"
and a ContentSkill belongs_to :skill and belongs_to :course
Then Course.ransack({user_assigned_content_skills_skill_name_not_cont: 'ruby'}).result.to_sql returns the following:
"SELECT courses.* FROM courses LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user' LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id WHERE (skills.name NOT ILIKE '%ruby%')"
This means false positives again if a course has multiple content_skills. Any ideas how to retrieve all courses not being associated with a given skill name?
Many thanks for any insights!
You can get ids of courses associated with a given skill name, and then get a list of courses with ids that don't match the previous found. You can even make it as one composite SQL query.
Course.where.not(id: Course.ransack({user_assigned_content_skills_skill_name_cont: 'ruby'}).result)
This will generate an SQL like this:
SELECT courses.*
FROM courses
WHERE courses.id NOT IN (
SELECT courses.id FROM courses
LEFT OUTER JOIN content_skills ON content_skills.course_id = courses.id AND content_skills.source = 'user'
LEFT OUTER JOIN skills ON skills.id = content_skills.skill_id
WHERE (skills.name ILIKE '%ruby%')
)

Get all records through a join with an extra condition

"Neighborhood" belongs to "city". "City" has many "neighborhoods".
How can I select all neighborhoods that belong to a particular city by the city's name? City name is an attribute stored in the city table.
Neighborhood.joins(:city) will get me all neighborhoods, using this SQL:
SELECT "neighborhoods".*
FROM "neighborhoods"
INNER JOIN "cities"
ON "cities"."id" = "neighborhoods"."city_id"
But what is the Rails way to execute a query with this SQL (only difference is the last line)?
SELECT "neighborhoods".*
FROM "neighborhoods"
INNER JOIN "cities"
ON "cities"."id" = "neighborhoods"."city_id"
AND "cities"."name" = "New York"
The 'rails' way of doing this would be to use active record associations so, in your models
class City < ActiveRecord::Base
has_many :neighborhoods
....
end
class Neighborhood < ActiveRecord::Base
belongs_to :city
...
end
Then you can call:
neighborhoods = City.find_by(name: "New York").neighborhoods
Just apply a where condition using cities.name:
Neighborhood.joins(:city).where("cities.name = ?", "New York")
If you use Rails model, not exactly sql, but same result could be:
city = City.where(:name => "New York").first
city.neighborhoods
You should try this code-
City.joins(:neighborhoods).select("neighborhoods.*").where("cities.name = 'New York'")
Explaination-
It will be 'City.joins(:neighbourhoods)' because City has_many neightbours and 'city_id' will work as foreign key for neighbourhood model.
select("neighborhoods.*") will select all columns from neighbourhoods table.
where("cities.name = 'New York'") is your codition to get all records having 'city name' equal to 'New York'.

Resources