Wednesday, June 20, 2007

Clone ActiveRecord Models

I posted this in my tumblelog too, but it's just so cool, I have to post it here in case anybody misses it:

model_first.attributes.each {|attr, value| eval("new_model.#{attr}= first_model.#{attr}")}

Awesome.

If you need to clone a model from another model, this is all you need to do. It can throw validation errors in some cases, where it violates validates_uniqueness_of constraints, but you can check the model and adjust accordingly. In fact, you can do that programmatically, so this could be added to ActiveRecord::Base. And if you add it to Base, it works. I know because I've done it.

So cool!

Update: this'll be available in plugin form soon. Probably after this coming weekend. I'm mostly just doing it for fun, but I could see this being useful for people. I created it because I need it for a project.

8 comments:

  1. Isn't this the same as:

    new_model.attributes = old_model.attributes


    Or am I missing something?

    ReplyDelete
  2. Marcel,

    Your way will ignore any attr_protected attributes, while Giles' implementation will clone them as well. Depends on what behavior is desired then, I suppose.

    One thing I've found with stuff like this is often times I don't want to clone foreign keys. It'd be trivial to write an attr_not_cloneable macro though of course.

    ReplyDelete
  3. Also the cool kids would use
    self.send("#{attr}="), model.send(attr)
    instead of eval

    ReplyDelete
  4. Any chance you might adjust your feed to show a few blank lines before the Feedburner switch notice? It looks like a part of the last paragraph, and that's a wee bit annoying. :)

    Also, is there a reason to show that notice when I've already switched to your Feedburner feed?

    ReplyDelete
  5. rytmis - sorry about the Feedburner lameness. Feedburner pulls that notice from the original feed, so I'm just turning off the switch notice in the original feed. Thanks for switching, by the way.

    marcel - I would have said yes, except for that attr_protected thing Pat mentioned. I don't know exactly what that is, though, so I'm going to have to settle for maybe.

    pat - yeah, cloning foreign keys is bad, also unique attributes obviously if you're cloning two models of the same class. thanks for telling me how my code would have looked if I was cool. :-p

    ReplyDelete
  6. attr_protected protects attributes from mass assignment.

    class Employee < ActiveRecord::Base
    attr_protected :salary
    end


    >> e = Employee.new :name => "Pat Maddox", :salary => "10000000"
    => #<Employee:0x35e4964 @attributes={"name"=>"Pat Maddox", "salary"=>nil}, @new_record=true>


    See how the salary is nil? We have to set it using the accessor:

    >> e.salary = 10000000
    => 10000000
    >> e
    => #<Employee:0x35e4964 @attributes={"name"=>"Pat Maddox", "salary"=>10000000}, @new_record=true>


    attr_protected also kicks in with Marcel's example of self.attributes = other.attributes:

    >> e2 = Employee.new
    => #<Employee:0x35d3aec @attributes={"name"=>nil, "salary"=>nil}, @new_record=true>
    >> e2.attributes = e.attributes
    => {"name"=>"Pat Maddox", "salary"=>10000000}
    >> e2.name
    => "Pat Maddox"
    >> e2.salary
    => nil


    However when we do it your way, we're not doing a mass assignment - rather we're iterating over the attributes and using the individual accessors:

    >> e2 = Employee.new
    => #<Employee:0x35b5f4c @attributes={"name"=>nil, "salary"=>nil}, @new_record=true>
    >> e2.clone_from e
    => {"name"=>"Pat Maddox", "salary"=>10000000}
    >> e2.name
    => "Pat Maddox"
    >> e2.salary
    => 10000000


    Hope that makes sense.

    ReplyDelete
  7. Ah yes. See, that's exactly what I was thinking of at the time. That's why I didn't use attributes.

    ReplyDelete
  8. Looks to me that this does not work for models which have has_and_belongs_to_many relationships. I keep getting complaints about duplicate keys. I'm currently copying like this:
    result.dishes << original.dishes

    ReplyDelete

Note: Only a member of this blog may post a comment.