ActiveRecordの#updateについて　

# 前提
Rails 6.1.4
ruby 2.7.1

# はじめに
ActiveRecordの`update`メソッドを使う際、引数に一部の変更したいカラムだけ指定する時があるが、この時指定されていない`attirubte`は`nil`となりバリデーションに引っかかる場合があると思うがどうやって回避されているのだろうか？という疑問を持ったので挙動について詳しく調べていきたいと思う。

今回以下のソースコードを参考にした。
このmoduleには、普段よく使う様々なメソッドについてのコメントやコードが載っており眺めているだけでもかなり面白かった。
[Github](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/persistence.rb#L616)

# `save`
まず始めに`update`メソッドの基礎となる`save`メソッドに紐解いてみる。
[Github](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/persistence.rb#L469)

## 概要

ソースコードを眺める前にコメントを翻訳し、動作の概要を把握する。

```
モデルを保存する。

新規レコードならDBに保存され、既存レコードなら更新する。

デフォルトではバリデーションを走らせる。そのうちのどれかが失敗したら動作はキャンセルされfalseを返し、レコードは保存されない。
しかし、+validate: false+オプションを渡すことで、バリデーションを回避できる。

デフォルトでは、+updated_at+属性に現在の時間を設定する。
しかし、+touch: false+を渡せば、タイムスタンプは更新されない。

+before_*+ callbackが+:abort+を投げたら、動作はキャンセルされfalseを返す。
```

`save`メソッドは新規レコードを保存するだけでなく、既存レコードに対しては更新する動作も含むという点は重要そう！！
タイムスタンプに対するオプションがあったのは初めて知った。

## ソースコード
次に実際のソースコードをみていく。
`save`メソッド`create_or_update`をよび例外をキャッチするだけの処理をしている。

`create_or_update`についてみていこう。

```ruby
def create_or_update(**, &block)
  _raise_readonly_record_error if readonly?
  return false if destroyed?
  result = new_record? ? _create_record(&block) : _update_record(&block)
  result != false
end
```

[creat_or_update](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/persistence.rb#L903)

始めにreadonlyがtrueの場合例外を発生させている。
[readonly?](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/core.rb#L489)

ちなみにこの`readonly？`はレコード全体がreadonlyかどうかを判定しているが、クラスメソッドに各属性に対してreadonlyを付与できるメソッドもある。
[attr_readonly](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/readonly_attributes.rb#L14)

次に、`destroyed?`がtrueの場合falseを返している。
これはrailsのメモリ上に存在するインスタンスが以前DBからdeleteされていたら`update`する対象がないということなので`false`を返しているのだと思う。

次に、`new_record?`で新規レコードか既存レコードかを確認し、それに応じて`_create_record`と`_update_record`を実行させている。

### `_create_record`
```ruby
    def _create_record(attribute_names = self.attribute_names)
      attribute_names = attributes_for_create(attribute_names)

      new_id = self.class._insert_record(
        attributes_with_values(attribute_names)
      )

      self.id ||= new_id if @primary_key

      @new_record = false

      yield(self) if block_given?

      id
    end
```
`attribute_names`メソッドで属性名を文字列の配列として引数`attribute_names`に設定している。
*属性名は　DBのカラムに由来するのか、`attr_accessor`などを追加すればそれも含まれて返されるのかは調査が必要。

[attribute_names](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/attribute_methods.rb#L154)

`attributes_for_create`は、先ほどの`attribute_names`を引数に渡して実行している。これは、`attribute_names`の中から、次の条件「`id`がnilかつprimary key」を満たすattributeを削除した`attirubte_names`を返す仕様になっている。

[attributes_for_create](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/attribute_methods.rb#L395)

`attributes_with_values`により、`{"title": "test"}`のような属性とその値をハッシュに変換したものを`self.class._insert_record`に渡している。

[attributes_with_values](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/attribute_methods.rb#L379)


`_insert_record`では、最初に`attribute_names`に`id`がない場合に`id`をセットしている。
その後の処理についてはちょっと追うのが辛くなってきたので関数名からして多分DBにインサートする処理を行なっているのだろう（白目）
返り値はおそらく新しく生成されたidであり、これを自身のインスタンス変数に格納していると思われる。
最後に`new_record`インスタンス変数を`false`にして`id`を返り値としている。

### `_update_record`

```ruby
    def _update_record(attribute_names = self.attribute_names)
      attribute_names = attributes_for_update(attribute_names)

      if attribute_names.empty?
        affected_rows = 0
        @_trigger_update_callback = true
      else
        affected_rows = _update_row(attribute_names)
        @_trigger_update_callback = affected_rows == 1
      end

      yield(self) if block_given?

      affected_rows
    end
```

始めに、`attributes_for_update`で、readonlyが付与されていない属性の配列を取得している。

[attributes_for_update](https://github.com/rails/rails/blob/f33d52c95217212cbacc8d5e44b5a8e3cdc6f5b3/activerecord/lib/active_record/attribute_methods.rb#L386)

それらの属性名から`attributes_with_values`で属性名をキーに、値をバリューにしたハッシュを取得し、`_update_record`に渡している。
先ほどと同様に一旦`_update_record`について読み解くのはスルーして関数名からおそらくDBに対して更新処理を行なっているという予測に留める。
最後に返り値として`affected_row`を返しているが、これは変数名から変更されたカラム数だと思う。。

---------------------------------------

```ruby:create_or_update.rb
result = new_record? ? _create_record(&block) : _update_record(&block)
result != false
```
ここで、やっと`create_or_update`に戻ってきて、`_create_record`や`_update_record`の返り値と`fase`を`!=`で比較した結果を返り値としている。

これが`save`に返り値として渡されこの値が`save`の返り値となる！

ここまでが`save`メソッドの流れである。では次にようやく`update`について読み解いていく。

# `update`
```ruby
def update(attributes)
  # The following transaction covers any possible database side-effects of the
  # attributes assignment. For example, setting the IDs of a child collection.
  with_transaction_returning_status do
    assign_attributes(attributes)
    save
  end
end
```

[update](https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/persistence.rb#L623)

[with_transaction_returning_status](https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/persistence.rb#L623)にブロックを渡している。このメソッドはソースコードを読み解くのが難しかったのでコメントで大まかな動作を把握するだけに留める。

```
transaction内で渡されたブロックを実行し、返り値をステータスフラグをしてキャプチャする。
trueの場合transactionはコミットされ、そうでなければROLLBACKされる。
いかなる場合でもステータスフラグを返す。
```

ほぼtransactionのような挙動をしてくれるという解釈で進める。
次に`assign_attributes(attributes)`により属性の値をインスタンス変数に設定する。
ちなみに、`assign_attributes`はActiveModelモジュールのものを使っている。と思う。
[Github](https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/attribute_assignment.rb#L7)

最後に`save`を実行している。
結局`update`メソッドは`save`を実行していたのだった〜

# まとめ
- `update`のほとんどは`save`メソッドの処理
- `save`は引数でattributesを指定できないが、`update`ではできる。

### `update`に渡されなかったattributeについて
`post = Post.find(1)`
のように既存のレコードをメモリ上に取得する時にインスタンス変数として全てのカラムの値を取得している。よって、
`post.update({title: 'hode'})`
のようにtitle以外の属性を渡さなかったといってその属性が`nil`の状態で`update`が走るわけではなくレコードの値を保持した状態で`update`が走る。故に、バリデーションエラーが起こらないのだと思う。

ここについては、いつバリデーションが走るのか、どのようにバリデーションが行われているのかをコードから読み解くことはできなかったので憶測になってしまう。。

ちなみに`update`実行時のSQLを見てみると引数に渡した属性（と
`update_at`)のみUPDATEされている。

```ruby
post.update(title: 'changed')

# UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["title", "changed"], ["updated_at", "2021-10-23 06:38:35.361966"], ["id", 1]]
```

`save`についても検証してみると

```ruby
post.title = 'changed'
post.save

# UPDATE "posts" SET "title" = $1, "updated_at" = $2 WHERE "posts"."id" = $3  [["title", "fix"], ["updated_at", "2021-10-23 06:41:36.512688"], ["id", 1]]
```

のように変更された属性に対してのみUPDATE SQLが実行されている。
どの属性の値も変えてない場合は、SQLが発行されなかった。

また、属性の値を同じ値で更新しようとすると、、

```ruby
p.title
# => 'changed'
p.update(title: 'changed')
# => true
```

```ruby
p.title
# => 'changed'
p.title = 'changed'
p.save
# => true
```

なんと、SQLが発行されなかった！
しかもこの挙動はrelationshipが作成されていて関連先のオブジェクトを代入する時にも使える！！

```ruby
post.comment_ids
# => [1,2,3]

post.comment_ids = [1,2,3]
# => 更新処理はされない　comment.idが1,2,3のレコードを取得するのみ。

post.comment_ids = [1,2]
# => SQLは省略するが、comment.id = 3のレコードに対してcomment.post_idをnilにする更新処理のみ。
```

ここら辺の挙動も面白かったのでコードを調査したいなあ・・・


# 疑問点
`save`に`validate: false`渡しても無視されてない？

```ruby
                    #ここ
def create_or_update(** , &block)
```


# 余談
今回`ActiveRecord::Persistence`モジュールを調べていたのだが、本当にこのモジュールであっているのかどうか`.method(:save).owner`で確認してたら`save`に関しては`ActiveRecord::Suppressor`が出てきた。
これは一体・・・