Ian C.
Ian C.

Reputation: 3963

Keeping the same attribute in sync on two models in a Rails 2.x application?

I'm working in a large Rails 2.3 application and I have data on a model that would like to move to another model. I need to do this is phases as there are places in the Rails code base that are reading and writing this model data and outside applications reading the table data directly via SQL. I need to allow a period of time where the attribute is synchronized on both models and their associated tables before I drop one model and table altogether.

My models have a has_one and belongs_to relationship like this:

class User < ActiveRecord::Base
  has_one :user_email, :inverse_of => :user
  accepts_nested_attributes_for :user_email
  validates_presence_of :email

  def email=( value )
    write_attribute(:email, value)
    user_email.write_attribute(:email, value)
  end
end

class UserEmail < ActiveRecord::Base
  belongs_to :user, :inverse_of => :user_email
  validates_presence_of :email

  def email=( value )
    write_attribute(:email, value)
    user.write_attribute(:email, value)
  end
end

I'd like to do away with UserEmail and its associated table altogether, but for a time I need to keep email up-to-date on both models so if it's set on one model, it's changed on the other. Overriding email= on each model is straightforward, but coming up with a commit strategy is where I'm hitting a wall.

I have places in the code base that are doing things like:

user.user_email.save!

and I'm hoping to find a way to continue to allow this kind of code for the time being.

I can't figure out a way to ensure that saving an instance of User ensures the corresponding UserEmail data is committed and saving an instance of UserEmail ensures the corresponding User instance data is also committed without creating an infinite save loop in the call backs.

This is the flow I would like to be able to support for the time being:

params = { user: { email: '[email protected]', user_email: { email: '[email protected]' } } }
user = User.create( params )
user.email = "[email protected]"
user.save
puts user.user_email # puts "[email protected]"
user.user_email.email = "[email protected]"
user.user_email.save
user.reload
puts user.email # puts "[email protected]"

Is there a way to achieve this sort of synchronization between the User and UserEmail models so they are kept in sync?

If it helps, I can probably do away with accepts_nested_attributes_for :user_email on User.

Upvotes: 0

Views: 2206

Answers (2)

Vivek Sampara
Vivek Sampara

Reputation: 457

Using ActiveModel::Dirty

In User model

after_save :sync_email, :if => :email_changed?
def sync_email
  user_email.update_column(:email, email) if user_email.email != email
end

In UserEmail model

after_save :sync_email, :if => :email_changed?
def sync_email
  user.update_column(:email, email) if user.email != email
end

Upvotes: 4

Max Williams
Max Williams

Reputation: 32945

Let's assume, for sanity's sake, that the models are "User" and "Cart", and the shared field is "email". I would do this:

#in User
after_save :update_cart_email

def update_cart_email
  if self.changes["email"]
    cart = self.cart
    if cart.email != self.email
      cart.update_attributes(:email => self.email)
    end
  end
end

#in Cart
after_save :update_user_email

def update_user_email
  if self.changes["email"]
    user = self.user
    if user.email != self.email
      user.update_attributes(:email => user.email)
    end
  end
end    

Because we check if the other model's email has already been set, it shouldn't get stuck in a loop.

This works if you drop accepts_nested_attributes_for :user_email -- otherwise you'll get a save loop that never ends.

Upvotes: 3

Related Questions