ActiveRecordを継承したクラスに対してActiveModelをincludeしたときのDB処理について

# 前提
ActiveModelはDBに保存しない値を保持するModelに対して適用するのが通例なので、DBに保存するためにModelとして作成するActiveRecord(ApplicationRecord)と併用して保存するのはあまり適切ではありません。
(厳密には外部APIなどを使用して異なる必須条件のパラメータを保持する場合などには利用することがあるので、寧ろ相性は良い方です)

ただ、ActiveRecordとしての機能が使えなくなる部分が多く、慎重にならざるを得ないので実装の際には気を付けましょう。

なお、こちらのQiitaを見て安易に実装は行わないようにお願いします。
**ソースを見た上でその危険性やリスクを理解し、他者に対してそれを明確に説明することができてリスク回避の方法を知っている
とまではいかなくても、理解して説明できることを前提として実装を行ってください。**

# 障害内容と解消方法
以下のようなModelがあったとします

```ruby:user.rb
class TAct < ApplicationRecord
  include ActiveModel::Model

  attr_accessor :profile
########################################
##  DBのカラムとしては以下の通り
##  id:Integer Primary Key
##  name:String
##  age:Integer
########################################
```

このModelに対して登録・削除処理を行おうとすると様々な不都合が発生します。

## 登録処理

```
user = User.create(user_params)      # エラーが発生

user = User.new(user_params)         #
user.save                            # エラーが発生

user = User.new                      #
user.assign_attributes(user_params)  # attributesによる設定でも正常に動きます
user.save                            # 正常に動作
```

原因は`include ActiveModel::Model`によるModelの属性が消去されることです。
`create`は内部的には`new(user_params)`を使用しているため、newと同様のエラーが発生します。

## 更新処理
更新処理は`include ActiveModel::Model`をする前の書き方でも問題なく動作します。
内部で`assign_attributes`と`save`を呼び出しているから正常に動作するのだと思われます。

**ただし、FormにModelを指定している場合、Modelの属性が消えることで自動的に`create`アクションに飛ばされますので注意してください。**

## 削除処理

```
User.find(1).delete
User.find_by(id: 1).delete
User.where(id: 1).delete
User.find(1).destroy
User.find_by(id: 1).destroy
User.where(id: 1).destroy
User.where(id: 1).destroy_all

# 上記の書き方だとエラーが発生します

User.delete(1)                # 正常に動作
User.delete_all               # 正常に動作
User.where(id: 1).delete_all  # 正常に動作
```

原因は登録処理の時と同じです。

# 原因についての考察
`include ActiveModel::Model`はModelに設定されているDBとの紐づきを解消してしまいます。

そのため、`User`Modelを宣言しただけでは属性が設定されていない状態になり、関連してActiveRecordとしての登録・更新・削除処理が適切に動かないようになります。

特に
`<%= form_with model: @user, url: { action: :update } %>`
といったFormを実装している場合、`update`アクションではなく`create`アクションに飛んでしまいます。
理由は`id`属性が設定されていないためなので、Modelを渡さない、`attr_accessor`で設定するなどの工夫が必要になります。
[form_withの動作についてはこちらを参照してください。](https://qiita.com/snskOgata/items/44d32a06045e6a52d11c)

## 内部ソースを見ながら考察
### 削除処理
こちらはRailsの削除処理周りのみを抜き出したソースです。
(以下、deleteの引数有は`delete()`、deleteの引数無は`delete`として表現します。)

```ruby:rails/activerecord/lib/activerecord/persistence.rb
def delete(id_or_array)
  delete_by(primary_key => id_or_array)
end

-----

def _delete_record(constraints) # :nodoc:
  constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }

  dm = Arel::DeleteManager.new
  dm.from(arel_table)
  dm.wheres = constraints

  connection.delete(dm, "#{self} Destroy")
end

-----

def delete
  _delete_row if persisted?
  @destroyed = true
  freeze
end

-----

def destroy
  _raise_readonly_record_error if readonly?
  destroy_associations
  @_trigger_destroy_callback = if persisted?
    destroy_row > 0
  else
    true
  end
  @destroyed = true
  freeze
end

def destroy!
  destroy || _raise_record_not_destroyed
end

-----

def destroy_row
  _delete_row
end

def _delete_row
  self.class._delete_record(@primary_key => id_in_database)
end
```

実際に見てみると、`delete()`は与えられた引数を基に`delete_by`を実行しています。引数には`id`を渡すため、基本的に正しく動作させることができます。

対して、`delete`や`destroy`、`destroy!`は前提として`persisted?`がtrueであれば削除処理を行うように設計されています。削除処理自体は問題なく動作します。
しかし、前述したように`include ActiveMode::Model`によってDBとの紐づきは失われるため、`persisted?`の結果はfalseが返却されます。
結果として、`destroy`前のSELECT句のみ走り、ログ上には何も動作していない`BEGIN ~ COMMIT`文が流れてしまう、という現象になります。

`destroy_all`は内部で`destroy`を利用しているため削除処理が動作せず、`delete_all`は内部でDELETE SQLを直接発行しているので正常に動作します。
(画像のModel名は都合上、加工を行っています)
![キャプチャ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1492288/d2df360f-c1ad-22fa-88c5-a37c230ce449.png)

ちなみに、Rails5系であれば`delete()`は

```ruby:rails/activerecord/lib/activerecord/persistence.rb
def delete(id_or_array)
  where(primary_key => id_or_array).delete_all
end
```

と記載されており、こちらの`delete()`でも正常に動作することを確認しています。

[Railsの公式ソース](https://github.com/rails/rails/blob/75ac626c4e21129d8296d4206a1960563cc3d4aa/activerecord/lib/active_record/persistence.rb)

https://github.com/rails/rails/blob/75ac626c4e21129d8296d4206a1960563cc3d4aa/activerecord/lib/active_record/persistence.rb

# まとめ

* ActiveRecord、ApplicationRecordを継承したModelに対してActiveModelをincludeした場合、ActiveRecordに準ずるDB処理機能が正常に動作しなくなる
 * 直接SQLを発行するタイプの処理は動作するため、そちらで対処する
 * `delete()`はValidationやROLLBACKが効かないので、実装する場合はトランザクションや`valid?`に注意しながら実装を行う
* 中身のない`BEGIN ~ COMMIT`文などを見かけた場合、ActiveModelか否か、その場合に各処理はどのように動いているのか、ActiveRecord基準の動作が想定されていないか、を確認する