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

今回のサンプルの作成は、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

   1|#!/usr/bin/env ruby
   2|require 'rorm'
   3|
   4|class Student
   5|  attr_accessor :no, :name
   6|  def introduce
   7|    return "Hello, my name is %s." % name
   8|  end 
   9|end
  10|
  11|db = Rorm::Rorm.new(DATA.read)
  12|query = db.query(Student)
  13|query.add_criteria(Rorm::Criteria.equals('name', 'Alan Mathison Turing'))
  14|alan = query.execute[0]
  15|puts alan.introduce
  16|
  17|__END__
  18|<?xml version="1.0" ?>
  19|<config>
  20|  <database driver="DBI:Pg:RLR_DB" user="babie" password="" />
  21|  <mapper>
  22|    <mapping class="Student" table="Student">
  23|      <id column="id" />
  24|      <property field="no" column="no" />
  25|      <property field="name" column="name" />
  26|    </mapping>
  27|  </mapper>
  28|<

__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

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

    <mapping class="Student" table="Student">
      <id column="id" last_id="currval('student_id_seq')" />
      <property field="no" column="no" />
      <property field="name" column="name" />
      <property field="department_id" column="department_id" />
    </mapping>

    <mapping class="Account" table="Account">
      <id column="student_id" />
      <property field="username" column="username" />
      <property field="password" column="password" />
    </mapping>

    <mapping class="Course" table="Course">
      <id column="id" />
      <property field="code" column="code" />
      <property field="name" column="name" />
    </mapping>
  </mapper>

  <!-- for (1:1) -->
  <relation>
    <one_to_many
      source="Student" target="Account" 
      field="account" key="student_id"
    />
  </relation>

  <!-- for (1:N) -->
  <relation>
    <one_to_many
      source="Department" target="Student" 
      field="students" key="department_id"
    />
  </relation>

  <!-- for (N:N) -->
  <relation>
    <many_to_many
      source="Student" target="Course"
      field="courses"
      table="Registration"
      source_key="student_id" target_key="course_id"
    />
  </relation>
</config>

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

   1|#!/usr/bin/env ruby
   2|require 'rorm'
   3|
   4|class Student
   5|  attr_accessor :no, :name, :department_id
   6|  def introduce
   7|    "Hello, my name is %s." % @name
   8|  end 
   9|
  10|  attr_accessor :account	# for 1:1
  11|  def tellme_password
  12|    %Q|#{name}'s account is #{account[0].username}/#{account[0].password}|
  13|  end
  14|
  15|  attr_accessor :courses	# for N:N
  16|  def course_summary
  17|    output = ""
  18|    courses.each do |c|
  19|      output += "\t[#{c.code}] #{c.name}\n"
  20|    end
  21|    return output
  22|  end
  23|end
  24|
  25|class Department
  26|  attr_accessor :code, :name, :abbrev
  27|  attr_accessor :students	 # for 1:N
  28|end
  29|
  30|Account = Struct.new(:username, :password)
  31|Course = Struct.new(:code, :name)
  32|
  33|
  34|class Sample
  35|  def initialize
  36|    @db = Rorm::Rorm.new(File.open('rorm-sample.xml'){|f| f.read})
  37|  end
  38|
  39|  # (1) 1:1
  40|  def one2one
  41|    o2o_query = @db.query(Student)
  42|    o2o_query.add_criteria(Rorm::Criteria.equals('name', 'Charles Babbage'))
  43|    charles = o2o_query.execute[0]
  44|    puts charles.tellme_password
  45|  end
  46|
  47|  # (2) 1:N
  48|  def one2many
  49|    db = Rorm::Rorm.new(File.open('rorm-sample.xml'){|f| f.read})
  50|    o2m_query = db.query(Department)
  51|    o2m_query.add_criteria(Rorm::Criteria.equals('code', '20'))
  52|    ec = o2m_query.execute[0]
  53|    puts "#{ec.name}:"
  54|    ec.students.each do |s|
  55|      puts s.introduce
  56|    end
  57|  end
  58|
  59|  # (3) N:N
  60|  def many2many
  61|    m2m_query = @db.query(Student)
  62|    m2m_query.add_criteria(Rorm::Criteria.greater_than('no', '20-000', true))
  63|    m2m_query.add_criteria(Rorm::Criteria.less_than('no', '30-000', false))
  64|    students = m2m_query.execute
  65|    students.each do |s|
  66|      puts "#{s.name}'s courses are:"
  67|      puts s.course_summary
  68|    end
  69|  end
  70|
  71|  # (4) INSERT
  72|  def insert
  73|    @db.transaction do |d|
  74|      fillip = Student.new
  75|      fillip.no = "20-004"
  76|      fillip.name = "Fillip Estridge"
  77|      fillip.department_id = 2
  78|      mapper = d.mapper(Student)
  79|      mapper.insert(fillip)
  80|    end
  81|  end
  82|
  83|  # (5) UPDATE
  84|  def update
  85|    @db.transaction do |d|
  86|      fillip_query = d.query(Student)
  87|      fillip_query.add_criteria(Rorm::Criteria.equals('no', '20-004'))
  88|      fillip = fillip_query.execute[0]
  89|      fillip.name = "Fillip Don Estridge"
  90|      mapper = d.mapper(Student)
  91|      mapper.update(fillip)
  92|    end
  93|  end
  94|
  95|  # (6) DELETE
  96|  def delete
  97|    @db.transaction do |d|
  98|      fillip_query = d.query(Student)
  99|      fillip_query.add_criteria(Rorm::Criteria.equals('no', '20-004'))
 100|      fillip = fillip_query.execute[0]
 101|      mapper = d.mapper(Student)
 102|      mapper.delete(fillip)
 103|    end
 104|  end
 105|end
 106|
 107|if __FILE__ == $0
 108|  s = Sample.new
 109|  puts "(1) 1:1"
 110|  s.one2one
 111|  puts "(2) 1:N"
 112|  s.one2many
 113|  puts "(3) N:N"
 114|  s.many2many
 115|
 116|  puts "(4) INSERT"
 117|  s.insert
 118|  s.one2many
 119|  puts "(5) UPDATE"
 120|  s.update
 121|  s.one2many
 122|  puts "(6) DELETE"
 123|  s.delete
 124|  s.one2many
 125|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 Framework*3) を 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 では YAML*5 でマッピング定義であるモデルファイルを記述します。

付属の 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

--- 
adapter_name: PostgreSQL
connection: 
  url: dbi:Pg:RLR_DB
  user: babie
  password: ''
entities: 
  - 
    name: Student
    class_name: GenericRecord
    external_name: Student
    class_properties: 
      - id
      - "no"
      - name
      - department_id
    attributes: 
      - 
        name: id
        column_name: id
        class_name: Integer
        external_type: integer
        allow_null: false
        read_only: true
      - 
        name: "no"
        column_name: "no"
        class_name: String
        external_type: character varying
        allow_null: false
        read_only: true
        width: 6
      - 
        name: name
        column_name: name
        class_name: String
        external_type: character varying
        allow_null: false
        read_only: true
        width: 128
      - 
        name: department_id
        column_name: department_id
        class_name: Integer
        external_type: integer
        allow_null: false
        read_only: true
    primary_key_attributes: 
      - id

コードの説明に移る前に 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

   1|#!/usr/bin/env ruby
   2|require 'tapkit'
   3|
   4|def introduce(name)
   5|   "Hello, my name is #{name}."
   6|end
   7|
   8|include TapKit
   9|app = Application.new('tapkit-simple.yaml')
  10|
  11|context = app.shared_editing_context
  12|query = Qualifier.format("name = 'Alan Mathison Turing'")
  13|fs = FetchSpec.new('Student', query);
  14|
  15|alan = context.fetch(fs)[0]
  16|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

   1|#!/usr/bin/env ruby
   2|require 'tapkit'
   3|
   4|def introduce(name)
   5|  "\tHello, my name is #{name}."
   6|end
   7|
   8|class Sample
   9|  include TapKit
  10|  def initialize
  11|    @app = Application.new('tapkit-sample.yaml')
  12|  end
  13|
  14|  # (1) 1:1
  15|  def one2one
  16|    ec = @app.shared_editing_context
  17|    q = Qualifier.format("name like 'C*'")
  18|    fs = FetchSpec.new('Student', q);
  19|    charles, = ec.fetch(fs)
  20|
  21|    puts "\t#{charles['name']}'s Password: #{charles['account']['password']}"
  22|  end
  23|
  24|  # (2) 1:N
  25|  def one2many
  26|    app = Application.new('tapkit-sample.yaml')
  27|    ec = app.shared_editing_context
  28|    q = Qualifier.format("code = '20'")
  29|    fs = FetchSpec.new('Department', q);
  30|
  31|    dept = ec.fetch(fs)[0]
  32|    puts "\t#{dept['name']}:"
  33|    students = dept['students']
  34|    students.each do |s|
  35|      puts introduce(s['name'])
  36|    end
  37|  end
  38|
  39|  # (3) N:N
  40|  def many2many
  41|    ec = @app.shared_editing_context
  42|    q = Qualifier.format("no like '20-*'")
  43|    fs = FetchSpec.new('Student', q)
  44|
  45|    students = ec.fetch(fs)
  46|    students.each do |s|
  47|      puts "\t#{s['name']}'s courses are:"
  48|      rs = s['registration']
  49|      rs.each do |r|
  50|        puts "\t\t[#{r['course']['code']}] #{r['course']['name']}"
  51|      end
  52|    end
  53|  end
  54|
  55|  # (4) INSERT
  56|  def insert
  57|    ec = @app.create_editing_context
  58|
  59|    fillip = ec.create('Student')
  60|    fillip['no'] = '20-004'
  61|    fillip['name'] = 'Fillip Estridge'
  62|    fillip['department_id'] = 2
  63|    ec.save
  64|  end
  65|
  66|  # (5) UPDATE
  67|  def update
  68|    ec = @app.create_editing_context
  69|    q = Qualifier.format("no = '20-004'")
  70|    fs = FetchSpec.new('Student', q)
  71|
  72|    fillip = ec.fetch(fs)[0]
  73|    fillip['name'] = 'Fillip Don Estridge'
  74|    ec.save
  75|  end
  76|
  77|  # (6) DELETE
  78|  def delete
  79|    ec = @app.create_editing_context
  80|    q = Qualifier.format("no = '20-004'")
  81|    fs = FetchSpec.new('Student', q)
  82|
  83|    fillip = ec.fetch(fs)[0]
  84|    ec.delete fillip
  85|    ec.save
  86|  end
  87|end
  88|
  89|if __FILE__ == $0
  90|  s = Sample.new
  91|  puts "(1) 1:1"
  92|  s.one2one
  93|  puts "(2) 1:N"
  94|  s.one2many
  95|  puts "(3) N:N"
  96|  s.many2many
  97|
  98|  puts "(4) INSERT"
  99|  s.insert
 100|  s.one2many
 101|  puts "(5) UPDATE"
 102|  s.update
 103|  s.one2many
 104|  puts "(6) DELETE"
 105|  s.delete
 106|  s.one2many
 107|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

name: Sample
connection:
  url: "DBI:Pg:RLR_DB"
  user: babie
  password: "" 
  adaptor: PostgreSQL
entities:
  Student:
    ruby_class: Student
    table_name: student
    attributes:
      id:
        column_name: id
        primary_key: true
        ruby_type: Fixnum
        column_type: integer
        generate_value: false
      "no":
        column_name: "no"
        ruby_type: String
        column_type: varchar
        size: 13
        allows_null: false
      name:
        column_name: name
        ruby_type: String
        column_type: varchar
        size: 128
        allows_null: false
      department_id:
        column_name: department_id
        ruby_type: Fixnum
        column_type: integer
        allows_null: false

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

sds-simple.rb

   1|#!/usr/bin/env ruby
   2|require 'sds'
   3|
   4|include SDS
   5|store = Store.get('sds-simple.yaml')
   6|
   7|class Student
   8|   include SDS::Object
   9|   def introduce
  10|      puts "\tHello, my name is #{name}."
  11|   end
  12|end
  13|
  14|context = Context.new(store)
  15|alan = context.fetch('Student', "name = 'Alan Mathison Turing'")[0]
  16|alan.introduce

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

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

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

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

各種 参照・更新

sds-sample.yaml

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

sds-sample.rb

   1|#!/usr/bin/env ruby
   2|require 'sds'
   3|
   4|include SDS
   5|Store.get('sds-sample.yaml')
   6|
   7|class Department
   8|   include SDS::Object
   9|end
  10|
  11|class Student
  12|   include SDS::Object
  13|   def introduce
  14|      puts "\tHello, my name is #{name}."
  15|   end
  16|end
  17|
  18|class Account
  19|   include SDS::Object
  20|end
  21|
  22|class Course
  23|   include SDS::Object
  24|end
  25|
  26|class Registration
  27|   include SDS::Object
  28|end
  29|
  30|class Sample
  31|   def initialize
  32|      @store = Store.get('sds-sample.yaml')
  33|   end
  34|
  35|   # (1) 1:1
  36|   def one2one
  37|      context = Context.new(@store)
  38|      students = context.fetch('Student', "name like 'C%'")
  39|      charles = students[0]
  40|      puts "\t#{charles.name}'s Password: #{charles.account.password}"
  41|   end
  42|
  43|   # (2) 1:N FIXME
  44|   def one2many
  45|      context = Context.new(@store)
  46|      biz = context.fetch('Department', "code = '20'")[0]
  47|      puts "\t#{biz.name}:"
  48|      biz.students.each do |s|
  49|         puts s.introduce  # なんか nil も出力されちゃう
  50|      end
  51|   end
  52|
  53|   # (3) N:N
  54|   def many2many
  55|      context = Context.new(@store)
  56|      students = context.fetch('Student', "department_id = 2")
  57|      students.each do |s|
  58|         puts "\t#{s.name}'s courses are:"
  59|         s.registrations.each do |r|
  60|            puts "\t\t[#{r.course.code}] #{r.course.name}"
  61|         end
  62|      end
  63|   end
  64|
  65|   # (4) INSERT
  66|   def insert
  67|      context = Context.new(@store)
  68|      fillip = Student.create(context)
  69|      fillip.no = '20-004'
  70|      fillip.name = 'Fillip Estridge'
  71|    fillip.department_id = 2
  72|    puts "object to insert: #{context.objects_to_insert}"
  73|      context.save
  74|   end
  75|
  76|   # (5) UPDATE
  77|   def update
  78|      context = Context.new(@store)
  79|      fillip = context.fetch('Student', "no = '20-004'")[0]
  80|      fillip.name = 'Fillip Don Estridge'
  81|      context.save
  82|   end
  83|
  84|   # (6) DELETE FIXME
  85|   def delete
  86|      context = Context.new(@store)
  87|      fillip = context.fetch('Student', "no = '20-004'")[0]
  88|    #context.delete(fillip)
  89|      context.save
  90|   end
  91|end
  92|
  93|if __FILE__ == $0
  94|   s = Sample.new
  95|
  96|  puts "(1) 1:1"
  97|   s.one2one
  98|  puts "(2) 1:N"
  99|   s.one2many
 100|  puts "(3) N:N"
 101|   s.many2many
 102|
 103|  puts "(4) INSERT"
 104|   s.insert
 105|   s.one2many
 106|  puts "(5) UPDATE"
 107|   s.update
 108|   s.one2many
 109|  puts "(6) DELETE"
 110|   s.delete
 111|   s.one2many
 112|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

   1|#!/usr/bin/env ruby
   2|require 'active_record'
   3|
   4|class Student < ActiveRecord::Base
   5|  def self.table_name() "student" end
   6|
   7|  def introduce
   8|    return "Hello, my name is %s." % name
   9|  end
  10|end
  11|
  12|require 'logger'
  13|ActiveRecord::Base.logger = Logger.new("debug.log")
  14|ActiveRecord::Base.establish_connection(
  15|  :adapter  => "postgresql",
  16|  :host     => "localhost",
  17|  :username => "babie",
  18|  :password => "",
  19|  :database => "RLR_DB"
  20|)
  21|
  22|student = Student.find_all("name = 'Alan Mathison Turing'")[0]
  23|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

   1|#!/usr/bin/env ruby
   2|require 'active_record'
   3|# for RubyGems package
   4|=begin
   5|require 'rubygems'
   6|require_gem 'activerecord'
   7|=end
   8|
   9|class Student < ActiveRecord::Base
  10|  def self.table_name() "student" end
  11|  has_one :account
  12|  has_and_belongs_to_many :courses, :table_name => "course", :join_table => "registration"
  13|
  14|  def introduce
  15|    return "Hello, my name is %s." % name
  16|  end
  17|
  18|  def tellme_password
  19|    %Q|#{self.name}'s password is "#{self.account.password}"|
  20|  end
  21|end
  22|
  23|class Department < ActiveRecord::Base
  24|  def self.table_name() "department" end
  25|  has_many :students
  26|end
  27|
  28|class Account < ActiveRecord::Base
  29|  def self.table_name() "account" end
  30|end
  31|
  32|class Course < ActiveRecord::Base
  33|  def self.table_name() "course" end
  34|  has_and_belongs_to_many :students, :table_name => "student", :join_table => "registration"
  35|end
  36|
  37|require 'logger'
  38|ActiveRecord::Base.logger = Logger.new("debug.log")
  39|ActiveRecord::Base.establish_connection(
  40|  :adapter  => "postgresql",
  41|  :host     => "localhost",
  42|  :username => "babie",
  43|  :password => "",
  44|  :database => "RLR_DB"
  45|)
  46|
  47|
  48|class Sample
  49|  # (1) 1:1
  50|  def one2one
  51|    charles = Student.find_all("name = 'Charles Babbage'")[0]
  52|    puts charles.tellme_password
  53|  end
  54|
  55|  # (2) 1:N
  56|  def one2many
  57|    dept = Department.find_all("code = 20")[0]
  58|    puts "#{dept.name}:"
  59|    dept.students.each do |s|
  60|      puts s.introduce
  61|    end
  62|  end
  63|
  64|  # (3) N:N
  65|  def many2many
  66|    Student.find_all("no like '20-%'").each do |s|
  67|      puts "#{s.name}'s courses are:"
  68|      s.courses.each do |c|
  69|        puts "\t[#{c.code}] #{c.name}"
  70|      end
  71|    end
  72|  end
  73|
  74|  # (4) INSERT
  75|  def insert
  76|    dept = Department.find_all("code = 20")[0]
  77|    fillip = Student.new(
  78|      "name" => 'Fillip Estridge',
  79|      "no" => '20-004',
  80|      "department_id" => 2
  81|    )
  82|    fillip.save
  83|  end
  84|
  85|  # (5) UPDATE
  86|  def update
  87|    fillip = Student.find_all("no = '20-004'")[0]
  88|    fillip.name = 'Fillip Don Estridge'
  89|    fillip.save
  90|  end
  91|
  92|  # (6) DELETE
  93|  def delete
  94|    fillip = Student.find_all("no = '20-004'")[0]
  95|    fillip.destroy
  96|  end
  97|end
  98|
  99|if __FILE__ == $0
 100|  s = Sample.new
 101|  puts "(1) 1:1"
 102|  s.one2one
 103|  puts "(2) 1:N"
 104|  s.one2many
 105|  puts "(3) N:N"
 106|  s.many2many
 107|
 108|  puts "(4) INSERT"
 109|  s.insert
 110|  s.one2many
 111|  puts "(5) UPDATE"
 112|  s.update
 113|  s.one2many
 114|  puts "(6) DELETE"
 115|  s.delete
 116|  s.one2many
 117|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 が下働きに甘んじているのが悲しいところです。

*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 を生成すると、この名前でシーケンステーブル名が定義されています。