jgaskins / perpetuity

Persistence gem for Ruby objects using the Data Mapper pattern
http://jgaskins.org/blog/2012/04/20/data-mapper-vs-active-record/
MIT License
250 stars 14 forks source link

Object serialization #8

Closed jgaskins closed 11 years ago

jgaskins commented 11 years ago

Serializing objects into something we can store in the DB is a little tricky at the moment and not at all clean. I think we can clean it up by taking a more generic process:

  1. Inside Mapper#serialize, when iterating over the attributes to serialize, we ask the DB adapter (Perpetuity::MongoDB) if it can serialize the class of each one. If it can, we just add it to the hash of attributes.
  2. If the attribute is a collection (Array, Set, Hash, other Enumerable or anything responding to each), we need to run this same process over each of the elements.
  3. If we come to one that can't be serialized natively by the DB driver, we check to see if we have a mapper for it. If there is a mapper registered for that class, we run its serialize method on the object.
  4. If we get to this point, the object is unserializable and we can either Marshal.dump it or raise an error.

For example, let's say we have an Article class that contains some strings, an author (maybe a Person or User object) and an array of Comment objects:

class Article
  attr_accessor :title, :body, :comments, :author
  def initialize(title='', body='', author=nil, comments=[])
    @title, @body, @author, @comments = title, body, author, comments
  end
end

class Person
  attr_accessor :name

  # ...
end

Comment = Struct.new(:body, :author)

Perpetuity.generate_mapper_for Article do
  attribute :title
  attribute :body
  attribute :comments, embedded: true
  attribute :author
end

Perpetuity.generate_mapper_for Person do
  attribute :name
end

Perpetuity.generate_mapper_for Comment do
  attribute :body
  attribute :author
end

Let's say the author attribute is a reference to a document in another collection while the comments array embeds the Comment objects inside the document. When we serialize an article, we want the resulting data to look something like this:

{
  title: 'Perpetuity rocks!',
  body: 'Lorem ipsem dolor sit amet …',
  author: {
    __metadata__: {
      class: 'Person',
      id: 42
    }
  },
  comments: [
    {
      __metadata__: {
        class: 'Comment'
      }
      body: 'zomg lol',
      author: {
        __metadata__: {
          class; 'Person'
          id: 42,
        }
      }
    },
  ]
}

The author gets serialized as a referenced attribute, so enough information is stored in some metadata hash to be able to recreate it.

The comments in this case would be configured to be an embedded attribute, so the comments are saved entirely within the article''s serialized representation rather than within the Comment collection.

Both the author and comments attributes would need their respective mapper objects.

jgaskins commented 11 years ago

I've got this mostly working locally, except I'm not sure what to do about referenced objects (such as the author attribute in the above example). My original design for this was pretty shortsighted, I think — it simply stored the id of the referenced object as the attribute on the object to be loaded in with Mapper#load_association!. The way I ended up implementing it locally has it getting the referenced objects from the DB, but I don't like that idea, since we may not want to load an entire object graph just to find out a few pieces of information about that object.

I still think that load_association is the best thing to do, but probably unserializing the metadata into some sort of Perpetuity::Metadata object and letting the load_association! method grab the class and id from that instead. If I do it right, this could be an idempotent call, simply refreshing the attribute.