Active Record Scope modifies Active Record Relation. Why? - ruby-on-rails

Applying a scope to a an Active Record Relation is permanently modifying the relation. Why?
company_purchases.to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17"
company_purchases.by_state("finalized").to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17 AND \"purchases\".\"state\" = 'finalized'"
company_purchases.to_sql
=> "SELECT \"purchases\".* FROM \"purchases\" WHERE \"purchases\".\"company_id\" = 17 AND \"purchases\".\"state\" = 'finalized'"
I expect the SQL to look different when called on the scope, but I don't understand why the additional where from the scope remains on the next call to company_purchases without the scope.
The scope definition
scope :by_state, ->(state) { where(state: state) }
UPDATE
This appears to be a bug with the gem Octopus, see here: https://github.com/thiagopradi/octopus/issues/455
For additional context, the Octopus bug is being introduced because of how company_purchases is composed.
company_purchases = company.purchases
# in Company model
def purchases
Product.using(shard).where(company_id: id)
end

If you default_scope than it will display on every query you make on that model. Instead use scope to avoid above problem.

This appears to be an issue with Octopus, not Active Record scopes or relations.
See: https://github.com/thiagopradi/octopus/issues/455

Related

Creating ActiveRecord scope with multiple conditionals

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])
}

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'

Using state attributes to maintain old records in Rails

I want to keep old records that would be normally destroyed. For example, an user joins a project, and is kicked from it later on. I want to keep the user_project record with something that flags the record as inactive. For this I use a state attribute in each model to define the current state of each record.
Almost all my "queries" want just the "active" records, the records with state == 1, and I want to use the ActiveRecord helpers (find_by etc). I don't want to add to all the "find_by's" I use a "_and_state" to find only the records that are active.
This is what I have now:
u = UserProject.find_by_user_id_and_project_id id1, id2
This is what I will have for every query like this for all models:
u = UserProject.find_by_user_id_and_project_id_and_state id1, id2, 1
What is the most cleaner way to implement this (the state maintenance and the cleaner query code)?
create a scope in your model UserProject:
class UserProject < ActiveRecord::Base
scope :active, where(:state => 1)
end
and "filter" your queries:
u = UserProject.active.find_by_user_id_and_project_id id1, id2
if you "almost allways" query the active UserProjects only, you can define this scope as default_scope and use unscoped if you want to query all records:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
u = UserProject.find_by_user_id_and_project_id id1, id2 # only active UserProjects
u = UserProject.unscoped.find_by_user_id_and_project_id id1, id2 # all states
Here's a range of soft deletion gems you may want to choose from, which offer a nice abstraction that's already been thought through and debugged:
rails3_acts_as_paranoid
acts_as_archive
paranoia
Although if this happens to be your first Rails app, I second Martin's advice of rolling your own implementation.
I tried to just add this to Martin's answer, but my edit has to be reviewed, so even though Martin's answer was great, we can improve on it a little with the idea of default scopes. A default scope is always applied to finders on the model you add them to unless you specifically turn off the default scope:
class UserProject < ActiveRecord::Base
default_scope where(:state => 1)
end
The example Martin gave then becomes:
u = UserProject.find_by_user_id_and_project_id id1, id2
In this case, even without specifying that you want state == 1, you will only get active records. If this is almost always what you want, using a default scope will ensure you don't accidentally leave off the '.active' somewhere in your code, potentially creating a hard-to-find bug.
If you specify your default scope like this:
default_scope :conditions => {:state => 1}
then newly created UserProjects will already have state set to 1 without you having to explicitly set it.
Here's more information on default scopes: http://apidock.com/rails/ActiveRecord/Base/default_scope/class
Here's how to turn them off temporarily when you need to find all records:
http://apidock.com/rails/ActiveRecord/Scoping/Default/ClassMethods/unscoped

Select the complement of a set

I am using Rails 3.0. I have two tables: Listings and Offers. A Listing has-many Offers. An offer can have accepted be true or false.
I want to select every Listing that does not have an Offer with accepted being true. I tried
Listing.joins(:offers).where('offers.accepted' => false)
However, since a Listing can have many Offers, this selects every listing that has non-accepted Offers, even if there is an accepted Offer for that Listing.
In case that isn't clear, what I want is the complement of the set:
Listing.joins(:offers).where('offers.accepted' => true)
My current temporary solution is to grab all of them and then do a filter on the array, like so:
class Listing < ActiveRecord::Base
...
def self.open
Listing.all.find_all {|l| l.open? }
end
def open?
!offers.exists?(:accepted => true)
end
end
I would prefer if the solution ran the filtering on the database side.
The first thing that comes to mind is to do essentially the same thing you're doing now, but in the database.
scope :accepted, lambda {
joins(:offers).where('offers.accepted' => true)
}
scope :open, lambda {
# take your accepted scope, but just use it to get at the "accepted" ids
relation = accepted.select("listings.id")
# then use select values to get at those initial ids
ids = connection.select_values(relation.to_sql)
# exclude the "accepted" records, or return an unchanged scope if there are none
ids.empty? ? scoped : where(arel_table[:id].not_in(ids))
}
I'm sure this could be done more cleanly using an outer join and grouping, but it's not coming to me immediately :-)

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

Resources