ODB: A Ruby Object Database
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Failed to load latest commit information.


ODB: A Ruby Object Database


ODB attempts to be a transparent persistence layer in the Ruby interpreter inspired by Object Oriented Databases like Python's Zope Object Database and more recently, MagLev. The goal is to make object persistence as invisible as possible in Ruby, though full transparency (as MagLev attempts) can never really be possible.


ODB supports:

  • Transparent Persistent Objects

  • ACID Transactions

  • Implicit Key Names

  • Lazy Loading of Attributes

Quick Start

ODB looks very similar to a standard key-value store database such as Memcached, Redis, or Tokyo Tyrant. For instance, we create a database and access objects by key names as follows:

require 'odb'

db = ODB.new
db[:key] = "Hello"
# and now...
db[:key] == "Hello" # => true

This is very basic usage. Unlike key-value stores, however, it is possible to store and access objects implicitly, without directly assigning them a key. We will see this more with persistent objects

Transparent Persistent Objects

Persistent objects allow a more transparent form of storing objects. To mark a method as persistent, simply include the Persistent module:

class Post
  include ODB::Persistent

  attr_accessor :title, :body

  def initialize(title, body)
    super() # needed for Persistent's constructor
    @title, @body = title, body

You can now benefit from transparent saves in transactions.

ACID Transactions

ODB supports very basic transactions right now. A simple example of a transaction is:

db = ODB.new
db.transaction do
  post = Post.new("foo", "bar")

At this point, the Post object will now be persisted either in memory or on disk (whichever datastore you use). This implicit storage only works for newly created objects, not modified ones. If post were to be modified, it would need to be marked for a save. Hopefully this API can also become more transparent, though it seems unlikely with Ruby's object model. To save a modified object, queue it:

db.transaction do
  post.title = "something else"

Transactions occur in an all-or-nothing fashion, so if an exception is raised in the middle of a save, nothing will be persisted:

db.transaction do
  db[:post1] = Post.new
  raise "exception!"
  db[:post2] = Post.new

db[:post1] # => nil
db[:post2] # => nil

Implict Key Names

You may be wondering how to read the post back out after it's been persisted to disk. This is why symbolic key-names are used in key-value stores. We can use the following to save the object to a specific symbolic name as we did in the first example:

db[:key] = post

However we can create generalized key names for any new post object, to implicitly key any saved post object by a symbolic determinate name. All we need to do is override serialize_key to return a Symbol:

class Post
  def __serialize_key__; title.to_sym end

Here we return the title, since we assume it is unique for the purpose of this example. Now when we do:

posts = []
db.transaction do
  posts << Post.new("title1", "body1")
  posts << Post.new("title2", "body2")

We can access both objects as:

db[:title1] # => #<Post:0x000001011dfe30 @title="title1", @body="body1">
db[:title2] # => #<Post:0x00000101379550 @title="title2", @body="body2">

Lazy Loading of Attributes

ODB can perform lazy-loading on any standard Ruby attribute. When an object is deserialized, each instance variable is checked to see if there is a zero-argument method by the same name. If so, it (temporarily) replaces the method with a stub that will lazily load the object when the attribute is read. Consider this example:

class Post
  include ODB::Persistent
  attr_accessor :title, :comments

  def initialize(title) 
    @comments = [] 
    @title = title

  def __serialize_key__; @title.to_sym end

# Create a post with 100 comments
db = ODB.new
db.transaction do
  post = Post.new("hello")
  100.times {|i| post.comments << "This is comment ##{i}" }

db.clear_cache # force de-serialization 
p db[:hello]
# => #<Post:0x0b149008 @comments=(lazyload value), @title=(lazyload value)>
p db[:hello].title 
# => "hello"
p db[:hello].comments
# => ["This is comment #0", "This is comment #1", ...]

One of the benefits of shallow reads (a.k.a. lazy loading) is that you're not immediately deserializing associations. If you had a complex object structure that held cyclic associations or associations to a root object in your tree, you could end up de-serializing the entire database in one read, which would obviously not be a good thing. ODB's lazy attributes allow reads on an object to be very fast while maintaining associations transparently.

The Supported Data Stores

Currently ODB has support for in-memory, on-disk, JSON (also on-disk) and redis data stores. One benefit is that it is possible to write a wrapper for any key-value data store such as memcached, mongo, etc.. To implement a data store, you just need to implement the read_object and write_object methods in the Database class.

Copyright & License

Copyright Loren Segal © 2009, licensed under the {file:LICENSE MIT License}