DataMapperを使ってみる

DataMapper (Ruby向けO/Rマッパー) 以前使ったことがあるものの、最近のバージョンは触っていないので改めて試してみました。

動作環境

DataMapperのインストール

DataMpper 1.2.0 リリースノートより:

# gem install data_mapper dm-sqlite-adapter

実際に試してはいませんが、おそらく事前にlibsqlite3-dev (Ubuntu/Debianの場合) のインストールが必要です。

以下のサンプルプログラムは、DataMpper公式サイト内のGetting Startedを若干アレンジしたものです。

モデル定義

ブログ記事を表現するPost、ブログ記事へのコメントを表現するCommentの2つのモデルオブジェクトを定義します。
モデル定義は、この後作成するスクリプトからrequireできるように単独のファイルとして記述します。
ここでは、テーブルに相当するモデルオブジェクトに属性 (プロパティ) を定義するとともに、オブジェクト間の関連 (association) を has n, belongs_toで定義します。

【サンプルコード】dm_sample_model.rb

require 'data_mapper'

# ブログ記事
class Post
  include DataMapper::Resource

  property :id, Serial
  property :title, String
  property :body, Text
  property :created_at, DateTime

  has n, :comments
end

# コメント
class Comment
  include DataMapper::Resource

  property :id, Serial
  property :posted_by, String
  property :body, Text

  belongs_to :post
end

DataMapper.finalize

マイグレーション

作成したモデル定義に従って、データベース上にテーブル定義を作成します。
ここではデータベースとしてSQLiteを使うため、SQLiteデータベースの接続を記述します。

【サンプルコード】dm_sample_migration.rb

require 'data_mapper'
require 'dm-migrations'
require_relative 'dm_sample_model' # モデル定義

DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite:///tmp/project.db3')

DataMapper.auto_migrate!

【実行結果】

$ ruby dm_sample_migration.rb 
 ~ (0.000342) PRAGMA table_info("posts")
 ~ (0.000034) PRAGMA table_info("comments")
 ~ (0.000025) SELECT sqlite_version(*)
 ~ (0.000057) DROP TABLE IF EXISTS "posts"
 ~ (0.000014) PRAGMA table_info("posts")
 ~ (0.016498) CREATE TABLE "posts" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" VARCHAR(50), "body" TEXT, "created_at" TIMESTAMP)
 ~ (0.000076) DROP TABLE IF EXISTS "comments"
 ~ (0.000014) PRAGMA table_info("comments")
 ~ (0.008521) CREATE TABLE "comments" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "posted_by" VARCHAR(50), "body" TEXT, "post_id" INTEGER NOT NULL)
 ~ (0.002999) CREATE INDEX "index_comments_post" ON "comments" ("post_id")

念のため、sqlite3コマンドで生成されたテーブル定義を見てみます。
モデル定義に含まれる関連に従って、commentsテーブル (Commentクラスに対応) に post_id という属性が自動的に定義されていることが分かります。

$ sqlite3 /tmp/project.db3
sqlite> .schema
CREATE TABLE "comments" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "posted_by" VARCHAR(50), "body" TEXT, "post_id" INTEGER NOT NULL);
CREATE TABLE "posts" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" VARCHAR(50), "body" TEXT, "created_at" TIMESTAMP);
CREATE INDEX "index_comments_post" ON "comments" ("post_id");
(余談) はまりポイント

dm_sample_migration.rbから、同じディレクトリ上にあるモデル定義 (dm_sample_model.rb) を以下のようにしてもロードに失敗します。

require 'data_mapper'
require 'dm-migrations'
require 'dm_sample_model'
..

【実行結果】

$ ruby dm_sample_migration.rb
/usr/local/lib/ruby/1.9.1/rubygems/custom_require.rb:55:in `require': cannot load such file -- dm_sample_model (LoadError)
	from /usr/local/lib/ruby/1.9.1/rubygems/custom_require.rb:55:in `require'
	from dm_sample_migration.rb:3:in `<main>'

irbからライブラリ検索パス ($:) を確認してみると、カレントディレクトリ ('.') が含まれていないことが分かります。

$ irb
irb(main):001:0> $:
=> ["/usr/local/lib/ruby/site_ruby/1.9.1", "/usr/local/lib/ruby/site_ruby/1.9.1/x86_64-linux", "/usr/local/lib/ruby/site_ruby", "/usr/local/lib/ruby/vendor_ruby/1.9.1", "/usr/local/lib/ruby/vendor_ruby/1.9.1/x86_64-linux", "/usr/local/lib/ruby/vendor_ruby", "/usr/local/lib/ruby/1.9.1", "/usr/local/lib/ruby/1.9.1/x86_64-linux"]

結論としては、古いバージョンのRubyではライブラリ検索パス ($:) にカレントディレクトリが含まれていたのが、Ruby 1.9.2から含まれなくなり、代わりにrequire_relativeを使う必要があります。

記事の投稿

次に、新規のPostオブジェクトをデータベースに追加します。
実際のアプリケーションでは記事投稿フォームなどのUIを通して投稿することになりますが、ここではコマンドラインで実行します。

【サンプルコード】dm_sample_post.rb

# -*- coding: utf-8 -*-
require 'data_mapper'
require_relative 'dm_sample_model'

DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite:///tmp/project.db3')

# Postオブジェクトの永続化
post = Post.create(
  :title      => "DataMapper記事の投稿",
  :body       => "記事本文です。",
  :created_at => Time.now
)

【実行結果】

$ ruby dm_sample_post.rb 
 ~ (0.007423) INSERT INTO "posts" ("title", "body", "created_at") VALUES ('DataMapper記事の投稿', '記事本文です。', '2013-03-03T23:26:59+09:00')

ここで、テーブル作成のときと同様に、sqlite3コマンドでデータベースの内容を確認してみます。

$ sqlite3 /tmp/project.db3
sqlite> select * from posts;
1|DataMapper記事の投稿|記事本文です。|2013-03-03T23:26:59+09:00

記事の検索

DataMapperで永続化したオブジェクトの検索方法はいくつかありますが、ここではfirst (条件にマッチする最初のオブジェクトを取得) を使います。
ちょっと面白いのは、検索した時点ではすべての属性は取得しておらず、属性を参照した時点でSQLのSELECT文が発行される点です (遅延ローディング: lazy loading)。

【サンプルコード】dm_sample_find.rb

# -*- coding: utf-8 -*-
require 'data_mapper'
require 'pp'
require_relative 'dm_sample_model'

DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite:///tmp/project.db3')

post = Post.first(:title => "DataMapper記事の投稿")
pp post
puts "[id] #{post.id}"
puts "[title] #{post.title}"
puts "[body] #{post.body}"
puts "[created_at] #{post.created_at}"

【実行結果】

$ ruby dm_sample_find.rb 
 ~ (0.000358) SELECT "id", "title", "created_at" FROM "posts" WHERE "title" = 'DataMapper記事の投稿' ORDER BY "id" LIMIT 1
#<Post @id=1 @title="DataMapper記事の投稿" @body=<not loaded> @created_at=#<DateTime: 2013-03-03T23:26:59+09:00 ((2456355j,52019s,0n),+32400s,2299161j)>>
[id] 1
[title] DataMapper記事の投稿
 ~ (0.000038) SELECT "id", "body" FROM "posts" WHERE "id" = 1 ORDER BY "id"
[body] 記事本文です。
[created_at] 2013-03-03T23:26:59+09:00

作成したスクリプトによる出力とDataMapperのログが混在してやや分かりづらいですが、ppによる出力時点では @body= となっていて、その後SELECT文が発行されている様子が観察できます。

コメントの投稿

さらに、先ほどの記事に対するコメントの投稿を試してみます。
基本的には記事の投稿と同様ですが、関連 (association) の定義に従って、Commentオブジェクトのpost属性にPostオブジェクトをセットすることで、どの記事に対するコメントかを指定することができます (データベース上のpost_id属性に相当)。

【サンプルコード】dm_sample_comment.rb

# -*- coding: utf-8 -*-
require 'data_mapper'
require_relative 'dm_sample_model'

DataMapper::Logger.new($stdout, :debug)
DataMapper.setup(:default, 'sqlite:///tmp/project.db3')

post = Post.first(:title => "DataMapper記事の投稿")

comment = Comment.create (
  :posted_by => "m-kawato",
  :body => "コメントです。",
  :post => post
)

【実行結果】

$ ruby dm_sample_comment.rb 
 ~ (0.000359) SELECT "id", "title", "created_at" FROM "posts" WHERE "title" = 'DataMapper記事の投稿' ORDER BY "id" LIMIT 1
 ~ (0.000041) SELECT "id", "body" FROM "posts" WHERE "id" = 1 ORDER BY "id"
 ~ (0.015866) INSERT INTO "comments" ("posted_by", "body", "post_id") VALUES ('m-kawato', 'コメントです。', 1)

さらに、データベースの内容をsqlite3コマンドで確認してみます。

$ sqlite3 /tmp/project.db3
sqlite> select * from comments;
1|m-kawato|コメントです。|1

まとめ

この記事では、ブログ記事とコメントの投稿・検索を題材とした簡単なサンプルを通して、O/RマッパーDataMapperの基本的な機能を確認しました。