Rails acts_as_paranoid を覗く
前回のBLOG 続き,
では,init.rbを覗いてみる.(素人なのでー間違っている箇所が多いと思います.ツッコミいただければー幸いです.)
# vendor/plugins/acts_as_paranoid/init.rb
class << ActiveRecord::Base
def belongs_to_with_deleted(association_id, options = {})
with_deleted = options.delete :with_deleted
returning belongs_to_without_deleted(association_id, options) do
if with_deleted
reflection = reflect_on_association(association_id)
association_accessor_methods(reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
association_constructor_method(:build, reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
association_constructor_method(:create, reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
end
end
end
alias_method_chain :belongs_to, :deleted
end
ActiveRecord::Base.send :include, Caboose::Acts::Paranoid
1行目からわからない! " class A < B " なら AクラスはBクラスを継承するということだが…. これなんなん!?
これは,クラスメソッドの追加でした.つまり,ActiveRecord::Base クラスに belongs_to_with_deleted メソッドを追加している.下記にちょこっと試してみた!
$ irb
> class Base
> def hoge
> 'hoge'
> end
> end
> class << Base
> def hige
> 'hige'
> end
> end
>
> Base.hige # 追加されたクラスメソッドの実行
=> "hige"
> Base.hoge # インスタンスメソッドだから実行できない
NoMethodError: undefined method `hoge' for Base:Class
> obj = Base.new
> obj.hige # クラスメソッドだから実行できない
NoMethodError: undefined method `hige' for #<Base:0xb7e8738c>
> obj.hoge # インスタンスメソッドの実行
=> "hoge"
とういうことで納得.次に「alias_method_chain :belongs_to, :deleted」これ何してんだろ…. これrubyじゃなくてーrailsのメソッドだった…. このファイルを覗くと答えが見れる.
ruby/lib/ruby/gems/1.8/gems/activesupport-2.0.2/lib/active_support/core_ext/module/aliasing.rb`
alias という言葉通りな感じですね. 試しに使ってみる.
$ ./script/console
>> class Gly
>> def name
>> 'glycine'
>> end
>> end
>> class G < Gly
>> def name_with_short
>> name_without_short + '( GLY )'
>> end
>> alias_method_chain :name, :short
>> end
>> Gly.new.name
=> "glycine"
>> G.new.name
=> "glycine( GLY )"
name_without_short は,拡張前のメソッドを呼び出せる. name_with_short は,拡張したメソッドを呼び出せる. この説明だとわかりにくいですよね.
この段階になっても,Model に acts_as_paranoid と書いただけでープラグインがうまく動くことは理解できない. 最後の行の「ActiveRecord::Base.send :include, Caboose::Acts::Paranoid」これですね. ここは,(クラスメソッドが追加された)ActiveRecord::Base に Caboose::Acts::Paranoid モジュールを include している. Paranoidモジュールは,vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb にあります. しかし,このモジュールが読み込まれてもー (ClassMethodsモジュール内の)acts_as_paranoid メソッドは,Model から呼び出せない. と思ったらー(Paranoidモジュールの)self.included(base) ってーのが ClassMethodsモジュールをextend してるわ!ということで Model に acts_as_paranoid と書くとー Modelを 読み込んだとき に acts_as_paranoidメソッドが実行される.
メモがてらにモジュールのincludeを試してみる.
$ irb
> module Paranoid
> def self.included(base)
> p self #=> Pranoid
> p base #=> Base
> base.extend ClassMethods
> end
>
> module ClassMethods
> def acts_as_paranoid
> 'ACTS_AS_PARANOID'
> end
> end
> end
>
> class Base
> include Paranoid
> end
>
> Base.acts_as_paranoid
=> "ACT_AS_PARANOID"
これで何とかーModelにacts_as_paranoid と書くことだけでーModelにプラグインが反映されることが理解. いやー疲れた.やっぱ言語勉強しないとね.今回もー toshi78 さんにも協力してもらっちゃいました.
結局,このプラグインを全部解読してませんが,色々と疑問. なんで,こんなにモジュールがネストされているんだろ? また,ActiveRecord::Baseにクラスメソッドを最初に追加したんだろ? だってーこの init.rb は Rails がロードされたときに読み込まれる.だもんでー 論理削除を必要としないModelにまでー不必要なクラスメソッドが追加されちゃいません?パフォーマンス悪くなっちゃうんじゃない? 素人の俺には「ActiveRecord::Base.send :include, Caboose::Acts::Paranoid」の中でーうまく「belongs_to_with_deleted」クラスメソッドを追加する処理をすればいいのではないのかなぁ〜.
と,素人の考えが色々と浮かぶ….
Railsインストール
個人メモ
Railsの勉強をしなきゃいけなくなりそうなのでRailsインストールしてみた. とりあえず開発環境だけできればいいから必要最低限度のモノをインストール.
環境はFC5です.
rubyのインストール
$ wget ftp://ftp.ruby-lang.org/pub/ruby/1.8/ruby-1.8.6-p111.tar.bz2
$ bunzip2 ruby-1.8.6-p111.tar.bz2
$ tar xvf ruby-1.8.6-p111.tar
$ cd ruby-1.8.6-p111
$ ./configure --prefix=/usr/local/ruby
$ make
$ sudo make install
gemsのインストール
$ wget http://rubyforge.org/frs/download.php/29548/rubygems-1.0.1.tgz
$ tar xzvf rubygems-1.0.1.tgz
$ cd rubygems-1.0.1
$ sudo /usr/local/ruby/bin/ruby setup.rb
railsのインストール
$ sudo /usr/local/ruby/bin/gem install -v=1.2.6 rails
サンプルが画面作成することができた. MySQLとも連携できた.とりあえずOK.
Railsプラグイン acts_as_paranoid
Railsのプラグインとやらは結構色々あるみたい.ということで,Railsプラグインを使ってみた. 論理削除をするプラグインってーのがあるので試しに…. (論理削除するプラグインなんていらないでしょ!? と思う方も多いのではないかな!? 実際自分は思ったがープラグインを試すには簡単そうだったのでね)
単純な登録/閲覧/更新/物理削除 できるサンプルアプリに論理削除プラグインを追加してみた.
# db/migrate/001_create_entries.rb
class CreateEntries < ActiveRecord::Migration
def self.up
create_table :entries do |t|
t.column :name, :string, :null => false
t.column :created_at,:datetime, :null => false
t.column :updated_at,:datetime, :null => false
end
end
def self.down
drop_table :entries
end
end
プラグインのインストール.
# リポジトリの追加
$ ./script/plugin source http://techno-weenie.net/svn/projects/plugins
# 論理削除プラグイン(acts_as_paranoid)のインストール
$ ./script/plugin install acts_as_paranoid
インストールって,svn::export しているだけのようです.exportは,vendor/plugins/acts_as_paranoid/ にされてます. 本体の「lib/commands/plugin.rb」の中を覗くとー実行コマンドが覗ける.
カラム(deleted_at)の追加
# db/migrate/002_add_delete.rb
class AddDelete < ActiveRecord::Migration
def self.up
add_column :entries, :deleted_at, :datetime
end
def self.down
remove_column :entries, :deleted_at
end
end
Entryモデルに「acts_as_paranoid」を記述!
# app/models/entry.rb
class Entry < ActiveRecord::Base
acts_as_paranoid
end
これで終了! だけど下記のようなエラーが発生.
undefined method `construct_count_options_from_args' for Entry:Class
Trace覗くと,
vendor/plugins/acts_as_paranoid/lib/caboose/acts/paranoid.rb:91:in `count_with_deleted'
となっていたので,Google検索で調べたらー
90 def count_with_deleted(*args)
91 #calculate_with_deleted(:count, *construct_count_options_from_args(*args)) # 変更前
92 calculate_with_deleted(:count, *construct_count_options_from_legacy_args(*args)) # 変更後
93 end
とやったら修正できた. ほんで,試しにエントリーを削除してみたらー,論理削除された!
DBを覗いてみたら
mysql> SELECT * FROM entries;
+----+-----------+---------------------+---------------------+---------------------+
| id | name | created_at | updated_at | deleted_at |
+----+-----------+---------------------+---------------------+---------------------+
| 1 | ENDLESS | 2008-02-18 12:02:00 | 2008-02-18 12:02:36 | NULL |
| 2 | TEST NAME | 2008-02-18 12:02:00 | 2008-02-18 12:02:44 | 2008-02-18 12:16:09 |
+----+-----------+---------------------+---------------------+---------------------+
id=2 を削除したら, deleted_at に削除した日時が記録された. あれ!? ケド updated_at は更新されてないみたい.まぁ~削除日時が登録されているからいいのか….
と,プラグインを追加して論理削除が成功したのはいいのですがーこの「acts_as_paranoid」1行が何をしたのかが問題ですよね. Rubyをまともに読めない俺だからいけないのでしょう...
ということで,プラグインを覗いてみる! 怪しいファイル見っけ! vendor/plugins/acts_as_paranoid/init.rb これが勝手に読み込まれているようだ!
あっ...早速言語の壁!Ruby(Rails?)が読めない! alias_method_chain,association_accessor_methods この辺りから調べねば…. ということで,今日はここまでです.
単一テーブル継承
Rails「単一テーブルの継承」ってーのを利用して遊んでみたら,わけがわからなくなった. 単一テーブルの継承とは,同じようなテーブル(カラム)構成の時に複数のテーブルを作成するのを避けたりするときに,継承しちゃえー的な!
下記の例は,商品名と価格の情報を持つ食べ物(Food)と飲み物(Drink)を商品(Item)という1つのテーブルに任せましょ! だもんで,このitemsテーブルにはtypeカラムが必須となる.
・ テーブル作成.
$ vi db/migrate/001_create_items.rb
class CreateItems < ActiveRecord::Migration
def self.up
create_table :items do |t|
t.column :name, :string, :null => false
t.column :price, :integer, :null => false
t.column :created_at,:datetime, :null => false
t.column :updated_at,:datetime, :null => false
t.column :type, :string, :null => false #必須
end
end
def self.down
drop_table :items
end
end
・ Model Itemモデルを継承してあげるだけ.
class Item < ActiveRecord::Base
end
class Food < Item
end
class Drink < Item
end
では,実験.
$ ./script/console
>> Item.find(:all)
=> []
>> Food.find(:all)
=> []
>> Drink.find(:all)
=> []
>> Food.create(
?> :name => 'apple',
?> :price => 150
?> )
>> Item.find(:all)
=> [#<Food:0x229b088 @attributes={"name"=>"apple", "updated_at"=>"2008-02-17 00:00:00", "price"=>"150", "type"=>"Food", "is_active"=>"1", "id"=>"1", "created_at"=>"2008-02-17 00:00:00"}>]
>> Item.find(:all)
=> [#<Food:0x2297d5c @attributes={"name"=>"apple", "updated_at"=>"2008-02-17 00:00:00", "price"=>"150", "type"=>"Food", "is_active"=>"1", "id"=>"1", "created_at"=>"2008-02-17 00:00:00"}>]
>> Food.find(:all)
=> [#<Food:0x2294864 @attributes={"name"=>"apple", "updated_at"=>"2008-02-17 00:00:00", "price"=>"150", "type"=>"Food", "is_active"=>"1", "id"=>"1", "created_at"=>"2008-02-17 00:00:00"}>]
>> Drink.find(:all)
=> []
DBでは,
mysql> SELECT * FROM items;
+----+-------+-------+---------------------+---------------------+-----------+------+
| id | name | price | created_at | updated_at | is_active | type |
+----+-------+-------+---------------------+---------------------+-----------+------+
| 1 | apple | 150 | 2008-02-17 14:43:39 | 2008-02-17 14:43:39 | 1 | Food |
+----+-------+-------+---------------------+---------------------+-----------+------+
ここまではよかったんだが,実際にフォームを作ってWebアプリとして作成してみようとしたら,
# @tags = Tag.find(:all).collect {|t| [t.name, t.model]}
# => [['食べ物', 'Food'], ['飲み物', ''Drink]]
<% form_tag :action => "create" do %>
<%= text_field :item, :name %>
<%= text_field :item, :price %>
<%= select :item, :type, @tags %>
<%= submit_tag "Post" %>
<% end %>
てーな感じでフォームを作ろうとしたら,wrong argument type String (expected Module) てなエラーでて困った. ここ 見るとーtypeは駄目なんだと….でーも単一テーブルの継承するにはtype必要なんでしょ!?と思って名前を変えたりして無理矢理対応していたんだが….
よく考えるとー別にtypeでモデルを切り替えてーデータをつくるんだからー俺がtypeを意識する必要ないじゃん.つまり,意図的にtypeを params[:item] に含める必要がなかった….
まぁ結局俺がアホなだけですね.わけのわからんサンプルを作ってしまった.
ダイナミックにインスタンス化
Railsのメモ.
文字列(クラス名)からインスタンス化させようとしたとき,Rails上ならこれでいける.
cls = "ClassName".constantize
obj = cls.new
# とか,
module_name = "ModuleName"
class_name = "ClassName"
cls = "#{module_name}::#{class_name}".constantize
obj = cls.new
といける.
constantizeを覗いてみると,Object.module_eval() をしていた. 結局evalですよね.evalしないでうまくやる方法を探していたんですがね. もちろん,ハッシュでクラスを定義しちゃうっていうのが楽なんですがーちょっと数が多くてね.