Recipe 13.12. Using Object Relational Mapping with Og
Credit: Mauro Cicio
Problem
You want to store data in a database, without having to use SQL to create or access the database.
Solution
Use the Og (ObjectGraph) library, available as the og gem. Where ActiveRecord has a database-centric approach to object-relational mapping, Og is Ruby-centric. With ActiveRecord, you define the database schema ahead of time and have the library figure out what the Ruby objects should look like. With Og, you define the Ruby objects and let the library take care of creating the database schema.
The only restriction Og imposes on your class definitions is that you must use special versions of the decorator methods for adding attribute accessors. For instance, instead of calling attribute to define accessor methods, you call property.
Here we define a basic schema for a weblog program, like that defined in Recipe 13.11:
require 'cookbook_dbconnect'
require 'og'
class BlogPost
property :title, :content, String
end
class Comment
property :author, :content, String
belongs_to :
og_post,
BlogPost
end
# Now that Comment's been defined, add a reference to it in BlogPost.
class BlogPost
has_many :comments, Comment
end
After defining the schema, we call the og_connect method defined in the chapter introduction. Og automatically creates any necessary database tables:
og_connect
# Og uses the Mysql store.
# Created table 'ogcomment'.
# Created table 'ogblogpost'.
Now we can create a blog post and some comments:
post = BlogPost.new
post.title = "First post"
post.content = "Here are some pictures of our iguana."
post.save!
[["Alice", "That's one cute iguana!"],
["Bob", "Thank you, Alice!"]].each do |author, content|
comment = Comment.new
comment.blog_post = post
comment.author = author
comment.content = content
comment.save!
end
As with ActiveRecord, we can query the tables, relate blog posts to their comments, and relate comments back to their blog posts:
post = BlogPost.first
puts %{#{post.comments.size} comments for "#{post.title}"}
# 2 comments for "First post"
post.comments.each do |comment|
puts "Comment author: #{comment.author}"
puts "Comment: #{comment.content}"
end
# Comment author: Alice
# Comment: That's one cute iguana!
# Comment author: Bob
# Comment: Thank you, Alice!
puts %{The first comment was made on "#{Comment.first.blog_post.title}"}
# The first comment was made on "First post"
Discussion
Like the ActiveRecord library,
Og implements Martin Fowler's Active Record Pattern. While ActiveRecord does this by making all classes derive from the base class ActiveRecord::Base,
Og does it by using custom attribute accessors instead of the traditional Ruby accessors. In this example, Comment and BlogPost are POR (Plain Old Ruby) classes, with accessor methods like author and author=, but those methods were defined with Og decorators instead of the standard Ruby decorators. This table shows the mapping between the two sets of decorators.
Table 13-2. Standard Ruby accessors | Og accessors |
|---|
attribute | roperty | attr_accessor | prop_accessor | attr_reader | prop_reader | attr_writer | prop_writer |
Each of the Og decorator methods takes a Ruby class as its last argument: String, Integer, or the like. Og uses this to define the type of the corresponding database row. You can also specify
Object as a field type, and Og will transparently store YAML representations of arbitrary Ruby objects in the corresponding database field.
ActiveRecord defines all kinds of conventions about how you're supposed to name your database tables and fields. Og doesn't care: it names database tables and fields that correspond to the names you use in your Ruby code.
Just as with ActiveRecord, relationships between Og tables are defined within Ruby code, using decorator methods. The API is almost exactly the same as ActiveRecord's. In the Solution section, we saw how to create a one-to-many relationship between blog posts and comments: by calling belongs_to in Comment and has_many in BlogPost. This relationship makes it possible to simply call BlogPost#comments and get an array of comments on a post.
Og defines two more decorator methods for describing relationships between tables. One of them is the has_one association, which is rarely used: if there's a one-to-one relationship between the rows in two tables, then you should probably just merge the tables.
The other decorator is many_to_many, which lets you to join two different tables with an intermediate join table. This lets you create many-to-many relationships, common in (to take one example) permissioning systems.
For an example of many_to_many, let's make our blog a collaborative effort. We'll add a User class that holds the posts' authors' names, and fix it so that each blog post can have multiple authors. Of course, each author can also contribute to multiple posts, so we've got a many-to-many relationship between users and blog posts. Og needs to know the class definition in order to create the necessary database tables, so the following code snippet should appear before the
og_connect invocation in your
program:
class Person
property :name, String
many_to_many :posts, BlogPost
end
The many_to_many decorator tells Og to create a table to store the people, and a join table to map authors to their blog posts. It also defines methods that navigate the join table, as we'll see in a moment.
Of course, the many-to-many relationship goes both ways: BlogPost has a many-to-many relationship to Person. So add a many_to_many call to the definition of BlogPost (this, too, must show up before your og_connect call):
class BlogPost
many_to_many :authors, Person
end
With these relationships in place, it's easy to find blog posts for an author, and authors for a blog post:
og_connect
# Retroactively make Bob and Carol the collaborative authors of our
# first blog post.
['Bob', 'Carol'].each do |name|
p = Person.new
p.name = name
p.save
end
Person.find_by_name('Bob').add_post(post)
Person.find_by_name('Carol').add_post(post)
author = Person.first
puts "#{author.name} has made #{author.posts.size} blog post(s)."
# Bob has made 1 blog post(s).
puts %{The blog post "#{post.title}" has #{post.authors.size} author(s).}
# The blog post "First post" has 2 author(s).
To add an anonymous BlogPost on the fly, use the add_post method as follows:
author.add_post(BlogPost.create_with({
:title => 'Second post',
:content => 'We have some cats as well.'
} ))
Since Person posts returns an array-like
object, you can iterate over it to find all the blog posts to which a given user contributed:
author.posts.each do |post|
puts %{#{author.name}'s blog post "#{post.title}" has #{post.comments.size}
comments.}
end
# Bob's
blog post "First post" has 2 comments.
# Bob's
blog post "Second post" has 0 comments.
If you want to delete an
object from the database, you can use the delete method available to all Og database objects:
BlogPost.first.delete
Deleting a blog post will automatically remove all the comments associated with that blog post. This automatic deletion (i.e., cascade deletion) is not always a good idea. For instance, we don't want the authors of a blog post to be deleted when the post itself is deleted! We can avoid the cascade deletion by passing false in as an argument to the delete method:
BlogPost.first.delete(false)
If you want some associated objects (like comments) to get cascade-deleted, and other objects (like authors) to be left alone, the best strategy is to implement the cascade yourself, in post-delete hooks.
See Also
 |