Sinatra + Haml で MongoDB を使う - Mongoid, MongoMapper で関連とコンポジションを実装

Sinatra で MongoDB を使うために Mongoid と MongoMapper を試してみました。
以下のようなモデル構成を実装する事にします。

環境は以下の通り、テンプレートエンジンに Haml を使っています。

  • JRuby 1.5.6
    • Sinatra 1.1.3
    • Haml 3.0.25
    • Mongoid 1.9.5
    • MongoMapper 0.8.6
  • MongoDB 1.7.6

サンプルのソースは http://github.com/fits/try_samples/tree/master/blog/20110306/

事前準備

まず、今回使用するパッケージを gem でインストールしておきます。

インストール例
> gem install sinatra
> gem install haml
> gem install mongoid
> gem install mongo_mapper

Mongoid の場合

Mongoid のモデルクラスは以下のようにして定義します。

  • Mongoid::Document を include する事でモデルクラスを定義
  • field でフィールドを定義
    • :type を使って型を指定
  • embeds_many と embedded_in でコンポジションを定義(Book と Comment)
  • belongs_to_related で一方向への関連を定義(Comment から User へ)
models_mongoid/book.rb(Mongoid)
class Book
    include Mongoid::Document

    field :title
    field :isbn

    # コンポジションの定義
    embeds_many :comments
end
models_mongoid/comment.rb(Mongoid)
class Comment
    include Mongoid::Document

    field :content
    field :created_date, :type => Date

    # コンポジションの定義
    embedded_in :book, :inverse_of => :comments

    # User への関連
    belongs_to_related :user
end
models_mongoid/user.rb(Mongoid)
class User
    include Mongoid::Document

    field :name
end

Sinatra による実装は以下です。

Haml テンプレートを使うには haml メソッドにテンプレートのシンボルとオプション、テンプレート内で使用するパラメータを渡します。
以下では全ての Book, User オブジェクト(:books, :users)とコメント追加先のパス(:action)を渡しています。

Mongoid の設定は configure に渡すブロック内で指定できます。以下では使用する DB に book_review を指定しています。

sample_mongoid.rb(Sinatra
require "rubygems"
require "sinatra"
require "haml"
require "mongoid"

require "models_mongoid/book"
require "models_mongoid/user"
require "models_mongoid/comment"

# Mongoid 設定
Mongoid.configure do |config|
    config.master = Mongo::Connection.new.db('book_review')
end

# Top ページ
get '/' do
    haml :index, {}, :books => Book.all.order_by([[:title, :asc]]), :users => User.all.order_by([[:name, :asc]]), :action => '/comments'
end
・・・
# Book 追加
post '/books' do
    Book.create(params[:post])
    redirect '/books'
end

# Comment 追加
post '/comments' do
    b = Book.find(params[:post][:book_id])
    b.comments << Comment.new(:content => params[:post][:content], :created_date => Time.now, :user_id => params[:post][:user_id])
    b.save

    #以下でも可
    # Book.find(params[:post][:book_id]).comments.create(:content => params[:post][:content], :created_date => Time.now, :user_id => params[:post][:user_id])

    redirect '/'
end
・・・

Top ページの Haml テンプレートは以下の通りです。

注意点として、= や #{・・・} をそのまま使うとデフォルトでは HTML エスケープしてくれないので、クロスサイトスクリプティングなどの対策に HTML エスケープを行いたい場合、:escape_html オプションを true に設定するか(Sinatrahaml メソッドの第2引数で渡せば良い)、代わりに &= や & #{・・・} を使います。
今回は、後者のやり方で実装してみました。

views/index.hamlHaml
.menu Menu
%ul
  %li
    %a(href="/books") Books List
  %li
    %a(href="/users") Users List

.list Book Comments
%form.post(action='#{action}' method='post')
  %select(name='post[user_id]')
    - users.each do |u|
      %option(value='#{u._id}')&= u.name
  %select(name='post[book_id]')
    - books.each do |b|
      %option(value='#{b._id}')&= b.title
  %input(name='post[content]' type='text')
  %input(type='submit' value='Add')

- books.each do |b|
  %ul
    %li&= b.title
    %ul
      - b.comments.each do |c|
        %li
          & #{c.content} : #{c.user.name}, #{c.created_date}
実行例

MongoDB を実行しておきます。

> mongod -dbpath db

Sinatra を実行します。

> jruby sample_mongoid.rb

実行画面は以下のようになります。

ちなみに、mongo コマンドを使って DB 内の books コレクションの内容を確認してみると、関連とコンポジションの実現方法の違いがよく分かると思います。

mongo コマンドで books コレクションの内容を確認
> mongo
・・・
> use book_review
switched to db book_review
> db.books.find()
{
  "_id" : "4d7253961875e20940000003", 
  "comments" : [{
      "content" : "check",
      "created_date" : ISODate("2011-03-06T00:00:00Z"),
      "user_id" : "4d7253811875e20940000001",
      "_id" : "4d7253b31875e20940000006"
  }], 
  "isbn" : "1234", 
  "title" : "Railsレシピブック"
}
・・・

MongoMapper の場合

MongoMapper のモデルクラスは以下のようにして定義します。

  • MongoMapper::Document や MongoMapper::EmbeddedDocument を include する事でモデルクラスを定義
  • key でフィールドを定義
    • 第2引数で型を指定
  • many と MongoMapper::EmbeddedDocument でコンポジションを定義(Book と Comment)
  • belongs_to で一方向の関連を定義(Comment から User へ)

なお、以下では _id を String 型で定義していますが、これは Mongoid のサンプルで使った DB をそのまま使えるようにするためです。(MongoMapper の場合、_id はデフォルトの ObjectId 型で定義されるため、このままでは Mongoid 版で使った DB が使えません)

models_mongomapper/book.rb(Mongoid)
class Book
    include MongoMapper::Document

    # デフォルトで _id は ObjectId 型になるので String を指定
    key :_id, String
    key :title
    key :isbn

    # コンポジションの定義
    many :comments
end
models_mongomapper/comment.rb(Mongoid)
class Comment
    include MongoMapper::EmbeddedDocument

    # デフォルトで _id は ObjectId 型になるので String を指定
    key :_id, String
    key :content
    key :created_date, Date

    # User への関連
    belongs_to :user
end
models_mongomapper/user.rb(Mongoid)
class User
    include MongoMapper::Document

    # デフォルトで _id は ObjectId 型になるので String を指定
    key :_id, String
    key :name
end

Sinatra による実装は以下。
Mongoid 版とほとんど同じですが、order の指定の仕方が異なります。
なお、Haml テンプレートは Mongoid 版と共通です。

sample_mongomapper.rb(Sinatra
require "rubygems"
require "sinatra"
require "haml"
require "mongo_mapper"

require "models_mongomapper/book"
require "models_mongomapper/user"
require "models_mongomapper/comment"

# MongoMapper 設定
MongoMapper.connection = Mongo::Connection.new('localhost')
MongoMapper.database = 'book_review'

# Top ページ
get '/' do
    haml :index, {}, :books => Book.all(:order => 'title'), :users => User.all(:order => 'name'), :action => '/comments'
end
・・・
# Comment 追加
post '/comments' do
    b = Book.find(params[:post][:book_id])
    b.comments << Comment.new(:content => params[:post][:content], :created_date => Time.now, :user_id => params[:post][:user_id])
    b.save

    redirect '/'
end
・・・
実行例
> jruby sample_mongomapper.rb