Rails 3 Nested Query avoid SQL - ruby-on-rails

Suppose I have the following models:
class Foo < ActiveRecord::Base
belongs_to :bar
end
class Bar < ActiveRecord::Base
belongs_to :a
belongs_to :b
end
I want to find all the Foos, including Bar and grouping by a_id and b_id.
I'm pretty sure the following query will work:
Foo.joins(:bar).group('bar.a_id, bar.b_id').all
I'm wondering if there's a way of doing it without writing the SQL in the group statement?
Sub question
What is this query style called and where can I read the full documentation of it? The rails query guide has a mix of several styles and doesn't go into great detail about any of them.

The mix of styles is because styles using hashes cannot currently describe all possible SQL queries. Thus, there is always the fallback of using strings.
The query you gave works, and there is no reason why it shouldn't be used, since the string is very standard SQL, and shouldn't fail if a different database is used.
It is however possible to write it without strings.
Do note that your current query as written will throw an error since by default all the fields of Foo are selected - however with group, you can only select aggregate functions or the group by columns.
The query would be something like:
Foo.select("COUNT(*) as count").joins(:bar).group([:bar => :a_id, :bar => :b_id])
I just added the select clause so that only an aggregate function is selected.

Related

Rails ActiveRecord/Arel query aggregate column with select()

I've got two basic models with a join table. I've added a scope to compute a count through the relation and expose it as an attribute/psuedo-column. Everything works fine, but I'd now like to query a subset of columns and include the count column, but I don't know how to reference it.
tldr; How can I include an aggregate such as a count in my Arel query while also selecting a subset of columns?
Models are Employer and Employee, joined through Job. Here's the relevant code from Employer:
class Employer < ApplicationRecord
belongs_to :user
has_many :jobs
has_many :employees, through: :jobs
scope :include_counts, -> do
left_outer_joins(:employees).
group("employers.id").
select("employers.*, count(employees.*) as employees_count")
end
end
This allows me to load an employer with counts:
employers = Employer.include_counts.where(id: 1)
And then reference the count:
count = employers[0].employees_count
I'm loading the record in my controller, which then renders it. I don't want to render more fields than I need to, though. Prior to adding the count, I could do this:
employers = Employer.where(id: 1).select(:id, :name)
When I add my include_counts scope, it basically ignores the select(). It doesn't fail, but it ends up including ALL the columns, because of this line in my scope:
select("employers.*, count(employees.*) as employees_count")
If I remove employers.* from the scope, then I don't get ANY columns in my result, with or without a select() clause.
I tried this:
employers = Employer.include_counts.where(id: 1).select(:id, :name, :employee_counts)
...but that produces the following SQL:
SELECT employers.*, count(employees.*) as employees_count, id, name, employees_count FROM
...and an SQL error because column employees_count doesn't exist and id and name are ambiguous.
The only thing that sort of works is this:
employers = Employer.include_counts.where(id: 1).select("employers.id, employers.name, count(employees.*) as employees_count")
...but that actually selects ALL the columns in employers, due to the scope clause again.
I also don't want that raw SQL leaking into my controller if I can avoid it. Is there a more idiomatic way to do this with Rails/Arel?
If I can't find another way to do the query, I'll probably create another scope or custom finder in the model, so that the controller code is cleaner. I'm open to suggestions for doing that as well, but I'd like to know if there's a simple way to reference computed aggregate columns like this as though they were any other column.

ORDER BY and DISTINCT ON (...) in Rails

I am trying to ORDER by created_at and then get a DISTINCT set based on a foreign key.
The other part is to somehow use this is ActiveModelSerializer. Specifically I want to be able to declare:
has_many :somethings
In the serializer. Let me explain further...
I am able to get the results I need with this custom sql:
def latest_product_levels
sql = "SELECT DISTINCT ON (product_id) client_product_levels.product_id,
client_product_levels.* FROM client_product_levels WHERE client_product_levels.client_id = #{id} ORDER BY product_id,
client_product_levels.created_at DESC";
results = ActiveRecord::Base.connection.execute(sql)
end
Is there any possible way to get this result but as a condition on a has_many relationship so that I can use it in AMS?
In pseudo code: #client.products_levels
Would do something like: #client.order(created_at: :desc).select(:product_id).distinct
That of course fails for reasons that are beyond me.
Any help would be great.
Thank you.
A good way to structure this is to split your query into two parts: the first part manages the filtering of rows so that you get only your latest client product levels. The second part uses a standard has_many association to connect Client with ClientProductLevel.
Starting with your ClientProductLevel model, you can create a scope to do the latest filtering:
class ClientProductLevel < ActiveRecord::Base
scope :latest, -> {
select("distinct on(product_id) client_product_levels.product_id,
client_product_levels.*").
order("product_id, created_at desc")
}
end
You can use this scope anywhere that you have a query that returns a list of ClientProductLevel objects, e.g., ClientProductLevel.latest or ClientProductLevel.where("created_at < ?", 1.week.ago).latest, etc.
If you haven't already done so, set up your Client class with a has_many relationship:
class Client < ActiveRecord::Base
has_many :client_product_levels
end
Then in your ActiveModelSerializer try this:
class ClientSerializer < ActiveModel::Serializer
has_many :client_product_levels
def client_product_levels
object.client_product_levels.latest
end
end
When you invoke the ClientSerializer to serialize a Client object, the serializer sees the has_many declaration, which it would ordinarily forward to your Client object, but since we've got a locally defined method by that name, it invokes that method instead. (Note that this has_many declaration is not the same as an ActiveRecord has_many, which specifies a relationship between tables: in this case, it's just saying that the serializer should present an array of serialized objects under the key `client_product_levels'.)
The ClientSerializer#client_product_levels method in turn invokes the has_many association from the client object, and then applies the latest scope to it. The most powerful thing about ActiveRecord is the way it allows you to chain together disparate components into a single query. Here, the has_many generates the `where client_id = $X' portion, and the scope generates the rest of the query. Et voila!
In terms of simplification: ActiveRecord doesn't have native support for distinct on, so you're stuck with that part of the custom sql. I don't know whether you need to include client_product_levels.product_id explicitly in your select clause, as it's already being included by the *. You might try dumping it.

Rails Active Record Query - multiple values condition (AND instead of OR)

I am using the acts_as_taggable_on gem in a Rails 4.1 app. I am using a scope to search based on a tag or multiple tags. When multiple tags are given as the param I want the search to return only tagged items that contain ALL tags. Currently the scope I has returns tagged items that contain at least one of the tags (tag_a OR tag_b OR tag_c). I want a scope that instead is an AND condition (tag_a AND tag_b AND tag_c).
Example
document_a_tags = ['programming', 'ruby']
document_b_tags = ['programming', 'python']
class User < ActiveRecord::Base
has_many :documents, dependent: :destroy
scope :with_fields, proc { |fields|
if fields.reject!(&:empty?).present?
joins(documents: [{ taggings: :tag }]).where(tags: { slug: fields })
end
}
end
class Document < ActiveRecord::Base
acts_as_taggable
belongs_to :user
end
Currently, if the search params include programming and ruby both document_a and document_b will be returned. What I want is a scope where if the search params include programming and ruby only document_a will be returned.
Given the way mysql works I would think this is hard to accomplish in a single active record query.
The join adds one row for each tag that is connected to your entry and conditions are only applied to one row at the time. That is, a row can't be excluded by a condition saying that certain other rows in your result.
I think it might be possible to write some complex recursive query adding an undetermined number of columns to the result and applying the conditions on them. But I think it will be much easier and more performant to simply use your current scope and then in ruby code discard entries without all tags.
edit:
This might be done in the gem you are using. Look at:
User.tagged_with(["awesome", "cool"], :match_all => true)

How to order query results based on date field in parent model in rails4/activerecord?

I have models
class Run < ActiveRecord::Base
belongs_to :race
end
class Race < ActiveRecord::Base
has_many :runs
end
Race has a date field and run has a name field.
I would like a query to present all runs from specific name ordered by date field in the race.
I have tried following, but it doesn't sort correctly, results are presented randomly.
Run.where(:name => 'foo').joins(:race).order('races.date ASC')
Thinking that problem might be in the date field I also tried sorting based on id in Race, but results were incorrect as well. I also tried placing join as first element after Run, but no change in results.
SQL generated is as follows:
"SELECT `runs`.* FROM `runs` INNER JOIN `races` ON `races`.`id` = `runs`.`race_id` WHERE (name = 'foo') ORDER BY odds ASC, races.date ASC"
Any help appreciated.
Since you're using default_scope for your model, Active Record always uses your order method by 'odds' first. Do not use default_scope unless you really have to. You'll probably be better off with regular scopes(look at section 14).
You can use unscoped also, which makes AR "forget" the default_scopes. Use it like:
Run.unscoped.where(:name => 'foo').joins(:race).order('races.date ASC')
There is also the reorder method that also overrides the default scope order method:
Run.where(:name => 'foo').joins(:race).reorder('races.date ASC')

Optimize Rails eager loading query for find all

In a Rails 2.3.5 application I've got something like the following models:
class Foo < ActiveRecord::Base
has_many :bars
end
class Bar < ActiveRecord::Base
belongs_to :foo
end
And when I'm calling
Foo.all(:include => :bars)
I see the following queries in console:
SELECT * FROM "foos"
SELECT "bars".* FROM "bars" WHERE ("bars".foo_id IN (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21))
with all the foo's ids in the where clause.
I guess this is not an optimal query while the number of ids may be large, and I need to preload all the 'bars'. Also, actually I've got not two models, but a chain of them.
Is there a way to make the eager loading query be like
SELECT "bars".* FROM "bars"
when I'm using find all?
That's actually an optimization, in fact Rails changes the querying strategy if the number of id's goes high.
You could also use :join instead of using :include

Resources