おぴよの気まぐれ日記

おぴよの気まぐれ日記

岡山やプログラミング、ファッションのこと、子育てや人生、生き方についての備忘録。

【Rails】Active Recordで作ったオブジェクトを削除しよう!(destroy/delete)

Ruby on Railsでデータベースを操作するのに使うActive Record。

今回は、destroydeleteを使って実現するオブジェクトの削除方法紹介します。

今回利用するテーブル情報はこちらです。

今回使うテーブルのER図(new_users/posts)
今回使うテーブルのER図

基本的なこと

オブジェクトを削除するメソッドはdestroydeleteです。

# deleteはDELETE文が実行される
> user.posts.last
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>
> user.posts.last.delete
  Post Destroy (2.2ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 7]]
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>

# ActiveRecordのdestroyが実行される
> user.posts.last
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>
> user.posts.last.destroy
   (0.3ms)  BEGIN
   (0.2ms)  COMMIT
=> #<Post:0x00007fb4d78bb800 id: 7, user_id: nil, title: "5", body: nil, created_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, updated_at: Thu, 03 Oct 2019 22:47:03 JST +09:00, new_user_id: 1>

destroy

destroyを実行した場合は関連データを考慮してくれます。

NewUserオブジェクトを削除した時に、紐づくPostオブジェクトも同時に削除してくれます。

# new_user.rb
class NewUser < ApplicationRecord
  has_many :posts, dependent: :destroy
end

これを実現しているのがdependentという設定になるのですが、大きく2種類あります。

  • destory: 関連データを削除
  • nullify: 関連データを削除せずに、nullに更新する

dependent: :destroy

実行結果を見るとわかりますが、先ず始めにPostオブジェクトにDELETE文が走ります。その後にNewUserオブジェクトにDELETE文が実行され関連するデータが全て削除されました。

> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fed68ebe4e0 id: 14, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:20:16 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:16 JST +09:00, new_user_id: 4>,
 #<Post:0x00007fed68ebe350 id: 15, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:20:17 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:17 JST +09:00, new_user_id: 4>,
 #<Post:0x00007fed68ebe1c0 id: 16, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:20:21 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:21 JST +09:00, new_user_id: 4>]

> NewUser.last.destroy
  NewUser Load (0.5ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 4]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 14]]
  Post Destroy (0.2ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 15]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 16]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 4]]
   (0.5ms)  COMMIT
=> #<NewUser:0x00007fed4b098ab8 id: 4, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:20:02 JST +09:00, updated_at: Thu, 03 Oct 2019 23:20:02 JST +09:00>

> Post.all
  Post Load (0.5ms)  SELECT "posts".* FROM "posts"
=> []

dependent: :nullfy

実行結果を見るとわかりますが、Postオブジェクトは削除されずにnew_user_idがnullにupdateされています。

そのため、Postデータを明示的に残しておきたい場合はdependent: :nullifyを設定します。

> Post.all
  Post Load (0.7ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fbf14ebfb10 id: 8, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, new_user_id: 2>,
 #<Post:0x00007fbf14ebf958 id: 9, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, new_user_id: 2>,
 #<Post:0x00007fbf14ebf7c8 id: 10, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, new_user_id: 2>]

> NewUser.last.destroy
  NewUser Load (0.4ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
   (0.2ms)  BEGIN
  Post Update All (0.4ms)  UPDATE "posts" SET "new_user_id" = NULL WHERE "posts"."new_user_id" = $1  [["new_user_id", 2]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 2]]
   (1.9ms)  COMMIT
=> #<NewUser:0x00007fbef78b27f8 id: 2, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:14:35 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:35 JST +09:00>

> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fbef7927b70 id: 8, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:38 JST +09:00, new_user_id: nil>,
 #<Post:0x00007fbef79279e0 id: 9, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:40 JST +09:00, new_user_id: nil>,
 #<Post:0x00007fbef7927850 id: 10, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:14:42 JST +09:00, new_user_id: nil>]
[12] pry(main)> NewUser.all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
=> []

delete

deleteはイメージ的にSQLのDELETE文を実行するような感じで対象のデータを1件削除します。そのためdestroyよりも処理は速いです。

が、アプリケーションの処理の中ではほとんど使われないのではないでしょうか?

関連データがあった場合にdeleteしちゃうと整合性が取れず壊れちゃうし、関連データがない場合でもdestroyで削除することは可能なので。

全件削除する(destroy_all/delete_all)

1件だけじゃなくて全てのデータを削除したい場合はdestroy_alldelete_allがあります。

# destroy_allの実行例
> NewUser.all
  NewUser Load (0.5ms)  SELECT "new_users".* FROM "new_users"
=> [#<NewUser:0x00007fed66dc46b8 id: 5, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:25:58 JST +09:00, updated_at: Thu, 03 Oct 2019 23:25:58 JST +09:00>,
 #<NewUser:0x00007fed66dc4528 id: 6, name: "fuuzo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:26:26 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:26 JST +09:00>]
> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> [#<Post:0x00007fed68bac798 id: 17, user_id: nil, title: "1", body: nil, created_at: Thu, 03 Oct 2019 23:26:03 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:03 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac608 id: 18, user_id: nil, title: "2", body: nil, created_at: Thu, 03 Oct 2019 23:26:04 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:04 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac478 id: 19, user_id: nil, title: "3", body: nil, created_at: Thu, 03 Oct 2019 23:26:10 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:10 JST +09:00, new_user_id: 5>,
 #<Post:0x00007fed68bac2e8 id: 20, user_id: nil, title: "11", body: nil, created_at: Thu, 03 Oct 2019 23:26:32 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:32 JST +09:00, new_user_id: 6>,
 #<Post:0x00007fed68bac158 id: 21, user_id: nil, title: "12", body: nil, created_at: Thu, 03 Oct 2019 23:26:34 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:34 JST +09:00, new_user_id: 6>,
 #<Post:0x00007fed68ba7f68 id: 22, user_id: nil, title: "13", body: nil, created_at: Thu, 03 Oct 2019 23:26:35 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:35 JST +09:00, new_user_id: 6>]
> NewUser.destroy_all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 5]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 17]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 18]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 19]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 5]]
   (1.7ms)  COMMIT
   (0.2ms)  BEGIN
  Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 6]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 20]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 21]]
  Post Destroy (0.3ms)  DELETE FROM "posts" WHERE "posts"."id" = $1  [["id", 22]]
  NewUser Destroy (0.4ms)  DELETE FROM "new_users" WHERE "new_users"."id" = $1  [["id", 6]]
   (0.4ms)  COMMIT
=> [#<NewUser:0x00007fed66d75cc0 id: 5, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:25:58 JST +09:00, updated_at: Thu, 03 Oct 2019 23:25:58 JST +09:00>,
 #<NewUser:0x00007fed66d75b30 id: 6, name: "fuuzo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:26:26 JST +09:00, updated_at: Thu, 03 Oct 2019 23:26:26 JST +09:00>]
> NewUser.all
  NewUser Load (0.4ms)  SELECT "new_users".* FROM "new_users"
=> []
> Post.all
  Post Load (0.4ms)  SELECT "posts".* FROM "posts"
=> []

実践的に使う

destroy / destroy!

savesave!と同じように!を付けると例外を発生させることが可能です。

基本的には全てdestroy!で問題ないと思います。削除できなかったってことは何か問題が発生したってことなので例外を発生させて気付けるようにしたいので。

条件に応じて削除しないようにする

とはいえ、条件に応じて処理は続行させたい場合もあると思います。

今回の例の場合だと、1件でもPostデータがあればUserデータは削除させないようにするにはbefore_destroyを使う。

# post.rb
class Post < ApplicationRecord
  belongs_to :new_user

  before_destroy :do_not_destroy_posted

  private

  def do_not_destroy_posted
    throw :abort
  end
end
> user = NewUser.last
  NewUser Load (0.7ms)  SELECT  "new_users".* FROM "new_users" ORDER BY "new_users"."id" DESC LIMIT $1  [["LIMIT", 1]]
=> #<NewUser:0x00007fab7fe19150 id: 7, name: "opiyo", age: nil, gender: nil, birthday: nil, email: nil, created_at: Thu, 03 Oct 2019 23:43:42 JST +09:00, updated_at: Thu, 03 Oct 2019 23:43:42 JST +09:00>
> user.destroy
   (0.3ms)  BEGIN
  Post Load (0.8ms)  SELECT "posts".* FROM "posts" WHERE "posts"."new_user_id" = $1  [["new_user_id", 7]]
   (0.8ms)  ROLLBACK
=> false

まとめ

削除処理はデータをぶっ壊す可能性がありますので、ちゃんと考えて実装する必要があると思います。

しっかりと関連データまで意識した処理を書いていきたいですね。

  • 基本的にはdestroy!
  • dependentで関連データを扱いを定義する
  • 条件がある場合はbefore_destroyを使ってバリデーションをかける
  • なーんも関係なく削除したい場合はdeleteの方が早い