activerecordの様なDBラッパーを作ろうと思って、 @ujm の協力の下module_evalを使って実装してみた。
必要最小限のことしか書いてないのでDBラッパーとしては貧弱だけど、module_evalの動きに注目してみてほしい。
本物のactiverecordも似たような実装をしていたと思う。(うろ覚え)

RUBY:
  1. require "rubygems"
  2. require "mysql"
  3.  
  4. class ModelBase
  5.  
  6. DEFAULT_DBUSER = "user"
  7. DEFAULT_DBPASS = "pass"
  8. DEFAULT_DBHOST = "host"
  9. DEFAULT_DBNAME = "name"
  10.  
  11. @@options = {"dbhost" => DEFAULT_DBHOST,
  12. "dbuser" => DEFAULT_DBUSER,
  13. "dbpass" => DEFAULT_DBPASS,
  14. "dbname" => DEFAULT_DBNAME
  15. }
  16.  
  17. def self.get_db_object()
  18. @@mysql_obj ||= Mysql::new(@@options["dbhost"], @@options["dbuser"], @@options["dbpass"], @@options["dbname"])
  19. end
  20.  
  21. def self.table_name(tn)
  22. module_eval <<"EOS"
  23. class &lt;&lt;self
  24. def get_table_name
  25. '#{tn.to_s}'
  26. end
  27. end
  28. EOS
  29. end
  30.  
  31. def self.query(sql, place_holder=[])
  32. db_obj = get_db_object
  33. stmt = db_obj.prepare(sql)
  34. stmt.execute(*place_holder)
  35. stmt
  36. end
  37.  
  38. def self.count(condition="1", options=[])
  39. sql = "select count(*) as count from #{get_table_name} where " + condition
  40. res = query(sql, options)
  41. count = res.fetch
  42. count[0].to_i
  43. end
  44.  
  45. private_class_method :get_db_object
  46. end

これを次のように使う。たとえばuserテーブルを扱うクラスはこんな感じ。

RUBY:
  1. require "lib/ModelBase.rb"
  2.  
  3. class UserModel <ModelBase
  4. table_name "users"
  5.  
  6. def initialize(id, name, screen_name, location, description, profile_image_url, url, protected, followers_count, friends_count, time_zone, favorites)
  7. @id = id
  8. @name = name
  9. @screen_name = screen_name
  10. @location = location
  11. @description = description
  12. @profile_image_url = profile_image_url
  13. @url = url
  14. @protected = protected
  15. @followers_count = followers_count
  16. @friends_count = friends_count
  17. @time_zone = time_zone
  18. @favorites = favorites
  19. end
  20.  
  21. def save
  22.  
  23. sql = "insert ignore into #{UserModel::get_table_name} (id, name, screen_name, location, description, profile_image_url, url, protected, followers_count, friends_count, time_zone, favorites) values (?,?,?,?,?,?,?,?,?,?,?,?)"
  24. UserModel::query(sql, [@id, @name, @screen_name, @location, @description,
  25. @profile_image_url, @url, @protected, @followers_count,
  26. @friends_count, @time_zone, @favorites])
  27. end
  28.  
  29. end

ModelBaseクラスを継承し、table_nameメソッドでテーブル名を指示しているわけです。

で、ポイントはModelBaseクラスの次の部分。

RUBY:
  1. def self.table_name(tn)
  2. module_eval <<"EOS"
  3. class <<self
  4. def get_table_name
  5. '#{tn.to_s}'
  6. end
  7. end
  8. EOS
  9. end

UserModelの冒頭でtable_nameとこのメソッドを呼んでいます。

module_evalは、そのメソッドが呼ばれたところをselfにして実行されるメソッドであるため、このtable_nameメソッドの中で定義されているget_table_nameは、UserModelのクラスメソッドとして定義されます。

つまり、UserModel内でtable_nameが呼ばれた時点では、次のコードと同じことをします。

RUBY:
  1. class <<UserModel
  2. def get_table_name
  3. '#{tn.to_s}'
  4. end
  5. end

こうすることによって、プログラム中でテーブル名をハードコードしなくてもすむようになるとともに、countなどの関数を親クラスで定義できるようになるわけです。

例えば、この方法で作ったクラスで全レコード数を数えるコードはこうなります。

RUBY:
  1. require "lib/UserModel.rb"
  2.  
  3. UserModel::count

大変すっきりしていますね。

蛇足:

これはtwitterのポストを扱うために書いているプログラムの一部です。
rails使おうかとも思ったのだけど、railsはまだ多機能すぎるためこの程度なら書いてしまった方が悩みが少ない(重いとかファイルの配置とか拡張性とか)かと思って自分で書いています。

しかし、自分でフレームワーク作るかというと・・・・たぶんないですね。
フレームワークは必要じゃない機能もたくさん足すことになります。
なんの目的もなしに機能追加を繰り返していくと、フレームワーク自身のメンテナンスに負荷がかかるようになってしまうし、コードの自由度がどんどん下がっていきます。

それよりは、構造化を心がけながらプログラミングをし、拡張性を担保しながら、必要十分なものを作っていく・・・と、いうのが賢いプログラミングである気がしました。