Deep clone document with embedded associations

2020-02-10 07:59发布

问题:

How would you go about deep cloning a document in MongoDB (mongoid)

I've tried something like this;

original = Car.find(old_id)
@car = original.clone
@car._id = BSON::ObjectId.new

But I get problems deserialization of the values afterwards.

How can I make a deep clone with all the documents attributes except the _id?

Edit: After following Zachary's example I got some problems with a custom serialization class for the duplicated documents.

class OptionHash
  include Mongoid::Fields::Serializable

  # Convert the keys from Strings to Symbols
  def deserialize(object)
    object.symbolize_keys!
  end

  # Convert values into Booleans
  def serialize(object)
    object.each do |key, value|
    object[key] = Boolean::MAPPINGS[value]
  end
end

Object is nil for duplicated documents. Car.find(old_id).attributes indeed doesn't include the field with the custom serialization, why is that and how can I include it?

回答1:

You don't need to call .clone on this, you can use the raw data from attributes. For example the below method/example will give new ids throughout the entire document if it finds one.

def reset_ids(attributes)
    attributes.each do |key, value|
        if key == "_id" and value.is_a?(BSON::ObjectId)
            attributes[key] = BSON::ObjectId.new
        elsif value.is_a?(Hash) or value.is_a?(Array)
            attributes[key] = reset_ids(value)
        end        
    end
    attributes
end


original = Car.find(old_id)
car_copy = Car.new(reset_ids(original.attributes))

And you now have a copy of Car. This is inefficient though as it has to go through the entire hash for the record to figure out if there are any embedded documents in an embedded document. You would be better off resetting the structure yourself if you know how it'll be, for example, if you have a parts embedded into car, then you can just do:

original = Car.find(old_id)
car_copy = Car.new(original.attributes)
car_copy._id = BSON::ObjectId.new
car_copy.parts.each {|p| p._id = BSON::ObjectId.new}

Which is a lot more efficient than just doing a generic reset.



回答2:

You have to use Car.instantiate if you have localized fields so the code is

def reset_ids(attributes)
    attributes.each do |key, value|
        if key == "_id" && value.is_a?(Moped::BSON::ObjectId)
            attributes[key] = Moped::BSON::ObjectId.new
        elsif value.is_a?(Hash) || value.is_a?(Array)
            attributes[key] = reset_ids(value)
        end        
    end
    attributes
end

car_original = Car.find(id)
car_copy = Car.instantiate(reset_ids(car_original.attributes))
car_copy.insert

This solution is not very clean but i don't have found better.