Ruby on Rails Triggers Update on Serialized Attribute Every Time

Ruby on Rails Triggers Update on Serialized Attribute Every Time

This is expected behavior. It is very difficult to detect changes within a serialized attribute, so they are updated on every save.

Consider the following (ruby 1.8.7) irb session:

ruby-1.8.7-p352 :001 > x = "--- \n:b: 2\n:a: 1\n"
=> "--- \n:b: 2\n:a: 1\n"
ruby-1.8.7-p352 :002 > y = "--- \n:a: 1\n:b: 2\n"
=> "--- \n:a: 1\n:b: 2\n"
ruby-1.8.7-p352 :003 > x == y
=> false
ruby-1.8.7-p352 :004 > YAML.load(x) == YAML.load(y)
=> true

Rails 3 only update serialized attribute when changed

Model with serialized columns being saved all the time is the expected behaviour according to this answer.

I haven't tried "store configurable gem" but it sounds like it should be smart enough to detect that.

Is there a way to prevent serialized attributes in rails from getting updated even if there are not changes?

Yes, that was bugging me too. This is what I did for Rails 2.3.14 (or lower):

# config/initializers/nopupdateserialize.rb

module ActiveRecord
class Base
class_attribute :no_serialize_update
self.no_serialize_update = false
end
end

module ActiveRecord2
module Dirty

def self.included(receiver)
receiver.alias_method_chain :update, :dirty2
end

private

def update_with_dirty2
if partial_updates?
if self.no_serialize_update
update_without_dirty(changed)
else
update_without_dirty(changed | (attributes.keys & self.class.serialized_attributes.keys))
end
else
update_without_dirty
end
end

end
end

ActiveRecord::Base.send :include, ActiveRecord2::Dirty

Then in your controller use:

model_item.no_serialize_update = true
model_item.update_attributes(params[:model_item])
model_item.increment!(:hits)
model_item.update_attribute(:nonserializedfield => "update me")

etc.

Or define it in your model if you do not expect any changes to the serialized field once created (but update_attribute(:serialized_field => "update me" still works!)

class Model < ActiveRecord::Base
serialize :serialized_field

def no_serialize_update
true
end

end

Fields_for build repeating itself every time I update

The point in your code is, when Photo.friendly.find(params[:id]) have some sponsor already created and then when you @photo.sponsors.build, you are adding an empty sponsor, so, you need to build only when a Photo.friendly has not a sponsor, I think this can help you

def edit
@photo = Photo.friendly.find(params[:id])
@photo.sponsors.build unless @photo.sponsors.any?
end

Is there a way to avoid automatically updating Rails timestamp fields?

Do this in a migration or in a rake task (or in the new database seeds if you're on edge rails):

ActiveRecord::Base.record_timestamps = false
begin
run_the_code_that_imports_the_data
ensure
ActiveRecord::Base.record_timestamps = true # don't forget to enable it again!
end

You can safely set created_at and updated_at manually, Rails won't complain.

Note:
This also works on individual models, e.g.
User.record_timestamps = false

Rails create or update magic?

Rails 6

Rails 6 added an upsert and upsert_all methods that deliver this functionality.

Model.upsert(column_name: value)

[upsert] It does not instantiate any models nor does it trigger Active Record callbacks or validations.

Rails 5, 4, and 3

Not if you are looking for an "upsert" (where the database executes an update or an insert statement in the same operation) type of statement. Out of the box, Rails and ActiveRecord have no such feature. You can use the upsert gem, however.

Otherwise, you can use: find_or_initialize_by or find_or_create_by, which offer similar functionality, albeit at the cost of an additional database hit, which, in most cases, is hardly an issue at all. So unless you have serious performance concerns, I would not use the gem.

For example, if no user is found with the name "Roger", a new user instance is instantiated with its name set to "Roger".

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

Alternatively, you can use find_or_initialize_by.

user = User.find_or_initialize_by(name: "Roger")

In Rails 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

You can use a block, but the block only runs if the record is new.

User.where(name: "Roger").first_or_initialize do |user|
# this won't run if a user with name "Roger" is found
user.save
end

User.find_or_initialize_by(name: "Roger") do |user|
# this also won't run if a user with name "Roger" is found
user.save
end

If you want to use a block regardless of the record's persistence, use tap on the result:

User.where(name: "Roger").first_or_initialize.tap do |user|
user.email = "email@example.com"
user.save
end

Searching serialized data, using active record

Basically, you can't. The downside of #serialize is that you're bypassing your database's native abstractions. You're pretty much limited to loading and saving the data.

That said, one very good way to slow your application to a crawl could be:

MyModel.all.select { |m| m.mycode.include? 43402 }

Moral of the story: don't use #serialize for any data you need to query on.

How to update_attributes without executing before_save?

In rails 3.1 you will use update_column.

Otherwise:

In general way, the most elegant way to bypass callbacks is the following:

class Message < ActiveRecord::Base
cattr_accessor :skip_callbacks
before_save lambda { foo(publisher); bar }, :unless => :skip_callbacks # let's say you do not want this callback to be triggered when you perform batch operations
end

Then, you can do:

Message.skip_callbacks = true # for multiple records
my_message.update_attributes(:created_at => ...)
Message.skip_callbacks = false # reset

Or, just for one record:

my_message.update_attributes(:created_at => ..., :skip_callbacks => true)

If you need it specifically for a Time attribute, then touch will do the trick as mentioned by @lucapette .



Related Topics



Leave a reply



Submit