My Brain is Open.

思いついたことを適当に列列と

埋め込みドキュメントの参照

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ではおおよそ用いられる以下の方法が使えない。

  1. joinがない
  2. transactionがない

そのかわり、次の特徴がある

  1. 埋め込みドキュメントが使える
  2. ドキュメントの各要素について、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の関係にあるモデルの作り方と簡単な使いかたを前回書いたが、では画面からそれらを登録していくにはどうすればいいか。

考えられる方法はとりあえず二つ

  1. 作成済みのParentのページにChildを作るリンクを作成する
  2. 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 という部分が無いとエラーになる。
これはおそらく "<<" で挿入してくのではなく "=" で代入するように作用するためだろう。