Optimize associated queries - mysql

Lets take the following example. We have two models with this association
class Post < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_many :posts
end
So as you know, we can do the following:
<%= #post.title %>
<%= #post.user.name %>
etc etc
This association usually generates the following queries:
SELECT * FROM `posts` WHERE `post`.`id` = 1;
SELECT * FROM `users` WHERE `user`.`id` = 15; # from #post.user
Now if I want to select some specific fields (lets assume for a moment there's no association) when listing all posts or show just a single one, I do something like this:
#posts = Post.select("id, title, slug, created_at").all()
# or
#post = Post.select("id, title, slug, created_at").find(1)
In case of association how can I select specific fields for the associated query? In other words instead of having
SELECT * FROM `users`
SELECT * FROM `posts` WHERE `user_id` IN ( user IDs here )
to have
SELECT `id`, `name` FROM `users`
SELECT `id`, `user_id`, `title`, `slug` FROM `posts` WHERE `user_id` IN ( user IDs here )

When you retrieve a single Post restricting it's attributes in select clause, it behaves like any other Post, but in order to have access to the User it should have the user_id attribute selected.
#post = Post.select("id").first
# this post doesn't know about it's user since it has no user_id attribute
#post.user # => nil
#post = Post.select("id", "user_id").first
# this post has a user
#post.user # => #<User...>
When you retrieve a list of posts you should solve the 'n+1 queries problem'. You can read about it here http://guides.rubyonrails.org/active_record_querying.html (13 Eager Loading Associations)
You can use includes for eager loading associations:
#posts = Post.includes(:user)
#posts.each do |post|
post.user # this will not make an extra DB query since users are already eager loaded
end
Or if you don't need to instantiate users and want just get specific attributes, the better solution is to join with users and take attributes that you need
#posts = Post.joins(:user).select("posts.id", "posts.title", "users.name AS user_name")
#posts.each do |post|
post.user_name # each post now has `user_name` attribute
end
Edited
As far as I know using includes will ignore select. You can't use select here to specify which attributes of a Post or a User you need. The whole Post will be retrieved and the whole Users will be eager loaded. However, there is a way to restrict user's loading attributes by specifying them in belongs_to association. There is a similar question about this: ActiveRecord includes. Specify included columns
Also, keep in mind, that Post.joins(:user) will not return posts that have no user. So, if it is possible and you need all posts use .left_outer_joins instead.
# works in Rails 5
Post.left_outer_joins(:user).select(...)

You need to refer below link about association
http://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html

Related

How to call a model method inside MySQL query?

Let's say I have an instance variable in my Rails method like this(which I will later use)
def index
#users = User.select("users.id as usr_id,
users.name as user_name,
users.first_price as price,
users.tax as tax,
(#here's model method)=> users.sum_all as grand_total")
.where("users.deleted_at is null AND
users.id IN (?)
#users.pluck(:id))
.order(#order_by)
end
And I want to implement a model method in this query which looks like this
User < ApplicationRecord
def sum_all
self.first_price + self.tax
end
end
If I put this method as above I'm getting an error
{"status":500,"error":"Internal Server Error","exception":"#\u003cActionView::Template::Error: Mysql2::Error: Unknown column 'users.sum_all' in 'field list'}
I suggest you should avoid doing that. Instead, you can make the above query work by adding the two field values in the query itself.
def index
#users = User.select("users.id as usr_id,
users.name as user_name,
users.first_price as price,
users.tax as tax,
users.first_price + users.tax as grand_total")
.where("users.deleted_at is null AND users.id IN (?)", #users.pluck(:id))
.order(#order_by)
end

Rails 5 - Activerecord association query

I have two models associated with each other as follows.
class Comment < ApplicationRecord
belongs_to :user
end
class User < ActiveRecord::Base
has_many :comments
end
Following record query
comments_list = Comment.where(:post_id => post_id, :is_delete => false).joins(:user).select('comments.*,users.*')
Generates the following mysql query in logger
SELECT comments.*,users.* FROM `comments` INNER JOIN `users` ON `users`.`id` = `comments`.`user_id` WHERE `comments`.`post_id` = '81' AND `comments`.`is_delete` = 0.
This seems generating very ligitimate query, but comments_list object contain columns only from comments table.
Thanks
It depends on what you want to do, if you want to display the username next to the comment, Mert B.'s answer is fine, all you have to do is include(:user) and the users from the comment list will be fetched along when you do something like this:
comments_list = Comment.where(:post_id => post_id, :is_delete => false).joins(:user).select('comments.*,users.*')
comments_list.each do |comment|
puts "#{comment.text} by #{comment.user.name}"
end
Or maybe if you want only users who have at least one comment, you can always select users from the user_ids on the comments table:
User.where(id: Comment.select(:user_id))

Rails 4 Active Record uniq by one column query

Lets say I have a model Account with columns :email, :name, etc. Emails are not unique. What I want is to add a chainable scope that selects rows with distinct emails but with all other fields. By 'chainable' I mean that I could do like this: Account.uniq_by_email.where(bla bla).
What I've tried:
With select:
def self.uniq_by_email
select('distinct email')
end
Doesn't work for me as it selects only email field.
With group
def self.uniq_by_email
group(:email)
end
This almost what I want: I can chain and it selects all fields. But there is a strange thing about count method: it, as you already guessed, returns a hash of email counts. But I want it to return "simple" ActiveRecord_Relation where count returns just a count not a hash. Is that possible to achieve?
My basic idea is to select only the first entry in every group of email.
Make it easy, I create a scope like this instead of using a class method:
scope :uniq_by_email, -> { joins("JOIN (
SELECT MIN(id) as min_id
FROM accounts
GROUP BY email
) AS temp
ON temp.min_id = id") }
From this you can do something like chainable as you described:
Account.uniq_by_email.where(bla bla)
Well, you can use group by, but then u can count as,
Account.uniq_by_email.where(bla bla).flatten.count

Ruby on rails mysql searches

I am totally new to Ruby on Rails and I am trying to search through some relational database tables, I am trying to search for a given ID number in a Customer table then from the results look at who the sales_rep for that customer is. With this
#salesrepcust = Customer.find(:all, :conditions => ["id = ?",#data])
I am able to get back the correct customer given there ID number but I dont see how in ruby on rails to then pull from those results just one column value, in this it would be the value for sales_rep, and then use that as my #result for
#salesrepcustdata = Salesrep.find(:all, :conditions => ["id = ?", #result])
I have searched for this but i guess im not wording it correctly because i am not able to find anything specifically on this, can anyone help?
It's pretty straightforward to select a single column; you can try something like this:
#salesrepcustids = Customer.where(id: #data).select(:id)
This will generate a SELECT id FROM ... statement.
And now you can do this:
#salesrepcustdata = Salesrep.where(id: #salesrepcustids)
This will generate an SELECT...IN statement with those ids.
(You might find it easier to set up proper ActiveRecord has_many and belongs_to relationships in your models, or whatever relationship is appropriate.)
Assuming the sales rep is represented in the Customer table as sales_rep_id you can just do:
Salesrep.find(Customer.find(#data).sales_rep_id)
The find method assumes you're looking for id and if there's just one item with that id, there's no need to specify :all.
This is all discussed in http://guides.rubyonrails.org/active_record_querying.html
That Customer query could be simplified to just:
#customer = Customer.find(#data)
You don't mention if you've setup a relationship between Customer and Salesrep but here goes:
# app/models/customer.rb
class Customer < ActiveRecord::Base
belongs_to :salesrep, class_name: 'Salesrep' # => the customers table should have a salesrep_id column
end
# app/models/salesrep.rb
class Salesrep < ActiveRecord::Base
has_many :customers
end
customer_id = 1
#customer = Customer.find(customer_id)
#salesrep = #customer.salesrep
# from the other way, assuming you need both the salesrep and customer:
salesrep_id = 10
#salesrep = Salesrep.find(salesrep_id)
# the following will only find the customer if it's owned by the Salesrep
#customer = #salesrep.customers.find(customer_id)

Query intersection with activerecord

I'd really like to do the following query with the help with active record
(select *
from people p join cities c join services s
where p.city_id = c.id and p.id = s.person_id and s.type = 1)
intersect
(select *
from people p join cities c join services s
where p.city_id = c.id and p.id = s.person_id and s.type = 2)
Problem is, first of all, mysql doesn't support intersect. However, that can be worked around of. The thing is that I can get active record to output anything even close to that.
In active record the best I could do was to issue multiple queries then use reduce :& to join them, but then I get an Array, not a Relation. That's a problem for me because I want to call things like limit, etc. Plus, I think it would be better to the intersection to be done by the database, rather than ruby code.
Your question is probably solvable without intersection, something like:
Person.joins(:services).where(services: {service_type: [1,2]}).group(
people: :id).having('COUNT("people"."id")=2')
However the following is a general approach I use for constructing intersection like queries in ActiveRecord:
class Service < ActiveRecord::Base
belongs_to :person
def self.with_types(*types)
where(service_type: types)
end
end
class City < ActiveRecord::Base
has_and_belongs_to_many :services
has_many :people, inverse_of: :city
end
class Person < ActiveRecord::Base
belongs_to :city, inverse_of: :people
def self.with_cities(cities)
where(city_id: cities)
end
def self.with_all_service_types(*types)
types.map { |t|
joins(:services).merge(Service.with_types t).select(:id)
}.reduce(scoped) { |scope, subquery|
scope.where(id: subquery)
}
end
end
Person.with_all_service_types(1, 2)
Person.with_all_service_types(1, 2).with_cities(City.where(name: 'Gold Coast'))
It will generate SQL of the form:
SELECT "people".*
FROM "people"
WHERE "people"."id" in (SELECT "people"."id" FROM ...)
AND "people"."id" in (SELECT ...)
AND ...
You can create as many subqueries as required with the above approach based on any conditions/joins etc so long as each subquery returns the id of a matching person in its result set.
Each subquery result set will be AND'ed together thus restricting the matching set to the intersection of all of the subqueries.
UPDATE
For those using AR4 where scoped was removed, my other answer provides a semantically equivalent scoped polyfil which all is not an equivalent replacement for despite what the AR documentation suggests. Answer here: With Rails 4, Model.scoped is deprecated but Model.all can't replace it
I was struggling with the same issue, and found only one solution: multiple joins against the same association. This may not be too rails-ish since I'm constructing the SQL string for the joins, but I haven't found another way. This will work for an arbitrary number of service types (cities doesn't seem to factor in, so that join was omitted for clarity):
s = [1,2]
j = ''
s.each_index {|i|
j += " INNER JOIN services s#{i} ON s.person_id = people.id AND s#{i}.type_id = #{s[i]}"
}
People.all.joins(j)