How do you "OR" a two or more scope - ruby-on-rails

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

Related

Retrive records which are not referenced in other table, ActiveRecord query

There are 2 tables : User and Teacher. Teacher.user_id is from User. So, how do I find in a single query, all the users who are not in teachers.
I meant something along the lines :
User.not_in(Teacher.all)
You can use where.not query from ActiveRecord try something like below:
User.where.not(id: Teacher.pluck(:user_id).reject {|x| x.nil?})
Note: used reject method, in case you have nil values in some records.
The other users seem to have neglected the rails 3 tag (since removed based on the approved answer. My answer left for posterity) : Please try this
User.where("id NOT IN (?)",Teacher.pluck(:user_id).join(","))
This will become SELECT * FROM users WHERE id NOT IN (....) (two queries one to get the user_id from teachers and another to get the user(s) not in that list) and may fail based on the size of teacher table.
Other option is an arel table:
users = User.arel_table
User.where(users[:id].not_in(Teacher.select(:user_id).where("user_id IS NOT NULL")))
This should produce a single query similar to
SELECT * FROM users
WHERE id NOT IN ( SELECT user_id FROM teachers WHERE user_id IS NOT NULL)
(one query better performance) * syntax was not fully tested
Another single query option might be
User.joins("LEFT OUTER JOIN teachers ON teachers.user_id = users.id").
where("teachers.user_id IS NULL")
I think you should be able to do something like this
User.where.not(id: Teacher.ids)

rails fetch records with no has many relationship and ones with a condition

I have a HomeMaker and PerDateUnavailability models. HomeMaker has_many per_date_unavailabilities. I want all home makers who don't have a record in per_date_unavailabilities and home makers who have record but not when the per_date_unavailabilties.unavailable_date = somedate
I usually do the first part when I want HomeMakers without a PerDateUnavailability record using HomeMaker.includes(:per_date_unavailabilities).where(per_date_unavailabilities: {id: nil})
and the second part of the condition using HomeMaker.joins(:per_date_unavailabilities).where.not(per_date_unavailabilities: {unavailable_date: Date.today})
How do I mix these?
The sql you need is somewhat like this:
select a.*
from home_makers a left join per_date_unavailabilities b
on a.id = b.home_maker_id
where b.home_maker_id is NULL
or b.unavailable_date IS NOT ?;
Expressing the or clause is a little tough in ActiveRecord, and its best we don't fight it. (Edited after OP's comment)
HomeMaker.joins("LEFT JOIN per_date_unavailabilities on per_date_unavailabilities.home_maker_id = home_makers.id")
.where("per_date_unavailabilities.home_maker_id IS NULL OR per_date_unavailabilities.unavailable_date != ?", somedate)

Find all records that don't have any of an associated model

I'm using Rails 3.2.
I have a product model, and a variant model. A product can have many variants. A variant can belong to many products.
I want to make a lookup on the Products model, to find only products that have a specific variant count, like such (pseudocode):
Product.where("Product.variants.count == 0")
How do you do this with activerecord?
You can use a LEFT OUTER JOIN to return the records that you need. Rails issues a LEFT OUTER JOIN when you use includes.
For example:
Product.includes(:variants).where('variants.id' => nil)
That will return all products where there are no variants. You can also use an explicit joins.
Product.joins('LEFT OUTER JOIN variants ON variants.product_id = products.id').where('variants.id' => nil)
The LEFT OUTER JOIN will return records on the left side of the join, even if the right side is not present. It will place null values into the associated columns, which you can then use to check negative presence, as I did above. You can read more about left joins here: http://www.w3schools.com/sql/sql_join_left.asp.
The good thing about this solution is that you're not doing subqueries as a conditional, which will most likely be more performant.
products= Product.find(:all,:select => 'variant').select{|product| product.varients.count > 10}
This is rails 2.3 , but only the activeRecord part, you need to see the select part
I don't know of any ActiveRecord way to do this but the following should help with your problem. The good thing about this solution is that everything's done on the db side.
Product.where('(SELECT COUNT(*) FROM variants WHERE variants.product_id = products.id) > 0')
If you want to pull products which have a specific non-0 number of variants, you could do that with something like this (admittedly untested):
Product.select('product.id, product.attr1_of_interest, ... product.attrN_of_interest, variant.id, COUNT(*)')
.joins('variants ON product.id = variants.product_id')
.group('product.id, product.attr1_of_interest, ... product.attrN_of_interest, variant.id')
.having('COUNT(*) = 5') #(or whatever number manipulation you want to do here)
If you want to allow for 0 products, you would have to use Sean's solution above.

Using Named Scopes as Subqueries in 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.

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