Mongoid はリレーションの子要素をいつ反映しているか

# 既存のドキュメントに追加する場合

## has_many

以下のモデルで考える。

```rb:app/models/article.rb
class Article
  include Mongoid::Document

  field :title
  field :author
  has_many :comments, class_name: 'Comment'
end
```

```rb:app/models/comment.rb
class Comment
  include Mongoid::Document

  field :author
  field :text

  belongs_to :article
end
```

上記のモデルを利用して、以下の手順を行う。

1. Article のデータを1件作る
2. Comment のデータを1件作る
3. Article のデータの comments に 2. で作ったデータを格納する

```rb
irb(main):001:0> article = Article.create(title: 'test article', author: 'John')
=> #<Article _id: 5def694121b6206de44b31f5, title: "test article", author: "John">
irb(main):002:0> comment = Comment.create(author: 'Tom', text: 'comment text')
=> #<Comment _id: 5def697321b6206de44b31f6, author: "Tom", text: "comment text", article_id: nil>
rb(main):003:0> comment
=> #<Comment _id: 5def697321b6206de44b31f6, author: "Tom", text: "comment text", article_id: nil>
irb(main):004:0> article.comments << comment
MONGODB | [8] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"comments", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def697321b6206de44b31f6'), "author"=>"Tom", "text"=>"comment text", "article_id"=>BSON::ObjectId('5def694121b6206de44b31f5')}], "lsid"=>{"id"=><BSON::Binary:0x7034371467232...
MONGODB | [8] localhost:28001 | sandbox.insert | SUCCEEDED | 0.001s
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"comments", "filter"=>{"article_id"=>BSON::ObjectId('5def694121b6206de44b31f5')}, "lsid"=>{"id"=><BSON::Binary:0x70343714672320 type=uuid data=0x425a912cb15c4242...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> [#<Comment _id: 5def697321b6206de44b31f6, author: "Tom", text: "comment text", article_id: BSON::ObjectId('5def694121b6206de44b31f5')>]
```

上記の `irb(main):003` の時点ででは MongoDB 上は保存されていない。
`irb(main):004` で `article.comments` に格納した瞬間に insert される。

```js
> db.comments.find() // irb(main):003 のあと
> db.comments.find() // irb(main):004 のあと
{ "_id" : ObjectId("5def697321b6206de44b31f6"), "author" : "Tom", "text" : "comment text", "article_id" : ObjectId("5def694121b6206de44b31f5") }
```

## has_one

モデルを以下の形に変更する。

```rb:app/models/article.rb
class Article
  include Mongoid::Document

  field :title
  has_one :author
  has_many :comments, class_name: 'Comment'
end
```

```rb:app/models/author.rb
class Author
  include Mongoid::Document

  field :name
  field :age

  belongs_to :article
end
```

上記のモデルを利用して、以下の手順を行う。

1. Article のデータを1件作る
2. Author のデータを1件作る
3. Article のデータの author に 2. で作ったデータを格納する

```rb
irb(main):001:0> article = Article.create(title: 'second article')
=> #<Article _id: 5def73cb21b620e0c14b31f5, title: "second article">
irb(main):002:0> author = Author.create(name: 'Tom', age: 20)
=> #<Author _id: 5def73e521b620e0c14b31f6, name: "Tom", age: 20, article_id: nil>
irb(main):003:0> article.author = author
MONGODB | [8] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"authors", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def73e521b620e0c14b31f6'), "name"=>"Tom", "age"=>20, "article_id"=>BSON::ObjectId('5def73cb21b620e0c14b31f5')}], "lsid"=>{"id"=><BSON::Binary:0x70343916007920 type=uuid data...
MONGODB | [8] localhost:28001 | sandbox.insert | SUCCEEDED | 0.010s
=> #<Author _id: 5def73e521b620e0c14b31f6, name: "Tom", age: 20, article_id: BSON::ObjectId('5def73cb21b620e0c14b31f5')>
```

has_many の例と同様に `article.author = author` のタイミングで authors にドキュメントが書き込まれている。

```js
> db.authors.find() // irb(main):002 のあと
> db.authors.find() // irb(main):003 のあと
{ "_id" : ObjectId("5def73e521b620e0c14b31f6"), "name" : "Tom", "age" : 20, "article_id" : ObjectId("5def73cb21b620e0c14b31f5") }
```

## embeds_many

モデルを以下のように変更する。
（まとめて embeds_one の検証分も変更している）

```rb:app/models/article.rb
class Article
  include Mongoid::Document

  field :title
  embeds_one :author
  embeds_many :comments, class_name: 'Comment'
end
```

```rb:app/models/comment.rb
class Comment
  include Mongoid::Document

  field :author
  field :text

  embedded_in :article
end
```

先程と同様の手順を行う。

```rb
irb(main):001:0> article = Article.create(title: 'third article')
=> #<Article _id: 5def78c221b62011e762e62c, title: "third article">
irb(main):002:0> comment = Comment.create(author: 'Ken', text: 'new comment')
Traceback (most recent call last):
        1: from (irb):3
Mongoid::Errors::NoParent ()
message:
  Cannot persist embedded document Comment without a parent document.
summary:
  If the document is embedded, in order to be persisted it must always have a reference to its parent document. This is most likely caused by either calling Comment.create or Comment.create! without setting the parent document as an attribute.
resolution:
  Ensure that you've set the parent relation if instantiating the embedded document directly, or always create new embedded documents via the parent relation.
irb(main):003:0> comment = Comment.new(author: 'Ken', text: 'new comment')
=> #<Comment _id: 5def792a21b62011e762e62d, author: "Ken", text: "new comment">
irb(main):004:0> article.comments << comment
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"articles", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5def78c221b62011e762e62c')}, "u"=>{"$push"=>{"comments"=>{"_id"=>BSON::ObjectId('5def792a21b62011e762e62d'), "author"=>"Ken", "text"=>"new comment"}}}}], "lsid"=>{"id"=>...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> [#<Comment _id: 5def792a21b62011e762e62d, author: "Ken", text: "new comment">]
```

`Comment.create()` はエラーになる。
`embedded_in` 要素があるため、親が指定されている必要があるからだ。

これも `article.comments << comment` したタイミングで article に対して更新が走っている。

## embeds_one

author.rb も embedded_in にしておく。

```rb:app/models/author.rb
class Author
  include Mongoid::Document

  field :name
  field :age

  embedded_in :article
end
```

これも同様の手順で。

```rb
irb(main):001:0> article = Article.create(title: 'fourth article')
=> #<Article _id: 5def7a8721b620366a62e62c, title: "fourth article">
irb(main):002:0> author = Author.create(name: 'Sam', age: 22) # 先と同様にエラーになる。エラーは省略。
irb(main):003:0> author = Author.new(name: 'Sam', age: 22)
=> #<Author _id: 5def7ab121b620366a62e62d, name: "Sam", age: 22>
irb(main):004:0> article.author = author
MONGODB | [8] localhost:28001 #1 | sandbox.update | STARTED | {"update"=>"articles", "ordered"=>true, "updates"=>[{"q"=>{"_id"=>BSON::ObjectId('5def7a8721b620366a62e62c')}, "u"=>{"$set"=>{"author"=>{"_id"=>BSON::ObjectId('5def7ab121b620366a62e62d'), "name"=>"Sam", "age"=>22}}}}], "lsid"=>{"id"=><BSON::Binary:0x7...
MONGODB | [8] localhost:28001 | sandbox.update | SUCCEEDED | 0.002s
=> #<Author _id: 5def7ab121b620366a62e62d, name: "Sam", age: 22>
```

has_one と同様に `article.author = author` のタイミングで author にドキュメントが書き込まれている。

# 新規のデータに追加する場合

モデルは全て上記のそれぞれと同様の形とする。
やることはだいたい一緒なのでここからはコンソールでの操作とコメントのみ。

## has_many

```rb
irb(main):001:0> article = Article.new(title: 'fifth article', author: 'John')
=> #<Article _id: 5def7ef821b6206f8662e62c, title: "fifth article", author: "John">
irb(main):002:0> comment = Comment.create(author: 'Ken', text: 'next comment')
=> #<Comment _id: 5def7f1621b6206f8662e62d, author: "Ken", text: "next comment", article_id: nil>
irb(main):003:0> article.comments << comment # ここではどちらも保存されない
=> [#<Comment _id: 5def7f1621b6206f8662e62d, author: "Ken", text: "next comment", article_id: BSON::ObjectId('5def7ef821b6206f8662e62c')>]
irb(main):004:0> Article.where(title: 'fifth article', author: 'John').first # まだないことの確認
MONGODB | [8] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"fifth article", "author"=>"John"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [8] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> nil
irb(main):005:0> article.save
MONGODB | [9] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"articles", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def7ef821b6206f8662e62c'), "title"=>"fifth article", "author"=>"John"}], "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [9] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
=> true
irb(main):006:0> Article.where(title: 'fifth article', author: 'John').first # 保存されたことの確認
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"fifth article", "author"=>"John"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Article _id: 5def7ef821b6206f8662e62c, title: "fifth article", author: "John">
irb(main):007:0> Comment.where(author: 'Ken', text: 'next comment').first # comment が保存されていないことの確認
MONGODB | [11] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"comments", "filter"=>{"author"=>"Ken", "text"=>"next comment"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [11] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> nil
irb(main):008:0> comment.save
MONGODB | [12] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"comments", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def7f1621b6206f8662e62d'), "author"=>"Ken", "text"=>"next comment", "article_id"=>BSON::ObjectId('5def7ef821b6206f8662e62c')}], "lsid"=>{"id"=><BSON::Binary:0x7016285296608...
MONGODB | [12] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
=> true
irb(main):009:0> Comment.where(author: 'Ken', text: 'next comment').first # 保存されたことの確認
MONGODB | [13] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"comments", "filter"=>{"author"=>"Ken", "text"=>"next comment"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [13] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Comment _id: 5def7f1621b6206f8662e62d, author: "Ken", text: "next comment", article_id: BSON::ObjectId('5def7ef821b6206f8662e62c')>
irb(main):010:0> Article.where(title: 'fifth article', author: 'John').first.comments # リレーションが成立していることの確認
MONGODB | [14] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"fifth article", "author"=>"John"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [14] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [15] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"comments", "filter"=>{"article_id"=>BSON::ObjectId('5def7ef821b6206f8662e62c')}, "lsid"=>{"id"=><BSON::Binary:0x70162852966080 type=uuid data=0xa7f9242254954cfb...>}}
MONGODB | [15] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> [#<Comment _id: 5def7f1621b6206f8662e62d, author: "Ken", text: "next comment", article_id: BSON::ObjectId('5def7ef821b6206f8662e62c')>]
```

`article.comments` に `comment` を追加した状態で `article` を保存しても `comment` の方は保存されない。
両方を保存するとリレーションが成立している状態になる。

ちなみに、先に `comment` を保存しても `article` は保存されていない状態になる。
そのため、 MongoDB 側では `comment` が持っている `article_id` に対応する `articles` コレクションが存在しない状態になる。

## has_one

```rb
irb(main):001:0> article = Article.new(title: 'sixth article')
=> #<Article _id: 5def815721b62004d962e62c, title: "sixth article">
irb(main):002:0> author = Author.create(name: 'Kenny', age: 21)
=> #<Author _id: 5def816421b62004d962e62d, name: "Kenny", age: 21, article_id: nil>
irb(main):003:0> article.author = author
=> #<Author _id: 5def816421b62004d962e62d, name: "Kenny", age: 21, article_id: BSON::ObjectId('5def815721b62004d962e62c')>
irb(main):004:0> Article.where(title: 'sixth article').first
MONGODB | [7] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"sixth article"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [7] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> nil
irb(main):005:0> article.save
MONGODB | [8] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"articles", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def815721b62004d962e62c'), "title"=>"sixth article"}], "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [8] localhost:28001 | sandbox.insert | SUCCEEDED | 0.001s
=> true
irb(main):006:0> Article.where(title: 'sixth article').first # article は保存されている
MONGODB | [9] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"sixth article"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [9] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Article _id: 5def815721b62004d962e62c, title: "sixth article">
irb(main):007:0> Author.where(name: 'Kenny', age: 21).first # author は保存されていない
MONGODB | [10] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"authors", "filter"=>{"name"=>"Kenny", "age"=>21}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [10] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> nil
irb(main):008:0> article.author.save
MONGODB | [11] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"authors", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def816421b62004d962e62d'), "name"=>"Kenny", "age"=>21, "article_id"=>BSON::ObjectId('5def815721b62004d962e62c')}], "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid da...
MONGODB | [11] localhost:28001 | sandbox.insert | SUCCEEDED | 0.002s
=> true
irb(main):009:0> Author.where(name: 'Kenny', age: 21).first
MONGODB | [12] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"authors", "filter"=>{"name"=>"Kenny", "age"=>21}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [12] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
=> #<Author _id: 5def816421b62004d962e62d, name: "Kenny", age: 21, article_id: BSON::ObjectId('5def815721b62004d962e62c')>
irb(main):010:0> Article.where(title: 'sixth article').first.author # リレーションが成立していることの確認
MONGODB | [13] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"articles", "filter"=>{"title"=>"sixth article"}, "sort"=>{"_id"=>1}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [13] localhost:28001 | sandbox.find | SUCCEEDED | 0.002s
MONGODB | [14] localhost:28001 #1 | sandbox.find | STARTED | {"find"=>"authors", "filter"=>{"article_id"=>BSON::ObjectId('5def815721b62004d962e62c')}, "limit"=>1, "singleBatch"=>true, "lsid"=>{"id"=><BSON::Binary:0x70163658507040 type=uuid data=0xab53b65883964307...>}}
MONGODB | [14] localhost:28001 | sandbox.find | SUCCEEDED | 0.001s
=> #<Author _id: 5def816421b62004d962e62d, name: "Kenny", age: 21, article_id: BSON::ObjectId('5def815721b62004d962e62c')>
```

has_many と同様。

## embeds_many

```rb
irb(main):001:0> article = Article.new(title: 'seventh article')
=> #<Article _id: 5def82ee21b620184662e62c, title: "seventh article">
irb(main):002:0> comment = Comment.new(author: 'Lita', text: 'wild comment')
=> #<Comment _id: 5def832421b620184662e62d, author: "Lita", text: "wild comment">
irb(main):003:0> article.comments << comment
=> [#<Comment _id: 5def832421b620184662e62d, author: "Lita", text: "wild comment">]
irb(main):004:0> article.save # comment.save でも同じ挙動をする
MONGODB | [7] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"articles", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5def82ee21b620184662e62c'), "title"=>"seventh article", "comments"=>[{"_id"=>BSON::ObjectId('5def832421b620184662e62d'), "author"=>"Lita", "text"=>"wild comment"}]}], "lsid"...
MONGODB | [7] localhost:28001 | sandbox.insert | SUCCEEDED | 0.001s
=> true
```

`article.comments` に `comment` を格納してから保存することで、どちらも DB 上反映されている状態になる。

## embeds_one

```rb
irb(main):001:0> article = Article.new(title: 'eighth article')
=> #<Article _id: 5df0561821b6205f2062e62c, title: "eighth article">
irb(main):002:0> author = Author.new(name: 'Alex', age: 27)
=> #<Author _id: 5df0563221b6205f2062e62d, name: "Alex", age: 27>
irb(main):003:0> article.author = author
=> #<Author _id: 5df0563221b6205f2062e62d, name: "Alex", age: 27>
irb(main):004:0> article.save
MONGODB | [7] localhost:28001 #1 | sandbox.insert | STARTED | {"insert"=>"articles", "ordered"=>true, "documents"=>[{"_id"=>BSON::ObjectId('5df0561821b6205f2062e62c'), "title"=>"eighth article", "author"=>{"_id"=>BSON::ObjectId('5df0563221b6205f2062e62d'), "name"=>"Alex", "age"=>27}}], "lsid"=>{"id"=><BSON::Bina...
MONGODB | [7] localhost:28001 | sandbox.insert | SUCCEEDED | 0.001s
=> true
```

embeds_many と同様。

# まとめ

親要素のドキュメントが既に MongoDB にあれば `has_*`, `embeds_*` のいずれも、親要素に子要素を代入／格納したタイミングで対応するドキュメントに対して update が走る。

親要素のドキュメントがまだ MongoDB に insert されていない場合、リレーションが `has_*` であれば親要素になるデータを save しても子要素は保存されず、別途保存する必要がある。 `embeds_*` であれば親要素、子要素のどちらを保存しても両方が保存される。