Creating ActiveRecord scope with multiple conditionals - ruby-on-rails

I have a Rails application with a number of Products, some of which are associated with an Issue model. Some of these products have an issue_id (so an Issue has_many products) and some do not. The products without an issue ID are a new addition I'm working on.
I previously had a named scope so that I can list products using Product.published, which looks like this:
scope :published, -> {
joins(:issue).reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ?', Issue.statuses[:published])
}
The result of this is that I can find only products that are associated with a published issue (think magazine issue).
I'm now adding products that will not be associated with a particular issue but will still have a draft/available state. The above scope does not find these products, as it looks for an issue_id that does not exist.
I thought I could modify the scope like this, adding the OR issue_id IS NULL part in the last line:
scope :published, -> {
joins(:issue).reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ? OR issue_id IS NULL', Issue.statuses[:published])
}
But this doesn't work. I still only get 'available' products associated with a 'published' issue. The products without an issue_id are not included in the returned collection.
(There is a window in which a product will be set to available before its associated issue is published, so for these situations I do need to check the status of both records.)
Here's the SQL generated by the above (wrapped for readability):
pry(main)> Product.published.to_sql
=> "SELECT `products`.* FROM `products` INNER JOIN `issues` ON `issues`.`id` =
`products`.`issue_id` WHERE (products.status = 1) AND (issues.status = 1 OR
issue_id IS NULL) ORDER BY products.created_at ASC, products.number ASC"
I've already created a Product class method that takes an argument as an alternate approach but doesn't work in all cases because I'm often looking up a product based on the ID without knowing in advance whether there's an Issue association or not (eg, for the product's show view).
Also, Product.published is nice and concise and the alternative is to load all published products (eg, Product.where(:status => :published)) and then iterate through to remove those associated with a not-yet-published issue in a second operation.
I feel like there's something I'm not quite grasping about doing more complex queries within a scope. My ideal outcome is a modified scope that can return available products, both with and without an issue, and without supplying an argument.
Is this possible, or should I resign myself to finding an alternate approach now that I'm adding these unassociated products?

The problem is that you are using joins(:issue). That method does an INNER JOIN between products and issues tables and discards all the products that doesn't have an issue_id. Maybe you could use LEFT JOIN so you can keep all the products regardless they have an issue.
scope :published, -> {
joins('LEFT JOIN issues ON issues.id = products.issue_id')
.select('products.*')
.reorder('products.created_at ASC, products.number ASC')
.where('products.status = ?', Product.statuses[:available])
.where('issues.status = ? OR products.issue_id IS NULL', Issue.statuses[:published])
}

Related

Rails query: get all parent records based on latest child records

An order has_many order_events.
An order_event is a record of the state of the order, e.g., pending_approval, confirmed, etc.
Changes to an order are tracked by creating order_events.
I usually get the current state of an order by doing something like: order.order_events.last.try(:state).
I would like a query to get all of the orders with a given current state, e.g., all orders where the current state is cancelable.
I initially had a scope in the OrderEvent model:
scope :cancelable, -> { where('state = ? OR state = ?', 'pending_approval', 'pending_confirmation') }
and then a scope in the Order model:
scope :with_dates_and_current_state_cancelable, -> { with_dates.joins(:order_events).merge(OrderEvent.cancelable) }
and simply used the latter for other purposes.
The problem here is that it returns all orders that are currently or have in the past satisfied the condition.
What is the best way to get all of the orders that currently satisfy the condition?
I ended up using a query like this:
scope :with_dates_and_current_state_cancelable, -> {
with_dates
.joins(:order_events)
.where('order_events.created_at = (SELECT MAX(order_events.created_at) FROM order_events WHERE order_events.order_id = orders.id)')
.where('order_events.state = ? OR order_events.state = ?', 'pending_approval', 'pending_confirmation')
.group('orders.id')
}
A bit hard to read, but it seems to work.
A classic solution here would be to use Rails enum.
Add this to your order model:
class Order
enum status: [ :pending_approval, :confirmed, etc.. ]
...
end
The status can be changed by doing the following:
# order.update! status: 0
order.pending_approval!
order.pending_approval? # => true
order.status # => "pending_approval"
No need for the order_events model.
To query all the orders that are pending approval:
Order.where(status: :pending_approval)
Edit:
Alternate solution when order_event has necessary columns.
Add a column to the order_event called archived which can either be set to 1 or 0. Set the default scope in the order_event model to this:
default_cope where(:archived => 0)
Assuming 0 is not archived.
Now, when you create a new order event set the old event to 1.
old_event = OrderEvent.find(params[:order_id])
old_event.update(archived: 1)
new_event = OrderEvent.create(...archived: 0)
Whenever you query for pending review like so:
OrderEvent.where(:status => pending_approval)
Only events that are not archived will be shown.
I think I figured out a query that might work. I didn't turn it in to ActiveRecord methods, but here it is:
SELECT t.order_id
FROM
(SELECT MAX(created_at) AS created, order_id
FROM order_events
GROUP BY order_id) as t
INNER JOIN order_events
ON t.order_id = order_events.order_id AND
t.created = order_events.created_at
WHERE order_events.state = 'whatever_state_you_want'

Exclude object if one of the has_many related entities has the attribute with value x

I came across about the problem excluding data, if the attribute x of one of the associated data has the value 'a'.
Example:
class Order < ActiveRecord::Base
has_many :items
end
class Item < ActiveRecord::Base
belongs_to :order
validate_presence_of :status
end
The query should return all Orders that don't have an Item with status = 'paid' (status != 'paid').
Because of the 1:n association an Order can have many Items. And one of the Itmes can have the status = 'paid'. These Orders must be excluded from the result of my query even if the order has other items with status different from 'paid'.
How would I solve this problem:
paid_items = Items.where(status: 'paid').pluck(:order_id)
orders_wo_paid = Order.where('id NOT IN (?)', paid_items)
Is there an ActiveRecord solution, that solves this problem in one query.
Or are there other ways to solve this question?
I 'm not looking for ruby solution such as:
Order.select do |order|
!order.items.pluck(:status).include?('paid')
end
thx for ideas and inspirations.
You can do:
Order.where('orders.id NOT IN (?)', Item.where(status: 'paid').select(:order_id))
If you're using Rails 4.x then:
Order.where.not(id: Item.where(status: 'paid').select(:order_id))
The query you are interested in is the following, but creating with activerecord will be hard/no very readable:
SELECT
orders.*
FROM
orders
LEFT JOIN
order_items ON orders.id = order_items.order_id
GROUP BY
order_items.order_id
HAVING
COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')
Sorry for the sql indentation, I have no idea which are the conventions for it.
A way (not the best one at all) to it with rails (unfortunately writing sql for the most important parts) would be the following:
Order.group(:order_id).joins("LEFT JOIN order_items ON orders.id = order_items.order_id")
.having("COUNT(DISTINCT order_items.id) = COUNT(DISTINCT order_items.status <> 'paid')")
Of course you can play with AREL to get rid of the hard coded sql, but in my opinion it will not be easier to read.
You can have an example of creating lefts joins in this gist: https://gist.github.com/mildmojo/3724189

How do I select only the associated objects in a Rails "where" query?

I have a model Category, which has_many Products, and a Product in turn has_many Categories. When a user searches for a Category, I'd like to return the products of the matching Categories without losing my Arel object. Here's what I have so far:
Category.where("upper(title) like ?", search_term.upcase).map {|category| category.products}.flatten
This does the trick of returning the products, but of course what I have is an array and not Arel. I can get as far as adding an :includes(:products) clause, so I do indeed get the products back but I still have them attached to their categories. How do I adjust my query so that all I get back is an Arel that only addresses products?
If it is products that you want then you should probably start with the Product object when you are searching. For example ,you could do it like this:
Product.joins(:categories).where("upper(categories.title) like ?", search_term.upcase)
The reason I use joins instead of includes is that joins will perform an INNER JOIN instead of LEFT OUTER JOIN which is what you need to only return the products that are actually associated with the found categories.
To make it a little more elegant you could wrap it all up in a scope in your Product model like this:
# In Product.rb
scope :in_categories_like, Proc.new{ |search_term|
joins(:categories).where("upper(categories.title) like ?", search_term.upcase)
}
# In use
#products = Product.in_categories_like(params[:search_term])

Is it possible to delete_all with inner join conditions?

I need to delete a lot of records at once and I need to do so based on a condition in another model that is related by a "belongs_to" relationship. I know I can loop through each checking for the condition, but this takes forever with my large record set because for each "belongs_to" it makes a separate query.
Here is an example. I have a "Product" model that "belongs_to" an "Artist" and lets say that artist has a property "is_disabled".
If I want to delete all products that belong to disabled artists, I would like to be able to do something like:
Product.delete_all(:joins => :artist, :conditions => ["artists.is_disabled = ?", true])
Is this possible? I have done this directly in SQL before, but not sure if it is possible to do through rails.
The problem is that delete_all discards all the join information (and rightly so). What you want to do is capture that as an inner select.
If you're using Rails 3 you can create a scope that will give you what you want:
class Product < ActiveRecord::Base
scope :with_disabled_artist, lambda {
where("product_id IN (#{select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})")
}
end
You query call then becomes
Product.with_disabled_artist.delete_all
You can also use the same query inline but that's not very elegant (or self-documenting):
Product.where("product_id IN (#{Product.select("product_id").joins(:artist).where("artist.is_disabled = TRUE").to_sql})").delete_all
In Rails 4 (I tested on 4.2) you can almost do how OP originally wanted
Application.joins(:vacancy).where(vacancies: {status: 'draft'}).delete_all
will give
DELETE FROM `applications` WHERE `applications`.`id` IN (SELECT id FROM (SELECT `applications`.`id` FROM `applications` INNER JOIN `vacancies` ON `vacancies`.`id` = `applications`.`vacancy_id` WHERE `vacancies`.`status` = 'draft') __active_record_temp)
If you are using Rails 2 you can't do the above. An alternative is to use a joins clause in a find method and call delete on each item.
TellerLocationWidget.find(:all, :joins => [:widget, :teller_location],
:conditions => {:widgets => {:alt_id => params['alt_id']},
:retailer_locations => {:id => #teller_location.id}}).each do |loc|
loc.delete
end

Find all objects with broken association

I have two models in my rails app with a has many and belongs to association.
Category has many items and Item belongs to category.
These models are associated in the normal way through a category_id column in the Item model.
I'm looking for a quick way of finding all elements in the database with broken associations.
i.e. find all categories that exist with no associated items and items that exist with no associated category.
For example, if I have an item with a category_id of 7, but the category with id 7 has been deleted then this would be considered broken.
For your example, to find items with category_id's for categories which don't exist any more:
Item.where('NOT EXISTS (SELECT * FROM categories where category.id = item.category_id)')
You might want to look at this as well:
A rake task to track down missing database indexes (not foreign keys though, but indexes): https://github.com/eladmeidar/rails_indexes
A very effective way is using find_by_sql to let the database do the heavy lifting:
uncategorized_items = Item.find_by_sql("select * from items where category_id IS NULL")
Another way is a named scope:
class Item < ActiveRecord::Base
scope :uncategorized, where(:category_id => nil) # rails 3
# or...
named_scope :uncategorized, :conditions => 'category_id IS NULL'
end
These are just a couple of ideas. I assume that once you've found these broken associations you plan to fix them, right? You might want to use validates_associated in both models if it's important to you that this not happen again.
You can use find_by_sql and a left outer join to find all the items in one table but not another. Here, I use a downloads table and an image_files table (I've only included the SQL):
SELECT d.*, d.image_file_id
from downloads as d
LEFT OUTER JOIN image_files as i
ON i.id = d.image_file_id
WHERE d.image_file_id IS NULL

Resources