Ruby ActiveRecord の(超)簡易実装で学ぶメタプログラミング
しっかり抑えておきたいと思うメタプログラミング
Ruby のメタプロミングは奥が深くて楽しいが、やり過ぎると暗黒面に堕ちてしまう。。
効果的な使い方を心掛けていきたい。
Rails でもよく利用されているように、CoC (Convention over Configuration) と組み合わせることで更に効果的となる。
なかなかとっつきにいところもあり、コンパクトにまとまった文書がないかと探していた。
上記サイトなどはコンパクトによくまとまっているように思う。
メタプログラミングに必要となる、通常はあまり使用しないメソッドをメタプログラミングツールボックスというかたちでリストアップし、その説明はドキュメントへのレファレンスで構成している。 最後にメタプログラミングの適用例として ActiveRecord の簡易実装をサンプルとして掲載している。
記載されているツールボックスの利用の前に、基本となる、
- Ruby のクラス階層
Object クラス、Module クラス、Class クラスと任意のクラスとの関連。 - メソッド参照のルール
もしっかり抑えておく必要があるようには思う。その方が混乱が少なくて済む。。
個人的にはメタプログミングと言えば、why の Metaid が思い付くのだが、why はもう姿を現してくれないのだろうか・・・
"why's (poignant) guide to ruby" が以下のサイトで再掲されているようだ。
時間を見つけて以下のサイトを読み込んでみるのも良さそうだ。
また、"Metaprogramming Ruby" は非常に楽しみしている書籍で、まだベータブックではあるが、The Pragmatic Bookshelf | Metaprogramming Ruby を購入して読んでみようと思う。
と、今回のこのメモの中では、先に挙げた Ruby's Metaprogramming Toolbox で掲載されている ActiveRecord の簡易実装サンプルを SQLite 版で書いてみて、使用されているツールボックスのツールを確認してみたいと思う。
ActiveRecord の簡易実装 (ちょっとした検索だけ)
Ruby's Metaprogramming Toolbox では最後にメタプログミングの適用例として、
ActiveRecord の簡易実装を記載している。
これが MySQL 用のものであるので、もっと手軽に試せる SQLite 版にしてみた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
require 'rubygems' require 'sqlite3' # SQLite バージョン # AR の CoC はとりあえず無視 class PoorManWithSqlite # クラスインスタンス変数に生成されたクラスのリストを保持する class << self; attr_reader :generated_classes; end @generated_classes = [] def initialize(attributes = nil) if attributes attributes.each_pair do |key, value| instance_variable_set('@' + key, value) end end end def self.connect(database = 'data.db') @@db = SQLite3::Database.new(database) sql = <<-SQL select tbl_name from sqlite_master where type = 'table' and tbl_name != 'sqlite_sequence' order by tbl_name; SQL @@db.execute(sql).flatten.each do |table_name| class_name = table_name.split('_').map { |word| word.capitalize }.join # Module#const_set を使用してテーブル用の新しいクラスを作成する @generated_classes << klass = Object.const_set(class_name, Class.new(PoorManWithSqlite)) klass.module_eval do @@fields = [] @@table_name = table_name def fields; @@fields; end end # テーブルのフィールドのリストを収集し、 # それらに対する getter/setter を用意する。 @@db.table_info(table_name) do |field| # getter/setter の追加 klass.send :attr_accessor, field['name'].to_sym # field セットに追加 klass.module_eval { @@fields << field['name'] } end end @@db end def self.close @@db.close end # id でレコードを取得する def self.find(id) # SQLite3::Database#execute2 は、最初の行としてカラム名称の配列を返す。 result = @@db.execute2("select * from #{@@table_name} where id = #{id} limit 1") attributes = Hash[*result.transpose.flatten] new(attributes) if attributes end # 全てのレコードを取得する def self.all results = @@db.execute2("select * from #{@@table_name}") fields = results.shift records = [] results.each do |result| records << new(Hash[*fields.zip(result).flatten]) end results end end PoorManWithSqlite.connect table_classes = PoorManWithSqlite.generated_classes # => [Departments, Employees] table_classes[0].superclass # => PoorManWithSqlite employee = Employees.find(1) # => #<Employees:0x11f20d8 @department_id=nil, @name="taka", @id="1"> employee.name # => "taka" employees = Employees.all # => [["1", "taka", nil], ["2", "goro", nil]] PoorManWithSqlite.close # => true |
上記の実行にあたっては、簡単な DB を用意しておく必要がある。
create-table.sql
ファイルの内容は以下の通り。
1 2 3 4 5 6 7 8 9 |
create table departments ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(255) ); create table employees ( id integer PRIMARY KEY AUTOINCREMENT NOT NULL, name varchar(255), department_id integer ); |
data.db を作成し、上記 DDL 文を流し込む。
$ sqlite3 data.db < creat-table.sql |
ポイントメモ
幾つかポイントをメモ。
- 動的なクラスの生成(しかもサブクラス)
- 動的なアクセサの生成とコンストラクタによる値のセット
- クラスインスタンス変数の利用とそのアクセサの生成
Object#sendを使った private メソッドへのアクセス
1. 動的なクラスの生成(しかもサブクラス)
Module#const_set を利用して、テーブルの名前から動的に PoorManWithSqlite クラスのサブクラスとなる各テーブルのクラスを生成している。
1 2 3 |
# Module#const_set を使用してテーブル用の新しいクラスを作成する @generated_classes << klass = Object.const_set(class_name, Class.new(PoorManWithSqlite)) |
Module#const_set(name, value) -> object
モジュールに name で指定された名前の定数を value という値に定義し、value を返す。
1 2 3 |
C = Object.const_set('Con', 10) Con # => 10 C # => 10 |
Ruby の場合、クラス名は全て定数となる。name にテーブル名から生成した名称のクラス名を指定し、
value には、PoorManWithSqlite のサブクラスを指定している。
その value の指定に使用されているのは Class::new メソッド。Class の new メソッドには、インスタンスメソッドとクラスメソッドがある。
上記で利用しているのはクラスメソッド(Class::new)の方になる。
Class::new(superclass = Object) -> Class
Class::new(superclass = Object) {|klass| ... } -> Class
新しく superclass の無名のサブクラスを生成する。
1 2 3 4 5 |
class SuperAClass; end sub_k = Class.new(SuperAClass) # => #<Class:0x2533c> sub_k.class # => Class sub_k.superclass # => SuperAClass |
コードブロックを預けてメソッドを定義することもできる。
1 2 3 4 5 6 7 8 |
k = Class.new(SuperAClass) do |klass| def hoge 'hoge hoge' end end o = k.new # => #<#<Class:0x273e4>:0x273a8> o.hoge # => "hoge hoge" |
各テーブルのクラスを PoorManWithSqlite クラスのサブクラスとすることで、各テーブルクラスにおいて PoorManWithSqlite の機能が利用できる。
2. 動的なアクセサの生成とコンストラクタによる値のセット
動的なアクセサを用意しているところ。
1 2 3 4 5 6 7 |
# テーブルのフィールドのリストを収集し、それらに対する getter/setter を用意する。 @@db.table_info(table_name) do |field| # getter/setter の追加 klass.send :attr_accessor, field['name'].to_sym # field セットに追加 klass.module_eval { @@fields << field['name'] } end |
テーブルのフィールドをイテレートし、そのテーブルクラスの読み書き両用のインスタンス変数を生成している。
次に値をセットしているところ。
1 2 3 4 5 6 7 |
def initialize(attributes = nil) if attributes attributes.each_pair do |key, value| instance_variable_set('@' + key, value) end end end |
コンストラクタで引数にフィールド名を key、そのフィールドの値を value とした Hash を受け取り、
Object#instance_variable_set で値のセットを行う。'@' を文字列連結しているところはちょっとカッコ悪いが致し方ない。
上記 initialize メソッドが利用される実際に値をセットしているところは以下の箇所となる。
1 2 3 |
results.each do |result| records << new(Hash[*fields.zip(result).flatten]) end |
Hash[*fields.zip(result).flatten] は検索結果の配列からフィールド名を key、そのフィールドの値を value とした Hash を生成している。
3. クラスインスタンス変数の利用とそのアクセサの生成
各テーブルクラスは、PoorManWithSqlite クラスを継承する。
継承の際のクラス変数の扱いには注意が必要だ。
1.8 系まではクラス変数は継承される(1.9 はまだ触っていない。。)。
Database への接続の参照は唯一のインスタンスとして持たせたいので、クラス変数が適している。
ただ、作成されたテーブルクラスの管理情報を各テーブル毎に持たせても仕方ない。このサンプルでは PoorManWithSqlite クラスのクラスインスタンス変数にその情報を保持している。
1 2 3 |
# クラスインスタンス変数に生成されたクラスのリストを保持する class << self; attr_reader :generated_classes; end @generated_classes = [] |
アクセサは、PoorManWithSqlite クラスオブジェクトの特異クラスで記述する必要がある。
4. Object#send を使った private メソッドへのアクセス
1 2 3 4 5 6 7 |
# テーブルのフィールドのリストを収集し、それらに対する getter/setter を用意する。 @@db.table_info(table_name) do |field| # getter/setter の追加 klass.send :attr_accessor, field['name'].to_sym # field セットに追加 klass.module_eval { @@fields << field['name'] } end |
klass というのは、PoorManWithSqlite を継承したテーブル毎のクラスなのだが、このクラスの attr_accessor メソッドには、klass をレシーバとしてはアクセスできない。
Object#send を使うことにより、アクセスコントロールをバイパスしている。
例示のため使っているのだと思うが、その下にある Module#module_eval のブロックの中で一緒に書いてもよいと思う。
1 2 |
# getter/setter の追加とfield セットの追加 klass.module_eval { attr_accessor field['name'].to_sym; @@fields << field['name'] } |