DataMapperを使ってみる
DataMapper (Ruby向けO/Rマッパー) 以前使ったことがあるものの、最近のバージョンは触っていないので改めて試してみました。
動作環境
DataMapperのインストール
# 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=
コメントの投稿
さらに、先ほどの記事に対するコメントの投稿を試してみます。
基本的には記事の投稿と同様ですが、関連 (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の基本的な機能を確認しました。