How to query object associations? - ruby-on-rails

I am building a Rails 5.2 app.
In this app I got three main objects, Conversation, User and Assignment.
Conversation
Has_many :assignments
- Id
- Entity_type
- Entity_subtype
User
Has_many :assignments
- Id
- Name
Assignment
This is a polymorphic association.
- Id
- User_id
- Assignable_id
- Assignable_type
<Assignment id: "5475728b-a0c0-4959-a8a7-17af9a5e224b", account_id: "243c9502-c3ab-4362-907b-ca630c2caf04", user_id: "cdd3c6be-ac78-46f2-a7ae-7f2299b6fedb", assignable_type: "Conversation", assignable_id: "f753e48c-6a67-4410-9748-8454e5904405", role: "owner">
I want to create a query where I find (check) if two individual Users are connected to the same Conversation through an Assignment.
I tried this and it returns conversations but how can I check both user IDs?:
Conversation.joins(:assignments).where(:assignments => {:user_id => "cdd3c6be-ac78-46f2-a7ae-7f2299b6fedb"})
But how can I check the to User IDs are connected to the same Conversation?

Have you tried this?
Conversation.joins(:assignments)
.where(:assignments => {:user_id => ["cdd3c6be-ac78-46f2-a7ae-7f2299b6fedb", "c974419c-1bbd-4537-9d4f-d8df262ace82"]})
.group("assignable_id", "conversations.id")
.having("count(1) > ?", 1)
What it should do is to first fetch all the assignments for given user_ids, then group it by assignable_id and filter those that have more than one assignments.
Note: As user_id I added just some random UUID - I suppose you want to search for the specific Users in your code.

Related

Grouping and counting records by their associated record in rails

class JobPosition < AR
belongs_to :recruiter, class_name: 'User'
end
class User < AR
# first_name, last_name
end
Recruiters (or rather Users) can be attached to many job positions. I want to get a count of how many requisitions each recruiter is assigned to. This is easy via
JobPosition.group(:recruiter_id).count
which returns the count for each recruiter id.
{
1 => 3,
2 => 5
}
However I dont want the recruiter id, but instead the recruiters full name like so
{
"Peter Henry" => 3,
"Hugh Kavener" => 5
}
I don't have a full_name column on the users table but do have first_name and last_name.
How do I achieve this using PG and ActiveRecord. I'd prefer not to resort to manipulating active record objects in memory.
You could do it by writing a little extra SQL, I believe the following syntax should work for postgresql:
JobPosition.joins(:recruiters).group("user.first_name || ' ' || user.last_name").count

How to use joint query in this association - Ruby on Rails

I am working in ruby 2.1.5 and rails 3.2.1. I want to list all the company in grid which is associated to company_name = John
Company table:
company model:
has_many :partner_relationships, :class_name => "CompanyRelationship",:foreign_key => 'partner_id',
company_relationships table:
I want to get all the company information from company table where company.id = partner_id. I tried the below query
Company.joins(:partner_relationships).where('company_relationships.partner_id' => company.id)
This is returning 3 set of same data that is <#id:2, company_name:John, description:I am John#>
I want to return the records as follows <#id:1, company_name:Jose, description:I am Jose#>, <#id:3, company_name:George, description:I am George#>,..<#id:5, company_name:Alwin, description:''#>
Please help me in solving this.
Shouldn't you use "partner_id"?

Rails 4. How show movies list where purchases is nil? [duplicate]

Consider a simple association...
class Person
has_many :friends
end
class Friend
belongs_to :person
end
What is the cleanest way to get all persons that have NO friends in ARel and/or meta_where?
And then what about a has_many :through version
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
end
class Friend
has_many :contacts
has_many :people, :through => :contacts, :uniq => true
end
class Contact
belongs_to :friend
belongs_to :person
end
I really don't want to use counter_cache - and I from what I've read it doesn't work with has_many :through
I don't want to pull all the person.friends records and loop through them in Ruby - I want to have a query/scope that I can use with the meta_search gem
I don't mind the performance cost of the queries
And the farther away from actual SQL the better...
Update 4 - Rails 6.1
Thanks to Tim Park for pointing out that in the upcoming 6.1 you can do this:
Person.where.missing(:contacts)
Thanks to the post he linked to too.
Update 3 - Rails 5
Thanks to #Anson for the excellent Rails 5 solution (give him some +1s for his answer below), you can use left_outer_joins to avoid loading the association:
Person.left_outer_joins(:contacts).where(contacts: { id: nil })
I've included it here so people will find it, but he deserves the +1s for this. Great addition!
Update 2
Someone asked about the inverse, friends with no people. As I commented below, this actually made me realize that the last field (above: the :person_id) doesn't actually have to be related to the model you're returning, it just has to be a field in the join table. They're all going to be nil so it can be any of them. This leads to a simpler solution to the above:
Person.includes(:contacts).where(contacts: { id: nil })
And then switching this to return the friends with no people becomes even simpler, you change only the class at the front:
Friend.includes(:contacts).where(contacts: { id: nil })
Update
Got a question about has_one in the comments, so just updating. The trick here is that includes() expects the name of the association but the where expects the name of the table. For a has_one the association will generally be expressed in the singular, so that changes, but the where() part stays as it is. So if a Person only has_one :contact then your statement would be:
Person.includes(:contact).where(contacts: { person_id: nil })
Original
Better:
Person.includes(:friends).where(friends: { person_id: nil })
For the hmt it's basically the same thing, you rely on the fact that a person with no friends will also have no contacts:
Person.includes(:contacts).where(contacts: { person_id: nil })
smathy has a good Rails 3 answer.
For Rails 5, you can use left_outer_joins to avoid loading the association.
Person.left_outer_joins(:contacts).where( contacts: { id: nil } )
Check out the api docs. It was introduced in pull request #12071.
This is still pretty close to SQL, but it should get everyone with no friends in the first case:
Person.where('id NOT IN (SELECT DISTINCT(person_id) FROM friends)')
Persons that have no friends
Person.includes(:friends).where("friends.person_id IS NULL")
Or that have at least one friend
Person.includes(:friends).where("friends.person_id IS NOT NULL")
You can do this with Arel by setting up scopes on Friend
class Friend
belongs_to :person
scope :to_somebody, ->{ where arel_table[:person_id].not_eq(nil) }
scope :to_nobody, ->{ where arel_table[:person_id].eq(nil) }
end
And then, Persons who have at least one friend:
Person.includes(:friends).merge(Friend.to_somebody)
The friendless:
Person.includes(:friends).merge(Friend.to_nobody)
Both the answers from dmarkow and Unixmonkey get me what I need - Thank You!
I tried both out in my real app and got timings for them - Here are the two scopes:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends_v1, -> { where("(select count(*) from contacts where person_id=people.id) = 0") }
scope :without_friends_v2, -> { where("id NOT IN (SELECT DISTINCT(person_id) FROM contacts)") }
end
Ran this with a real app - small table with ~700 'Person' records - average of 5 runs
Unixmonkey's approach (:without_friends_v1) 813ms / query
dmarkow's approach (:without_friends_v2) 891ms / query (~ 10% slower)
But then it occurred to me that I don't need the call to DISTINCT()... I'm looking for Person records with NO Contacts - so they just need to be NOT IN the list of contact person_ids. So I tried this scope:
scope :without_friends_v3, -> { where("id NOT IN (SELECT person_id FROM contacts)") }
That gets the same result but with an average of 425 ms/call - nearly half the time...
Now you might need the DISTINCT in other similar queries - but for my case this seems to work fine.
Thanks for your help
Unfortunately, you're probably looking at a solution involving SQL, but you could set it in a scope and then just use that scope:
class Person
has_many :contacts
has_many :friends, :through => :contacts, :uniq => true
scope :without_friends, where("(select count(*) from contacts where person_id=people.id) = 0")
end
Then to get them, you can just do Person.without_friends, and you can also chain this with other Arel methods: Person.without_friends.order("name").limit(10)
A NOT EXISTS correlated subquery ought to be fast, particularly as the row count and ratio of child to parent records increases.
scope :without_friends, where("NOT EXISTS (SELECT null FROM contacts where contacts.person_id = people.id)")
Also, to filter out by one friend for instance:
Friend.where.not(id: other_friend.friends.pluck(:id))
Here is an option using a subquery:
# Scenario #1 - person <-> friend
people = Person.where.not(id: Friend.select(:person_id))
# Scenario #2 - person <-> contact <-> friend
people = Person.where.not(id: Contact.select(:person_id))
The above expressions should generate the following SQL:
-- Scenario #1 - person <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT friends.person_id
FROM friends
)
-- Scenario #2 - person <-> contact <-> friend
SELECT people.*
FROM people
WHERE people.id NOT IN (
SELECT contacts.person_id
FROM contacts
)

ActiveRecord Association select counts for included records

Example
class User
has_many :tickets
end
I want to create association which contains logic of count tickets of user and use it in includes (user has_one ticket_count)
Users.includes(:tickets_count)
I tried
has_one :tickets_count, :select => "COUNT(*) as tickets_count,tickets.user_id " ,:class_name => 'Ticket', :group => "tickets.user_id", :readonly => true
User.includes(:tickets_count)
ArgumentError: Unknown key: group
In this case association query in include should use count with group by ...
How can I implement this using rails?
Update
I can't change table structure
I want AR generate 1 query for collection of users with includes
Update2
I know SQL an I know how to select this with joins, but my question is now like "How to get data" . My question is about building association which I can use in includes. Thanks
Update3
I tried create association created like user has_one ticket_count , but
looks like has_one doesn't support association extensions
has_one doesn't support :group option
has_one doesn't support finder_sql
Try this:
class User
has_one :tickets_count, :class_name => 'Ticket',
:select => "user_id, tickets_count",
:finder_sql => '
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
WHERE b.user_id = #{id}
GROUP BY b.user_id
'
end
Edit:
It looks like the has_one association does not support the finder_sql option.
You can easily achieve what you want by using a combination of scope/class methods
class User < ActiveRecord::Base
def self.include_ticket_counts
joins(
%{
LEFT OUTER JOIN (
SELECT b.user_id, COUNT(*) tickets_count
FROM tickets b
GROUP BY b.user_id
) a ON a.user_id = users.id
}
).select("users.*, COALESCE(a.tickets_count, 0) AS tickets_count")
end
end
Now
User.include_ticket_counts.where(:id => [1,2,3]).each do |user|
p user.tickets_count
end
This solution has performance implications if you have millions of rows in the tickets table. You should consider filtering the JOIN result set by providing WHERE to the inner query.
You can simply use for a particular user:
user.tickets.count
Or if you want this value automatically cached by Rails.
Declare a counter_cache => true option in the other side of the association
class ticket
belongs_to :user, :counter_cache => true
end
You also need a column in you user table named tickets_count.
With this each time you add a new tickets to a user rails will update this column so when you ftech your user record you can simply accs this column to get the ticket count without additional query.
Not pretty, but it works:
users = User.joins("LEFT JOIN tickets ON users.id = tickets.user_id").select("users.*, count(tickets.id) as ticket_count").group("users.id")
users.first.ticket_count
What about adding a method in the User model that does the query?
You wouldn't be modifying the table structure, or you can't modify that either?
How about adding a subselect scope to ApplicationRecord:
scope :subselect,
lambda { |aggregate_fn, as:, from:|
query = self.klass
.select(aggregate_fn)
.from("#{self.table_name} _#{self.table_name}")
.where("_#{self.table_name}.id = #{self.table_name}.id")
.joins(from)
select("(#{query.to_sql}) AS #{as}")
}
Then, one might use the following query:
users = User.select('users.*').subselect('COUNT(*)', as: :tickets_count, from: :tickets)
users.first.ticket_count
# => 5

Trying to find a count of items from a two level deep association, any ideas?

I am working on an application where I need to find the count of submitted items by users that have been referred by a user.
For Example -
User1 has referred 3 people (User2, User3, User4) and each of those users has submitted 5 articles.
I am trying to find a way to get the count of submitted items in User1's tree (should be 15 in this case).
My user model looks like the following (simplified)
class User < ActiveRecord::Base
# Code for user referrals
belongs_to :referrer, :class_name => "User"
has_many :referrals, :class_name => "User", :foreign_key => "referrer_id"
has_many :items
end
I can find out the count for each user easily (User.items.size), but I am having trouble finding a solution to get the referral counts as one sum.
Any ideas?
Try this:
user = User.find(1)
total_items_size = user.referrals.map(&:items).flatten.size
You can use select_value to manually run the SQL query:
def referred_items_count
select_value("select count(*) as referred_items
from items inner join users on users.id = items.user_id
where users.referrer_id = #{self.id};", "referred_items")
end
The benefit is that it is a lot more scalable than using Ruby to count.
Get all items of User with id of 1
total = 0
Users.find(1).referrals.each do |refer|
total += refer.items.size
end

Resources