2021-04-18

`destroyed_by_association` で親モデルの関連により削除されるかどうかを判定する

Rails (ActiveRecord) ネタ。
#destroyed_by_association を使うと、モデル間の関連設定により削除されるのか、それとも自身で削除するのかを判定することができる。

例えば、次のように TeamUser という2つのモデルがあるとする。

class Team < ApplicationRecord
  has_many :users, dependent: :destroy
end

class User < ApplicationRecord
  belongs_to :team
end

Teamが親モデルに相当し、その下に複数の子モデルUserが紐づくという単純な関係のモデル構造。

Teamモデルには has_many :users, dependent: :destroy というdependentオプションが設定されているので、Teamを削除したときに関連づくUserは一緒に削除される。以下は、それを示すサンプルコード。

# チームを1つ作る
team = Team.create!(name: 'awesome team')

# 作成したチームの下に3ユーザを紐付ける
team.users.create!(name: 'user1')
team.users.create!(name: 'user2')
team.users.create!(name: 'user3')

team.destroy!
#  TRANSACTION (0.1ms)  begin transaction
#  User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."team_id" = ?  [["team_id", 8]]
#  User Destroy (0.6ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 15]]
#  User Destroy (0.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 16]]
#  User Destroy (0.1ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 17]]
#  Team Destroy (0.3ms)  DELETE FROM "teams" WHERE "teams"."id" = ?  [["id", 8]]
#  TRANSACTION (1.6ms)  commit transaction

この機能はとても便利なのだが、状況によっては、Userは上のように親モデルの関連により削除されるのか、それとも user.destroy! のようにしてUserが自ら削除をしたのか判定したくなるときがある。

そこで #destroyed_by_association

https://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html#method-i-destroyed_by_association

このメソッドを使うと、Userモデル側でどちらの方法で削除されるかを判定することができる。以下、その例。

class User < ApplicationRecord
  belongs_to :team

  before_destroy do
    if destroyed_by_association
      puts 'destroyed by Team association'
    else
      puts 'destoryed by myself'
    end
  end
end

before_destroy フックと組み合わせることで判定している。以下、この動きを示すサンプルコード。

team = Team.create!(name: 'awesome team')

team.users.create!(name: 'user1')
team.users.create!(name: 'user2')
user3 = team.users.create!(name: 'user3')

user3.destroy!
# => "destoryed by myself"

team.reload.destroy!
# => "destroyed by Team association"
# => "destroyed by Team association"

この機能をうまく使えば、User側から削除する場合だけ何らかの処理をはさみたい(あるいはその逆)の機能の実装をモデル層で吸収することができる(かもしれない)。

以上、最近知った機能の紹介でした。

※ 参考