Consider the model
class Product < ActiveRecord::Base
scope :queued, lambda { where(queued: true) }
scope :unqueued, lambda { where(queued: false) }
default_scope unqueued
end
Product.first yields the SQL query
SELECT "products".* FROM "products" WHERE "products"."queued" = 'f'
LIMIT 1
Now what if I want to create a record that is not "true to the default scope?" Like so:
Product.queued.create!
The product is in fact created, but ActiveRecord yields an error since it tries to find the product by it's id AND the default scope:
ActiveRecord::RecordNotFound:
Couldn't find Product with id=15 [WHERE "products"."queued" = 'f']
Is there a workaround for this? I need to make sure that the product I create is queued. A simple workaround would be
p = Product.create
p.update_column(:queued, true)
It seems like the wrong answer to another problem though, or perhaps it is the right answer. Are there alternatives?
Thanks for your time.
The best solution would be to not use default_scope. default_scope should only be used when you always need to apply the scope when searching for records. So, if you ever need to find a record where queued is true, then you should not be using default_scope.
One other way to get past default_scope is to use the unscoped method, i.e.:
Product.unscoped.queued
But, in general, if you need to use ActiveRecord to find queued Products, I would recommend removing your default_scope.
Related
I can disable STI for a complete model but I'd like to just disable it when doing certain queries. Things were working swell in Rails 3.2, but I've upgraded to Rails 4.1.13 and a new thing is happening that's breaking the query.
I have a base class called Person with a fairly complex scope.
class Person < ActiveRecord::Base
include ActiveRecord::Sanitization
has_many :approvals, :dependent => :destroy
has_many :years, through: :approvals
scope :for_current_or_last_year, lambda {
joins(:approvals).merge(Approval.for_current_or_last_year) }
scope :for_current_or_last_year_latest_only, ->{
approvals_sql = for_current_or_last_year.select('person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved').group(:id).to_sql
approvals_sql = select("*").from("(#{approvals_sql}) AS ap, approvals AS ap2").where('ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year').to_sql
select("people.id, ap_approved, ap_year_id, ap_status_name, ap_active").
joins("JOIN (#{approvals_sql}) filtered_people ON people.id =
filtered_people.person_id").uniq
}
end
And inheriting classes called Member and Staff. The only thing related to this I had to comment out to get my tests to pass with Rails 4. It may be the problem, but uncommenting it hasn't helped in this case.
class Member < Person
#has_many :approvals, :foreign_key => 'person_id', :dependent => :destroy, :class_name => "MemberApproval"
end
The problem happens when I do the query Member.for_current_or_last_year_latest_only
I get the error unknown column 'people.type'
When I look at the SQL, I can see the problem line but I don't know how to remove it or make it work.
Member.for_current_or_last_year_latest_only.to_sql results in.
SELECT DISTINCT people.id, ap_approved, ap_year_id, ap_status_name, ap_active
FROM `people`
JOIN (SELECT * FROM (SELECT person_id AS ap_person_id, MAX(year_id) AS max_year, year_id AS ap_year_id, status_name AS ap_status_name, active AS ap_active, approved AS ap_approved
FROM `people` INNER JOIN `approvals` ON `approvals`.`person_id` = `people`.`id`
WHERE `people`.`type` IN ('Member') AND ((`approvals`.`year_id` = 9 OR `approvals`.`year_id` = 8))
GROUP BY `people`.`id`) AS ap, approvals AS ap2
WHERE `people`.`type` IN ('Member') AND (ap2.person_id = ap.ap_person_id AND ap2.year_id = ap.max_year)) filtered_people ON people.id = filtered_people.person_id
WHERE `people`.`type` IN ('Member')
If I remove people.type IN ('Member') AND from the beginning of the second to last WHERE clause the query runs successfully. And btw, that part isn't in the query generated from the old Rails 3.2 code, neither is the one above it (only the last one matches the Rails 3.2 query). The problem is, that part is being generated from rails Single Table Inheritance I assume, so I can't just delete it from my query. It's not the only place that is getting added into the original query, but that's the only one that is causing it to break.
Does anybody have any idea how I can either disable STI for only certain queries or add something to my query that will make it work? I've tried putting people.type in every one of the SELECT queries to try and make it available but to no avail.
Thanks for taking the time to look at this.
I was apparently making this harder than it really was...I just needed to add unscoped to the front of the two approval_sql sub-queries. Thanks for helping my brain change gears.
I have created a validation rule to limit the number of records a member can create.
class Engine < ActiveRecord::Base
validates :engine_code, presence: true
belongs_to :group
delegate :member, to: :group
validate :engines_within_limit, on: :create
def engines_within_limit
if self.member.engines(:reload).distinct.count(:engine_code) >= self.member.engine_limit
errors.add(:engine, "Exceeded engine limit")
end
end
end
The above doesn't work, specifically this part,
self.member.engines(:reload).distinct.count(:engine_code)
The query it produces is
SELECT "engines".*
FROM "engines"
INNER JOIN "groups"
ON "engines"."group_id" = "groups"."id"
WHERE "groups"."member_id" = $1 [["member_id", 22]]
and returns the count 0 which is wrong
Whereas the following
Engine.distinct.count(:engine_code)
produces the query
SELECT DISTINCT COUNT(DISTINCT "engines"."engine_code")
FROM "engines"
and returns 3 which is correct
What am I doing wrong? It is the same query just with a join?
After doing long chat, we found the below query to work :
self.member
.engines(:reload)
.count("DISTINCT engine_code")
AR:: means ActiveRecord:: below.
The reason for the "wrong" result in the question is that the collection association isn't used correct. A collection association (e.g. has_many) for a record is not a AR::Relation it's a AR::Associations::CollectionProxy. It's a sub class of AR::Relation, and e.g. distinct is overridden.
self.member.engines(:reload).distinct.count(:engine_code) will cause this to happen:
self.member.engines(:reload) is a
AR::Associations::CollectionProxy
.distinct on that will first
fire the db read, then do a .to_a on the result and then doing
"it's own" distinct which is doing a uniq on the array of records
regarding the id of the records.
The result is an array.
.count(:engine_code) this is doing Array#count on the array which is returning
0 since no record in the array equals to the symbol :engine_code.
To get the correct result you should use the relation of the association proxy, .scope:
self.member.engines(:reload).scope.distinct.count(:engine_code)
I think it's a little bit confusing in Rails how collection associations is handled. Many of the "normal" methods for relations works as usual, e.g. this will work without using .scope:
self.member.engines(:reload).where('true').distinct.count(:engine_code)
that is because where isn't overridden by AR::Associations::CollectionProxy.
Perhaps it would be better to always have to use .scope when using the collection as a relation.
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
The model:
class Venue < ActiveRecord::Base
has_many :free_unids, :class_name => "Unid",
:conditions => ['id not in (?)',
(Spot.where('unid_id is not null').map(&:unid_id) + [-1])]
end
Accessing #venue.free_unids triggers an evaluation of the condition itself as we can see in the log:
Unid Load (0.4ms) SELECT "unids".* FROM "unids" WHERE "unids"."venue_id" = 79 AND (id not in (4,8723,8889,-1)) ORDER BY id LIMIT 1
Problem is that the subquery (Spot.where('unid_id is not null') / (4,8723,8889,-1)) often does not reflect the new records inserted into Spot a few seconds ago. And debugging at the line where the relation is accessed (pp Spot.where('unid_id is not null')) yields the correct set of records, including the new ones.
To me it looks like if the subquery expression result is cached, but I have to admit that I do not quite understand the logic behind the curtains here...
Is it possible to force the evaluation of the expression on every access? Or do we need another approach here?
I gave up the relation approach and went for the query method solution like this:
def free_unids
return Unid.where(
'venue_id = ? and id not in (?)',
id,
(Spot.where('unid_id is not null').map(&:unid_id) + [-1])).
order('id')
end
See my comment to Rob's answer for details on why I chose this solution.
Using:
#venue.reload
before accessing the association should work.
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 :-)