0
0

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 3 years have passed since last update.

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

Last updated at Posted at 2021-05-26

前提

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

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

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

障害内容と解消方法

以下のようなModelがあったとします

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_attributessaveを呼び出しているから正常に動作するのだと思われます。

ただし、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との紐づきを解消してしまいます。

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

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

内部ソースを見ながら考察

削除処理

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

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を渡すため、基本的に正しく動作させることができます。

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

destroy_allは内部でdestroyを利用しているため削除処理が動作せず、delete_allは内部でDELETE SQLを直接発行しているので正常に動作します。
(画像のModel名は都合上、加工を行っています)
キャプチャ.png

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

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

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

Railsの公式ソース

まとめ

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?