Mongoid の更新メソッドの挙動を整理する

意図しないものが更新されるリスクを避けるため、 Mongoid が提供する更新メソッドの挙動を整理しておく。

# tl;dr

- 引数で指定した値 **以外** に変更が及ぶのは `update_attribute`, `update_attributes`, `update`
    - たぶんこれが最も正しく認識しておくべきこと
    - このあたりは ActiveRecord と同じ（だと思う）
- `upsert` と `touch` はそもそもの用途が異なる
- `inc`, `rename`, `set`, `unset` はアトミックな処理であるため、引数で指定した以外の変更は行われない


# 準備

データを1件用意しておく。
（ `_id` は削除して作り直したりする都合で途中から変わっている）

```js
> db.items.find()
{ "_id" : ObjectId("5dee1c456694f6bc7bc3c52c"), "name" : "item1", "memo" : "initialized", "size" : "M" }
```

検証する各メソッドに対して、以下のオペレーションを行う。

- 更新対象のドキュメントを変数に格納する
- 変数の `size` の値を変更する
- 更新メソッドの引数で `memo` に更新をかける
    - `memo` の更新ができないメソッドでは他のフィールドに更新をかける

# `update_attribute`

単一のフィールドを更新するメソッド。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee1c456694f6bc7bc3c52c, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.update_attribute(:memo, 'updated')
=> true
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee1c456694f6bc7bc3c52c, name: "item1", memo: "updated", size: "L">
```

`memo` はもちろん、 `size` も更新される。

# `update_attributes`

複数のフィールドを更新するメソッド。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee1c456694f6bc7bc3c52c, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.update_attributes(memo: 'updated')
=> true
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee1c456694f6bc7bc3c52c, name: "item1", memo: "updated", size: "L">
```

`memo` も `size` も更新される。

# `update`

複数のフィールドを更新するメソッド。
これは `update_attributes` のエイリアスなので上と同じ挙動をする。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee342b6694f6bc7bc3c535, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.update(memo: 'updated')
=> true
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee342b6694f6bc7bc3c535, name: "item1", memo: "updated", size: "L">
```

当然 `memo` も `size` も更新される。

# `upsert`

対象のデータに一致するものがあれば更新、なければ作成するメソッド。

```rb
irb(main):001:0> item = Item.new({name: 'item1'})
=> #<Item _id: 5dee2d1e21b62029414b31f5, name: "item1", memo: nil, size: nil>
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.upsert(memo: 'updated')
=> true
irb(main):004:0> Item.where(name: 'item1').to_a
=> [#<Item _id: 5dee1c456694f6bc7bc3c52c, name: "item1", memo: "initialized", size: "M">, #<Item _id: 5dee2d1e21b62029414b31f5, name: "item1", memo: nil, size: "L">]
```

当然ながら `{name: 'item1', size: 'L'}` のドキュメントは存在しないので作成される。
そして引数で指定した `{memo: 'updated'}` は追加されない。
（これも当然のことで、 upsert の引数はバリデーションオプションを設定する箇所であり、今回の指定はスルーされる）

# `touch`

`updated_at` を更新する。
引数を指定すると、その項目にも現在時刻を指定する。

今回の検証とは用途が異なるので割愛。
（ちなみに `item.touch(memo: 'updated')` とかやると `"{:memo => 'updated'}" : ISODate({現在時刻})` などというフィールドが追加されてしまい破滅する）

# `inc`

指定したフィールドをインクリメントする。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee30896694f6bc7bc3c52f, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.inc(generation: 1)
=> #<Item _id: 5dee30896694f6bc7bc3c52f, name: "item1", memo: "initialized", size: "L">
```

Mongoid 上は変化がないが、実際のドキュメントは変更される。

```js
> db.items.find()
{ "_id" : ObjectId("5dee30896694f6bc7bc3c52f"), "name" : "item1", "memo" : "initialized", "size" : "M", "generation" : 1 }
```

`size` は変更されず、 `generation` が追加されている。

# `rename`

指定したフィールドの名前を変更する。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee32226694f6bc7bc3c530, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.rename(memo: :note)
=> #<Item _id: 5dee32226694f6bc7bc3c530, name: "item1", memo: nil, size: "L">
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee32226694f6bc7bc3c530, name: "item1", memo: nil, size: "M">
```

`size` は更新されず `memo` の値が null になっている。
実際には `memo` フィールドはなく `note` フィールドに変更されており、 Mongoid ではモデルに定義されている `memo` があるものとして扱っているため `{memo: null}` に見える。

```js
> db.items.find()
{ "_id" : ObjectId("5dee32226694f6bc7bc3c530"), "name" : "item1", "size" : "M", "note" : "initialized" }
```

# `set`

指定したフィールドの値を更新する。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee32fe6694f6bc7bc3c532, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.set(memo: 'updated')
=> #<Item _id: 5dee32fe6694f6bc7bc3c532, name: "item1", memo: "updated", size: "L">
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee32fe6694f6bc7bc3c532, name: "item1", memo: "updated", size: "M">
```

`size` は更新されず、 `memo` だけが更新される。

```js
> db.items.find()
{ "_id" : ObjectId("5dee32fe6694f6bc7bc3c532"), "name" : "item1", "memo" : "updated", "size" : "M" }
```

# `unset`

指定したフィールドを削除する。

```rb
irb(main):001:0> item = Item.find_by(name: 'item1')
=> #<Item _id: 5dee33956694f6bc7bc3c534, name: "item1", memo: "initialized", size: "M">
irb(main):002:0> item.size = 'L'
=> "L"
irb(main):003:0> item.unset(:memo)
=> #<Item _id: 5dee33956694f6bc7bc3c534, name: "item1", memo: nil, size: "L">
irb(main):004:0> Item.find_by(name: 'item1')
=> #<Item _id: 5dee33956694f6bc7bc3c534, name: "item1", memo: nil, size: "M">
```

`size` の値は変更されず、 `memo` の値が null になっている様に見える。
これも実際には `{memo: null}` ではなくフィールドごとなくなっている。

```js
> db.items.find()
{ "_id" : ObjectId("5dee33956694f6bc7bc3c534"), "name" : "item1", "size" : "M" }
```

# まとめ

メソッド | 変数の値の変更 | 引数での値の変更
:--: | :--: | :--:
update_attribute | 反映される | 反映される
update_attributes / update | 反映される | 反映される
upsert | 別ドキュメントに反映される | 反映されない（オプションとして認識される）
touch | 反映されない | 新規のフィールドという形で反映される
inc | 反映されない | 反映される
rename | 反映されない | 反映される
set | 反映されない | 反映される
unset | 反映されない | 反映される