ActiveRecord Rails querying efficiently - ruby-on-rails

Take a look at this question.
id = current_user.id
#given_gifts = User.find(id).following_users.includes(:given_gifts).collect{|u| u.given_gifts}.flatten
#received_gifts = User.find(id).following_users.includes(:received_gifts).collect{|u| u.received_gifts}.flatten
#gifts = #given_gifts.zip(#received_gifts).flatten.compact
I'm trying to retrieve the list of gifts received and given by all users that the current user follows. Is there a better way to do this? The results are all out of order and are showing some duplicates. I'm trying to do an efficient query that I can paginate.
In my User.rb
has_many :given_gifts, class_name: 'Gift', foreign_key: 'giver_id'
has_many :received_gifts, class_name: 'Gift', foreign_key: 'receiver_id'
acts_as_followable
acts_as_follower
In my Gift.rb
belongs_to :giver, class_name: 'User'
belongs_to :receiver, class_name: 'User'
For the following functionality, i'm using the acts_as_follower gem.

Try something like this:
following_user_ids = current_user.following_users.pluck(:id)
Gift.where("(giver_id IN ?) OR (receiver_id IN ?)",
following_user_ids, following_user_ids).order(:created_at)
Basically this gets the list of following_users ids, and then uses an OR query to get gifts that are given or received by any user in the list.
The performance of this query will depend on the underlying database and the number of records you're juggling. But for small #s of following users (<= 100) it will generally perform fine. Performance will degrade as the number of following_user_ids climbs.

Related

Rails EXCEPT query (filter out records present in a joint table)

I'm trying to list the model instances that do not have the association with another model created yet.
Here is how my models are related:
Ticket.rb:
has_one :purchase
has_one :user, through: :purchase
User.rb:
has_many :purchases
has_many :tickets, through: :purchases
Purchase.rb:
belongs_to :ticket
belongs_to :user
I have an SQL query but have troubles when translating it to rails:
SELECT id FROM tickets
EXCEPT
SELECT ticket_id FROM purchases;
It works great as it returns all ids of the tickets that are not purchased yet.
I've tried this:
Ticket.joins('LEFT JOIN ON tickets.id = purchases.ticket_id').where(purchases: {ticket_id: nil})
but it seems not to be the right direction.
If you're just trying to get the list of Ticket records with no associated purchases, use .includes instead. In my experience a join will fail with no associated records, and this will keep you from needing to write any actual SQL.
Ticket.includes(:purchase).where(purchases: { ticket_id: nil} )
The generated SQL query is a bit more difficult to read as a human, but I've used it several times and not seen any real difference in performance.

ActiveRecord custom has_one relations

I'm using Rails 5.0.0.1 ATM and i've come across issue with ActiveRecord relations when optimizing count of my DB requests.
Right now I have:
Model A (let's say 'Orders'), Model B ('OrderDispatches'), Model C ('Person') and Model D ('PersonVersion').
Table 'people' consists only of 'id' and 'hidden' flag, rest of the people data sits in 'person_versions' ('name', 'surname' and some things that can change over time, like scientific title).
Every Order has 'receiving_person_id' as for the person which recorded order in DB and every OrderDispatch has 'dispatching_person_id' for the person, which delivered order. Also Order and OrderDispatch have creation time.
One Order has many dispatches.
The straightforward relations thus is:
has_many :receiving_person, through: :person, foreign_key: "receiving_person_id", class_name: 'PersonVersion'
But when I list my order with according dispatches I have to deal with N+1 situation, because to find accurate (according to the creation date of Order/OrderDispatch) PersonVersion for every receiving_person_id and dispatching_person_id I'm making another requests.
SELECT *
FROM person_versions
WHERE effective_date_from <= ? AND person_id = ?
ORDER BY effective_date_from
LIMIT 1
First '?' is Order/OrderDispatch creation date and second '?' is receiving/ordering person id.
Using this query I'm getting accurate person data for the time of Order/OrderDispatch creation.
It's fairly easy to write query with subquery (or subqueries, as Order comes with OrderDispatches on one list) in raw SQL, but I have no idea how to do that using ActiveRecord.
I tried to write custom has_one relation as this is as far as I've come:
has_one :receiving_person. -> {
where("person_versions.id = (
SELECT id
FROM person_versions sub_pv1
WHERE sub_pv1.date_from <= orders.receive_date
AND sub_pv1.person_id = orders.receiving_person_id
LIMIT 1)")},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
It works if I use this only for receiving or dispatching person. When I try to eager_load this for joined orders and order_dispatches tables then one of 'person_versions' has to be aliased and in my custom where clause it isn't (no way to predict if it's gonna be aliased or not, it's used both ways).
Different aproach would be this:
has_one :receiving_person, -> {
where(:id => PersonVersion.where("
person_versions.date_from <= orders.receive_date
AND person_versions.person_id = orders.receiving_person_id").order(date_from: :desc).limit(1)},
through: :person, class_name: "PersonVersion", primary_key: "person_id", source: :person_version
Raw 'person_versions' in where is OK, because it's in subquery and using symbol ':id' makes raw SQL get correct aliases for person_versions table joined to orders and order_dispatches, but I get 'IN' instead of 'eqauls' for person_versions.id xx subquery and MySQL can't do LIMIT in subqueries which are used with IN/ANY/ALL statements, so I just get random person_version.
So TL;DR I need to transform 'has_many through' to 'has_one' using custom 'where' clause which looks for newest record amongst those which date is lower than originating record creation.
EDIT: Another TL;DR for simplification
def receiving_person
receiving_person_id = self.receiving_person_id
receive_date = self.receive_date
PersonVersion.where(:person_id => receiving_person_id, :hidden => 0).where.has{date_from <= receive_date}.order(date_from: :desc, id: :desc).first
end
I need this method converted to 'has_one' relation so that i could 'eager_load' this.
I would change your schema as it's conflicting with your business domain, restructuring it would alleviate your n+1 problem
class Person < ActiveRecord::Base
has_many :versions, class_name: PersonVersion, dependent: :destroy
has_one :current_version, class_name: PersonVersion
end
class PersonVersion < ActiveRecord::Base
belongs_to :person, inverse_of: :versions,
default_scope ->{
order("person_versions.id desc")
}
end
class Order < ActiveRecord::Base
has_many :order_dispatches, dependent: :destroy
end
class OrderDispatch < ActiveRecord::Base
belongs_to :order
belongs_to :receiving_person_version, class_name: PersonVersion
has_one :receiving_person, through: :receiving_person_version
end

Rails ActiveRecord includes with run-time parameter

I have a few models...
class Game < ActiveRecord::Base
belongs_to :manager, class_name: 'User'
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :game
belongs_to :voter, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :games, dependent: :destroy
has_many :votes, dependent: :destroy
end
In my controller, I have the following code...
user = User.find(params[:userId])
games = Game.includes(:manager, :votes)
I would like to add an attribute/method voted_on_by_user to game that takes a user_id parameter and returns true/false. I'm relatively new to Rails and Ruby in general so I haven't been able to come up with a clean way of accomplishing this. Ideally I'd like to avoid the N+1 queries problem of just adding something like this on my Game model...
def voted_on_by_user(user)
votes.where(voter: user).exists?
end
but I'm not savvy enough with Ruby/Rails to figure out a way to do it with just one database roundtrip. Any suggestions?
Some things I've tried/researched
Specifying conditions on Eager Loaded Associations
I'm not sure how to specify this or give the includes a different name like voted_on_by_user. This doesn't give me what I want...
Game.includes(:manager, :votes).includes(:votes).where(votes: {voter: user})
Getting clever with joins. So maybe something like...
Game.includes(:manager, :votes).joins("as voted_on_by_user LEFT OUTER JOIN votes ON votes.voter_id = #{userId}")
Since you are already includeing votes, you can just count votes using non-db operations: game.votes.select{|vote| vote.user_id == user_id}.present? does not perform any additional queries if votes is preloaded.
If you necessarily want to put the field in the query, you might try to do a LEFT JOIN and a GROUP BY in a very similar vein to your second idea (though you omitted game_id from the joins):
Game.includes(:manager, :votes).joins("LEFT OUTER JOIN votes ON votes.voter_id = #{userId} AND votes.game_id = games.id").group("games.id").select("games.*, count(votes.id) > 0 as voted_on_by_user")

Rails ActiveRecord how to order by a custom named association

Ok so have created 2 models User and Following. Where User has a username attribute and Following has 2 attributes which are User associations: user_id, following_user_id. I have set up these associations in the respective models and all works good.
class User < ActiveRecord::Base
has_many :followings, dependent: :destroy
has_many :followers, :class_name => 'Following', :foreign_key => 'following_user_id', dependent: :destroy
end
class Following < ActiveRecord::Base
belongs_to :user
belongs_to :following_user, :class_name => 'User', :foreign_key => 'following_user_id'
end
Now I need to order the results when doing an ActiveRecord query by the username. I can achieve this easily for the straight-up User association (user_id) with the following code which will return to me a list of Followings ordered by the username of the association belonging to user_id:
Following.where(:user_id => 47).includes(:user).order("users.username ASC")
The problem is I cannot achieve the same result for ordering by the other association (following_user_id). I have added the association to the .includes call but i get an error because active record is looking for the association on a table titled following_users
Following.where(:user_id => 47).includes(:user => :followers).order("following_users.username ASC")
I have tried changing the association name in the .order call to names I set up in the user model as followers, followings but none work, it still is looking for a table with those titles. I have also tried user.username, but this will order based off the other association such as in the first example.
How can I order ActiveRecord results by following_user.username?
That is because there is no following_users table in your SQL query.
You will need to manually join it like so:
Following.
joins("
INNER JOIN users AS following_users ON
following_users.id = followings.following_user_id
").
where(user_id: 47). # use "followings.user_id" if necessary
includes(user: :followers).
order("following_users.username ASC")
To fetch Following rows that don't have a following_user_id, simply use an OUTER JOIN.
Alternatively, you can do this in Ruby rather than SQL, if you can afford the speed and memory cost:
Following.
where(user_id: 47). # use "followings.user_id" if necessary
includes(:following_user, {user: :followers}).
sort_by{ |f| f.following_user.try(:username).to_s }
Just FYI: That try is in case of a missing following_user and the to_s is to ensure that strings are compared for sorting. Otherwise, nil when compared with a String will crash.

How do I use ActiveRecord to find unrelated records?

I have a many-to-many relationship set up through a join model. Essentially, I allow people to express interests in activities.
class Activity < ActiveRecord::Base
has_many :personal_interests
has_many :people, :through => :personal_interests
end
class Person < ActiveRecord::Base
has_many :personal_interests
has_many :activities, :through => :personal_interests
end
class PersonalInterest < ActiveRecord::Base
belongs_to :person
belongs_to :activity
end
I now want to find out: in which activities has a particular user not expressed interest? This must include activities that have other people interested as well as activities with exactly zero people interested.
A successful (but inefficent) method were two separate queries:
(Activity.all - this_person.interests).first
How can I neatly express this query in ActiveRecord? Is there a (reliable, well-kept) plugin that abstracts the queries?
I think the easiest way will be to just use an SQL where clause fragment via the :conditions parameter.
For example:
Activity.all(:conditions => ['not exists (select 1 from personal_interests where person_id = ? and activity_id = activities.id)', this_person.id])
Totally untested, and probably doesn't work exactly right, but you get the idea.

Resources