Rails find Parent that has two Children with certain attribute - ruby-on-rails

I have over 100,000 objects in my database for different Product. Each Product has 4-6 Variants. Because of this, it is not easy to lazily edit large amount of data by iterating through everything. Because of this, I am trying to get only the exact number of Products I need.
So far, I can get all the Products that have a Variant with the size attribute 'SM'.
The hang up, is getting all the Products that have both a Variant with size 'MD' and 'SM'.
This is the code I am using Product.joins(:variants).where('variants.size = ?', 'SM')
I have tried adding .where('variants.size = ?', 'MD') to it, but that does work.

How about this
Product.where(
id: Variant.select(:product_id)
.where(size: 'SM')
).where(id: Variant.select(:product_id)
.where(size: 'MD')
)
This should generate something akin to
SELECT products.*
FROM products
WHERE products.id IN (SELECT
variants.product_id
FROM variants
WHERE size = 'SM')
AND products.id IN (SELECT
variants.product_id
FROM variants
WHERE size = 'MD')
so the product id must be in both lists to be selected.
Additionally This should also work (Not 100% certain)
Product.where(id: Product.joins(:variants)
.where(variants: {size: ['SM', 'MD']})
.group(:id)
.having('COUNT(*) = 2').select(:id)
Which should generate something like
SELECT products.*
FROM products
WHERE
products.id IN ( SELECT products.id
FROM products
INNER JOIN variants
ON variants.product_id = products.id
WHERE
variants.size IN ('SM','MD')
GROUP BY
products.id
HAVING
Count(*) = 2
One more option
p_table = Products.arel_table
v_table = Variant.arel_table
sm_table = p_table.join(v_table)
.on(v_table[:product_id].eq(p_table.[:id])
.and(v_table[:size].eq('SM'))
)
md_table = p_table.join(v_table)
.on(v_table[:product_id].eq(p_table.[:id])
.and(v_table[:size].eq('MD'))
)
Product.joins(sm_table.join_sources).joins(md_table.join_sources)
SQL
SELECT products.*
FROM products
INNER JOIN variants on variants.product_id = products.id
AND variants.size = 'SM'
INNER JOIN variants on variants.product_id = products.id
AND variants.size = 'MD'
These 2 joins should enforce the small and medium because of the INNER JOIN

IMHO you need to use a bit more SQL instead of Rails magic to build database queries like that.
Product
.joins('INNER JOIN variants as sm_vs ON sm_vs.product_id = products.id')
.joins('INNER JOIN variants as md_vs ON md_vs.product_id = products.id')
.where(sm_vs: { size: 'SM' })
.where(md_vs: { size: 'MD' })
Or simplified - as #engineersmnky suggested:
Product
.joins("INNER JOIN variants as sm_vs ON sm_vs.product_id = products.id AND sm_vs.size = 'SM'")
.joins("INNER JOIN variants as md_vs ON md_vs.product_id = products.id AND sm_vs.size = 'MD'")
Both queries do basically the same. Just choose the version you like better.

Product.joins(:variants).where('variants.size = ? OR variants.size = ?', 'SM','MD')

Related

Ruby on Rails - Has Many, Through: find multiple conditions

I realised its quite difficult to explain my problem with only words, so i'm going to use an example to describe what i am trying to do instead.
So for example:
#model Book
has_many: book_genres
has_many: genres, through: :book_genres
#model Genre
has_many: book_genres
has_many: books, through: :book_genres
So finding books that belong to one genre only would be relatively straightforward, such as:
#method in books model
def self.find_books(genre)
#g = Genre.where('name LIKE ?' , "#{genre}").take
#b = #g.books
#get all the books that are of that genre
end
So in rails console i can do Book.find_books("Fiction") and then i would get all the books that are of fiction genre.
But how can i find all the books that are both "Young Adult" and "Fiction" ? Or what if i would like to query for books that have 3 genres, such as "Young Adult", "Fiction" and "Romance" ?
I could do g = Genre.where(name: ["Young Adult", "Fiction", "Romance"]) but subsequent to that i cannot do g.books and get all the books that are related to this 3 genres.
I am actually quite bad with active record so im not even sure if theres a better way to query through Books directly instead of finding Genre then finding all books that are associated with it.
But what i cannot wrap my head around is how do i get all the books that have multiple (specific)genres?
UPDATE:
So the current answers provided Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"]) works, but the problem is it returns all books that has the genre of Young Adult OR Fiction OR Romance.
What query do i pass so that the books return has ALL 3 Genres and not only 1 or 2 out of the 3?
Matching any of the given genres
The following should work for both an Array and a String:
Book.joins(:genres).where("genres.name" => ["Young Adult", "Fiction", "Romance"])
Book.joins(:genres).where("genres.name" => "Young Adult")
In general, it's better to pass a Hash to where, rather than trying to write a SQL snippet yourself.
See the Rails Guides for more details:
http://guides.rubyonrails.org/active_record_querying.html#hash-conditions
http://guides.rubyonrails.org/active_record_querying.html#specifying-conditions-on-the-joined-tables
Matching all of the given genres with one query
A single query could be built and then passed to .find_by_query:
def self.in_genres(genres)
sql = genres.
map { |name| Book.joins(:genres).where("genres.name" => name) }.
map { |relation| "(#{relation.to_sql})" }.
join(" INTERSECT ")
find_by_sql(sql)
end
This means that calling Book.in_genres(["Young Adult", "Fiction", "Romance"]) will run a query that looks something like this:
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Young Adult')
INTERSECT
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Fiction')
INTERSECT
(SELECT books.* FROM books INNER JOIN … WHERE genres.name = 'Romance');
It has the upside of letting the database do the heavy lifting of combining the result sets.
The downside is that we're using raw SQL, so we can't chain this with other ActiveRecord methods, for example Books.order(:title).in_genres(["Young Adult", "Fiction"]) will ignore the ORDER BY clause we've tried to add.
We're also manipulating SQL queries as strings. It's possible we could avoid this using Arel, but the way Rails and Arel handle binding query values makes this pretty complicated.
Matching all of the given genres with multiple query
It's also possible to use multiple queries:
def self.in_genres(genres)
ids = genres.
map { |name| Book.joins(:genres).where("genres.name" => name) }.
map { |relation| relation.pluck(:id).to_set }.
inject(:intersection).to_a
where(id: ids)
end
This means that calling Book.in_genres(["Young Adult", "Fiction", "Romance"]) will run four queries that look something like this:
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Young Adult';
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Fiction';
SELECT id FROM books INNER JOIN … WHERE genres.name = 'Romance';
SELECT * FROM books WHERE id IN (1, 3, …);
The downside here is that for N genres, we're making N+1 queries. The upside is that this can be combined with other ActiveRecord methods; Books.order(:title).in_genres(["Young Adult", "Fiction"]) will do our genre filtering, and sort by title.
I didn't try this but I think it will work
Book.joins(:genres).where("genres.name IN (?)", ["Young Adult", "Fiction", "Romance"])
Here is how I would do it in SQL:
SELECT *
FROM books
WHERE id IN (
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Young Adult'
INTERSECT
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Fiction'
INTERSECT
...
)
The inner query will contain only books belonging to all the genres you ask about.
Here is how I'd do it in ActiveRecord:
# book.rb
def self.in_genres(genre_names)
subquery = genre_names.map{|n|
<<-EOQ
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE ?
EOQ
}.join("\nINTERSECT\n")
where(<<-EOQ, *genre_names)
id IN (
#{subquery}
)
EOQ
end
Note that I am using ? to avoid sql injection vulnerabilities, which is a problem in the code you proposed in your question.
Another approach would be to use multiple EXISTS conditions with correlated sub-queries:
SELECT *
FROM books
WHERE EXISTS (SELECT 1
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Young Adult'
AND bg.book_id = books.id)
AND EXISTS (SELECT 1
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE g.name LIKE 'Fiction'
AND bg.book_id = books.id)
AND ...
You'd construct this query in ActiveRecord similarly to the first approach. I'm not sure which would be faster, so you could try both if you like.
Here is yet another way to do the SQL---possibly fastest:
SELECT *
FROM books
WHERE id IN (
SELECT bg.book_id
FROM book_genres bg
INNER JOIN genres g
ON g.id = bg.genre_id
WHERE (g.name LIKE 'Young Adult' OR g.name LIKE 'Fiction' OR ...)
GROUP BY bg.book_id
HAVING COUNT(DISTINCT bg.genre_id) >= 2 -- or 3, or whatever
)

SQL server join query to get all subjects

I have the three tables:
TBL_SUBJECT, TBL_SEMESTER and TBL_SUBJECT_SEMESTER_MAPPING
I am having subjectId with me say '1', I want to get All the subjects of the semester to which my subject belongs. i.e subject having Id '1'.
How is the query with joins in SQL server.
Your question is not as clear as it could be. Please post schemas to get a better answer.
Answers will be something like this:
SELECT
SEMESTER_NAME
FROM
TBL_SEMESTER
INNER JOIN
TBL_SUBJECT_SEMESTER_MAPPING ON TBL_SUBJECT_SEMESTER_MAPPING.SUBJECTID = TBL_SEMESTER.SUBJECTID
INNER JOIN
TBL_SUBJECT TBL ON TBL_SUBJECT.SUBJECTID = TBL_SUBJECT_SEMESTER_MAPPING.SUBJECTID
WHERE
SUBJECTNAME LIKE YOURSUBJECT
I have done it like this
SELECT * from tbl_subject S
INNER JOIN tbl_subject_semester_mapping SSP ON SSP.subId = S.subId
INNER JOIN tbl_semester SEM ON SEM.semId = SSP.semId
WHERE SEM.semId = (select semId from tbl_subject_semester_mapping TSSM where TSSM.subId = 1 )

Rails 4, Active record strict search by arrays in associations

Given these models:
class Farm < ActiveRecord::Base
has_and_belongs_to_many :animals
end
Class Animal < ActiveRecord::Base
has_and_belongs_to_many :farms
end
I need to search for farms that have ducks, pigs and cows but none of cats and dogs.
This kind of query doesn't work:
Animal.joins(:farms)
.where('animals.name IN ? AND animals.name NOT IN ?',
good_animal_names, bad_animal_names)
As it searches farms with ANY of the animals. I need to search farms that have ALL of the desired ones and none of the others.
I also tried with SQL with something like this:
SELECT farms.id, farms.name
FROM farms
INNER JOIN animals_farms ON animals_farms.farm_id = farms.id
INNER JOIN animals ON animals_farms.animal_id = animals.id
WHERE animals.name IN (['ducks', 'pigs', 'cows'])
AND animals.name NOT IN (['dogs', 'cats'])
GROUP BY farms.id, farms.name
HAVING COUNT(unique(animals.name)) = 3
But I'm not sure if animals.name NOT IN will really exclude farms that have none of the animals or only farms that don't have one of them. The real database is very difficult to verify the results.
Also, it should be great to be able to do the query in Active Record or Arel but any recommendation in SQL is more than welcome.
The database is Oracle, I don't have much experience with Oracle but most of the queries I use with PostgreSQL are working here.
Assuming that you have only two categories of animals -(good/bad), and you need only good animals. Can you try this?
Farm.joins(:animals).where.not(animals: {name: bad_animal_names})
Hope it helps!
Well, I think I have a possible solution in SQL. Please, comment if you can see any issue or know a better way to do it.
WITH included_animals as (SELECT farms.id, farms.name
FROM farms
INNER JOIN animals_farms ON animals_farms.farm_id = farms.id
INNER JOIN animals ON animals_farms.animal_id = animals.id
WHERE animals.name IN ('ducks', 'pigs', 'cows')
GROUP BY farms.id, farms.name
HAVING COUNT(unique(animals.name)) = 3),
not_included_animals as (SELECT farms.id, farms.name
FROM farms
INNER JOIN animals_farms ON animals_farms.farm_id = farms.id
INNER JOIN animals ON animals_farms.animal_id = animals.id
WHERE animals.name IN ('dogs', 'cats')
GROUP BY farms.id, farms.name)
SELECT id, name
FROM included_animals
MINUS --use EXCEPT if riding PostgreSQL
SELECT id, name
FROM not_included_animals

How to add aggregated columns from SQL SELECT to AR object in Rails

Suppose we have a model Order and Item where order has_many items.
I'd now like to get the sum of all items for each order.
#orders = Order.select("orders.*, sum(items.amount) as items_sum)
.joins("LEFT JOIN orders ON items.order_id = orders.id")
.group("orders.id")
produces
SELECT sum(items.amount) as items_sum, transfers.* FROM "orders"
LEFT JOIN items ON items.order_id = orders.id GROUP BY orders.id
The SQL itself works fine. The problem is that #orders doesn't include the items_sum. I'd like to be able to do #orders[0].items_sum. How is that possible?
you should be able to access it as
#orders.first.items_sum
or
#orders.first["items_sum"]
Documentation here

Left outer join query (i think)

I have two tables that look like this:
Products: id category name description active
Sales_sheets: id product_id link
product_id is a foreign key from the products id table
I wrote a prepared statement JOIN like this which works:
SELECT p.name, p.description, s.link FROM products AS p
INNER JOIN sales_sheets AS s ON p.id = s.product_id WHERE active=1 AND category=?
Basically a product can have a link to a PDF, but not every product will have a sales sheet. So if i try to bring up a product which doesn't have a sales sheet attached to it then it always returns no rows.
So i thought I'd have to use a LEFT OUTER JOIN in place of the INNER JOIN, but that returns no rows too, am I naming the tables in the wrong order? I've never had to use an OUTER join before?
SELECT p.name, p.description, s.link FROM products p
LEFT JOIN sales_sheets s ON p.id = s.product_id
WHERE active = 1 && category = ?

Resources