Ruby Library Report 【第 3 回】 O/R マッピング

著者:馬場 道明 編集・校正:立石 孝彰 協力:かずひこ、moriq

はじめに

今回は、Ruby で O/R マッピングを扱うライブラリとして、 rorm, TapKit, SDS, Active Record の4つを紹介します。

リレーショナルデータベースを扱う際、 SQL の組み立てや、それから得られた結果セットをオブジェクトに詰め直す作業は、 Ruby でプログラミングする場合に限らず、最近のオブジェクト指向言語を使っている方なら、誰しもが煩雑に感じるのではないでしょうか。

この煩雑さは、RDBとオブジェクト指向言語のデータに関する思想の違いに起因します。 RDBとオブジェクト指向は、ともにデータとそれらのリレーションを持つところは同じです。 しかし、 RDBにおけるデータモデルの設計では、 データ自身の整合性やパフォーマンスに基づいて モデルの最適解を得ようとするのに対して、 オブジェクトモデルの設計では、 プログラマの理解・再利用性等の開発効率に基づいて モデルの最適解を得ようとします。 ER 図と UML におけるクラス図は一見似ているのですが 大元のアプローチが違うため様々な相違点をもたらします。 これを、オブジェクトリレーショナル・インピーダンスミスマッチ 1 2 と呼びます。

オブジェクト指向パラダイムが登場した後に作られた、オブジェクト指向データベースや XML データベースならば、オブジェクト指向言語との親和性も高いかもしれませんが、長い間エンタープライズの現場で使われ続けてきたRDBと比べると実績やパフォーマンスという面で難があり、まだまだRDBと付き合っていかなければいけないのが現状でしょう。

そこで、このインピーダンス・ミスマッチを少しでも軽減する為に用いられるのが、今回ご紹介する O/R マッピングツールです。

始める前に

今回のレポートを通して使用するテーブルを定義したSQLを用意しました。

rlr-pg.sql rlr-pg.sql

今回のサンプルの作成は、RDBMS に PostgreSQL を用いて行い、この SQL も PostgreSQL 特有のカラム型がありますが、小さい定義なので、その他の RDBMS でも若干の修正で使用できると思います。

簡単ながら、テーブル定義を説明させて頂きますと、

  • Student (学生)
  • Account (学生のコンピューターアカウント)
  • Department (学生の所属する学部)
  • Registration (学生が履修する授業)
  • Course (授業)

という大学におけるデータを想定しており、

  • Student - Account (1 対 1)
  • Department - Student (1 対 多)
  • Student - Registration - Course (多 対 多)

というリレーションに O/R マッパーを用いてどうアクセスするかを見て行きたいと思います。

かなりディフォルメされていますが、各ライブラリの感触を掴むには充分だと思います。

試用レポート – rorm

登録データ

概要

rorm は、XML による定義ファイルを用いて、データベースへの接続からオブジェクトへのマッピングまでの仲立ちをしてくれるライブラリです。Ruby/DBI の対応する RDBMS をカバーしています。

作者からの声

rorm は P of EAA (Patterns of Enterprise Application Architecture [2]) を読んで、その理解を深めるために実装されました。rorm では、P of EAA で解説されているパターンのうち、Data Mapper パターンを採用し、付随するパターンである Identity Map, Lazy Load, Metadata Mapping, Query Object などが実装されています。

rorm のウェブページでは簡単にデータベースが扱えるということを強調していますが、rorm の実際の問題意識は、Ruby のオブジェクトとリレーショナルデータベースシステム (RDBMS) をどのようにつなぐべきかという設計の部分にあります。

ご存じの通り、オブジェクト指向プログラム (OOP) で扱われるオブジェクトと、RDBMSで扱われるデータとの間には多くの違いがあります。Ruby に限らず、OOP で RDBMS を利用するアプリケーションをどのように設計すべきかということで悩んだ方は多いのではないでしょうか。

そういった視点から rorm をながめると、あまりよくない実装も、ちょっとは興味深く思っていただけるのではないかと思っています。

第 1 回にも声を寄せて頂いた高井さんのコメントです。ありがとうございます。 筆者も P of EAA は所持しているのですが、拾い読みしただけで読み込めておりません。 これを機会に勉強させてもらいたいと思います。

サンプル

単純な参照

まずは Student テーブルからデータを取り出す単純なアクセスの方法を見てみましょう。

rorm-simple.rb

#!/usr/bin/env ruby
require 'rorm'

class Student
  attr_accessor :no, :name
  def introduce
    return "Hello, my name is %s." % name
  end 
end

db = Rorm::Rorm.new(DATA.read)
query = db.query(Student)
query.add_criteria(Rorm::Criteria.equals('name', 'Alan Mathison Turing'))
alan = query.execute[0]
puts alan.introduce

__END__
<?xml version="1.0" ?>
<config>
  <database driver="DBI:Pg:RLR_DB" user="babie" password="" />
  <mapper>
    <mapping class="Student" table="Student">
      <id column="id" />
      <property field="no" column="no" />
      <property field="name" column="name" />
    </mapping>
  </mapper>
</config>


END 以下に定義されている XML による設定の要素・属性から説明すると、

config
設定の root になります。
database
接続するデータベースの情報。属性は、driver が Ruby/DBI の URL、user が接続するデータベースのユーザー、password がそのパスワードとなります。
mapper
マッピング情報枠。
mapping
マッピング情報。属性 class に Ruby 側のクラス、table にテーブル名を記入します。
id
データベースのキー情報。属性 column がテーブルのカラム名です。
property
Ruby クラスの属性とテーブルの列の対応情報。属性 field が Ruby 側のアトリビュート、column が列名を表します。

となります。

コードの方は、 クラス Student を定義、 Rorm::Rorm.new で設定を読み込み DB 操作を一手に引き受けるオブジェクトを得ます。

db.query(class) で DB とそのマッパークラスを得るためのクエリオブジェクトを作成します。

Rorm::Criteria.equals(column, criteria) では、この例の場合、SQL で “name = ‘Alan Mathison uring’” となる条件を作成しています。

query.add_criteria でクエリオブジェクトに条件を追加します。

最後に、query.execute でクエリを実行します。 戻り値は Student オブジェクトの配列で、対象は 1 レコードなのでインデクサで 1 つだけ取り出しています。 そして、introduce メソッドでマッピングされたデータにアクセスし出力しています。

ここで驚きなのは、Student クラス定義が、全く DB を連想させず、とても自然なところです。ただのアトリビュートとメソッドを持つ普通のクラスにしか見えません。 db.query(クラス)で「全てがオブジェクト」である Ruby の「クラスもオブジェクト」という特性を活かした優れた設計だと思います。

各種 参照・更新

次は、ざっと、1対1、1対多、多対多、追加、更新、削除を見てみたいと思います。

rorm-sample.xml rorm-sample.xml

Student テーブルのマッピング定義の id 要素に last_id 属性が加わっています。 これは INSERT 時はキー id 値が自動採番の為未定義で、INSERT 後にオブジェクトにキーの値を詰め直す為の関数を記入します。

その他追加された要素を説明します。

relation
リレーション情報枠です。
one_to_many
1対多のリレーションを定義します。source 要素はリレーション元、target 要素はリレーション先、field 要素は Ruby コードからアクセスする時のアトリビュート名、key 要素は リレーション先のキー値を表します。
many_to_many
多対多のリレーションを定義します。RDB では多対多のリレーションは 中間テーブルがなければ実現できないので、1対多定義の場合と同じ sourse, target, field 以外も定義しなければなりません。table 要素は間に挟むテーブル、source_key 要素はリレーション元から中間テーブルを得る時のキー、target_key 要素は中間テーブルからターゲットテーブルを得る時のキーです。

Account へのリレーションは 1対1 を意図しているのですが、rorm には 1対1 の為の特別な記法はありませんので、1対多の記法で代用しています。

それではコードの方を見てみましょう。

rorm-sample.rb

#!/usr/bin/env ruby
require 'rorm'

class Student
	attr_accessor :no, :name, :department_id
	def introduce
		"Hello, my name is %s." % @name
	end 

	attr_accessor :account	# for 1:1
	def tellme_password
		%Q|#{name}'s account is #{account[0].username}/#{account[0].password}|
	end

	attr_accessor :courses	# for N:N
	def course_summary
		output = ""
		courses.each do |c|
			output += "\t[#{c.code}] #{c.name}\n"
		end
		return output
	end
end

class Department
	attr_accessor :code, :name, :abbrev
	attr_accessor :students	 # for 1:N
end

Account = Struct.new(:username, :password)
Course = Struct.new(:code, :name)


class Sample
	def initialize
		@db = Rorm::Rorm.new(File.open('rorm-sample.xml'){|f| f.read})
	end

	# (1) 1:1
	def one2one
		o2o_query = @db.query(Student)
		o2o_query.add_criteria(Rorm::Criteria.equals('name', 'Charles Babbage'))
		charles = o2o_query.execute[0]
		puts charles.tellme_password
	end

	# (2) 1:N
	def one2many
		db = Rorm::Rorm.new(File.open('rorm-sample.xml'){|f| f.read})
		o2m_query = db.query(Department)
		o2m_query.add_criteria(Rorm::Criteria.equals('code', '20'))
		ec = o2m_query.execute[0]
		puts "#{ec.name}:"
		ec.students.each do |s|
			puts s.introduce
		end
	end

	# (3) N:N
	def many2many
		m2m_query = @db.query(Student)
		m2m_query.add_criteria(Rorm::Criteria.greater_than('no', '20-000', true))
		m2m_query.add_criteria(Rorm::Criteria.less_than('no', '30-000', false))
		students = m2m_query.execute
		students.each do |s|
			puts "#{s.name}'s courses are:"
			puts s.course_summary
		end
	end

	# (4) INSERT
	def insert
		@db.transaction do |d|
			fillip = Student.new
			fillip.no = "20-004"
			fillip.name = "Fillip Estridge"
			fillip.department_id = 2
			mapper = d.mapper(Student)
			mapper.insert(fillip)
		end
	end

	# (5) UPDATE
	def update
		@db.transaction do |d|
			fillip_query = d.query(Student)
			fillip_query.add_criteria(Rorm::Criteria.equals('no', '20-004'))
			fillip = fillip_query.execute[0]
			fillip.name = "Fillip Don Estridge"
			mapper = d.mapper(Student)
			mapper.update(fillip)
		end
	end

	# (6) DELETE
	def delete
		@db.transaction do |d|
			fillip_query = d.query(Student)
			fillip_query.add_criteria(Rorm::Criteria.equals('no', '20-004'))
			fillip = fillip_query.execute[0]
			mapper = d.mapper(Student)
			mapper.delete(fillip)
		end
	end
end

if __FILE__ == $0
	s = Sample.new
	puts "(1) 1:1"
	s.one2one
	puts "(2) 1:N"
	s.one2many
	puts "(3) N:N"
	s.many2many

	puts "(4) INSERT"
	s.insert
	s.one2many
	puts "(5) UPDATE"
	s.update
	s.one2many
	puts "(6) DELETE"
	s.delete
	s.one2many
end


Account, Course を見てもらえば分かる通り、DTO として利用するだけならば、Struct で代用することが出来ます。

(1) 1:1

まずは 1対1 のリレーションです。 データの取得自体は最初の例で挙げた物と変わりません。Account テーブルデータへのアクセスですが、上述したように 1対多 のリレーションで代用したので account 配列の最初の要素を明示的に指定しています。

(2) 1:N

次は 1対多のリレーションです。ここで Rorm::Rorm オブジェクトを新規に作り直して使用していますが気にしないで下さい。後述する INSERT, UPDATE, DELETE の結果の確認のため別コンテキストで実行しています。XML で定義した

<relation>
  <one_to_many
    source="Department" target="Student"
    field="students" key="department_id"
  />
</relation>

の通り、students アトリビュートで Department.id Student.departmetn_id が一致する Student テーブルのデータを取得できていますね。

(3) N:N

RDB 上では多対多のリレーションの為に Registration テーブルが中間テーブルとして存在しているのですが、その存在を意識することなく Course データにアクセス出来ています。

(4) INSERT

rorm では transaction メソッドを使用することにより、アトミックな データ の変更が行えます。普通の Ruby オブジェクトと同様に生成しています。ここではセッターによる値の代入を行いましたが、initialize メソッドを定義してコンストラクタによって各アトリビュートの値を埋めることも出来ます。

mapper メソッドによって作成されたオブジェクトは引数とされたクラスのマッピング情報を保持しています。そのマッピング情報を元に INSERT, UPDATE, DELETE が行われます。

(5) UPDATE (6) DELETE については特に説明する必要は無いでしょう。

感想

機能という点で見ると、 更新系がカスケードに対応してない等で若干不備な点がありますが、 参照系はまったく不都合ありません。 ストレス無く使用できる良いライブラリではないでしょうか。

なにより、全 8 ファイル、総計 約 600 行でここまでのものが出来ることに感動しました。

試用レポート – TapKit

登録データ

概要

TapKit は EOF (Enterprise Objects Framework3) を Ruby で実装した物です。 YAML による定義ファイルを用いて DB や マッピングの情報を記述します。 対応する RDBMS は、MySQL, PostgreSQL, OpenBase, CSV(実験的対応)ですが、 Ruby/DBI の対応しているものならば概ね動くようです。4

作者からの声

TapKit は単純な興味から作りました。WebObjects の Web ライブラリを真似た CGIKit の開発がある程度進んだところで、ついでに DB ライブラリである EOF も真似てみようと思い、EOF の理解も兼ねてとりかかりました。 実は EOF に関して無知の状態でしたから仕様をコンパクトにまとめることもできず、とりあえずコア部分をほぼそのまま Ruby らしく作ってみたのが TapKit です。

TapKit の特徴は「変更を管理するオブジェクト」を通してオブジェクトを操作する点にあります。 CVS を使った作業サイクルを思い浮かべてみてください。リポジトリから作業用コピーを取得し、その中で作業します。作業内容をコミットするまでソースコードには何も影響せず、気に入らなければ作業コピーを捨てても構いません。TapKit でもデータベースから取得したオブジェクトが「編集コンテキスト」という作業用コピーにあたるオブジェクトによって管理されます。データベースへの問い合わせ、更新などの作業はすべてこのオブジェクトを通して行います。

TapKit は CGIKit と対になっているわけではなく、あくまで汎用的なライブラリとして使えるように開発を続ける予定です。

特徴を簡潔に述べて頂いて助かりました。この「変更を管理するオブジェクト」「編集コンテキスト」の理解がスムーズだと、EOF 独特の世界にも戸惑わないでしょう。

サンプル

ユーティリティー

TapKit では YAML5 でマッピング定義であるモデルファイルを記述します。

付属の modeler コマンドでモデルファイルを DB から直接リレーション等の情報を読み取って生成することが出来ます。早速使用してみましょう。

$ modeler PostgreSQL modeler-sample.yaml
Login database with DBI
URL: dbi:Pg:RLR_DB
Username: babie
Password:
Selectable tables - ...(アクセスできるテーブルの一覧 省略)...
(If you want to select the all tables, input 'all')
Select tables (separate table names with comma): department,student,account,registration,course
Create modeler-sample.yaml

引数で与える adapter 名は MySQL, PostgreSQL, OpenBase, CSV の何れかを指定できます。注意するところは DBIの接続文字列のものとは違うということです。例えば PostgreSQL の DBI 接続文字列は “Pg” ですが、ここでは “PostgreSQL” という文字列を与えなければなりません。大文字小文字も区別しますので注意してください。

“URL:” プロンプトでは DBIの接続文字列を入力します。 “Username:”, “Password:” では、それぞれ DB に接続するユーザーとパスワードを入力してください。

最後に、”Select tables:” プロンプトでモデルファイルに書き出すテーブルをカンマ区切りで指定しますが、この時、空白を入れてはいけません。例えば “department, student” と入力した場合 student テーブルは “ student” テーブルとみなされて出力されてしまいます。 また、all を選択すると、データベースユーザーがアクセスできるテーブルが全て出力されるので、データベースユーザーに大きな権限を与えている場合はアプリケーションで使用するテーブル名だけを指定したほうが良いでしょう。

modeler コマンドは設定ファイル自動生成してくれますが、未だ発展途上のようで、精度は今回のサンプルを用いた限りではイマイチです。 例えば、後述する 1:N の例での Department - Student のリレーションシップが、お互いの Department.id と Student.id がキーとなるものとして出力されていました。 現段階では雛形として利用するのが良さそうです。6

単純な参照

rorm と同じく単純な参照を行ってみましょう。

tapkit-simple.yaml tapkit-simple.txt

コードの説明に移る前に YAML の各要素について解説します。

設定は大きく分けて 3 つのセクションがあります。

adapter_name
TapKit の対応するアダプターを指定します。modeler コマンドと同じくMySQL, PostgreSQL, OpenBase, CSV が対応されています。
connection
DB との接続に関するセクションです。
entities
エンティティ(テーブル)に関するセクションです。

connection セクションでは以下の情報を記述します。

url
使用する DBI の接続文字列を記入します。
user
DB にアクセスするユーザー名を指定します。
password
DB にアクセスするユーザーのパスワードを使用します。

entities セクションでは以下の設定が並びます。

name
TapKit がアクセスする際のエンティティ名です。
class_name
TapKit が作成するオブジェクトの継承元クラスです。
external_name
テーブル名
class_properties
Ruby コード側から使用するアトリビュートのリストを記入します。ここに記入されていない物は Ruby コードから触ることはできません。
attributes
各カラム情報を記入するカテゴリです。
primary_key_attributes
プライマリキーを指定します。

attributes には以下のデータを記述します。

name
Ruby コード側から使用する際の名前です。
column_name
テーブルのカラム名です。
class_name
Ruby コード側の型です。
external_type
DB のカラム型です。
allow_null
NULL 制約です。false 時は NULL 不許可となります。
read_only
参照専用設定です。
width
DB 側で文字列等のサイズ設定が必要な場合に定義します。

それではコードの方を覗いてみましょう。

tapkit-simple.rb

#!/usr/bin/env ruby
require 'tapkit'

def introduce(name)
   "Hello, my name is #{name}."
end

include TapKit
app = Application.new('tapkit-simple.yaml')

context = app.shared_editing_context
query = Qualifier.format("name = 'Alan Mathison Turing'")
fs = FetchSpec.new('Student', query);

alan = context.fetch(fs)[0]
puts introduce(alan['name'])


include TapKit で Tapkit モジュールをインクルードして諸々の機能を使用します。

Application.new(modelfile) で設定ファイルからエンティティやリレーションの情報を読み込みます。

app.create_editing_context は読み込まれたモデルから「編集コンテキスト」オブジェクトを作成します。

query = Qualifier.format(query_str) でフェッチ時の条件オブジェクトを作成します。

fs = FetchSpec.new(class, query) 条件オブジェクトとマッピングクラスを関連付けます。

context.fetch(fs) フェッチします。

object[‘attr_name’] で モデルファイルで定義したアトリビュートにアクセスできます。

各種 参照・更新

次も同じく、各種参照と更新系を試してみましょう。 tapkit-sample.yaml

定義ファイルは 200 行ほどの長大なリストになるのでリンクだけに留めて、追加要素を解説したいと思います。

relationships
リレーションに関する設定を記述します。
name
Ruby コードからアクセスする時の名前を入力します。
destination
リレーション先のクラス名を取得します。
joins
子要素でリレーションのキーを定義します。
source
リレーション元のアトリビュート名。
destination
リレーション先のアトリビュート名。
join_semantic
結合する方法。inner, left_outer, right_outer, full_outer を指定できます。
mandatory
リレーションシップが必須かどうかを設定します。
to_many
1対多の場合に true を指定します。
delete_rule
削除時に削除レコードを参照しているリレーション先の扱いを指定します。nullify (NULL で埋める), cascade (参照している他のテーブルのレコードも削除する), deny (リレーション先にレコードがある時削除できなくする) の 3 つが指定できます。

tapkit-sample.rb

#!/usr/bin/env ruby
require 'tapkit'

def introduce(name)
	"\tHello, my name is #{name}."
end

class Sample
	include TapKit
	def initialize
		@app = Application.new('tapkit-sample.yaml')
	end

	# (1) 1:1
	def one2one
		ec = @app.shared_editing_context
		q = Qualifier.format("name like 'C*'")
		fs = FetchSpec.new('Student', q);
		charles, = ec.fetch(fs)

		puts "\t#{charles['name']}'s Password: #{charles['account']['password']}"
	end

	# (2) 1:N
	def one2many
		app = Application.new('tapkit-sample.yaml')
		ec = app.shared_editing_context
		q = Qualifier.format("code = '20'")
		fs = FetchSpec.new('Department', q);

		dept = ec.fetch(fs)[0]
		puts "\t#{dept['name']}:"
		students = dept['students']
		students.each do |s|
			puts introduce(s['name'])
		end
	end

	# (3) N:N
	def many2many
		ec = @app.shared_editing_context
		q = Qualifier.format("no like '20-*'")
		fs = FetchSpec.new('Student', q)

		students = ec.fetch(fs)
		students.each do |s|
			puts "\t#{s['name']}'s courses are:"
			rs = s['registration']
			rs.each do |r|
				puts "\t\t[#{r['course']['code']}] #{r['course']['name']}"
			end
		end
	end

	# (4) INSERT
	def insert
		ec = @app.create_editing_context

		fillip = ec.create('Student')
		fillip['no'] = '20-004'
		fillip['name'] = 'Fillip Estridge'
		fillip['department_id'] = 2
		ec.save
	end

	# (5) UPDATE
	def update
		ec = @app.create_editing_context
		q = Qualifier.format("no = '20-004'")
		fs = FetchSpec.new('Student', q)

		fillip = ec.fetch(fs)[0]
		fillip['name'] = 'Fillip Don Estridge'
		ec.save
	end

	# (6) DELETE
	def delete
		ec = @app.create_editing_context
		q = Qualifier.format("no = '20-004'")
		fs = FetchSpec.new('Student', q)

		fillip = ec.fetch(fs)[0]
		ec.delete fillip
		ec.save
	end
end

if __FILE__ == $0
	s = Sample.new
	puts "(1) 1:1"
	s.one2one
	puts "(2) 1:N"
	s.one2many
	puts "(3) N:N"
	s.many2many

	puts "(4) INSERT"
	s.insert
	s.one2many
	puts "(5) UPDATE"
	s.update
	s.one2many
	puts "(6) DELETE"
	s.delete
	s.one2many
end

(1) 1:1

TapKitでは、条件式に “*” や “?” といったワイルドカードが使えます。 SQL の “%” も使えます。

(2) 1:N は特にひっかかるところは無いと思います。

(3) N:N

多対多アクセスでは、中間オブジェクトを明示的に記述しないと目的のデータにアクセス出来ないようです。

(4) INSERT

編集オブジェクトを生成する時に、shared_editing_context メソッドでなく、 create_editing_context メソッドを使用しています。 shared_editing_context は読み取り専用、 create_editing_context は変更もできます。

編集コンテキストオブジェクトの save メソッドを呼ぶのを忘れないで下さい。 save メソッドを呼ぶまでは DB は実際には更新されません。

(5) UPDATE (6) DELETE も特に言及するところはありません。

編集コンテキストオブジェクトを各メソッドで生成していますが、実際のアプリケーションでは大体 1 つのクラスに 1 つで済むでしょう。

感想

残念ながら、今回のサンプルでは 編集コンテキストの効用や カスケードした更新の 例を示すことが出来ませんでしたが、 次号以降で CGIKit と TapKit の記事が予定されていますので、 実用的な例はそちらに任せたいと思います。

個人的には、 object[‘attr_name’] というハッシュ風アクセスで Ruby らしいオブジェクトの手触りを感じられないところや id というアトリビュートにアクセスすると Object#id が返る仕様といった 細かい点に若干不満がありますが、機能的な不足はなく概ね満足です。

あと、 懇切丁寧な日本語マニュアル TapKitユーザーガイド があるのは大きな魅力です。

試用レポート – SDS

登録データ

概要

SDS は TapKit と同じく EOF を Ruby で実装したものです。

作者からの声

SDS is modeled after Apple’s Enterprise Objects Framework (EOF), which is an excellent O/R mapping library for Java and (formerly) Objective-C. In contrast with Tapkit (which follows EOF class layout quite literally), SDS mimics only some major concepts from EOF. O/R mapping is modelled using the YAML file (more model file formats coming) and a graphical modelling tool (similar to Apple’s EOModeler) is under way. SDS is indented to help to get rid of SQL (in all but most complicated cases), so it is possible to switch to another RDBMS just by changing the adaptor line in the model file. Relationship fetching/deleting is handled transparently.

複数の開発者が実装してしまう EOF はよほど魅力的なフレームワークなのでしょうね。 EOF だけでなく WebObjects には一度は触れてみたいものです。

今後の計画として、 GUI によるモデリングツール・ SQL のより一層の排除・ RDBMS 依存性の排除 といった機能強化が挙げられています。 意欲的に開発を行っているようなので期待がもてますね。

サンプル

単純な参照

sds-simple.yaml sds-simple.txt

TapKit の YAML とも似てますし、キー名も素直なので特に解説する必要はないのではないでしょうか。

sds-simple.rb

#!/usr/bin/env ruby
require 'sds'

include SDS
store = Store.get('sds-simple.yaml')

class Student
   include SDS::Object
   def introduce
      puts "\tHello, my name is #{name}."
   end
end

context = Context.new(store)
alan = context.fetch('Student', "name = 'Alan Mathison Turing'")[0]
alan.introduce


Store.get により、エンティティやりレーションの定義を読み込みます。

Ruby のクラスに SDS::Object モジュールをインクルードして、マッピングできるようになります。

TapKit とは違って、通常のオブジェクトのように object.attr_name といった形式でアクセスできます。

また、TapKit の Querifiler のようなオブジェクトは生成せず、context.fetch メソッドに直接条件式を与えることができます。

各種 参照・更新

sds-sample.yaml sds-sample.yaml

relationships が増えましたが、これも TapKit とそう変わりませんので、難なく読み書きできると思います。

sds-sample.rb

#!/usr/bin/env ruby
require 'sds'

include SDS
Store.get('sds-sample.yaml')

class Department
   include SDS::Object
end

class Student
   include SDS::Object
   def introduce
      puts "\tHello, my name is #{name}."
   end
end

class Account
   include SDS::Object
end

class Course
   include SDS::Object
end

class Registration
   include SDS::Object
end

class Sample
   def initialize
      @store = Store.get('sds-sample.yaml')
   end

   # (1) 1:1
   def one2one
      context = Context.new(@store)
      students = context.fetch('Student', "name like 'C%'")
      charles = students[0]
      puts "\t#{charles.name}'s Password: #{charles.account.password}"
   end

   # (2) 1:N FIXME
   def one2many
      context = Context.new(@store)
      biz = context.fetch('Department', "code = '20'")[0]
      puts "\t#{biz.name}:"
      biz.students.each do |s|
         puts s.introduce  # なんか nil も出力されちゃう
      end
   end

   # (3) N:N
   def many2many
      context = Context.new(@store)
      students = context.fetch('Student', "department_id = 2")
      students.each do |s|
         puts "\t#{s.name}'s courses are:"
         s.registrations.each do |r|
            puts "\t\t[#{r.course.code}] #{r.course.name}"
         end
      end
   end

   # (4) INSERT
   def insert
      context = Context.new(@store)
      fillip = Student.create(context)
      fillip.no = '20-004'
      fillip.name = 'Fillip Estridge'
		fillip.department_id = 2
		puts "object to insert: #{context.objects_to_insert}"
      context.save
   end

   # (5) UPDATE
   def update
      context = Context.new(@store)
      fillip = context.fetch('Student', "no = '20-004'")[0]
      fillip.name = 'Fillip Don Estridge'
      context.save
   end

   # (6) DELETE FIXME
   def delete
      context = Context.new(@store)
      fillip = context.fetch('Student', "no = '20-004'")[0]
		#context.delete(fillip)
      context.save
   end
end

if __FILE__ == $0
   s = Sample.new

	puts "(1) 1:1"
   s.one2one
	puts "(2) 1:N"
   s.one2many
	puts "(3) N:N"
   s.many2many

	puts "(4) INSERT"
   s.insert
   s.one2many
	puts "(5) UPDATE"
   s.update
   s.one2many
	puts "(6) DELETE"
   s.delete
   s.one2many
end

(2) 1:N で students.each メソッドの出力で、途中に nil という行が出力されてしまいます。残念ながら、時間的な制約もあり、回避策を施したサンプルを用意できませんでした。

(3) N:N はと TapKit と同じく、目的のオブジェクトに直接のアクセスはできませんでした。

(4) INSERT は、実はそのままでは動きません。SDS は INSERT 時に連番を取得する際、 (table_name)Seq というシーケンステーブル名を期待している7のですが、 PostgreSQL の serial カラム型によるデフォルトのシーケンステーブル名は (table_name)(column_name)_seq なので、エラーを起こしてしまいます。 今回は、adaptor.rb.patch というパッチを作成し凌ぎました。 (6) DELETE が動かないのもここら辺が関係するかもしれません。

感想

SDS には YAML 定義ファイルからテーブル定義 SQL ファイルを出力できる sds_generate_db_schema.rb スクリプトがあるのですが、 今回はテーブルを先に用意していたので使用しませんでした。 DB 定義が先にあることも往々にしてあるので、TapKit のように実際の DB から YAML 定義ファイルを出力するコマンドも欲しいところです。

全体を通してみると TapKit より Ruby 側への歩み寄りがなされていると思います。 細かいバグ?がありますが、開発は継続しているので、機能もこれから徐々に充実していくのではないでしょうか。

今回のサンプルコードには一部不具合があるので、 修正できた時は、るびま編集部 まで是非ご連絡下さい。 追記を明示して差し替えたいと思います。

試用レポート – Active Record

登録データ

概要

Active Record は ウェブアプリケーションフレームワークである、RubyOnRails の O/R マッピングを担うライブラリです。RubyOnRails が依存しているわけではなく、単体で利用することもできます。

マッピング定義を Ruby クラス内で表現できるところに特徴があります。

サンプル

単純な参照

ar-simple.rb

#!/usr/bin/env ruby
require 'active_record'

class Student < ActiveRecord::Base
	def self.table_name() "student" end

	def introduce
		return "Hello, my name is %s." % name
	end
end

require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.establish_connection(
	:adapter  => "postgresql",
	:host     => "localhost",
	:username => "babie",
	:password => "",
	:database => "RLR_DB"
)

student = Student.find_all("name = 'Alan Mathison Turing'")[0]
puts student.introduce


ActiveRecord::Base を継承して マッピングクラスを定義します。 このようにテーブル名が複数形でない場合は table_name クラスメソッドを定義します。 テーブル名に複数形を使用することができれば table_name メソッド定義を省略でき一層楽をすることが出来ます。

require 'logger'
ActiveRecord::Base.logger = Logger.new(logfile)

でログファイルを記録することによりエラー時のトレースを簡単にできます。

ActiveRecord::Base.establish_connection で DB との接続を定義します。

find_all(query_str) メソッドで マッピングされたオブジェクトを取得することが出来ます。 あとは object.column_name という形式でデータにアクセスすることが出来ます。

各種 参照・更新

ar-sample.rb

#!/usr/bin/env ruby
require 'active_record'
# for RubyGems package
=begin
require 'rubygems'
require_gem 'activerecord'
=end

class Student < ActiveRecord::Base
	def self.table_name() "student" end
	has_one :account
	has_and_belongs_to_many :courses, :table_name => "course", :join_table => "registration"

	def introduce
		return "Hello, my name is %s." % name
	end

	def tellme_password
		%Q|#{self.name}'s password is "#{self.account.password}"|
	end
end

class Department < ActiveRecord::Base
	def self.table_name() "department" end
	has_many :students
end

class Account < ActiveRecord::Base
	def self.table_name() "account" end
end

class Course < ActiveRecord::Base
	def self.table_name() "course" end
	has_and_belongs_to_many :students, :table_name => "student", :join_table => "registration"
end

require 'logger'
ActiveRecord::Base.logger = Logger.new("debug.log")
ActiveRecord::Base.establish_connection(
	:adapter  => "postgresql",
	:host     => "localhost",
	:username => "babie",
	:password => "",
	:database => "RLR_DB"
)


class Sample
	# (1) 1:1
	def one2one
		charles = Student.find_all("name = 'Charles Babbage'")[0]
		puts charles.tellme_password
	end

	# (2) 1:N
	def one2many
		dept = Department.find_all("code = 20")[0]
		puts "#{dept.name}:"
		dept.students.each do |s|
			puts s.introduce
		end
	end

	# (3) N:N
	def many2many
		Student.find_all("no like '20-%'").each do |s|
			puts "#{s.name}'s courses are:"
			s.courses.each do |c|
				puts "\t[#{c.code}] #{c.name}"
			end
		end
	end

	# (4) INSERT
	def insert
		dept = Department.find_all("code = 20")[0]
		fillip = Student.new(
			"name" => 'Fillip Estridge',
			"no" => '20-004',
			"department_id" => 2
		)
		fillip.save
	end

	# (5) UPDATE
	def update
		fillip = Student.find_all("no = '20-004'")[0]
		fillip.name = 'Fillip Don Estridge'
		fillip.save
	end

	# (6) DELETE
	def delete
		fillip = Student.find_all("no = '20-004'")[0]
		fillip.destroy
	end
end

if __FILE__ == $0
	s = Sample.new
	puts "(1) 1:1"
	s.one2one
	puts "(2) 1:N"
	s.one2many
	puts "(3) N:N"
	s.many2many

	puts "(4) INSERT"
	s.insert
	s.one2many
	puts "(5) UPDATE"
	s.update
	s.one2many
	puts "(6) DELETE"
	s.delete
	s.one2many
end

1対1 は has_one、 1対多は has_many、 多対多は has_and_belongs_to_many メソッドを使用しリレーションを定義します。

INSERT, UPDATE, DELETE についても、コードを読めばすぐわかるでしょう。

感想

驚きです。 うまく嵌まればこれ以上のものは無いのではないでしょうか。 特に、定義ファイル要らずというのは嬉しい利点です。 1 からアプリケーションを作れるならば今回一押しです。

しかし、 プライマリーキー名が “id” であるという事を前提としている部分があったりして、 全てのケースで使える訳では無いようです。 活動は活発で徐々に痒いところに手が届くようになっていますので、この辺も期待して見守りたいと思います。

今号から連載記事「RubyOnRails を使ってみる」が始まっていますのでこちらも参考にしてください。

まとめ

今回は O/R マッピングライブラリとして、rorm, TapKit, SDS, Active Record を取り上げました。 各種ライブラリ多種多様のアプローチがあり、大変興味深く試用させてもらいました。 rorm 以外は、カスケードの INSERT, UPDATE, DELETE に対応しているのですが、紹介できなかったのが残念なところです。

O/R マッピングライブラリは盛況のようで、 今回紹介した以外にも vaporKansas といったものもあるのですが、時間と分量の面で折り合いがつかず紹介できませんでした。 どちらかというと IoC/DI コンテナがメインですが、Seasar2 の Ruby 実装である Akabeko も要注目です。 機会があればこれらも取り上げてみたいと思います。

最後に、忙しい中睡眠時間を削って付き合ってくれた立石さん、動作検証やサンプルコードの作成で協力していただいた かずひこさん、moriq さんに感謝致します。

参考リソース

[1] Scott W. Ambler, 2003, “Agile Database Techniques: Effective Strategies for the Agile Software Developer”, ISBN:0471202835, MA:John Wiley & Sons Inc

[2] Fowler, Martin, 2002, “Patterns of Enterprise Application Architecture”, Boston, MA:Addison-Wesley.

著者について

馬場 道明 はソフトウェア技術者です。現在は関西某大学に常駐して、システム運用チームの一員として働いています。

仕事で使ってきたプログラミング言語は、 C, C++, C#, Perl, PHP と変遷を辿っているので、 並びからいって次はきっと Python です。 今のところ Ruby が下働きに甘んじているのが悲しいところです。

Ruby Library Report 連載一覧


  1. The Object-Relational Impedance Mismatchに詳しい 

  2. 上記の邦訳 オブジェクト・リレーショナル・インピーダンス・ミスマッチ 

  3. 詳細は http://www.apple.com/jp/webobjects/wo_docs_j.html の “Enterprise Objects Framework Developers Guide” を参照して下さい 

  4. SQLite でも今回のサンプルのプロトタイプを動かすことができました。 

  5. http://www.yaml.org/ 

  6. http://www.spice-of-life.net/tapkit/ja/TapKitUserGuide_J_c5_s1.html#doc7_1176 によると、「主キー名に id と設定しないでください。主キーを id とすると、オブジェクトにアクセスしても Object#id が呼び出されてしまいます。」とあるので、今回のサンプルのテーブル定義が適切でない可能性があります。 

  7. 付属コマンド sds_generate_db_schema.rb で YAML から テーブル定義 SQL を生成すると、この名前でシーケンステーブル名が定義されています。