Ruby ActiveRecord の(超)簡易実装で学ぶメタプログラミング

Written by @dr_taka_n at 2009/10/26 19:46 []

しっかり抑えておきたいと思うメタプログラミング

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](http://pragprog.com/titles/ppmetr/metaprogramming-ruby) を購入して読んでみようと思う。
Paolo Perrotta
Pragmatic Bookshelf ( 2010-01-31 )
ISBN: 9781934356470

と、今回のこのメモの中では、先に挙げた Ruby’s Metaprogramming Toolbox で掲載されている ActiveRecord の簡易実装サンプルを SQLite 版で書いてみて、使用されているツールボックスのツールを確認してみたいと思う。

ActiveRecord の簡易実装 (ちょっとした検索だけ)

Ruby’s Metaprogramming Toolbox では最後にメタプログミングの適用例として、 ActiveRecord の簡易実装を記載している。
これが MySQL 用のものであるので、もっと手軽に試せる SQLite 版にしてみた。

require 'rubygems'
require 'sqlite3'

# for SQLite
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

      @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

      @@db.table_info(table_name) do |field|
        klass.send :attr_accessor, field['name'].to_sym
        klass.module_eval { @@fields << field['name'] }
      end
    end
    @@db
  end

  def self.close
    @@db.close
  end

  def self.find(id)
    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

ファイルの内容は以下の通り。

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

ポイントメモ

幾つかポイントをメモ。

  1. 動的なクラスの生成(しかもサブクラス)
  2. 動的なアクセサの生成とコンストラクタによる値のセット
  3. クラスインスタンス変数の利用とそのアクセサの生成
  4. Object#send を使った private メソッドへのアクセス

1. 動的なクラスの生成(しかもサブクラス)

Module#const_set を利用して、テーブルの名前から動的に PoorManWithSqlite クラスのサブクラスとなる各テーブルのクラスを生成している。

@generated_classes << klass = Object.const_set(class_name,
                                               Class.new(PoorManWithSqlite))

Module#const_set(name, value) -> object

モジュールに name で指定された名前の定数を value という値に定義し、value を返す。

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 の無名のサブクラスを生成する。

class SuperAClass; end
sub_k = Class.new(SuperAClass)  # => #<Class:0x2533c>

sub_k.class                     # => Class
sub_k.superclass                # => SuperAClass

コードブロックを預けてメソッドを定義することもできる。

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. 動的なアクセサの生成とコンストラクタによる値のセット

動的なアクセサを用意しているところ。

 @@db.table_info(table_name) do |field|
   klass.send :attr_accessor, field['name'].to_sym
   klass.module_eval { @@fields << field['name'] }
 end

テーブルのフィールドをイテレートし、そのテーブルクラスの読み書き両用のインスタンス変数を生成している。

次に値をセットしているところ。

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 メソッドが利用される実際に値をセットしているところは以下の箇所となる。

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 クラスのクラスインスタンス変数にその情報を保持している。

class << self; attr_reader :generated_classes; end
@generated_classes = []

アクセサは、PoorManWithSqlite クラスオブジェクトの特異クラスで記述する必要がある。

4. Object#send を使った private メソッドへのアクセス

 @@db.table_info(table_name) do |field|
   klass.send :attr_accessor, field['name'].to_sym
   klass.module_eval { @@fields << field['name'] }
 end

klass というのは、PoorManWithSqlite を継承したテーブル毎のクラスなのだが、このクラスの attr_accessor メソッドには、klass をレシーバとしてはアクセスできない。 Object#send を使うことにより、アクセスコントロールをバイパスしている。

例示のため使っているのだと思うが、その下にある Module#module_eval のブロックの中で一緒に書いてもよいと思う。

klass.module_eval { attr_accessor field['name'].to_sym; @@fields << field['name'] }
blog comments powered by Disqus