1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

MongoDBAdvent Calendar 2019

Day 13

Mongoid での set の挙動を整理する

Posted at

Mongoid が提供する set メソッドの挙動を整理する。

set はバリデーションをチェックしない

app/models/item.rb
class Item
  include Mongoid::Document

  field :name
  field :status

  validates_inclusion_of :status, in: ['open', 'hidden']
end

上記のように statusopen, 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.rbafter_save を追加する。

app/models/item.rb
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

以下のようなモデルを想定する。

app/models/item.rb
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
app/models/manufacturer.rb
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

以下のようなモデルを想定する。

app/models/item.rb
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
app/models/manufacturer.rb
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 はやり方によってはできる
    • 多くのケースではバリデーションは検証されるしコールバックも呼ばれる
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?