How to write Rails finder with several subqueries - ruby-on-rails

This is a library system, people can borrow books here. And each book belongs to a category. We'd like to give people some suggestions according to what kind of books they borrowed most.
Here are four models:
class Person < AR
has_many :borrows
end
class Borrow < AR
belongs_to :person
belongs_to :book
end
class Category < AR
has_many :books
end
class Book < AR
has_many :borrows
belongs_to :category
end
And I wrote SQL to find the books
SELECT * FROM books WHERE category_id =
(SELECT category_id FROM books WHERE id IN
(SELECT book_id FROM borrows WHERE person_id =10000)
GROUP BY category_id ORDER BY count(*) DESC LIMIT 1)
AND id NOT IN
(SELECT book_id FROM borrows WHERE person_id =10000)
This seems to be working, but I wonder how could I write the finder in the Rails way...

You can do following things, write following in person.rb
has_many :books, :through => :borrows
has_many :categories_of_books, :through => :books, :source => :category
&
def suggested_books
Book.where("category_id IN (?) AND id NOT IN (?)", self.categories_of_books, self.books)
end
Though it results in more than 1 query, but its clean, you just have to do:
#user.suggested_books

With active record, you can eliminate two of the three subqueries in favor of joins:
Book.where(
category_id: Category.limit(1)
.joins(:books => :borrows)
.where("borrows.person_id = ?", 10000)
.group("categories.id")
.order("COUNT(*) DESC")
.pluck("categories.id")
).joins(:borrows).where("borrows.person_id != ?", 10000)
Still not the best solution because it generates two separate queries (one for the inner query on Category). Depending on your needs, this may not be so bad, if, say, you decide to use the result of the inner query (the most borrowed category of the user in question) for something else.

May be something like that :
#person = Person.find(10000)
#categories = #person.books.map{|b| b.category}.uniq!
#suggestions = #categories.map{|c| c.books} - #person.books
In order to have '#person.books' working, you have to add in your Person model :
has_many :books, :through => :borrows

Related

How do I get the records with exact has_many through number of entries on rails

I have a many to many relationship through a has_many through
class Person < ActiveRecord::Base
has_many :rentals
has_many :books, through rentals
end
class Rentals < ActiveRecord::Base
belongs_to :book
belongs_to :person
end
class Book < ActiveRecord::Base
has_many :rentals
has_many :persons, through rentals
end
How can I get the persons that have only one book?
If the table for Person is called persons, you can build an appropriate SQL query using ActiveRecord's query DSL:
people_with_book_ids = Person.joins(:books)
.select('persons.id')
.group('persons.id')
.having('COUNT(books.id) = 1')
Person.where(id: people_with_book_ids)
Although it's two lines of Rails code, ActiveRecord will combine it into a single call to the database. If you run it in a Rails console, you may see a SQL statement that looks something like:
SELECT "persons".* FROM "persons" WHERE "deals"."id" IN
(SELECT persons.id FROM "persons" INNER JOIN "rentals"
ON "rentals"."person_id" = "persons"."id"
INNER JOIN "books" ON "rentals"."book_id" = "books"."id"
GROUP BY persons.id HAVING count(books.id) > 1)
If this is something you want to do often, Rails offers what is called a counter cache:
The :counter_cache option can be used to make finding the number of belonging objects more efficient.
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Effectively this places a new attribute on your Person called books_count that will allow you to quite simply filter by the number of associated books:
Person.where(books_count: 1)

Multiple joins with count and having with ActiveRecord

My application is about Profiles that have many Wishes, that are related to Movies:
class Profile < ApplicationRecord
has_many :wishes, dependent: :destroy
has_many :movies, through: :wishes
end
class Wish < ApplicationRecord
belongs_to :profile
belongs_to :movie
end
class Movie < ApplicationRecord
has_many :wishes, dependent: :destroy
has_many :profiles, through: :wishes
end
I would like to return all the Movies that are all "wished" by profiles with id 1,2, and 3.
I managed to get this query using raw SQL (postgres), but I wanted to learn how to do it with ActiveRecord.
select movies.id
from movies
join wishes on wishes.movie_id = movies.id
join profiles on wishes.profile_id = profiles.id and profiles.id in (1,2,3)
group by movies.id
having count(*) = 3;
(I'm relying on count(*) = 3 because I have an unique index that prevents creation of Wishes with duplicated profile_id-movie_id pairs, but I'm open to better solutions)
At the moment the best approach I've found is this one:
profiles = Profile.find([1,2,3])
Wish.joins(:profile, :movie).where(profile: profiles).group(:movie_id).count.select { |_,v| v == 3 }
(Also I would begin the AR query with Movie.joins, but I didn't manage to find a way :-)
Since belongs_to puts the foreign key in the wishes table, you should be able to just query it for your profiles like so:
Wish.where("profile_id IN (?)", [1,2,3]).includes(:movie).all.map{|w| w.movie}
This should get you an array of all of the movies by those three profiles, eager loading the movies.
Since what I want from the query is a collection of Movies, the ActiveRecord query needs to start from Movie. What I was missing was that we can specify the table in the query, like where(profiles: {id: profiles_ids}).
Here it is the query I was looking for. (Yes, using count might sound a little bit brittle, but the alternative was an expensive SQL subquery. Also, I think it's safe if you're using a multiple-column unique index.)
profiles_ids = [1,2,3]
Movie.joins(:profiles).where(profiles: {id: profiles_ids}).group(:id).having("COUNT(*) = ?", profiles_ids.size)

How to fetch records with exactly specified has_many through records?

I feel like I have read all SO "has_many through" questions but none helped me with my problem.
So I have a standard has_many through setup like this:
class User < ActiveRecord::Base
has_many :product_associations
has_many :products, through: :product_associations
end
class ProductAssociation < ActiveRecord::Base
belongs_to :user
belongs_to :product
end
class Product < ActiveRecord::Base
has_many :product_associations
has_many :users, through: :product_associations
end
IMO, What I want is pretty simple:
Find all users that have a product association to products A, B, and C, no more, no less
So I have a couple of products and want to find all users that are connected to exactly those products (they shouldn't have any other product associations to other products).
This is the best I came up with:
products # the array of products that I want to find all connected users for
User
.joins(:product_associations)
.where(product_associations: { product_id: products.map(&:id) })
.group('products.id')
.having("COUNT(product_associations.id) = #{products.count}")
It doesn't work though, it also returns users connected to more products.
I also toyed around with merging scopes but didn't get any result.
All hints appreciated! :)
select * from users
join product_associations on product_associations.user_id = users.id
where product_associations.product_id in (2,3)
and not exists (
select *
from product_associations AS pa
where pa.user_id = users.id
and pa.product_id not in (2,3)
)
group by product_associations.user_id
having count(product_associations.product_id) = 2
It does two things, find users with: 1) all the product associations and 2) no other product associations.
Sqlfiddle example: http://sqlfiddle.com/#!2/aee8e/5
It can be Railsified™ (somewhat) in to:
User.joins(:product_associations)
.where(product_associations: { product_id: products })
.where("not exists (select *
from product_associations AS pa
where pa.user_id = users.id
and pa.product_id not in (?)
)", products.pluck(:id))
.group('product_associations.user_id')
.having('count(product_associations.product_id) = ?', products.count)

Rails associations NOT EXISTS. Better way? [duplicate]

This question already has answers here:
Want to find records with no associated records in Rails
(9 answers)
Closed 10 months ago.
Using Rails 3.2.9
I'm attempting to get a list of items that are tied to a organization that do NOT have a owner.
I was able to get a array list using the below but just seems ugly to me. Is there a better way to do this?
Items.all(:select => "items.id, items.name",
:joins => "INNER JOIN organizations on items.organization_id = organizations.id",
:conditions => "NOT EXISTS (select * from items k JOIN items_owners on items.id = items_owners.item_id) and items.organization_id = 1")
Table Setup:
owners:
id
name
items:
id
name
organization_id
items_owners:
owner_id
item_id
organizations:
id
List item
Models:
class Organization < ActiveRecord::Base
attr_accessible :name
has_many :items
end
class Item < ActiveRecord::Base
attr_accessible :description, :name, :owner_ids, :organization_id
has_many :items_owner
has_many :owners, :through => :items_owner
belongs_to :organization
end
class Owner < ActiveRecord::Base
attr_accessible :name
has_many :items_owner
has_many :items, :through => :items_owner
end
class ItemsOwner < ActiveRecord::Base
attr_accessible :owner_id, :item_id
belongs_to :item
belongs_to :owner
end
Items.joins(:organization).includes(:owners).references(:owners).
where('owners.id IS NULL')
And if you want to use includes for both:
Items.includes(:organization, :owners).references(:organization, :owners).
where('organisations.id IS NOT NULL AND owners.id IS NULL')
And as #Dario Barrionuevo wrote, it should be belongs_to :organisation in Item.
Using arel_table in the first example:
Items.joins(:organization).includes(:owners).references(:owners).
where(Owner.arel_table[:id].eq(nil))
In Rails 5 (from comment by #aNoble):
Items.joins(:organization).left_joins(:owners).
where(Owner.arel_table[:id].eq(nil))
But using includes is still preferable if the relations should be referenced in the code, to avoid extra reads.
There are a number of ways to do NOT EXISTS in rails 5, 6:
distinct items OUTER JOIN item_owners where item_owners.id is null
items.id NOT IN (select item_id from item_owners)
NOT EXISTS (select 1 from item_owners where item_id = items.id)
where (select COUNT(*) from item_owners where item_id = items.id) = 0
Off my head I can think of 4 approaches, but I seem to remember there being 7. Anyway, this is a tangent but may give you some ideas that work better for your use case.
I found using the NOT IN approach was the easiest for my team to create and maintain.
Our goals were to avoid arel, support WHERE clauses in the owner table (e.g.: admin owner), and supporting multiple levels of rails :through.
Items.where.not(id: Items.joins(:owners).select(:id))
.select(:id, :name)
Items.where.not(id: Items.joins(:items_owners).select(:id))
.select(:id, :name)
Items.where.not(id: ItemOwners.select(:item_id))
We use the first, but those examples should be in order from least optimized to best. Also in order from least knowledge of the models to the most.
Try this
Items.joins(:organisations).where(Items.joins(:items_owners).exists.not).select('items.id,items.name')

How to filter by more than 1 habtm association

I'm pretty new at Rails, so don't kill me if this a stupid question =P
I have the following models:
class Profile < ActiveRecord::Base
has_and_belongs_to_many :sectors
has_and_belongs_to_many :languages
class Sector < ActiveRecord::Base
has_and_belongs_to_many :profiles
end
class Language < ActiveRecord::Base
has_and_belongs_to_many :profiles
end
I'm looking for an elegant way (without writing sql joins or anything, if possible) to get all the profiles that have a particular sector and a particular language.
I've googled but all I could find is how to do it for 1 habtm, but I need it for 2.
All I have is the following:
def some_method(sector_id, language_id)
Sector.find(sector_id).profiles
end
But I don't know then how to add the filter by language_id without messing with joins conditions or writing sql, and of course, all in one query... Is there a clean/elegant way to do this?
Thanks!
In your example above you've already generated 2 sql requests,
first Sector.find(#id) (select on
sectors table to get record
with id == #id)
second .profiles (select on profiles
table to get all profiles with
following sector - in this select
you already have inner join
profiles_selectors on
profiles_selectors.profile_id =
profiles.id generated automatically by rails)
I hope this is what you are looking for: (but I use :joins key)
class Profile < ActiveRecord::Base
has_and_belongs_to_many :sectors
has_and_belongs_to_many :languages
def self.some_method(language_id, sector_id)
all(:conditions => ["languages.id = ? and sectors.id = ?", language_id, sector_id], :joins => [:languages, :sectors])
end
end
Result of this method is 1 sql query and you get profiles filtered by language and sector.
Best regards
Mateusz Juraszek
Try this:
Profile.all(:joins => [:sectors, :languages],
:conditions => ["sectors.id = ? AND languages.id ?", sector_id, language_id])

Resources