# Ruby on Railsで孤立したレコードを見つけて削除する方法
以下の三つのテープルを定義してみます。
# app/models/notification.rb
class Notification < ActiveRecord::Base
has_many :user_notifications
has_many :users, through: :user_notifications
end
# app/models/user.rb
class User < ActiveRecord::Base
has_many :user_notifications
has_many :notifications, through: :user_notifications
end
# app/models/user_notifications.rb
class UserNotification < ActiveRecord::Base
belongs_to :user
belongs_to :notification
end
残念ながら、has_many:の通知にdependent::destroyを追加するのを忘れた。 ユーザーまたは通知が削除されたときに、孤立したUserNotificationが残っていました。 この問題はdependent::destroyによって修正されましたが、それでもたくさんの孤立したレコードが残っていました。 孤立したレコードを削除する方法は3つあります。
Approach #1 — Bad
UserNotification.find_each do |user_notification|
if user_notification.notification.nil? || user_notification.user.nil?
user_notification.destroy
end
end
これは各レコードに対して別々のSQLクエリを実行し、それが孤立しているかどうかをチェックし、そうであれば破棄します。
Approach #2 — Better, but still pretty bad
UserNotification.all.each do |user_notification|
if user_notification.notification.nil? || user_notification.user.nil?
user_notification.destroy
end
end
これは、すべてのレコードをメモリにロードしてから、上記と同じチェックを実行しながらそれらを繰り返します。
Approach #3 — Good
UserNotification.where([
"user_id NOT IN (?) OR notification_id NOT IN (?)",
User.pluck("id"),
Notification.pluck("id")
]).destroy_all
このアプローチでは、最初にすべてのユーザーと通知のIDを取得し、次に1つのクエリを実行してユーザーまたはクエリのいずれにも属していないすべてのユーザー通知を見つけます。
Benchmarks
それぞれの場合にかかる時間を見てみましょう。
user system total real
bad 3.020000 0.160000 3.180000 ( 4.058246)
better 2.950000 0.170000 3.120000 ( 3.982329)
good 0.010000 0.000000 0.010000 ( 0.030346)
ユーザー通知の数が減っても、UserNotification.all.eachはUserNotifications.find_eachよりも高速です。これは、SQLクエリの実行回数が少ないためです。
Approach #4
- データが多い場合、pluckよりselectのほうが高速です
UserNotification.where([
"user_id NOT IN (?) OR notification_id NOT IN (?)",
User.select("id"),
Notification.select("id")
]).destroy_all