著者:馬場 道明 編集・校正:立石 孝彰 協力:かずひこ、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を用意しました。
今回のサンプルの作成は、RDBMS に PostgreSQL を用いて行い、この SQL も PostgreSQL 特有のカラム型がありますが、小さい定義なので、その他の RDBMS でも若干の修正で使用できると思います。
簡単ながら、テーブル定義を説明させて頂きますと、
という大学におけるデータを想定しており、
というリレーションに O/R マッパーを用いてどうアクセスするかを見て行きたいと思います。
かなりディフォルメされていますが、各ライブラリの感触を掴むには充分だと思います。
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 テーブルからデータを取り出す単純なアクセスの方法を見てみましょう。
#!/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 による設定の要素・属性から説明すると、
となります。
コードの方は、 クラス 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対多、多対多、追加、更新、削除を見てみたいと思います。
Student テーブルのマッピング定義の id 要素に last_id 属性が加わっています。 これは INSERT 時はキー id 値が自動採番の為未定義で、INSERT 後にオブジェクトにキーの値を詰め直す為の関数を記入します。
その他追加された要素を説明します。
Account へのリレーションは 1対1 を意図しているのですが、rorm には 1対1 の為の特別な記法はありませんので、1対多の記法で代用しています。
それではコードの方を見てみましょう。
#!/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 で定義した
の通り、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 は 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 から直接リレーション等の情報を読み取って生成することが出来ます。早速使用してみましょう。
引数で与える 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 と同じく単純な参照を行ってみましょう。
コードの説明に移る前に YAML の各要素について解説します。
設定は大きく分けて 3 つのセクションがあります。
connection セクションでは以下の情報を記述します。
entities セクションでは以下の設定が並びます。
attributes には以下のデータを記述します。
それではコードの方を覗いてみましょう。
#!/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 行ほどの長大なリストになるのでリンクだけに留めて、追加要素を解説したいと思います。
#!/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 は 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 依存性の排除 といった機能強化が挙げられています。 意欲的に開発を行っているようなので期待がもてますね。
TapKit の YAML とも似てますし、キー名も素直なので特に解説する必要はないのではないでしょうか。
#!/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 メソッドに直接条件式を与えることができます。
relationships が増えましたが、これも TapKit とそう変わりませんので、難なく読み書きできると思います。
#!/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 は ウェブアプリケーションフレームワークである、RubyOnRails の O/R マッピングを担うライブラリです。RubyOnRails が依存しているわけではなく、単体で利用することもできます。
マッピング定義を Ruby クラス内で表現できるところに特徴があります。
#!/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 メソッド定義を省略でき一層楽をすることが出来ます。
でログファイルを記録することによりエラー時のトレースを簡単にできます。
ActiveRecord::Base.establish_connection で DB との接続を定義します。
find_all(query_str) メソッドで マッピングされたオブジェクトを取得することが出来ます。 あとは object.column_name という形式でデータにアクセスすることが出来ます。
#!/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 マッピングライブラリは盛況のようで、 今回紹介した以外にも vapor や Kansas といったものもあるのですが、時間と分量の面で折り合いがつかず紹介できませんでした。 どちらかというと 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 が下働きに甘んじているのが悲しいところです。
詳細は http://www.apple.com/jp/webobjects/wo_docs_j.html の “Enterprise Objects Framework Developers Guide” を参照して下さい ↩
SQLite でも今回のサンプルのプロトタイプを動かすことができました。 ↩
http://www.yaml.org/ ↩
http://www.spice-of-life.net/tapkit/ja/TapKitUserGuide_J_c5_s1.html#doc7_1176 によると、「主キー名に id と設定しないでください。主キーを id とすると、オブジェクトにアクセスしても Object#id が呼び出されてしまいます。」とあるので、今回のサンプルのテーブル定義が適切でない可能性があります。 ↩
付属コマンド sds_generate_db_schema.rb で YAML から テーブル定義 SQL を生成すると、この名前でシーケンステーブル名が定義されています。 ↩