忍者ブログ
ブログツール、2007/11/06作成
[1] [2] [3] [4] [5] [6
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

久しぶりにRailsに戻ってきたら、あまりにもバージョンが違いすぎてて、なんか浦島太郎気分。

で、ActiveRecordで関連を持ったときに、保存の伝播の仕方が混乱してきたので整理してみた。

結論を先に言うと、
親のメソッドで代入したときはsaveの伝播はなされるが、子のメソッドから代入したときはsaveの伝播はなされない。
つまり、has_many, has_oneとbelongs_toは独立して動作する、ということ。

ふむ。シンプル。

試しにプロジェクト作ってみて、動かして確認。
HogeParent
has_many :hoge_children
has_one :hoge_has_one

HogeChildren
belongs_to :hoge_parent

HogeHasOne
belongs_to :hoge_parent

親は未保存
hoge_parent = HogeParent.new
hoge_parent.name = "post_save hoge"
hoge_parent.new_record? = true
hoge_child = HogeChild.new
hoge_child.name = "post_save child"
hoge_child.new_record? = true
hoge_has_one = HogeHasOne.new
hoge_has_one.name = "post_save has_one"
hoge_has_one.new_record? -> true
親が未保存の時、親のメソッドで代入する
hoge_parent.hoge_children << hoge_child
hoge_child.hoge_parent_id -> nil
hoge_child.new_record? -> true
hoge_parent.hoge_has_one = hoge_has_one
hoge_has_one.hoge_parent_id -> nil
hoge_has_one.new_record? -> true
当然のことながら、この時点では何も起こらない

親が未保存の時に親のメソッドで代入したもので、親を保存したとき
hoge_parent.save
hoge_parent.new_record? -> false
hoge_child.new_record? -> false
hoge_child.hoge_parent_id -> 10
hoge_has_one.new_record? -> false
hoge_has_one.hoge_parent_id -> 10
親の保存が伝播して、子のhoge_prent_idに自動的にIDが保存され、子も一緒にsaveされる

親を先に保存する
saved_hoge_parent = HogeParent.new
saved_hoge_parent.name = "pre_save hoge"
saved_hoge_parent.save -> true
saved_hoge_parent.new_record? -> false
saved_hoge_parent.id -> 11 
saved_hoge_parent.new_record? = false
saved_hoge_child = HogeChild.new
saved_hoge_child.name = "pre_save child"
saved_hoge_child.new_record? = true
saved_hoge_has_one = HogeHasOne.new
saved_hoge_has_one.name = "pre_save has_one"
saved_hoge_has_one.new_record? -> true
親を先に保存して、親のメソッドで代入
saved_hoge_parent.hoge_children << saved_hoge_child
saved_hoge_child.new_record? -> false
saved_hoge_child.hoge_parent_id -> 11
saved_hoge_parent.hoge_has_one = saved_hoge_has_one
saved_hoge_has_one.new_record? -> false
saved_hoge_has_one.hoge_parent_id -> 11
代入が成功した時点で、自動的にhoge_parent_idが格納されてsaveが呼ばれる

親が未保存
post_save_hoge_parent = HogeParent.new
post_save_hoge_parent.name = "post_save hoge"
post_save_hoge_parent.new_record? = true
post_save_hoge_child = HogeChild.new
post_save_hoge_child.name = "post_save child"
post_save_hoge_child.new_record? = true
post_save_hoge_has_one = HogeHasOne.new
post_save_hoge_has_one.name = "post_save has_one"
post_save_hoge_has_one.new_record? -> true
親が未保存で、子のメソッドで代入
post_save_hoge_child.hoge_parent = post_save_hoge_parent
post_save_hoge_child.new_record? -> true
post_save_hoge_child.hoge_parent_id -> nil
post_save_hoge_has_one.hoge_paent = post_save_hoge_parent
post_save_hoge_has_one.new_record? -> true
post_save_hoge_has_one.hoge_parent_id -> nil
親が未保存で、子のメソッドで代入したもので、親を保存したとき
post_save_hoge_parent.save -> true
post_save_hoge_parent.new_record? -> false
post_save_hoge_parent.id -> 12
post_save_hoge_child.new_record? -> true
post_save_hoge_child.hoge_parent_id -> nil
post_save_hoge_has_one.new_record? -> true
post_save_hoge_has_one.hoge_parent_id -> nil
親のsaveは伝播しない。子のhoge_parent_idも更新されない

親を先に保存
pre_saved_hoge_parent = HogeParent.new
pre_saved_hoge_parent.name = "pre_save hoge"
pre_saved_hoge_parent.save -> true
pre_saved_hoge_parent.new_record? -> false
pre_saved_hoge_parent.id -> 13 
pre_saved_hoge_parent.new_record? = false
pre_saved_hoge_child = HogeChild.new
pre_saved_hoge_child.name = "pre_save child"
pre_saved_hoge_child.new_record? = true
pre_saved_hoge_has_one = HogeHasOne.new
pre_saved_hoge_has_one.name = "pre_save has_one"
pre_saved_hoge_has_one.new_record? -> true
親を先に保存して、子のメソッドで代入
pre_saved_hoge_child.hoge_parent = pre_saved_hoge_parent
pre_saved_hoge_child.new_record? -> true
pre_saved_hoge_child.hoge_parent_id -> 13
pre_saved_hoge_has_one.hoge_parent = pre_saved_hoge_parent
pre_saved_hoge_has_one.new_record? -> true
pre_saved_hoge_has_one.hoge_parent_id -> 13
自動的に保存はされない。当たり前だが、すでに親のIDはわかっているので、hoge_parent_idは格納されている
PR
日付の選択は日時の選択で便利なビューヘルパのdate_selectとdatetime_select。

こんかいのハマりは初期値の設定。

特定の日付を指定するのは、第一引数にTimeデータを格納してやればいいのは知ってる。

しかし、

:prompt=>true
とか、
:include_blank=>true
とかやって、
初期値をこのプロンプトもしくはブランクに指定する方法がわからなくて探した…

ドキュメントに書いてないやん。

ググって見ても、初期値に特定の日付を指定する方法ばかり出てくる。
そんなんドキュメントにしっかり書いてあるやろが!!

と半ばキレながら試した見た。

そう、答えは簡単。
第一引数にnilを入れると。

でたよ~Rails得意の怪しかったら試してみろパターン。

というわけで、
解決しました…orz

スケジューラー実装中のこと。

いろんな時間の予定を作って、特定の日時で切って取得するテストをする。
が、いざやってみると思ってない時間の予定が取得されてる。

おかしい。
とりあえずトレースしてみると、変な時間が入ってる。

午前0時に設定したはずが、午後3時とか。

境界テストのためにかなりの種類の予定を作っているので、
打ち間違いとか、間違って上書きとかの可能性を探って見るもだめ。

しょうがないので、
代入する時間、代入された時間をトレースしてみる。
代入するまではきちんとしてるのに、
オブジェクトに代入したとたんにおかしなことになってる。

あっれ~とか思いつつ、よくよく見てみると、
オブジェクトに代入した時間がJST +00:00になってるよ。

あ~、タイムロケールか~というわけで、タイムロケール設定をしていなかったんですねえ・・・

/config/environment.rbで、

config.time_zone = 'UTC'

config.time_zone = 'Tokyo'
に変更。

ちゃんとJST +09:00になりました。

ちゃんちゃん。

# ってダメダコレ
どうもDBへの入出力でおかしくなってるみたいで一度保存するとおかしな状態のままになってる。
しょうがないので、国内限定、ってことで
# config.time_zone = 'Tokyo'
とコメントアウトして対応。

orz
テーブル関連、has_oneとかhas_manyとかの関連を持っているとき、
ついついActiveRecord::Base.destroyとかをオーバーライドしたくなるときがある。

例:

class Parent < AciveRecord::Base
  has_many :chidren, :dependent=>:destroy

  # 属性:deleted, :boolean, :default=>false

end

class Child < AciveRecord::Base
  belongs_to :parent

  # 属性:deleted, :boolean, :default=>false

end

親も子も関連する全員が@deletedがtrueになったときだけ全削除の実行に入る、
という必要があったとする。

親の方は簡単で、エイリアスを作ってオーバーライドすればいい。

class Parent < AciveRecord::Base
  has_many :children, :dependent=>:destroy

  alias :destroy! :destroy
 
  def  check_destory
    if # 自分と子が全員削除可能
      return self.destory!
    end
  end

  def destroy
    self.deleted = true
    return self.save
    retrun check_destroy
  end

end

てな感じ。

問題は子のほう。

メソッドを普通にオーバーライドすると、親の方が削除された時に、
自動的にdestoryが呼ばれるので無限ループに陥ってしまう。

で、苦肉の策で考えたのがこの方法。

class Child < ActiveRecord::Base
  belongs_to :parent
 
  def destroy
    if caller.to_s =~ /.+has_many_dependent_destroy_for_message_receiver_relations.+/
      return super
    else
      self.deleted = true
      self.save
      return self.parent.check_destroy
    end
  end

end

callerで呼び出し元をたどって、
そこにhas_many_dependent_destroy_for_message_receiver_relations
(関連で:dependent=>:destroyを設定したときに、自動で呼び出される)
が含まれていたら親が削除された一連の動作で削除されていると判断して、
superを呼び出し、
それ以外はフラグを設定してそこで処理を握りつぶすという方法。

これなら、親が削除されたとき以外は削除されずにすむ。

callerのチェックとかってあんまりやりたくないんだけど、
この方法ってなんか問題あるのかしら。

滅多に使わない方法なのでちょっと悩む。
てか、なんかほかのもっといい方法があったら教えてほしい。


Railsアプリの最大のボトルネックとなりうるのはSQLとのやりとりである。
(Erbのレンダラーだという話は置いておく)

なので、Railsアプリでチューニングを行う時に真っ先に手をつけるのは
AR.find系のDBアクセスな部分になる。

ちょっとしたアプリを作れば、(普通にモデリングしている限り)複数の関連モデルができあがる。
この場合、チューニングのポイントはEagerLoadingにあり、
:hoge has_many :fugas
:fuga has_many :piyos
という関連にある場合は、一発ですべて取得できるこれは非常に便利。

Hoge.find(id, :include=>{:fugas=>:piyos})
とやれば、すべての関連をインスタンス化した状態で取得できる。

これは便利なので、結構めくら滅法使ってしまう。
関連先をループで回して表示したりすることがあるときは、これだけでかなりのチューニングになる。

さて、
ここからが本題。

たいていの場合、親になるモデルをインスタンス化するのはコントローラーの責務になる。
(当たり前ですが)

で、ちょっと複雑なことになったモデルクラスで、
parent.childlen.find(:all, :conditions=>[conditions])
みたいに取得するか、find_by_sqlするか、
それともループを回して配列にコピーするのがいいのか、
判断に迷うことがある。

こんな時、すでにEagerLoadingでロードされているのか、
はたまたロードされていないのか判断ができれば処理分岐で対応できる。

loaded?

これで判定がつく

parent has_many :children

だったとき、parentクラス上で、

if self.childlen.loaded?
  # ループで回す処理
else
  # DBアクセスを伴う処理
end

という感じ。

ちなみにテスト上では反映されないので注意。

ついめ~じ
ブログ内検索
フリーエリア
サニーカメラ
Powered by Ninja Blog    template by Temp* factory    icon by MiniaureType

忍者ブログ [PR]