Rails associations NOT EXISTS. Better way? [duplicate] - ruby-on-rails

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')

Related

ActiveRecord: Search for multiple ids in association with AND condition

I have the following models
class Employee < ApplicationRecord
has_many :employee_skills
has_many :skills, throught: :employee_skills
end
class Skill < ApplicationRecord
has_many :employee_skills
has_many :employees, through: :employee_skills
end
class EmployeeSkill < ApplicationRecord
belongs_to :employee
belongs_to :skill
end
How can i query for employees which have skill 1 AND 2 AND 3 (or more skills). Array conditions (see Rails Guide) selects with OR not with AND.
One possible way is using your own custom "raw" join condition in joins:
Employee
.joins('INNER JOIN skills s1 ON s1.id = 1 AND s1.employee_id = employees.id')
.joins('INNER JOIN skills s2 ON s2.id = 2 AND s2.employee_id = employees.id')
Here s1.id and s2.id are both the skills ids you have.
You can simply use includes if you want to apply for all employees and for single employee you can do like this
#employee.skills.where(id: array_of_ids)
With Includes for all or more than 1 employees
Employee.includes(:skills).where('skills.id' => array_of_ids)
As per #sebastian comment, you can use joins
Employee.joins(:skills).where(skills: { id: [1,2,3,4] })
Inspired by the solution here: SELECTING with multiple WHERE conditions on same column, this is the solution using only active records
Employee.joins(:employee_skills).where(employee_skills: {skill_id: #skills.ids})
.group('employees.id, employee_skills.employee_id')
.having('COUNT(*) = ?', #skills.count)

Rails Associations has_one Latest Record

I have the following model:
class Section < ActiveRecord::Base
belongs_to :page
has_many :revisions, :class_name => 'SectionRevision', :foreign_key => 'section_id'
has_many :references
has_many :revisions, :class_name => 'SectionRevision',
:foreign_key => 'section_id'
delegate :position, to: :current_revision
def current_revision
self.revisions.order('created_at DESC').first
end
end
Where current_revision is the most recently created revision. Is it possible to turn current_revision into an association so I can perform query like Section.where("current_revision.parent_section_id = '1'")? Or should I add a current_revision column to my database instead of trying to create it virtually or through associations?
To get the last on a has_many, you would want to do something similar to #jvnill, except add a scope with an ordering to the association:
has_one :current_revision, -> { order created_at: :desc },
class_name: 'SectionRevision', foreign_key: :section_id
This will ensure you get the most recent revision from the database.
You can change it to an association but normally, ordering for has_one or belongs_to association are always interpreted wrongly when used on queries. In your question, when you turn that into an association, that would be
has_one :current_revision, class_name: 'SectionRevision', foreign_key: :section_id, order: 'created_at DESC'
The problem with this is that when you try to combine this with other queries, it will normally give you the wrong record.
>> record.current_revision
# gives you the last revision
>> record.joins(:current_revision).where(section_revisions: { id: 1 })
# searches for the revision where the id is 1 ordered by created_at DESC
So I suggest you to add a current_revision_id instead.
As #jvnill mentions, solutions using order stop working when making bigger queries, because order's scope is the full query and not just the association.
The solution here requires accurate SQL:
has_one :current_revision, -> { where("NOT EXISTS (select 1 from section_revisions sr where sr.id > section_revisions.id and sr.section_id = section_revisions.section_id LIMIT 1)") }, class_name: 'SectionRevision', foreign_key: :section_id
I understand you want to get the sections where the last revision of each section has a parent_section_id = 1;
I have a similar situation, first, this is the SQL (please think the categories as sections for you, posts as revisions and user_id as parent_section_id -sorry if I don't move the code to your need but I have to go):
SELECT categories.*, MAX(posts.id) as M
FROM `categories`
INNER JOIN `posts`
ON `posts`.`category_id` = `categories`.`id`
WHERE `posts`.`user_id` = 1
GROUP BY posts.user_id
having M = (select id from posts where category_id=categories.id order by id desc limit 1)
And this is the query in Rails:
Category.select("categories.*, MAX(posts.id) as M").joins(:posts).where(:posts => {:user_id => 1}).group("posts.user_id").having("M = (select id from posts where category_id=categories.id order by id desc limit 1)")
This works, it is ugly, I think the best way is to "cut" the query, but if you have too many sections that would be a problem while looping trough them; you can also place this query into a static method, and also, your first idea, have a revision_id inside of your sections table will help to optimize the query, but will drop normalization (sometimes it is needed), and you will have to be updating this field when a new revision is created for that section (so if you are going to be making a lot of revisions in a huge database it maybe would be a bad idea if you have a slow server...)
UPDATE
I'm back hehe, I was making some tests, and check this out:
def last_revision
revisions.last
end
def self.last_sections_for(parent_section_id)
ids = Section.includes(:revisions).collect{ |c| c.last_revision.id rescue nil }.delete_if {|x| x == nil}
Section.select("sections.*, MAX(revisions.id) as M")
.joins(:revisions)
.where(:revisions => {:parent_section_id => parent_section_id})
.group("revisions.parent_section_id")
.having("M IN (?)", ids)
end
I made this query and worked with my tables (hope I named well the params, it is the same Rails query from before but I change the query in the having for optimization); watch out the group; the includes makes it optimal in large datasets, and sorry I couldn't find a way to make a relation with has_one, but I would go with this, but also reconsider the field that you mention at the beginning.
If your database supports DISTINCT ON
class Section < ApplicationRecord
has_one :current_revision, -> { merge(SectionRevision.latest_by_section) }, class_name: "SectionRevision", inverse_of: :section
end
class SectionRevision < ApplicationRecord
belongs_to: :section
scope :latest_by_section, -> do
query = arel_table
.project(Arel.star)
.distinct_on(arel_table[:section_id])
.order(arel_table[:section_id].asc, arel_table[:created_at].desc)
revisions = Arel::Nodes::TableAlias.new(
Arel.sql(format("(%s)", query.to_sql)), arel_table.name
)
from(revisions)
end
end
It works with preloading
Section.includes(:current_revision)

How to write Rails finder with several subqueries

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

named scope based on number of associated records

I am doing some basic sql logic and I want to use a named scope. I'm trying to find out how many members of a season have also participated in another season (i.e. they are returning members).
class Season
has_many :season_members
has_many :users, :through => :season_members
def returning_members
users.select { |u| u.season_members.count > 1 }
end
end
class SeasonMember
belongs_to :season
belongs_to :user
end
class User
has_many :season_members
end
Is it possible to use :group and friends to rewrite the returning_members method as a scope?
I happen to be using Rails 2.3 but I'll also accept solutions that rely on newer versions.
Not sure if you really want to put this scope on Season, since that would imply that you are looking for Seasons which have repeat users. But, I assume you want Users that have repeat seasons. With that assumption, your scope would be:
class User < ActiveRecord::Base
scope :repeat_members,
:select=>"users.*, count(season_members.season_id) as season_counter",
:joins=>"JOIN season_members ON season_members.user_id = users.id",
:group=>"users.id",
:having=>"season_counter > 1"
end
which would result in the following query:
SELECT users.*, count(season_members.season_id) as season_counter FROM "users" JOIN season_members ON season_members.user_id = users.id GROUP BY users.id HAVING season_counter > 1
Confirmed with: Rails 3.1.3 and SQLite3

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