mm-partial-update

mm-partial-update is a MongoMapper plugin that enables a change-only persistence strategy for MongoMapper. mm-partial-update uses the enhanced dirty tracking provided by mm-dirtier to send only changes to MognoDB, rather than the entire document on each save. *note: mm-partial update requires the rails3 branch of MongoMapper. It will not work with the master branch, the one you get when you ‘gem install mongo_mapper’

Installation

mm-partial-update is available as a RubyGem:

gem install mm_partial_update

To activate the plugin, add ‘mm_partial_update’ to your gemfile

gem 'mm_partial_update'

Usage

mm-partial-update does not, by default, change MongoMappers normal mode of operation. Although mm-partial-update can be configured, either at a global level or on a model by model basis, to use a partial update strategy for all persistence operations, out of the box it simply adds #save_changes and #save_changes! methods to both MongoMapper::Document and MongoMapper::EmbeddedDocument. #save_changes(!) will persist any changes to the target document using MongoDB’s atomic operators, ideal for making small changes to large documents or for concurrent modification of documents by multiple processes.

As you might expect, #save_changes and #save_changes! mirror the behavior of #save and #save!, in that #save_changes will return false if the document (or embedded document) having its changes saved fails validation, whereas #save_changes! will raise a MongoMapper::InvalidDocument error.

Both methods behave identically to their native MongoMapper counterparts with respect to both validations and callbacks.

For example:

class Person
  include MongoMapper::Document
  key :name, String
  many :pets
end

class Pet
  include MongoMapper::EmbeddedDocument
  key :name, String
end

person = Person.create! :name=>"Willard"
person.name = "Poe"
person.save! #as always, overwrites the document in the database with the in memory copy

person = Person.create! :name=>"Willard"
person.name = "Poe"
person.save_changes! #only saves the changed fields (in this case name=>"Poe"

You can also persist only a part of a document:

person = Person.create! :name=>"Willard"
person.name = "Benji"
pet = person.pets.build :name=>"Magma"

person.changed? #= > true
pet.changed? # => true

pet.save_changes

pet.changed? # => false
person.changed? # => true

In addition to #save_changes(!), partial saves can be enabled globally:

MmPartialUpdate.default_persistence_strategy = :changes_only

Or on a model by model basis:

class Person
  include MongoMapper::Document
  persistence_strategy :changes_only
  key :name, String
  many :pets
end

When enabled globally, all calls to #save or #save! across all models will simply delegate to #save_changes or #save_changes!, resulting in a partial update. When enabled on a particular model, calls to save or save! on instances of that model and any subclasses of that model will result in partial updates.

Known Issues & Limitations

  • mm_partial_update will persist fully embedded documents on a change_only basis (i.e. issue $push, $pull and $set commands as appropriate). However, for the time being in_array_proxies behave like embedded Array keys, and persist as they always have in an all or nothing fashion. This will be changed in a future version.

  • mm_partial_update will detect and persist changes to embedded Array and Hash keys, but only at a single level of depth. This means that modifying an array or hash contained within your embedded array or hash key will not trigger the dirty tracking mechanism (i.e. my_doc.tags.meta = 5 would not cause my_doc.tags to appear changed). This is a very solvable problem, and a fix will likely appear in a future version, particularly if someone needs it (I myself do not).

  • mm-partial-update does not (perhaps cannot?) behave in a truly atomic fashion. Because MongoDB appears to take a shotgun approach to detecting and preventing ‘conflicting updates’ in a single command, mm-partial-update breaks a single set of changes into multiple isolated commands to the database. Currently, the algorithm used is not very sophisticated, and results in a single update command for each unique embedded array that requires a push or a pull. In the future if this ends up presenting a performance problem, more sophisticated (but complicated) algorithms can be implemented to intelligently batch non-conflicting pushes and pulls together, resulting in a minimum of database calls. I’m holding off on that optimization until it proves necessary, however, mostly for maintainability reasons.

Note on Patches/Pull Requests

  • Fork the project.

  • Make your feature addition or bug fix.

  • Add tests for it. This is important so I don’t break it in a future version unintentionally.

  • Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)

  • Send me a pull request. Bonus points for topic branches.

Copyright © 2089 Nathan Stults. See LICENSE for details.