ActiveRecord custom after callback current_scope is not nil - ruby-on-rails

I have a model (let's call it Blog). It looks like:
class Blog < ApplicationRecord
belongs_to :author
def self.foo_all
all.each(&:bar)
end
def bar
author.baz
end
end
Now, the problem I'm having is that when author.blogs.foo_all method is called, it seems that the Blog class has a current_scope! This means that when inside the author.baz method, any query on Blog has the author as a scope on the query (WHERE authors.id == 123)!
Using an example where
blog.id == 123
author.id == 456
this is what happens:
Class Author < ApplicationRecord
has_many :blogs
def baz
id # 456
blogs.count # "SELECT count(*) FROM blogs WHERE blogs.author_id == 456"
Blog.count # "SELECT count(*) FROM blogs WHERE blogs.author_id == 456"
Blog.current_scope # "[Blog id: 123, author_id: 456]"
end
end
Yes, I can work around this why explicitly removing that scope, but it's SO unexpected! In what world does Blog.count not perform an unscoped query?!
Note: I do not use a default_scope anywhere.
Rails: 6.1.3.2
Copy of code with rspec test that replicates the problem https://github.com/hrdwdmrbl/StockOverflow69020511

Related

Rails make model methods appear as attributes

I have a model connecting to a Postgres db.
class Person < ApplicationRecord
def say_id
"#{name} has id: #{id}"
end
end
I have some attributes id,name,email as well as the method above: say_id that can be accessed via:
person = Person.new
person.id => 1
person.say_id => "John has id: 1"
I would like to have the method 'say_id' listed as an attribute as well, now when running person.attributes, I'm only seeing: id, name, email
How can I have my method included as a listable information in full, as with person.attributes but which will include my method? A usecase would be for lazily just laying out all these fields in a table of the Person-object.
In Rails 5+ you can use the attributes api to create attributes that are not backed by a database column:
class Person < ApplicationRecord
attribute :foo
end
irb(main):002:0> Person.new.attributes
=> {"id"=>nil, "email"=>nil, "name"=>nil, "created_at"=>nil, "updated_at"=>nil, "foo"=>nil}
Unlike if you used attr_accessor these actually behave very much like database backed attributes.
You can then override the getter method if you wanted to:
class Person < ApplicationRecord
attribute :foo
def foo
"foo is #{super}"
end
end
irb(main):005:0> Person.new(foo: 'bar').foo
=> "foo is bar"
But for whatever you're doing its still not the right answer. You can get a list of the methods of an class by calling .instance_methods on a class:
irb(main):007:0> Person.instance_methods(false)
=> [:foo]
Passing false filters out inherited methods.

What's the right way to list all paper_trail versions including associations?

Question regarding the paper_trail gem.
When only associations change, a version record won't be created for the main model. So what's the right way to list all versions for a certain record including its associations?
Should I query something like this? (The bad point is this SQL query might be long and low performance.)
f = "(item_type = 'Place' AND item_id = ?) OR (item_type = 'PlaceName' AND item_id IN (?))"
PaperTrail::Version.where(f, #place.id, #place.names.map { |n| n.id })
Or should I create a version record when only associations changed? I think #DavidHam tried the same thing and asked a similar question but nobody has answered it yet.
So, I sort of found a way to do this, but it's not exactly pretty and it doesn't create a new version everytime an association is changed. It does, however, give you an efficient way to retrieve the versions chronologically so you can see what the version looked like before/after association changes.
First, I retrieve all the ids for for the asscoiation versions given the id of that model:
def associations_version_ids(item_id=nil)
if !item_id.nil?
ids = PaperTrail::VersionAssociation.where(foreign_key_id: item_id, foreign_key_name: 'item_id').select(:version_id)
return ids
else
ids = PaperTrail::VersionAssociation.where(foreign_key_name: 'item_id').select(:version_id)
return ids
end
end
Then I get all versions together using the VersionAssociation ids from this function. It will return a chronological array of PaperTrail::Version's. So the information is useful for an activity log, etc. And it's pretty simple to piece back together a version and its associations this way:
def all_versions
if !#item_id.nil?
association_version_ids = associations_version_ids(#item_id)
all_versions = PaperTrail::Version
.where("(item_type = ? AND item_id = ?) OR id IN (?)", 'Item', #item_id, association_version_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
else
assocation_ids = associations_version_ids
all_versions = PaperTrail::Version
.where("item_type = ? OR id IN (?)", 'Item', association_ids)
.where("object_changes IS NOT NULL")
.order(created_at: :desc)
return all_versions
end
end
Again, not a perfect answer since there isn't a new version everytime there's a change, but it's manageable.
This is more of an approach than a specific answer, but here goes.
In my case, I needed a version history such that any time anyone changed a Child, they also changed a flag on the `Parent'. But I needed a way to show an audit trail that would show the initial values for all the children, and an audit line for the parent whenever anyone changed a child.
So, much simplified, it's like this:
class Parent < ActiveRecord::Base
has_paper_trail
has_many :children
end
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent
end
So, whenever there's a change on a Child we need to create a version on the Parent.
First, try changing Child as follows:
class Child < ActiveRecord::Base
has_paper_trail
belongs_to :parent, touch: true
end
This should (should! have not tested) create a timestamp on the Parent whenever the Child changes.
Then, to get the state of the :children at each version of Parent, you search the Child's versions for the one where the transaction_id matches the Parent.
# loop through the parent versions
#parent.versions.each do |version|
#parent.children.versions.each do |child|
# Then loop through the children and get the version of the Child where the transaction_id matches the given Parent version
child_version = child.versions.find_by transaction_id: version.transaction_id
if child_version # it will only exist if this Child changed in this Parent's version
# do stuff with the child's version
end
This worked in my situation, hope something in here is useful for you.
[UPDATED]
I found a better way. You need to update associations inside transaction to make this code work.
class Place < ActiveRecord::Base
has_paper_trail
before_update :check_update
def check_update
return if changed_notably?
tracking_has_many_associations = [ ... ]
tracking_has_has_one_associations = [ ... ]
tracking_has_many_associations.each do |a|
send(a).each do |r|
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
tracking_has_one_associations.each do |a|
r = send(a)
if r.send(:changed_notably?) || r.marked_for_destruction?
self.updated_at = DateTime.now
return
end
end
end
end
class Version < PaperTrail::Version
def associated_versions
Version.where(transaction_id: transaction_id) if transaction_id
end
end
[Original Answer]
This is the best way I've found so far. (#JohnSchaum's answer helps me a lot, thanks!)
Before starting, I've added polymorphic_type column to the versions table.
class AddPolymorphicTypeToVersions < ActiveRecord::Migration
def change
add_column :versions, :polymorphic_type, :string
end
end
And setup models like this:
# app/models/version.rb
class Version < PaperTrail::Version
has_many :associations, class_name: 'PaperTrail::VersionAssociation'
end
# app/models/link.rb
class Link < ActiveRecord::Base
has_paper_trail meta: { polymorphic_type: :linkable_type }
belongs_to :linkable, polymorphic: true
end
# app/models/place.rb
class Place < ActiveRecord::Base
has_paper_trail
has_many :links, as: :linkable
def all_versions
f = '(item_type = "Place" AND item_id = ?) OR ' +
'(foreign_key_name = "place_id" AND foreign_key_id = ?) OR ' +
'(polymorphic_type = "Place" AND foreign_key_id = ?)'
Version.includes(:associations).references(:associations).where(f, id, id, id)
end
end
And we can now get versions including associations like following:
#place.all_versions.order('created_at DESC')

Rails scope or def based on related table field

I have a Rails 3 application that contains a table called worequests.
worequest.rb model contains:
belongs_to :statuscode
Statuscodes has a boolean field called closed.
I want to be able to get a list of all worequests where worequest.statuscode.closed == true.
For example:
def index2
#search = Worequest.closed.search(params[:q])
#worequests = #search.result
end
OR
worequests.notclosed.count
I tried these in the worequest model:
scope :closed, joins(:statuscode).where(:statuscode.closed => true)
scope :closed, joins(:statuscode).& Statuscode.closed
scope :closed, joins(:statuscode) & Statuscode.closed
def self.closed
joins(:statuscode) & Statuscode.closed
end
def self.notclosed
joins(:statuscode) & Statuscode.notclosed
end
Thanks for the help!
Since Rails 4, named scopes must be given a callable, such as a lambda:
scope :closed, -> { joins(:statuscode).where(statuscodes: { closed: true }) }
should do the trick (note the singular :statuscode in the join and the plural statuscodes table name in the where condition).
I'm not sure I understand the question correctly.
Do you just want a scope on your class so you get all closed ones?
class StatusCode < ActiveRecord:Base
belongs_to :worequest
end
class Worequest < ActiveRecord:Base
has_one :status_code do
def closed
where(closed: true)
end
end
end
closed_quests: Worequest.all.closed
I've based my answer on the question asked here: How do you scope ActiveRecord associations in Rails 3?
As this is how I interpreted your question.
For rails 4, I recommend using the other answer.

ActiveRecord firing 2 query for search

Here's what is happening with my application.
My model looks like this
class Model1 < ActiveRecord::Base
def self.foo(value)
Model1.where(:field => value)
end
end
and then i have a controller using this model
...
Model1.foo('foo)
...
Now, i am expecting it to trigger a single query to get the records. Instead of that, what i am getting is 2 queries.
SELECT COUNT(*) FROM `MODEL1` WHERE `MODEL1`.`field` = 'foo'
SELECT * FROM `MODEL1` WHERE `MODEL1`.`field` = 'foo'
Not able to understand why the first query is being fired and how to avoid it. Couldn't find anything on net.
I'm a bit confused (like others in the comments) but here's what you can try -
class Model1 < ActiveRecord::Base
def self.foo(value)
Model1.where(:field => value)
end
end
should be
class Model1 < ActiveRecord::Base
def foo(value)
self.where("field = ?", value) #see http://guides.rubyonrails.org/active_record_querying.html#array-conditions
end
end

Rails named_scope inheritance?

I'm trying to generalize some of my models by providing a common base model to inherit from that contains some mutual named_scope declarations and a filter method that activates that search for simpler querying on the controller side. This appears to be working when I run it in the console, but fails when in the controller:
# in the base model
class GenericModel < ActiveRecord::Base
named_scope :by_name, lambda { |name|
( name.blank? ) ? {} : { :conditions => [ "#{self.table_name}.name like ?", "%#{name}%" ] }
}
def filter(params)
res = []
res = self.by_name( (params[:name] or '') ) if params[:name]
return res
end
end
class MyModel < GenericModel
set_table_name 'my_models'
end
# works in in console!
>> params = { :name => 'jimmy' }
>> MyModel.filter(params)
=> [ <#MyModel ...>, ... ]
nil
# fails in controller
#model = MyModel.filter(params)
# ActiveRecord::StatementInvalid (Mysql::Error Unknown column 'generic_models.name' in where clause...)
Apparently the parent class' named_scope is being called when in rails, but works fine in rails console. Any ideas how to mend this? thanks.
That's a bit of a train-wreck because of the way ActiveRecord is trying to interpret what you're saying. Generally the first class derived from ActiveRecord::Base is used to define what the base table name is, and sub-classes of that are defined to use Single Table Inheritance (STI) by default. You're working around this by using set_table_name but, as is often the case, while it's possible to go against the grain in Rails, things often get messy.
You should be able to do this a lot more cleanly using a mixin as suggested by Beerlington.
module ByNameExtension
def self.extended(base)
# This method is called when a class extends with this module
base.send(:scope, :by_name, lambda { |name|
name.blank? ? nil : where("#{self.table_name}.name LIKE ?", "%#{name}%")
})
end
def filter(params)
params[:name].present? ? self.by_name(params[:name]) : [ ]
end
end
class MyModel < ActiveRecord::Base
# Load in class-level methods from module ByNameExtension
extend ByNameExtension
end
You should be able to keep your extensions contained to that module. If you want to clean this up even further, write an initializer that defines a method like scoped_by_name for ActiveRecord::Base that triggers this behavior:
class ActiveRecord::Base
def scoped_by_name
extend ByNameExtension
end
end
Then you can tag all classes that require this:
class MyModel < ActiveRecord::Base
scoped_by_name
end

Resources