埋め込みドキュメントの参照
Mongoidのドキュメントに微妙に書いてないのでメモ。
class Person include Mongoid::Document embeds_many :phones end class Phone include Mongoid::Document field :number embedded_in :person, :inverse_of => :phones end
となっているときに、ある電話番号の電話を所有するような人物を探す方法はどうすればいいだろう。
原点に立ち戻ってMongoDBの"dot notation"を使って以下のようなクエリでさがすことができる。
Person.where("phones.number" => "03-XXXX-XXXX")
Mongoidのドキュメントだと、埋め込みドキュメントへのインデックス作成のところで少し出てくるけど、肝心のクエリのところに書いてないから少し戸惑ってました。
なにはともあれ、解決。
Mongoidによるatomicな更新
MongoDBの特徴として、RDBMSではおおよそ用いられる以下の方法が使えない。
- joinがない
- transactionがない
そのかわり、次の特徴がある
- 埋め込みドキュメントが使える
- ドキュメントの各要素について、atomicな更新ができる
前者は例えば「紐付けて必ず参照するドキュメント」を内包させておくことで、一度のクエリで紐付く情報を一括取得できる。
後者は行ロックしてupdate(ロジック的には行ロック、データコピー、要素の更新、DB上書きの順になるのかな)などを行わず、各要素を上書きしてしまう方法である。
これらについて困っていたことがあって、まず、あるドキュメントの普通の要素を上書きする場合は以下で可能だった。
@parent = Parent.all.first # 既存ドキュメントの取り出し。 puts @parent.name #=> hogehoge @parent.update_attributes({ :name => "fugafuga" }) puts @parent.name #=> fugafuga Parent.all.first.name #=> fugafuga # @parent.save してないが上書きされる
一方で、埋め込みドキュメントに対する push (1要素追加するatomicな操作)がわからなかったのだが、公式ドキュメントの最後にちらっと出てくる方法で次のように解決した。
@parent = Parent.all.first # 既存ドキュメントの取り出し。 p @parent.children.size #=> 1 @parent.children.create({ :name => "fuga-chan" }) p @parent.children.size #=> 2 Parent.all.first.children.size #=> 2 # @parent.save してないがDB上も挿入されている
まだpullとか判っていない部分もあるが、とりあえずatomicな操作が可能であることが分かってよかった。
参照のあるモデルをviewから作る
ParentとChildの関係にあるモデルの作り方と簡単な使いかたを前回書いたが、では画面からそれらを登録していくにはどうすればいいか。
考えられる方法はとりあえず二つ
- 作成済みのParentのページにChildを作るリンクを作成する
- Childを新規作成するページで既存のParentを指定できるようにする
モデルは仮に以下のようにしておく
class Parent include Mongoid::Document field :name, :type => String field :age, :type => Integer references_many :child end class Child include Mongoid::Document field :name, :type => String field :age, :type => Integer referenced_in :parent end
1.についてはParentのObjectIDを引き渡せば良いと思われる。後は2.の途中からと同じ手順になるはず。
2.について、まずは(scaffoldで作った場合の)newをいじる。
Parentをプルダウンで選択するようにするには、selectを用いる。viewを全部書くと非常に面倒なので部分的に書くと、以下のような感じ。
form_for :child do |f| f.label :parent f.select :parent, Parent.all.map { |parent| [parent.name, parent.id] } end
意味は params[:child][:parent]に関するセレクタを[name, id]の組み合わせの配列で作成する、ということになる。
ここで、配列 [ [n1,m1], [n2,m2] ] が渡されたときのselectは「表示"n1"に対してvalue="m1", 表示"n2"に対してvalue="m2"」となり、上記の例で実際にparams[:child][:parent]に渡される値はparent.id(つまり選んだParentのBSON::Objectid)となる。
このままだと、controller側のChild.newで:parentが余計に含まれてしまい失敗してしまうので、:parentを消しつつ、対応するParentを呼び出して参照関係を作る。
@parent = Parent.criteria.id(params[:child][:parent]).first params[:child].delete(:parent) @child = Child.new(params[:child]) @parent.child << @child @child.save
諸々省略したが、これで参照を作る事ができる。
Mongoidのモデル間参照について備忘録
参照関係(Relation)を作るにはMongoidのモデルで以下を指定する。
- references_one
- references_mary
- referenced_in
使い方に注意が必要で、例えば Parent, Childな関係の場合
class Parent include Mongoid::Document references_many :child end class Child include Mongoid::Document referenced_in :parent end
という事になる。embed_many, embedded_in の関係と対比するとわかりやすいかも。
この時、普通のRDBだとChildテーブルにparent_idが入る点ではMongoDBも同じだが、Mongoidで違う点は、参照関係を作る際にEmbedを踏襲する点。
上記の例だと、こんな風になる。
@parent = Parent.new @child = Child.new @parent.child << @child # @child.parent = @parent ではない
ちなみに、コメントアウトした方でもエラーにならない。しばらく迷ったのは秘密。
Parentを変更する場合は単純にもう一度実行する。
@parent_other = Parent.new @parent_other.child << @child @parent.child.first #=> nil
自動的に以前の @parent からは排除された形になる。
Fabrication
FabricationというのがMongoidのドキュメントに対応しているらしいので、テストの為に使ってみる。
とりあえず、次の事はわかった。
- spec/fabricators/model_fabricator.rb で Fabricator(:model)を作成。
- Fabricate(:model)でモデルを作成し保存。いろいろ試す。
今問題なのは、Fabricator作成時にembed documentをどう記述するか。
Mongoid側のドキュメントにある例が参考になりそうなので、やってみよう。
http://mongoid.org/docs/extensions/
[2010-11-25 追記]
次のような埋め込みドキュメントのモデルを想定する
class Person include Mongoid::Document field :name, :type => String embed_many :phones end class Phone include Mongoid::Document field :number, :type => String embedded_in :person, :inverse_of => :phones end
このときFabricatorはそれぞれこんな感じ。
Fabricator(:person) do name "hogehoge" phones(:count => 3) { |phone,i| Fabricate.build(:phone, :number => "090-0000-000#{i}") } end Fabricator(:phone) do number "03-0000-0000" end
ポイントはPhonesというMongoDBのコレクションが無いため、Fabricate.buildを用いてsaveしないようにする事。
たったこれだけだった。
[2011-01-15 追記]
embeds_manyの場合、上記の例での :count => 3 という部分が無いとエラーになる。
これはおそらく "<<" で挿入してくのではなく "=" で代入するように作用するためだろう。
Mongoid generate
Mongoidでモデルを作るときにTimestampとか付ける方法について軽くメモ。
rails g model model_name [field:type [...] ] [--timestamps=true] [--parent=parent_model_name]続きを読む