Mongoid が提供する set メソッドの挙動を整理する。
set はバリデーションをチェックしない
class Item
include Mongoid::Document
field :name
field :status
validates_inclusion_of :status, in: ['open', 'hidden']
end
上記のように status
に open
, hidden
のどちらかが入ることを前提とする。
このバリデーションは以下のように保存時にチェックされる。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200774064200 type=uuid data=0x83b34ce665194b82...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.status = 'closed'
=> "closed"
irb(main):003:0> item.save
=> false
irb(main):004:0> item.errors
=> #<ActiveModel::Errors:0x00007fb198914640 @base=#<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "closed">, @messages={:status=>["is not included in the list"]}, @details={:status=>[{:error=>:inclusion, :value=>"closed"}]}>
代入してsave
する代わりに set
を使って即座に変更を反映しようとすると以下のようにバリデーションエラーにならずに保存される。
irb(main):005:0> item = Item.find_by(name: 'item01')
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200774064200 type=uuid data=0x83b34ce665194b82...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):006:0> item.set(status: 'closed')
MONGODB | [9] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"status"=>"closed"}}}], "lsid"=>{"id"=><BSON::Binary:0x70200774064200 type=uuid data=0x83b34ce665194b82...>}}
MONGODB | [9] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "closed">
irb(main):007:0> Item.find_by(name: 'item01')
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200774064200 type=uuid data=0x83b34ce665194b82...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "closed">
set はコールバックを呼ばない
item.rb
に after_save
を追加する。
class Item
include Mongoid::Document
field :name
field :status
validates_inclusion_of :status, in: ['open', 'hidden']
after_save { |d| p "#{d.name}, #{d.status}" }
end
先程と同様に値をセットして更新すると以下のように保存後に after_save
が呼ばれる。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200973293780 type=uuid data=0x1b1ac6692cff4cd9...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.status = 'hidden'
=> "hidden"
irb(main):003:0> item.save
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"status"=>"hidden"}}}], "lsid"=>{"id"=><BSON::Binary:0x70200973293780 type=uuid data=0x1b1ac6692cff4cd9...>}}
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
"item01, hidden"
=> true
set
を使って保存をすると after_save
は呼ばれない。
irb(main):004:0> item = Item.find_by(name: 'item01')
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200973293780 type=uuid data=0x1b1ac6692cff4cd9...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "hidden">
irb(main):005:0> item.set(status: 'open')
MONGODB | [10] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"status"=>"open"}}}], "lsid"=>{"id"=><BSON::Binary:0x70200973293780 type=uuid data=0x1b1ac6692cff4cd9...>}}
MONGODB | [10] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
set でリレーションに保存
has_one/has_many
以下のようなモデルを想定する。
class Item
include Mongoid::Document
field :name
field :status
has_one :manufacturer
validates_inclusion_of :status, in: ['open', 'hidden']
after_save { |d| p "#{d.name}, #{d.status}" }
end
class Manufacturer
include Mongoid::Document
field :name
field :country
belongs_to :item
validates_inclusion_of :country, in: ['japan']
after_save { |d| p "manufacturer: #{d.name}, #{d.country}" }
end
has_one のフィールドに対して set する
以下のように undefined method `_association'
で NoMethodError になる。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200973615600 type=uuid data=0x9c51763e627b42a7...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.set('manufacturer.country' => 'canada')
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"manufacturers", "filter"=>{"item_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200973615600 type=uuid data=0x9c51763e627b42a7...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
Traceback (most recent call last):
1: from (irb):3
NoMethodError (undefined method `_association' for {"country"=>"canada"}:Hash)
has_one そのものを set する
Manufacturer.new
した結果をセットすれば通る。
ただし、この場合別途 item.save
しなければ item.manufacturer
の結果は変わらない(これについては別記事で整理する)。
また、 Manufacturer 側のバリデーションが走るが、その結果がエラーの場合でも set はエラーを出力しない。
irb(main):003:0> item.set(manufacturer: {name: 'creator2', country: 'japan'})
Traceback (most recent call last):
2: from (irb):4
1: from (irb):4:in rescue in irb_binding'
NoMethodError (undefined method `_association' for {:name=>"creator2", :country=>"japan"}:Hash)
irb(main):004:0> item.set(manufacturer: Manufacturer.new(name: 'creator2', country: 'japan'))
MONGODB | [9] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"manufacturers", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df7360f21b62043a2b04334'), "name"=>"creator2", "country"=>"japan", "item_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}], "lsid"=>{"id"=><BSON::Binary:0x702009736156...
MONGODB | [9] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
"manufacturer: creator2, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):007:0> item.set(manufacturer: Manufacturer.new(name: 'creator3', country: 'canada')) # エラーが出ていないが Manufacturer の保存に失敗している
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
has_one のフィールドに紐づく変数経由で set する
この方法は本質的には最初に説明したのと同じ手順であり、バリデーションチェックも走らずコールバックも呼ばれず、保存される。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200374638340 type=uuid data=0x241a5c9ba6d748d7...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.manufacturer
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"manufacturers", "filter"=>{"item_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200374638340 type=uuid data=0x241a5c9ba6d748d7...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Manufacturer _id: 5df6ff5921b62081adb04334, name: "creator", country: "japan", item_id: BSON::ObjectId('5df5288495243d5498999e2a')>
irb(main):003:0> item.manufacturer.set(country: 'canada')
MONGODB | [9] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"manufacturers", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df6ff5921b62081adb04334')}, "u"=>{"$set"=>{"country"=>"canada"}}}], "lsid"=>{"id"=><BSON::Binary:0x70200374638340 type=uuid data=0x241a5c9ba6d748d7...>}}
MONGODB | [9] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> #<Manufacturer _id: 5df6ff5921b62081adb04334, name: "creator", country: "canada", item_id: BSON::ObjectId('5df5288495243d5498999e2a')>
irb(main):004:0> item.reload
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "lsid"=>{"id"=><BSON::Binary:0x70200374638340 type=uuid data=0x241a5c9ba6d748d7...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):005:0> item.manufacturer
MONGODB | [11] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"manufacturers", "filter"=>{"item_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70200374638340 type=uuid data=0x241a5c9ba6d748d7...>}}
MONGODB | [11] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Manufacturer _id: 5df6ff5921b62081adb04334, name: "creator", country: "canada", item_id: BSON::ObjectId('5df5288495243d5498999e2a')>
embeds_one/embeds_many
以下のようなモデルを想定する。
class Item
include Mongoid::Document
field :name
field :status
embeds_one :manufacturer
validates_inclusion_of :status, in: ['open', 'hidden']
after_save { |d| p "#{d.name}, #{d.status}" }
end
class Manufacturer
include Mongoid::Document
field :name
field :country
embedded_in :item
validates_inclusion_of :country, in: ['japan']
after_save { |d| p "manufacturer: #{d.name}, #{d.country}" }
end
embeds_one のフィールドに対して set する
これはエラーにならない。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200373029300 type=uuid data=0x049d23d448434f31...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.set('manufacturer.name' => 'creator02')
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"manufacturer"=>{"_id"=>BSON::ObjectId('5df747b521b6202786b04334'), "name"=>"creator02", "country"=>"japan"}}}}], "lsid"=>{"id"=...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
"manufacturer: creator02, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
が、この方法の利用には注意が必要である。
なぜなら同じ方法を2回以上使おうとするとエラーになるためだ。
reload
すれば問題は解消するが、そもそもこの方法で更新をするべきではなさそう。
irb(main):003:0> item.set('manufacturer.name' => 'creator03')
Traceback (most recent call last):
1: from (irb):3
FrozenError (can't modify frozen BSON::Document)
irb(main):004:0> item.reload
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "lsid"=>{"id"=><BSON::Binary:0x70200372631360 type=uuid data=0xd67817a79db44896...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):005:0> item.set('manufacturer.name' => 'creator04')
MONGODB | [10] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"manufacturer"=>{"_id"=>BSON::ObjectId('5df747b521b6202786b04334'), "name"=>"creator04", "country"=>"japan"}}}}], "lsid"=>{"id"=...
MONGODB | [10] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
"manufacturer: creator04, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
ちなみにこの場合でもバリデーションはチェックされる。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200772647620 type=uuid data=0x1f23c2846fb44ee3...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.set('manufacturer.country' => 'canada')
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):003:0> item.manufacturer.errors
=> #<ActiveModel::Errors:0x00007fb1a808c6e8 @base=#<Manufacturer _id: 5df747b521b6202786b04334, name: "creator", country: "canada">, @messages={:country=>["is not included in the list"]}, @details={:country=>[{:error=>:inclusion, :value=>"canada"}]}>
embeds_one そのものを set する
has_one と違って hash でも set できる。
Manufacturer のコールバックは呼ばれているしバリデーションもチェックされる。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200372582840 type=uuid data=0x8193f419dbb54950...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.003s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.set(manufacturer: {name: 'creator02', country: 'japan'})
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"manufacturer"=>{"_id"=>BSON::ObjectId('5df74aaa21b6204c99b04334'), "name"=>"creator02", "country"=>"japan"}}}}], "lsid"=>{"id"=...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
"manufacturer: creator02, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):003:0> item.set(manufacturer: {name: 'creator03', country: 'japan'})
MONGODB | [9] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"manufacturer"=>{"_id"=>BSON::ObjectId('5df74ab921b6204c99b04335'), "name"=>"creator03", "country"=>"japan"}}}}], "lsid"=>{"id"=...
MONGODB | [9] localhost:28001 | sandbox.update | SUCCEEDED | 0.003s
"manufacturer: creator03, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):004:0> item.set(manufacturer: {name: 'creator04', country: 'canada'})
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):005:0> item.manufacturer.errors
=> #<ActiveModel::Errors:0x00007fb198a752c8 @base=#<Manufacturer _id: 5df74ad921b6204c99b04336, name: "creator04", country: "canada">, @messages={:country=>["is not included in the list"]}, @details={:country=>[{:error=>:inclusion, :value=>"canada"}]}>
もちろん Manufacturer のインスタンスを代入して set もできる。
irb(main):006:0> item.set(manufacturer: Manufacturer.new(name: 'creator05', country: 'japan'))
MONGODB | [10] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "u"=>{"$set"=>{"manufacturer"=>{"_id"=>BSON::ObjectId('5df74b6c21b6204c99b04337'), "name"=>"creator05", "country"=>"japan"}}}}], "lsid"=>{"id"=...
MONGODB | [10] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
"manufacturer: creator05, japan"
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
embeds_one のフィールドに紐づく変数経由で set する
ここまでの流れから自明かとは思うが、これはもちろん通る。
irb(main):001:0> item = Item.find_by(name: 'item01')
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"name"=>"item01"}, "lsid"=>{"id"=><BSON::Binary:0x70200375178620 type=uuid data=0xbf7b298badda4b22...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):002:0> item.manufacturer
=> #<Manufacturer _id: 5df74b6c21b6204c99b04337, name: "creator", country: "japan">
irb(main):003:0> item.manufacturer.set(country: 'canada')
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"items", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a'), "manufacturer._id"=>BSON::ObjectId('5df74b6c21b6204c99b04337')}, "u"=>{"$set"=>{"manufacturer.country"=>"canada"}}}], "lsid"=>{"id"=><BSON::Bina...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> #<Manufacturer _id: 5df74b6c21b6204c99b04337, name: "creator", country: "canada">
irb(main):004:0> item.reload
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"items", "filter"=>{"_id"=>BSON::ObjectId('5df5288495243d5498999e2a')}, "lsid"=>{"id"=><BSON::Binary:0x70200375178620 type=uuid data=0xbf7b298badda4b22...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Item _id: 5df5288495243d5498999e2a, name: "item01", status: "open">
irb(main):005:0> item.manufacturer
=> #<Manufacturer _id: 5df74b6c21b6204c99b04337, name: "creator", country: "canada">
まとめ
- set を使ってフィールドを変更すると、バリデーションは検証されないしコールバックは呼ばれない
- has/embeds なフィールドに対しての set はやり方によってはできる
- 多くのケースではバリデーションは検証されるしコールバックも呼ばれる