Find only records which have all deleted associations - ruby-on-rails

The question goes as RoR but I guess the question applies more on a query level.
I have a model
ModelA has_many ModelB
and I'm using paranoid gem in both of them.
I am trying to find out all the ModelA which have either no ModelB associated or all its ModelB have been deleted (therefore deleted_at is not NULL).
Based on this question, I was able to achieve the desired result, but reading the query it does not make much sense to me how it is working.
ModelA.joins('LEFT OUTER JOIN model_bs ON model_bs.model_a_id = model_as.id AND model_bs.deleted_at IS NULL')
.where('model_bs.model_a_id IS NULL')
.group(model_as.id)
As mentioned, reading this query it does not make sense to me because the joins condition I'm using is also being as null on the where clause afterwards.
Can someone please help me out getting the query properly set and if this is the right way to go, explain me how does the query breakdown into the right result.
Update:
As mentioned on a comment below, after some raw sql I managed to understand what was going on. Also, wrote my AR solution as to make it more clear (at least from my perspective)
Breaking it down:
This query represents a regular left outer join, which returns all models on the left independently if there is an association on the right.
ModelA.joins('LEFT OUTER JOIN model_bs ON model_bs.model_a_id = model_as.id')
Adding the extra condition in the join
AND model_bs.deleted_at IS NULL
will return the rows with ModelB attributes available if there is one associated, else will return one row with only the ModelA.
Applying over these rows the
.where('model_bs.model_a_id IS NULL') will keep only rows that have no associated models.
Note:
In this case, using the attribute model_a_id or any other attribute of model_b will work, but if going this way, I'd recommend using an attribute that if the association exists, its always going to be there. Otherwise you might end up with wrong results.
Finally, the AR that I've ended up by using to translate what I wanted:
ModelA.joins('LEFT OUTER JOIN model_bs ON model_bs.model_a_id = model_as.id AND model_bs.deleted_at IS NULL')
.group('model_as.id')
.having('count(model_bs.id) = 0')

Hey you can try this way i have tested it in mysql you can verify it with PG
ModelA.joins('LEFT OUTER JOIN model_bs ON model_bs.model_a_id = model_as.id')
.group('model_as.id')
.having('SUM(CASE WHEN model_bs.deleted_at IS NULL THEN 0 ELSE 1 END) AS OCT = 0')

Related

ActiveRecord, find all records where associated record EITHER does not exist OR exists with nil status

3 things:
- Logistic has_many RentalRequests through Type_Logistic association table.
- Logistic does not have to have a RentalRequest to exist (i.e., there's no Type_Logistic association present)
- RentalRequest has a status_id column
I would like to find all the Logistic records where EITHER there are no associated RentalRequests OR the associated RentalRequest has a status_id of nil. My current solution is multi-step:
assoc_RR_has_nil_status = Logistic.includes(:rental_requests).where(rental_requests: {status_id: nil}).pluck(:id)
no_assoc_RR = Logistic.includes(:rental_requests).where(rental_requests: {id:nil}).pluck(:id)
inc_logistics_ids = assoc_RR_has_nil_status | no_assoc_RR
#Incomplete_logistics = Logistic.find(inc_logistics_ids.sort)
But I'm wondering if there's a way to do this in one single where or chain of wheres.
Thanks!
EDIT! Updated my code above because the original merge represents an AND not an OR.
Don't be afraid to use SQL. It is a great tool!
Logistic.
joins("LEFT OUTER JOIN type_logistics ON type_logistics.logistic_id = logistics.id").
joins("LEFT OUTER JOIN rental_requests ON type_logistics.rental_request_id = rental_requests.id").
where("rental_requests.id IS NULL OR rental_requests.status_id IS NULL")
If you must avoid SQL:
It is possible to generate the same query as above using just Ruby code (no SQL snippets). For example you could use ARel, such as is suggested in this SO discussion.
ARel is great for constructing queries at a high level of abstraction. For the majority of cases I've encountered it is much simpler to use SQL snippets than ARel. Choose whichever is best for your use case.

Count, empty? fails for ActiveRecord with outer joins

I have two models, Monkey and Session, where Monkey has_many Session. I have a scope for Monkey:
scope :with_session_counts, -> {
joins("LEFT OUTER JOIN `sessions` ON `sessions`.`monkey_id` = `monkeys`.`id`")
.group(:id)
.select("`monkeys`.*, COUNT(DISTINCT `sessions`.`id`) as session_count")
}
in order to grab the number of associated Sessions (even when 0).
Querying #monkeys = Monkey.with_session_counts works as expected. However, when I test in my view:
<% unless #monkeys.empty?%>
I get this error:
Mysql2::Error: Column 'id' in field list is ambiguous:
SELECT COUNT(*) AS count_all, id AS id FROM `monkeys`
LEFT OUTER JOIN `sessions` ON `sessions`.`monkey_id` = `monkeys`.`id`
GROUP BY `monkeys`.`id`
How would I convince Rails to prefix id with the table name in presence of the JOIN?
Or is there a better alternative for the OUTER JOIN?
This applies equally to calling #monkeys.count(:all). I'm using RoR 4.2.1.
Update:
I have a partial fix for my issue (specify group("monkeys.id") explicitly) I wonder whether this is a bug in the code that generates the SELECT clause for count(:all). Note that in both cases (group("monkeys.id") and group(:id)) the GROUP BY part is generated correctly (i.e. with monkeys.id), but in the latter case the SELECT only contains id AS id. The reason I say 'partial' is because it works in that it does not break a call to empty?, but a call to count(:all) returns a Hash {monkey_id => number_of_sessions} instead of the number of records.
Update 2:
I guess my real question is: How can I get the number of associated sessions for each monkey, so that for all intents and purposes I can work with the query result as with Monkey.all? I know about counter cache but would prefer not to use it.
I believe it is not a bug. Like you added on your update, you have to specify the table that the id column belongs to. In this case group('monkeys.id') would do it.
How would the code responsible for generating the statement know the table to use? Without the count worked fine because it adds points.* to the projection and that is the one used by group by. However, if you actually wanted to group by Sessions id, you would have to specify it anyway.

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.

In Ruby on Rails, how do I query for items that have no associated elements in a has_many :through relationship

I have a Contact model, and a User model, and a join table and each is HABTM more or less.
How can I query for the Contacts that have no users assigned to them? Driving me nuts.
Thanks
IMHO you should do a raw SQL query something along the lines of....
select c.*
from contacts c left join contacts_users cu on c.id = cu.contact_id
where cu.contact_id is null
I don't know of any pretty ORM-specific way to do it. Obviously you'll want to tailor the query to use the actual fields from your table.
I believe this thread is looking for the same thing right?
Want to find records with no associated records in Rails 3
If I understand you correctly, then I think it could be something like:
Contact.includes(:jointablenames).where( :jointablenames => {:contact_id => nil } )

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