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

6

10

This is probably one of the things that all new users find out about Rails sooner or later. I just realized that rails is updating all fields with the serialize keyword, without checking if anything really changed inside. In a way that is the sensible thing to do for the generic framework.

But is there a way to override this behavior? If I can keep track of whether the values in a serialized fields have changed or not, is there a way to prevent it from being pushed in the update statement? I tried using "update_attributes" and limiting the hash to the fields of interest, but rails still updates all the serialized fields.

Suggestions?

Bradberry answered 5/1, 2012 at 15:39 Comment(0)
B
1

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
Bouleversement answered 24/1, 2012 at 11:57 Comment(4)
that looks pretty promising. Thanks! Now that I know what to override, I might just do that (in conjunction with super) instead of using the method chaining, unless there are caveats in taking that approach. Let me know if you see any issues if I simply use override + super.Bradberry
I tried that too, but noticed that didn't work. The problem is that update_with_dirty is already loaded (through the original chain) before you have the chance to override it and use super.Bouleversement
@Tabrez: Warning: I had a strange bug in my application that the acts-as-versioned and friendly_id gems didn't create a record in my database when a model was updated. It looked like the before_update callback stopped working in my models. It took me a long time before I could link it to the above code in my application. I removed it now. Not sure (yet) why this happened.Bouleversement
@Bouleversement I just added an answer that explains your problemUnregenerate
A
2

Here is a similar solution for Rails 3.1.3.

From: https://sites.google.com/site/wangsnotes/ruby/ror/z00---topics/fail-to-partial-update-with-serialized-data

Put the following code in config/initializers/

ActiveRecord::Base.class_eval do
  class_attribute :no_serialize_update
  self.no_serialize_update = false
end

ActiveRecord::AttributeMethods::Dirty.class_eval do
  def update(*)
    if partial_updates?
      if self.no_serialize_update
        super(changed)
      else
        super(changed | (attributes.keys & self.class.serialized_attributes.keys))
      end
    else
      super
    end
  end
end
Amalee answered 18/5, 2012 at 4:5 Comment(1)
It works (tried on RoR 3.2.18). Have you added: def no_serialize_update true end to the model containing the serialize you wish you wont be updated by default?Coverdale
B
1

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
Bouleversement answered 24/1, 2012 at 11:57 Comment(4)
that looks pretty promising. Thanks! Now that I know what to override, I might just do that (in conjunction with super) instead of using the method chaining, unless there are caveats in taking that approach. Let me know if you see any issues if I simply use override + super.Bradberry
I tried that too, but noticed that didn't work. The problem is that update_with_dirty is already loaded (through the original chain) before you have the chance to override it and use super.Bouleversement
@Tabrez: Warning: I had a strange bug in my application that the acts-as-versioned and friendly_id gems didn't create a record in my database when a model was updated. It looked like the before_update callback stopped working in my models. It took me a long time before I could link it to the above code in my application. I removed it now. Not sure (yet) why this happened.Bouleversement
@Bouleversement I just added an answer that explains your problemUnregenerate
T
1

I ran into this problem today and ended up hacking my own serializer together with a getter and setter. First I renamed the field to #{column}_raw and then used the following code in the model (for the media attribute in my case).

require 'json'

...

def media=(media)
  self.media_raw = JSON.dump(media)
end

def media
  JSON.parse(media_raw) if media_raw.present?
end

Now partial updates work great for me, and the field is only updated when the data is actually changed.

Tropical answered 22/10, 2012 at 17:3 Comment(1)
awesome solution! i'd be interested to hear if there are any drawbacks, perhaps performance? having to parse the raw everytime?Audubon
U
1

The problem with Joris' answer is that it hooks into the alias_method_chain chain, disabling all the chains done after (like update_with_callbacks which accounts for the problems of triggers not being called). I'll try to make a diagram to make it easier to understand.

You may start with a chain like this

update -> update_with_foo -> update_with_bar -> update_with_baz

Notice that update_without_foo points to update_with_bar and update_without_bar to update_with_baz

Since you can't directly modify update_with_bar per the inner workings of alias_method_chain you might try to hook into the chain by adding a new link (bar2) and calling update_without_bar, so:

alias_method_chain :update, :bar2

Unfortunately, this will get you the following chain:

update -> update_with_bar2 -> update_with_baz

So update_with_foo is gone!

So, knowing that alias_method_chain won't let you redefine _with methods my solution so far has been to redefine update_without_dirty and do the attribute selection there.

Unregenerate answered 7/12, 2012 at 21:22 Comment(0)
R
1

Not quite a solution but a good workaround in many cases for me was simply to move the serialized column(s) to an associated model - often this actually was a good fit semantically anyway.

Romano answered 26/5, 2015 at 14:5 Comment(0)
M
0

There is also discussions in https://github.com/rails/rails/issues/8328.

Marvelmarvella answered 17/10, 2013 at 5:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.