Using Named Scopes as Subqueries in Rails - ruby-on-rails

In my Rails app, I want to join one table with a named scope of another table. Is there a way to do this without having to rewrite the named scope in pure SQL for my join statement?
Basically, is there a way to do something like this?
class Foo < ActiveRecord::Base
scope :updated_today, where('updated_at > ?', DateTime.now.prev_day)
end
Bar.joins(Foo.updated_today)
Where Bar.joins generates the following SQL:
SELECT * FROM bars
INNER JOIN
(SELECT * FROM foos WHERE updated_at > 2012-8-9) AS t0
ON bar_id = bars.id

I don't believe there's any method specifically designed for doing this. You can, however, use the to_sql method of ActiveRecord::Relation to get the full SQL Query for the scope as a string, which you can then use as a subquery in a join statement, like so:
Bar.joins("INNER JOIN (#{Foo.updated_today.to_sql}) as t0 ON bar_id = bars.id")

You can use the merge method to join scopes to another model's query:
Bar.joins(:foos).merge(Foo.updated_today)
I haven't seen a ton of documentation on this (the Rails API doesn't even have any documentation on the method itself), but here is a pretty decent blog post giving a reasonably detailed example.
Also, just noticed that this is mentioned in a RailsCast on Advanced Queries in Rails 3.

Related

Order on a associated column

I has Model that has_many ModelItems. ModelItems belongs_to OtherModel which has a 'code' column.
I am looking to do something like this:
Model.find(1).model_items.includes(:other_model).order("other_model.code" :desc)
I am trying to sort based on the text of that related code column.
I have even tried:
ModelItems.where(model_id: 1).includes(:other_model).order("other_model.code" :desc)
I know I need an include or join here but no matter what I do I get a variation of this error:
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "other_model"
UPDATE
This is an example of not using the real model names bites you. I had it right all along.
The include was singular and the order model name needed to be plural - for clarity lets change other_model to widgit:
Model.find(1).model_items.includes(:widgit).order("widgits.code ASC")
includes in this context will execute 2 queries to create a pseudo outer join (also preload) and is conceptually as follows
SELECT * FROM model_items WHERE model_items.model_id = 1
SELECT * FROM other_models WHERE other_models.model_item_id IN ( [IDS FROM PREVIOUS QUERY])
You can enforce single query execution in a few ways:
eager_load - (ModelItems.eager_load(:other_model)) This is how includes works when you have the join table referenced in a hash finder query condition or when you add references.
references - (ModelItems.includes(:other_model).references(:other_model)) This enforces the eager_load path for include
where Hash finder method (ModelItems.includes(:other_model).where(other_models: {name: 'ABC'})) Here includes intelligently realizes that you have placed a condition on the relationship with other_model and will automatically create the join so that the query is not malformed. However this sort of query represents as an outer join but performs like an inner join which is less efficient *
However if you do not need the information in other_model and just want to use this as a sort mechanism then you can use joins (INNER JOIN) or left_joins (OUTER JOIN) which will allow you to sort this data but will not retrieve the attributes or instantiate any related objects under the other_model relationship
ModelItems.joins(:other_model)
# OR
ModelItems.left_joins(:other_model)
*These options can be combined as well as in the case of the includes where hash finder method I always recommend the following ModelItems.joins(:other_model).includes(:other_model).where(other_models: { name: 'ABC'}) (INNER JOIN). This will return the same data set as ModelItems.includes(:other_model).where(other_models: {name: 'ABC'}) (LEFT OUTER JOIN) however by utilizing an INNER JOIN it becomes more efficient than its LEFT OUTER JOIN version
Sidenote order("other_models.code" :desc) this is not valid. Instead you need to include the order direction in the String or make that String and Symbol e.g. (("other_models.code DESC") or ("other_models.code": :desc))
Add references
ModelItems
.where(model_id: 1)
.includes(:other_model)
.references(:other_model)
.order("other_model.code DESC")

How do you "OR" a two or more scope

Let say User has two scopes, "non_member" and "new_user". I'd like to OR them, including joins and order.
I tried to extract where clauses to construct an OR statement for two scopes. However, joins and orders are not included.
e.g.
non_member_where = User.non_member.where_values.reduce(:and)
new_user_where = User.new_user.where_values.reduce(:and)
#users = User.where("#{non_member_where} OR #{new_user_where}")
Non member scope needs to join a memberships table. If you the code above, that does not join as it only concerns with where.
Is there a better way how to "OR" two scopes? Something like
#users = User.non_member.or.new_user
That output something like this:
SELECT users.* FROM users LEFT JOINS memberships ON memberships.user_id = users.id
WHERE((memberships.user_id IS NULL) OR (users.created_at < {{2 days ago}})
I'm using rails 3.2.12
ActiveRecord is built on top of a library called AReL.
You can drop down to the AReL table and get this functionality.
Alternatively you can use the Squeel gem which does the same thing but gives you a slightly higher level API.
Check out squeel: https://github.com/ernie/squeel

executing query from ruby on rails the right way

I'm just beginning with ruby on rails and have a question regarding a bit more complex query. So far I've done simple queries while looking at rails guide and it worked really well.
Right now I'm trying to get some Ids from database and I would use those Ids to get the real objects and do something with them. Getting those is a bit more complex than simple Object.find method.
Here is how my query looks like :
select * from quotas q, requests r
where q.id=r.quota_id
and q.status=3
and r.text is not null
and q.id in
(
select A.id from (
select max(id) as id, name
from quotas
group by name) A
)
order by q.created_at desc
limit 1000;
This would give me 1000 ids when executing this query from sql manager. And I was thinking to obtain the list of ids first and then find objects by id.
Is there a way to get these objects directly by using this query? Avoiding ids lookup? I googled that you can execute query like this :
ActiveRecord::Base.connection.execute(query);
Assuming Quota has_many :requests,
Quota.includes(:requests).
where(status:3).
where('requests.text is not null').
where("quotas.id in (#{subquery_string_here})").
order('quotas.created_at desc').limit(1000)
I'm by no means an expert but most basic SQL functionality is baked into ActiveRecord. You might also want to look at the #group and #pluck methods for ways to eliminate the ugly string subquery.
Calling #to_sql on a relationship object will show you the SQL command it is equivalent to, and may help with your debugging.
I would use find_by_sql for this. I wouldn't swear that this is exactly right, but as I recall you can pretty much plonk an SQL statement into a find_by_sql and the resulting columns will be returned as attributes of an array of objects of the class you call it on:
status = 3
Quota.find_by_sql('
select *
from quotas q, requests r
where q.id=r.quota_id
and q.status= ?
and r.text is not null
and q.id in
(
select A.id from (
select max(id) as id, name
from quotas
group by name) A
)
order by q.created_at desc
limit 1000;', status)
If you come to Rails as someone used to writing raw SQL, you're probably better off using this syntax than stringing together a bunch of ActiveRecord methods - the result is the same, so it's just a matter of what you find more readable.
Btw, you shouldn't use string interpolation (i.e. #{variable} syntax) inside an SQL query. Use the '?' syntax instead (see my example) to avoid SQL injection potential.

Advanced Rails 3 scope with CROSS JOIN

I want a scope which returns me a list of (eg.) books they have chapters. I found this post.
It helped me to select books they have chapters. But if I want want to select those they have no chapters like:
class Book
scope :long, joins(:chapters).
select('books.id, count(chapters.id) as n_chapters').
group('books.id').
having('n_chapters = 0')
end
This scope returns me nothing. Can you help me out?
Using joins leads to creating relation with all the combinations of book+chapter that already have connection. This is done vie INNER JOIN SQL-clause. You have to make OUTER JOIN if you want all the possible combinations to be created (including book+nullified chapter).
replace
joins(:chapters).
with
joins('LEFT OUTER JOIN chapters ON books.id=chapters.book_id').
But in this case consider using the NOT IN SQL-clause instead. Like here:
scope :long, lambda { where('id NOT IN (%s)' % Chapter.select(:book_id).to_sql) }
It is considerably smaller/faster and more readable/maintainable construction.

rails select and include

Can anyone explain this?
Project.includes([:user, :company])
This executes 3 queries, one to fetch projects, one to fetch users for those projects and one to fetch companies.
Project.select("name").includes([:user, :company])
This executes 3 queries, and completely ignores the select bit.
Project.select("user.name").includes([:user, :company])
This executes 1 query with proper left joins. And still completely ignores the select.
It would seem to me that rails ignores select with includes. Ok fine, but why when I put a related model in select does it switch from issuing 3 queries to issuing 1 query?
Note that the 1 query is what I want, I just can't imagine this is the right way to get it nor why it works, but I'm not sure how else to get the results in one query (.joins seems to only use INNER JOIN which I do not in fact want, and when I manually specifcy the join conditions to .joins the search gem we're using freaks out as it tries to re-add joins with the same name).
I had the same problem with select and includes.
For eager loading of associated models I used native Rails scope 'preload' http://apidock.com/rails/ActiveRecord/QueryMethods/preload
It provides eager load without skipping of 'select' at scopes chain.
I found it here https://github.com/rails/rails/pull/2303#issuecomment-3889821
Hope this tip will be helpful for someone as it was helpful for me.
Allright so here's what I came up with...
.joins("LEFT JOIN companies companies2 ON companies2.id = projects.company_id LEFT JOIN project_types project_types2 ON project_types2.id = projects.project_type_id LEFT JOIN users users2 ON users2.id = projects.user_id") \
.select("six, fields, I, want")
Works, pain in the butt but it gets me just the data I need in one query. The only lousy part is I have to give everything a model2 alias since we're using meta_search, which seems to not be able to figure out that a table is already joined when you specify your own join conditions.
Rails has always ignored the select argument(s) when using include or includes. If you want to use your select argument then use joins instead.
You might be having a problem with the query gem you're talking about but you can also include sql fragments using the joins method.
Project.select("name").joins(['some sql fragement for users', 'left join companies c on c.id = projects.company_id'])
I don't know your schema so i'd have to guess at the exact relationships but this should get you started.
I might be totally missing something here but select and include are not a part of ActiveRecord. The usual way to do what you're trying to do is like this:
Project.find(:all, :select => "users.name", :include => [:user, :company], :joins => "LEFT JOIN users on projects.user_id = users.id")
Take a look at the api documentation for more examples. Occasionally I've had to go manual and use find_by_sql:
Project.find_by_sql("select users.name from projects left join users on projects.user_id = users.id")
Hopefully this will point you in the right direction.
I wanted that functionality myself,so please use it.
Include this method in your class
#ACCEPTS args in string format "ASSOCIATION_NAME:COLUMN_NAME-COLUMN_NAME"
def self.includes_with_select(*m)
association_arr = []
m.each do |part|
parts = part.split(':')
association = parts[0].to_sym
select_columns = parts[1].split('-')
association_macro = (self.reflect_on_association(association).macro)
association_arr << association.to_sym
class_name = self.reflect_on_association(association).class_name
self.send(association_macro, association, -> {select *select_columns}, class_name: "#{class_name.to_sym}")
end
self.includes(*association_arr)
end
And you will be able to call like: Contract.includes_with_select('user:id-name-status', 'confirmation:confirmed-id'), and it will select those specified columns.
The preload solution doesn't seem to do the same JOINs as eager_load and includes, so to get the best of all worlds I also wrote my own, and released it as a part of a data-related gem I maintain, The Brick.
By overriding ActiveRecord::Associations::JoinDependency.apply_column_aliases() like this then when you add a .select(...) then it can act as a filter to choose which column aliases get built out.
With gem 'brick' loaded, in order to enable this selective behaviour, add the special column name :_brick_eager_load as the first entry in your .select(...), which turns on the filtering of columns while the aliases are being built out. Here's an example:
Employee.includes(orders: :order_details)
.references(orders: :order_details)
.select(:_brick_eager_load,
'employees.first_name', 'orders.order_date', 'order_details.product_id')
Because foreign keys are essential to have everything be properly associated, they are automatically added, so you do not need to include them in your select list.
Hope it can save you both query time and some RAM!

Resources