How to create a single query across associations - sql

I have a user model that has one profile and also has one goal. The profile has a privacy field which is of type hstore, and in that field there can be a hash that indicates that it is OK to publicly display the goal, the hash looks like { 'show_goals' => '1' } if the goal can be displayed. I want a list of all goals that can be displayed publicly. My initial attempt was
def list_public
profiles = Profile.includes(:user).where("privacy #> hstore('show_goals','1')")
goals = []
profiles.each do |p|
goals << p.user.goals.first
end
goals
end
end
This works fine when there was a small number of users opting into allow their goals to be displayed, but is clearly not scaleable. Is there a single or a couple of ActiveRecord sql queries that can do the job of this code? I am using ruby on rails 5.1.

The easiest way is to also eager load the goals in your query:
profiles = Profile.includes(:user, user: :goals).where("privacy #> hstore('show_goals','1')")
This will produce one additional query fetching all the goals. You can then still use the remaining code as is, p.user.goals.first will not generate an additional database query.

Related

How to avoid N+1 query

I have the following models:
User
Ability
PricingRule
defined with the following relationships:
user has many pricing rules
ability has one pricing rule
The idea is to fetch all abilities matching some criteria and for each, fetch its pricing rule. However a custom pricing rule for a particular ability can be defined on a per user basis.
Currently I fetch all matching abilities and iterate on them to either:
try to find a current ability matching a user's pricing rule
or default to the ability's pricing rule
I am using Rails and ActiveRecord and here what I have so far:
user = User.first
Ability.all.map do |a|
user.pricing_rules.matching_ability(a).first || a.pricing_rule
end
Per user pricing rule customization should be done on demand by the business. The common workflow is to get the pricing rule from the abilities.
Any ideas or help to get me on the right track would be much appreciated.
EDIT:
Where the matching_ability implementation is as follow:
def self.matching_ability(ability)
where(name: ability.name)
end
You can "eager load" to avoid N+1 queries like so:
user = User.includes(pricing_rules: :abilities).first
Ability.includes(:pricing_rule).map do |a|
user.pricing_rules.matching_ability(a).first || a.pricing_rule
end
You should see in the SQL generated that this adds a LEFT OUTER JOIN to your queries, so ActiveRecord is loading the associated records in just the two queries. In particular, the user will be loaded with its pricing_rules and the abilities on each pricing_rule, and the abilities will be loaded with their pricing_rules.
However, implementing matching_ability using where may generate additional queries, returning you to the N+1 problem. To take advantage of the "eager load" in the first query, you may need to refactor to:
self.matching_ability(ability)
select{|a| a.name == ability.name}
end

SQL complicated query with joins

I have problem with one query.
Let me explain what I want:
For the sake of bravity let's say that I have three tables:
-Offers
-Ratings
-Users
Now what I want to do is to create SQL query:
I want Offers to be listed with all its fields and additional temporary column that IS NOT storred anywhere called AverageUserScore.
This AverageUserScore is product of grabbing all offers, belonging to particular user and then grabbing all ratings belonging to these offers and then evaluating those ratings average - this average score is AverageUserScore.
To explain it even further, I need this query for Ruby on Rails application. In the browser inside application you can see all offers of other users , with AverageUserScore at the very end, as the last column.
Associations:
Offer has many ratings
Offer belongs to user
Rating belongs to offer
User has many offers
Assumptions made:
You actually have a numeric column (of any type that SQL's AVG is fine with) in your Rating model. I'm using a column ratings.rating in my examples.
AverageUserScore is unconventional, so average_user_score is better.
You don't mind not getting users that have no offers: average rating is not clearly defined for them anyway.
You don't deviate from Rails' conventions far enough to have a primary key other than id.
Displaying offers for each user is a straightforward task: in a loop of #users.each do |user|, you can do user.offers.each do |offer| and be set. The only problem here is that it will execute a separate query for every user. Not good.
The "fetching offers" part is a standard N+1 counter seen even in the guides.
#users = User.includes(:offers).all
The interesting part here is only getting the averages.
For that I'm going to use Arel. It's already part of Rails, ActiveRecord is built on top of it, so you don't need to install anything extra.
You should be able to do a join like this:
User.joins(offers: :ratings)
And this won't get you anything interesting (apart from filtering users that have no offers). Inside though, you'll get a huge set of every rating joined with its corresponding offer and that offer's user. Since we're taking averages per-user we need to group by users.id, effectively making one entry per one users.id value. That is, one per user. A list of users, yes!
Let's stop for a second and make some assignments to make Arel-related code prettier. In fact, we only need two:
users = User.arel_table
ratings = Rating.arel_table
Okay. So. We need to get a list of users (all fields), and for each user fetch an average value seen on his offers' ratings' rating field. So let's compose these SQL expressions:
# users.*
user_fields = users[Arel.star] # Arel.star is a portable SQL "wildcard"
# AVG(ratings.rating) AS average_user_score
average_user_score = ratings[:rating].average.as('average_user_score')
All set. Ready for the final query:
User.includes(:offers) # N+1 counteraction
.joins(offers: :ratings) # dat join
.select(user_fields, average_user_score) # fields we need
.group(users[:id]) # grouping to only get one row per user

Can't figure out how to query a 2nd level resource in Rails

I'm having trouble when querying Users.
My nesting resources are:
resources :users do
resources :photos do
resources :pins
end
end
1.) I have a user model, that has_many :photos.
2.) :photos has_many :pins
I want to list my users on which users have more pins in their photos.
So, I tried:
#members_ordered = User.includes(photos: :pins).group("users.id").group("photos.id").group("pins.id").order('COUNT(pins.id) DESC')
Not working though. Any ideas? Thanks guys
I have two observations, but neither directly fix the code in your example.
First, looking at my output from trying something similar, it seems like you either need quite complex SQL (which really isn't Rails' forte) or several simple queries (which, depending on the size of your app, could hit performance) to achieve this.
A little experimenting doesn't seem to show a significant difference (<1ms) in the time that one more complex query takes compared to that which three simple queries require (as in solution one)
Solution one, if performance is not crucial, for example, if this is a small, low-traffic solution, my instinct would be to add that the User model has_many :pins, through: :photos, which lets you call things like User.includes(photos: :pins).all, then user.pins.count, although, as I've mentioned, this causes a bit more database use.
Solution two, if performance is important, my suggestion would be to cache the count of pins against the user model. This could be as simple as an extra database column to store it, and have a background process (using delayed_job or similar) re-calculate the count each time it changes (so, maybe after_create in the Pin model.
The benefit of this is the slow, time-consuming query only gets run when the value changes, and the rest of the time, the value gets lifted from a single-table SELECT, which should take quite a bit less time than either solution one or the more complex query.
Both of these are less-than-perfect, and I think the most elegant and efficient way of working is to use a combination of a built-in function and a beautifully simple query:
The third solution, which brings together both of these options to some extent, is Rails' counter_cache option. As there are two levels to it, I can't see a native way to include all of these in one query, so we will automatically generate a count for each Photo, then add these up to get the User count.
Create a migration to add a pins_count field to the Photo model, so, in terminal, type;
rails g migration AddPinsCountToPhotos pins_count:integer
Update the belongs_to :photo line of the Pin model to;
belongs_to :photo, counter_cache: true
Now, every time a Pin gets created or deleted, the pins_count column of its Photo will be updated.
Now, to get the values for users;
Create a migration to add a pins_count field to the User model, so, in terminal, type;
rails g migration AddPinsCountToUsers pins_count:integer
Now we need to create an method in the Photo model, which we will run each time a pin is saved, so add this to your Photo model;
def update_user_counts
total_photos = self.user.photos.sum(:pins_count)
self.user.update_attribute(:pins_count, total_photos)
end
Finally, we need to tell Rails to call this whenever a pin is created or updated. We do this with a simple method that just calls the action from the Photo model;
after_save :update_photo_counts
def update_photo_counts
photo.update_user_counts
end
Now, whenever a pin is saved, it automatically updates the Photos pins_count, and then our new method totals the pins_counts from all of the Photos for that user, and saves them to the Users pins_count

Rails 3 Searching Multiple Models by created_at using sunspot

I'm trying to get a "What's new" section working in my Rails app that takes into account new records created for various tables that don't share any relationships. The one thing they do have in common is that they all have a created_at field, which I'm going to use to determine if they're indeed "new" and then I'm wanting to sort the results by that common field. I tried doing this with Sunspot, but I couldn't figure out how to make use of the the result set returned from the Sunspot search...
For instance in my Uploads and Article models I have:
searchable do
time :created_at
end
and in my search action I'll do this:
#updates = Sunspot.search(Upload,Article) do
with(:created_at).greater_than(1.hour.ago)
end
Which does seem to return something, if I do an #updates.total it returns the number of records I was expecting to find. Beyond this I'm not sure how to actually make use of the records. What I'd like to do is send #updates to a view and determine the model type of each record and then proceed to print out the relevant information, i.e names, descriptions, parent/child record information (for instance upload.user.username).
I might be going at this all wrong, perhaps there's a better option than sunspot for the simple search I'm attempting to perform?
Refer readme for details of how to use the search results. The method you are looking for is "results", which will give you first 30 results, by default:
#updates.results # array of first 30 results

Rails3: left join aggregate count - how to calculate?

In my application Users register for Events, which belong to a Stream. The registrations are managed in the Registration model, which have a boolean field called 'attended'.
I'm trying to generate a leaderboard and need to know: the total number of registrations for each user, as well as a count for user registrations in each individual event stream.
I'm trying this (in User.rb):
# returns an array of users and their attendence count
def self.attendance_counts
User.all(
:select => "users.*, sum(attended) as attendance_count",
:joins => 'left join `registrations` ON registrations.user_id = users.id',
:group => 'registrations.user_id',
:order => 'attendance_count DESC'
)
end
The generated SQL works for just returning the total attended count for each user when I run it in the database, but all that gets returned is the User record in Rails.
I'm about to give up and hardcode a counter_cache for each stream (they are fairly fixed) into the User table, which gets manually updated whenever the attended attribute changes on a Registration model save.
Still, I'm really curious as to how to perform a query like this. It must come up all the time when calculating statistics and reports on records with relationships.
Your time and consideration is much appreciated. Thanks in advance.
Firstly as a couple of points on style and rails functions to help you with building DB queries.
1) You're better writing this as a scope rather than a method i.e.
scope attendance_counts, select("users.*, sum(attended) as attendance_count").joins(:registrations).group('registrations.user_id').order('attendance_count DESC')
2) It's better not to call all/find/first on the query you've built up until you actually need it (i.e. in the controller or view). That way if you decide to implement action / fragment caching later on the DB query won't get called if the cached action / fragment is served to the user.
3) Rails has a series of functions to help with aggregating db data. for example if you only wanted a user's id and the sum of attended you could use something like the following code:
Registrations.group(:user_id).sum(:attended)
Other functions include count, avg, minimum, maximum
Finally in answer to your question, rails will create an attribute for you to access the value of any custom fields you have in the select part of your query. e.g.
#users = User.attendance_counts
#users[0].attendance_count # The attendance count for the first user returned by the query