Object changed handle with custom Serializer - ruby-on-rails

I have defined a custom Serializer
class CustomSerializer
def self.dump(obj)
obj.to_h
end
def self.load(obj)
CustomClass.new(obj)
end
end
and used in a active record model
class Klass < ActiveRecord::Base
serialize :my_column, CustomSerializer
end
Now when use an object of Klass k = Klass.first I always see k.changed? = true
I understand this is because of the class reference introduced by serializer I have defined
k.my_column_was # #<CustomClass:0x00007fd9063d6288>
k.my_column # #<CustomClass:0x00007fd9080d9088>
How can I fix this behaviour?

Rails 5 Attributes API will allow you to manipulate how dirty tracking is handled.
This is the recommended method for domain specific serialization.
For more complex cases, such as conversion to or from your application domain objects, consider using the ActiveRecord::Attributes API.
For rails < 5 unfortunately the documents state:
A notable side effect of serialized attributes is that the model will be updated on every save, even if it is not dirty.
Which is what you have stumbled across

Related

How to know if an "includes" call was made while working with a single record?

Motivation
The motivation was that I want to embed the serialization of any model that have been included in a Relation chain. What I've done works at the relation level but if I get one record, the serialization can't take advantage of what I've done.
What I've achieved so far
Basically what I'm doing is using the method includes_values of the class ActiveRecord::Relation, which simply tells me what things have been included so far. I.e
> Appointment.includes(:patient).includes(:slot).includes_values
=> [:patient, :slot]
To take advantage of this, I'm overwriting the as_json method at the ActiveRecord::Relation level, with this initializer:
# config/initializers/active_record_patches.rb
module ActiveRecord
class Relation
def as_json(**options)
super(options.merge(include: includes_values)) # I could precondition this behaviour with a config
end
end
end
What it does is to add for me the option include in the as_json method of the relation.
So, the old chain:
Appointment.includes(:patient).includes(:slot).as_json(include: [:patient, :slot])
can be wrote now without the last include:
Appointment.includes(:patient).includes(:slot).as_json
obtaining the same results (the Patient and Slot models are embedded in the generated hash).
THE PROBLEM
The problem is that because the method includes_values is of the class ActiveRecord::Relation, I can't use it at the record level to know if a call to includes have been done.
So currently, when I get a record from such queries, and call as_json on it, I don't get the embedded models.
And the actual problem is to answer:
how to know the included models in the query chain that retrieved the
current record, given that it happened?
If I could answer this question, then I could overwrite the as_json method in my own Models with:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend Associations
def as_json(**options)
super(options.merge(include: included_models_in_the_query_that_retrieved_me_as_a_record))
end
end
One Idea
One Idea I have is to overwrite the includes somewhere (could be in my initializer overwriting directly the ActiveRecord::Relation class, or my ApplicationRecord class). But once I'm there, I don't find an easy way to "stamp" arbitrary information in the Records produced by the relation.
This solution feels quite clumsy and there might be better options out there.
class ApplicationRecord < ActiveRecord::Base
def as_json(**options)
loaded_associations = _reflections.each_value
.select { |reflection| association(reflection.name).loaded? }
.map(&:name)
super(options.merge(include: loaded_associations))
end
end
Note that this only loads 1st level associations. If Appointment.includes(patient: :person) then only :patient will be returned since :person is nested. If you plan on making the thing recursive beware of circular loaded associations.
Worth pointing out is that you currently merge include: ... over the provided options. Giving a user no choice to use other include options. I recommend using reverse_merge instead. Or swap the placements around {includes: ...}.merge(options).

Override ActiveRecord::Base

some of my models has a "company_id" column, that I want to set automatically. So I thought to override some method in activerecord base.
I tried this, in config/initializers, but does not work:
class ActiveRecord::Base
after_initialize :init
def init
if (self.respond_to(:company_id))
self.company_id= UserSession.find.user.company_id
end
end
end
Solution after Simone Carletti answer:
I created a module:
module WithCompany
def initialize_company
self.company_id= UserSession.find.user.company_id
end
end
And included this in the model:
class Exam < ActiveRecord::Base
include WithCompany
after_initialize :init
def init
initialize_company
end
end
Is there something else that I can do?
update 2
Best practices says to do not set session related fields in models. Use controllers for that.
There are two problems here. The first, is that you are injecting a bunch of stuff into all ActiveRecord models, whereas it would be better to add the feature only to the relevant models.
Secondary, you are breaking the MVC pattern trying to inject into the model the session context.
What you should do instead, is to code your feature in a module, and mix the module only in the relevant models. As per the context, rather than overriding the default AR behavior, add a new method where you pass the current session context (dependency injection) and returns the model initialized with the required company, when the session is set properly and the model is company-aware.

Stop ActiveRecord saving a serialized column even if not changed

This is very similar to Rails partial updates problem with hashes , but the question has not really been answered IMHO.
The problem is this: I have a model with a serialized column:
class Import < AR::Base
serialize :data
In my case, this data will, and should, not change after the first save/creation of the model. So I want to disable the feature of AR that always saves serialized columns (which is normally a good idea, as it can't detect those changes). I want to disable the saving because the data can be quite large, and the model will be updated frequently.
I've already tried monkeypatching into ActiceRecord::AttributeMethods::Dirty like this:
class Import
def update(*)
if partial_updates?
super(changed | (attributes.keys & (self.class.serialized_attributes.keys - ["data"])))
else
super
end
end
but this seems to have no effect. Anybody got a better idea ?
This is under Rails 3.0.12
What I ended up doing, even though it's not really an answer to the original question, is the following:
class Import < AR::Base
belongs_to :storage
class Storage < AR::Base
serialize :data
...i.e. moving the data column into a model of its own, and associate that with the original model. Which is actually conceptually somewhat cleaner.
Here is an ugly monkey patch solution:
module ActiveRecord
module AttributeMethods
module Dirty
def update(*)
if partial_updates?
# Serialized attributes should always be written in case they've been
# changed in place. Unless it is 'spam', which is expensive to calculate.
super(changed | (attributes.keys & self.class.serialized_attributes.keys - ['spam']))
else
super
end
end
private :update
end
end
end
class Foo < ActiveRecord::Base
serialize :bar
serialize :spam
def calculate_spam
# really expensive code
end
def cache_spam!
calculated_spam = calculate_spam
#changed_attributes['spam'] = [spam, calculated_spam]
self.update_attribute(:spam, calculated_spam)
end
end
You will have to remember to call cache_spam!, or your serialized attribute will never be saved.

ActiveRecord Always Run Some Code After Retrieval

I'm trying to always run some code after .find or .where or whatever is used to retrieve objects.
For example, the following describes what I want, but does not work
Class Person < ActiveRecord::BA
#mortality=true
end
I want #mortality=true to run whenever a Person object is created
And based on my current understanding of ORM/ActiveRecord, a new object is created whenever retrieval is done. Hopefully that is correct.
You want to do this in the after_initialize method:
class Person < ActiveRecord::Base
def after_initialize
#mortality = true
end
end
Note that this is something you should avoid doing if possible because it happens on every object, even when you retrieve enormous result sets.
In this (albeit simple) case, you can do the assignment lazily by overriding the getter:
class Person < ActiveRecord::Base
def mortality
#mortality.nil? ? true : #mortality
end
end
(you can't use the nil gate ||= here because it filters false values as well)
http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html
Look for after_find and after_initialize callbacks.

usage of attr_accessor in Rails

When do you use attr_reader/attr_writer/attr_accessor in Rails models?
Never, unless you have specific need for it. Automatic database-backed accessors are created for you, so you don't need to worry.
Any attr_accessors you do create will change the relevant #attr on the rails object, but this will be lost when the object is destroyed, unless you stick it back in the database. Sometimes you do want this behavior, but it's unusual in a rails app.
Now in ruby, it's a different story, and you end up using these very frequently. But I'd be surprised if you need them in rails---especially initially.
attr_accessor can be used for values you don't want to store in the database directly and that will only exist for the life of the object (e.g. passwords).
attr_reader can be used as one of several alternatives to doing something like this:
def instance_value
"my value"
end
Rails models are just ruby classes that inherit from ActiveRecord::Base. ActiveRecord employs attr_accessors to define getters and setters for the column names that refer to the ruby class's table. It's important to note that this is just for persistence; the models are still just ruby classes.
attr_accessor :foo is simply a shortcut for the following:
def foo=(var)
#foo = var
end
def foo
#foo
end
attr_reader :foo is simply a shortcut for the following:
def foo
#foo
end
attr_writer :foo is a shortcut for the following:
def foo=(var)
#foo = var
end
attr_accessor is a shortcut for the getter and setter while attr_reader is the shortcut for the getter and attr_writer is a shortcut for just the setter.
In rails, ActiveRecord uses these getters and setters in a convenient way to read and write values to the database. BUT, the database is just the persistence layer. You should be free to use attr_accessor and attr_reader as you would any other ruby class to properly compose your business logic. As you need to get and set attributes of your objects outside of what you need to persist to the database, use the attr_s accordingly.
More info:
http://apidock.com/ruby/Module/attr_accessor
http://www.rubyist.net/~slagell/ruby/accessors.html
What is attr_accessor in Ruby?
If you are using it to validate the acceptance of the terms_of_service, you should really consider using validates :terms_of_service, :acceptance => true. It will create a virtual attribute and is much more concise.
http://guides.rubyonrails.org/active_record_validations.html#acceptance.
One example is to have a number of options stored in one serialized column. Form builder would complain if you try to have a text field for one of these options. You can use attr_accessor to fake it, and then in the update action save it in the serialized column.

Resources