9
2

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 1 year has passed since last update.

gorm.DeletedAt と Delete をあれこれ検証してみた [GORM]

Last updated at Posted at 2023-02-06

はじめに

Go言語のORMパッケージとして、GORM があります。
「データの取得」「論理削除」「物理削除」について色々検証してみたので、その結果を共有します。

前提

  • Go言語をある程度触ったことがある
  • GORM パッケージをある程度触ったことがある
    • 主に、gorm.DeletedAtとその周辺について話します
    • データベースの内容は、VSCode 拡張機能の MySQL で閲覧します
  • Gin パッケージを触ったことがある
    • 最終的にAPIレスポンスとして検証するので、Gin を使います
    • APIへのリクエストは、VSCode 拡張機能の RESTClient を用います
  • 論理削除と物理削除についてなんとなく知っている
    • 今回のメイントピック

この記事を読んでわかること

  • gorm.DeletedAtの機能
  • gorm.DeletedAtFind,Scanメソッドへの作用
  • 独自定義の構造体で、クエリ結果を受け取るときに、論理削除データがレスポンスに含まれる件
    • 独自構造体で論理削除データを含ませない方法を2種類
      • クエリにWHERE deleted_at IS NULLを指定する方法
      • 独自定義の構造体でWHERE deleted_at IS NULLを自動適用させる方法
  • 論理削除と物理削除の違い
  • GORMで論理削除データを参照する方法・参照させない方法
  • 関連付け(Association)のあるデータの「物理削除」方法を2種類
    • 対象データの関連付けのみ削除し、対象データを「物理削除」する方法
    • 対象データと関連付けされたデータもろとも「物理削除」する方法

GORMやGinの使い方、今回実装しているGoの細かい実装内容は解説しません。
長いので、お急ぎの方は まとめ が少し参考になるかもしれません。

もくじ

gorm.DeletedAtについて

gorm.DeletedAtは、GORMでdeleted_atカラムを使いたいときに指定する型で、レコードが作成された段階では、その値はNULLになります。

レコードが削除されたときには、自動的に削除日時で更新してくれますし、FindScanをするとき、自動でWHERE deleted_at IS NULLを指定してくれるので、論理削除済みのデータはレスポンスに含まれないように勝手に動いてくれます。

つまり、開発者が特に意識しなくとも「論理削除」の処理を実現してくれます。
なので、この型を指定するだけでそのモデルに対して、論理削除を実装できているようなものです。便利!

しかし、開発していくうちに、
「なんか期待通りにAPIレスポンスが返ってこないな?」
「論理削除できてるデータも一緒に返ってくるな?なんでだ?」
ってなったので、いろいろ試したのがこの記事のモチベーションです。

モデル定義

今回UserテーブルとCompanyテーブルの2つを用意します。
インポート関連は省略します。

// 共通の構造体(埋め込み用)
type General struct {
  ID        uint           `gorm:"primarykey" json:"id"`
  CreatedAt time.Time      `json:"-"`
  UpdatedAt time.Time      `json:"-"`
  DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

// Usreテーブル
type User struct {
  General   `gorm:"embedded"`
  Name      string   `gorm:"not null" json:"name"`
  Age       uint     `gorm:"not null" json:"age"`
  CompanyId uint     `json:"company_id"`
  Company   *Company `json:"-"`
}

// Companyテーブル
type Company struct {
  General   `gorm:"embedded"`
  Name      string `gorm:"not null" json:"name"`
  Employees []User `json:"employees"`
}

今回、gorm.Modelは使わずに、自前で用意したGeneral構造体をgorm:"embedded"タグを用いて埋め込んでいます。

Companyテーブルを例に取り上げると、上記は以下と等価です。

type Company struct {
  ID        uint           `gorm:"primarykey" json:"id"`
  CreatedAt time.Time      `json:"-"`
  UpdatedAt time.Time      `json:"-"`
  DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
  Name      string         `gorm:"not null" json:"name"`
  Employees []User         `json:"employees"`
}

また、ER図で表すと画像のようになります。
hasManyhasOne の関係です。

基本コード

基本的なコードは以下のようになり、後にルーティング部分のハンドラーを追加していきます。

細かいインポートとDBへの接続部分の細かい処理は省略しています。

デモ用のCompanyとして、株式会社USO(id:1)を起動時に作成します。

var (
  GormDB *gorm.DB
  DB     *sql.DB
)

// DBへの接続、マイグレーション、初期データ作成
func init() {
  gormDB, err := controllers.ConnectMySQL() // MySQLに接続
  if err != nil {
    log.Fatalf("main mysql.Open error err:%v", err)
  }

  db, err := gormDB.DB()
  if err != nil {
    log.Fatal(err)
  }

  if err := gormDB.AutoMigrate(&User{}, &Company{}); err != nil {
    log.Fatal(err)
  }
  DB = db
  GormDB = gormDB

  // Companyのデモ用レコード
  if err := GormDB.Create(&Company{Name: "株式会社USO"}).Error; err != nil {
    log.Fatal(err)
  }
}

func main() {
  defer DB.Close()

  r := gin.Default()

  r_user := r.Group("/user")
  {
    r_user.GET("/list", Middleware(UserList))
    r_user.GET("/list/name", Middleware(UserNameList))
    r_user.GET("/list/name-deletedAt", Middleware(UserNameListWithDeletedAt))
    r_user.GET("/list/name-where", Middleware(UserNameListWithWhere))
    r_user.POST("/create", UserCreate)
    r_user.DELETE("/delete/:id", UserDelete)
    r_user.DELETE("/soft-delete/:id", UserSoftDelete)
  }

  r_company := r.Group("/company")
  {
    r_company.GET("/info/:id", CompanyInfo)
    r_company.GET("/info-unscoped/:id", CompanyInfoUnscoped)
    r_company.DELETE("/association-delete/:id", CompanyMiddleware(CompanyAssociationDelete, "Company association was deleted"))
    r_company.DELETE("/association-unscoped-delete/:id", CompanyMiddleware(CompanyAssociationUnscopedDelete, "Company association(unscoped) was deleted"))
    r_company.DELETE("/force-delete/:id", CompanyMiddleware(CompanyForceDelete, "Company was force-deleted"))
    r_company.DELETE("/delete/:id", CompanyMiddleware(CompanyDelete, "Company was deleted"))
    r_company.DELETE("/soft-delete/:id", CompanyMiddleware(CompanySoftDelete, "Company was soft-deleted"))
  }

  r.Run(":8080")
}

APIへのリクエスト用に、.http拡張子のファイルを用意しました。
折り畳みに入れているので、気になる人だけ見てください。

test.httpファイル
### env
@BASEURL=http://localhost:8080
@USER={{BASEURL}}/user
@COMPANY={{BASEURL}}/company
@CTYPE=application/json

### user create
# @prompt name
POST {{USER}}/create HTTP/1.1
Content-Type: {{CTYPE}}

{
  "name": "{{name}}",
  "age": 22
}

### user list
GET {{USER}}/list HTTP/1.1
Content-Type: {{CTYPE}}

### user name list
GET {{USER}}/list/name HTTP/1.1
Content-Type: {{CTYPE}}

### user name list with where
GET {{USER}}/list/name-where HTTP/1.1
Content-Type: {{CTYPE}}

### user name list with deleted_at
GET {{USER}}/list/name-deletedAt HTTP/1.1
Content-Type: {{CTYPE}}

### user soft delete
DELETE {{USER}}/soft-delete/2 HTTP/1.1
Content-Type: {{CTYPE}}

### user delete
DELETE {{USER}}/delete/3 HTTP/1.1
Content-Type: {{CTYPE}}

### company info
GET {{COMPANY}}/info/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company info unscoped
GET {{COMPANY}}/info-unscoped/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company soft delete
DELETE {{COMPANY}}/soft-delete/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company delete
DELETE {{COMPANY}}/delete/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company delete only assosiation
DELETE {{COMPANY}}/association-delete/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company unscoped-delete only assosiation
DELETE {{COMPANY}}/association-unscoped-delete/1 HTTP/1.1
Content-Type: {{CTYPE}}

### company force delete
DELETE {{COMPANY}}/force-delete/1 HTTP/1.1
Content-Type: {{CTYPE}}

データの準備

デモで使うデータの準備を行います。

Userレコードの作成

まずは、ユーザーデータを作成するメソッドを定義します。
ユーザー名(name)と年齢(age)が、リクエストボディに含まれます。

/* リクエストボディ */
type UserCreateRequest struct {
  Name string `json:"name"`
  Age  uint   `json:"age"`
}

/* ユーザーの作成 */
func UserCreate(c *gin.Context) {
  // リクエスト取り出し
  var json UserCreateRequest
  if err := c.ShouldBindJSON(&json); err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  
  // Company(id:1)を取り出し
  company := models.Company{}
  if err := GormDB.First(&company).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  
  // ユーザーデータ作成
  user := models.User{
    Name:    json.Name,
    Age:     json.Age,
    Company: &company,
  }
  if err := GormDB.Create(&user).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  
  // API レスポンス
  c.JSON(http.StatusOK, user)
}

今回4人分のデータを作成しました。
全員株式会社USO(id:1)に所属しています。

ユーザーのリスト表示と削除

ここでは、論理削除した時のデータ取得の際、クエリによってそのデータがDBレスポンスに含まれたり含まれなかったりと、gorm.DeletedAtの恩恵に与れないことがあるため、この現象を検証します。

手順は以下の通りです。

  1. ユーザーを一覧表示(4つの方法を試す)
  2. ユーザーBさんを「論理削除」
  3. ユーザーCさんを「物理削除」
  4. ユーザーを一覧表示(4つの挙動の違いを見る)

期待としては、論理削除されているデータはAPIレスポンスに含めたくないです。

ユーザーのリストを取得するコードと削除するコードを示した後に、実際に手順通り試した結果を載せていく。

ユーザーのリストを取得するコード

今回、4つの方法でユーザーのリストを取得します。

  1. チュートリアルでよく見るお手軽に全部取得する方法
  2. 独自の構造体にSELECT句を使って必要なレスポンスを入れる方法
  3. 方法2. + WHERE deleted_at IS NULLを指定する
  4. 方法2. + 独自の構造体にDeletedAtフィールドを追加

また、本記事を見るにあたって、クエリ部分に集中できるように、なるべく共通の処理はミドルウェア化しています。
ここは特に意識しなくてよいですが、クエリ部分の関数がハンドラーっぽくないように見えるので紹介しておきました。

/* 共通処理をラップするミドルウェア */
func Middleware(exec func() (interface{}, error)) gin.HandlerFunc {
  return func(c *gin.Context) {
    users, err := exec()  // ここでクエリを走らせる
    if err != nil {
      c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err})
      return
    }
    c.JSON(http.StatusOK, gin.H{"users": users})
  }
}

以下に各方法での実装を示しますが、注目してほしいのはDBからデータを受け取るusers変数とクエリ実行をした結果が入るquery変数です。

/* 1. チュートリアルでよく見るお手軽に全部取得する方法 */
func UserList() (interface{}, error) {
  users := []models.User{}
  query := GormDB.Table("users").Find(&users)
  
  return users, query.Error
}
/* 2. 独自の構造体にSELECT句を使って必要なレスポンスを入れる方法 */
// id, nameだけ抽出したい構造体
type UserNameListResponse struct {
  ID   uint   `json:"id"`
  Name string `json:"name"`
}
func UserNameList() (interface{}, error) {
  users := []UserNameListResponse{}
  query := GormDB.Table("users").Select("id, name").Find(&users)
  
  return users, query.Error
}
/* 3. 方法2. + `WHERE deleted_at IS NULL`を指定する */
func UserNameListWithWhere() (interface{}, error) {
  users := []UserNameListResponse{}
  query := GormDB.Table("users").Select("id, name").Where("deleted_at IS NULL").Find(&users)
  
  return users, query.Error
}
/* 4. 方法2. + 独自の構造体に`DeletedAt`フィールドを追加 */
// DeletedAt カラムを UserNameListResponse 構造体に追加
type UserNameListResponseWithDeletedAt struct {
  UserNameListResponse
  DeletedAt gorm.DeletedAt `json:"-"`  // 追加
}
func UserNameListWithDeletedAt() (interface{}, error) {
  users := []UserNameListResponseWithDeletedAt{}
  query := GormDB.Table("users").Select("id, name").Find(&users)  // ここは方法2. と同じ

  return users, query.Error
}

ユーザーを削除するコード

論理削除コードと物理削除コードをそれぞれ実装しました。

チュートリアルでよく見る通り、Delete()を使うと「論理削除」になってしまいます。
「物理削除」を行いたいときは、Unscoped()を追加してあげればよいです。

/* ユーザーの論理削除(Soft Delete) */
func UserSoftDelete(c *gin.Context) {
  id := c.Param("id")
  if err := GormDB.Delete(&models.User{}, id).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  c.JSON(http.StatusOK, gin.H{"message": "User was soft-deleted"})
}

/* ユーザーの物理削除 */
func UserDelete(c *gin.Context) {
  id := c.Param("id")
  if err := GormDB.Unscoped().Delete(&models.User{}, id).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  c.JSON(http.StatusOK, gin.H{"message": "User was deleted"})
}

手順通り試す

  1. ユーザーを一覧表示(4つの方法を試す)
    - 全員分のデータが取得できた
    方法1 方法2 方法3 方法4
    image.png image.png image.png image.png
  2. ユーザーBさんを「論理削除」(UserSoftDelete)
    - deleted_atNULLじゃなくなった
    image.png
  3. ユーザーCさんを「物理削除」(UserDelete)
    - 完全にCさんのデータが消えた
    image.png
  4. ユーザーを一覧表示(4つの挙動の違いを見る)
    - 期待するレスポンスは、AさんとDさんのデータのみ
    - 方法1,3,4は期待通り
    - 方法2ではBさんのデータも含まれている
    方法1 方法2 方法3 方法4
    image.png image.png image.png image.png

これが今回の本題の1つで、チュートリアルやネット記事でよく見かけるこの「方法2.」でデータを取り出すと、WHERE deleted_at IS NULLを自動で行ってくれる恩恵に与ることができなくなるみたいです。

もちろん、「方法3.」のように自前でWHERE deleted_at IS NULLで条件を与えてあげればいいわけですが...(めんどくさい)

TIPS的な(あまりネットで見かけたことがない?)使い方かもしれませんが、DBレスポンス用の構造体にDeletedAt: gorm.DeletedAtフィールドを追加してあげるとちゃんと期待通りになります。
「方法1.」も確かに、DeletedAtフィールドありますし、挙動が同じになるのはなんとなく納得?

私的には、「方法4.」が好みです。
というのも、もともと「方法2.」で実装されていたコードのレスポンス型の部分に、単にフィールド追加してあげればいいだけなので、「方法3.」よりなんとなく汚れない感があって好きです。

正直好みだと思います。
もしかしたら、ちゃんとした方法があるのかもしれませんが調べきれていません。

会社の情報取得と削除

さて、データを「物理削除」するとき、データに対して 関連付け(Association) があると、データを「物理削除」できません。
もちろん、関連付け(Association) が削除されさえすれば、そのデータを「物理削除」できます。

今回、User から CompanyhasOne の関連付けがあります。
そのため、CompanyはUserから持たれていると「物理削除」できないはずです。
逆に会社の従業員が空の状態であれば、会社をたためるわけです。

では、論理削除されたデータと関連付けがあればどうなるのか?
これらを検証していきます。

試すこととその手順は以下の通り。

  • 論理削除データへのアクセス
    1. 会社の情報を取得(2つの方法を試す)
    2. 会社を「論理削除」
    3. 会社情報を取得(2つの挙動の違いを確認)
  • 関連付けのみ削除
    1. 会社の情報を取得(2つの方法を試す)
    2. 会社を「物理削除」してみる
    3. 会社の関連付け(Association)を削除
    4. 会社を「物理削除」してみる
  • 関連付けのみ削除(Unscoped)
    1. 会社の情報を取得(2つの方法を試す)
    2. 会社を「物理削除」してみる
    3. 会社の関連付け(Association)を削除(Unscoped)
    4. 会社を「物理削除」
  • 関連付けされたデータもろとも削除
    1. 会社の情報を取得(2つの方法を試す)
    2. 会社を「物理削除」してみる
    3. 会社の関連付け(Association)もろとも(社員すらも)削除

会社情報を取得するコードと会社を削除するコードを示した後に、実際に手順通り試した結果を載せていく。

会社情報を取得するコード

会社情報を取得するコードは2つ示します。

先ほどのユーザーの物理削除で見たのと同じで、論理削除された会社データにアクセスしたければ、Unscoped()を用いなければいけません。
以下の2つのコードで、Unscoped()の挙動を再確認します。

/* チュートリアルでよく見る形式 */
func CompanyInfo(c *gin.Context) {
  id := c.Param("id")
  company := models.Company{}
  if err := GormDB.Preload("Employees").First(&company, id).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  c.JSON(http.StatusOK, company)
}

/* Unscoped で情報取得 */
func CompanyInfoUnscoped(c *gin.Context) {
  id := c.Param("id")
  company := models.Company{}
  if err := GormDB.Preload("Employees").Unscoped().First(&company, id).Error; err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  c.JSON(http.StatusOK, company)
}

会社を削除するコード

会社の削除に関して5つコードを示す。

  1. 会社を「論理削除」するコード
    • チュートリアルでよく見るコード
  2. 会社を「物理削除」するコード
    • 関連付けがあると削除できないはず
  3. 会社の関連付けのみ削除するコード
    • こちらの実行後なら「物理削除」できるはず?
  4. 会社の関連付けのみ削除するコード(Unscoped)
    • こちらの実行後なら「物理削除」できるはず
  5. 会社の関連付けデータもろとも削除するコード
    • DBから会社が消えると社員もDBから消える

こちらも、なるべくクエリに集中できるよう、共通処理はミドルウェア化しています。

/* Company 共通処理をラップするミドルウェア */
func CompanyMiddleware(exec func(id string) error, message string) gin.HandlerFunc {
  return func(c *gin.Context) {
    id := c.Param("id")
    if err := exec(id); err != nil {  // ここでクエリを実行
      c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
    }
    c.JSON(http.StatusOK, gin.H{"message": message})
  }
}

以下に、クエリを示します。

/* 1. 会社を「論理削除」するコード */
func CompanySoftDelete(id string) error {
  return GormDB.Delete(&models.Company{}, id).Error
}
/* 2. 会社を「物理削除」するコード */
func CompanyDelete(id string) error {
  return GormDB.Unscoped().Delete(&models.Company{}, id).Error
}

関連付けを削除する場合は、Association()を用いて、関連付けのあるカラムを指定します。
指定したカラムのすべての関連付けを削除したい場合、Clear()を用います。

/* 3. 会社の関連付けのみ削除するコード */
func CompanyAssociationDelete(c *gin.Context) {
  company := models.Company{}
  if err := GormDB.Unscoped().First(&company, id).Error; err != nil {
    return err
  }
  return GormDB.Model(&company).Association("Employees").Clear()
}
/* 4. 会社の関連付けのみ削除するコード(Unscoped) */
func CompanyAssociationUnscopedDelete(id string) error {
  company := models.Company{}
  if err := GormDB.Unscoped().First(&company, id).Error; err != nil {
    return err
  }
  return GormDB.Unscoped().Model(&company).Association("Employees").Clear()
}

関連データをまとめて削除する場合は、Select().Delete()を用いて、削除したい関連カラムを指定します。

/* 5. 会社の関連付けデータもろとも削除するコード */
func CompanyForceDelete(id string) error {
  company := models.Company{}
  if err := GormDB.Unscoped().First(&company, id).Error; err != nil {
    return err
  }
  return GormDB.Unscoped().Select("Employees").Delete(company).Error
}

論理削除データへのアクセス

  1. 会社の情報を取得(2つの方法を試す)
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png
  2. 会社を「論理削除」(CompanySoftDelete)
    - deleted_atNULLでないので論理削除されている
    image.png
  3. 会社情報を取得(2つの挙動の違いを確認)
    - Unscopedなしであれば、やはりレコードを取得できない(期待通り)
    - Unscopedありであれば、レコードを取得できる(レコード復活時に必要)
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png

また上の結果を見ると、論理削除されているUser(Bさん)の情報は、期待通り取得されていない。

関連付けのみ削除

  1. 会社の情報を取得(2つの方法を試す)
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png
  2. 会社を「物理削除」してみる(CompanyDelete)
    - やはり、関連付けがあるため削除できない
    image.png
  3. 会社の関連付け(Association)を削除 (CompanyAssociationDelete)
    - 会社情報をみると、関連付けが空になっている
    image.png
    いわゆる、リストラってやつかな?
  4. 会社を「物理削除」してみる(CompanyDelete)
    - できない!?
    image.png

理由は単純で、論利削除済みのデータに対して、Clear()が働いてくれていないからです。
なので、データベースを除くと、論理削除されていないUserはcompany_idNULLになっているが、されていたBさんは、まだ会社に所属しています。
image.png
1人だけ、リストラ回避してましたね。

次の方法で、これを解消しています。

関連付けのみ削除(Unscoped)

  1. 会社の情報を取得(2つの方法を試す)
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png
  2. 会社を「物理削除」してみる(CompanyDelete)
    - ここはやはり、関連付けがあるため削除できない
    image.png
  3. 会社の関連付け(Association)を削除(Unscoped) (CompanyAssociationUnscopedDelete)
    - 会社情報をみると、関連付けが空になっている
    - データベースの方を確認しても、company_idが全員NULLになっている
    image.png
    image.png
    全員リストラです。
  4. 会社を削除
    - これで本当に関連付けがないので「物理削除」できる(CompanyDelete)
Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
image.png image.png

image.png

一応次のために見せておくが、この処理の場合Userのレコードは残っている。

image.png

関連付けされたデータもろとも削除

  1. 会社の情報を取得(2つの方法を試す)
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png
  2. 会社を「物理削除」してみる(CompanyDelete)
    - ここはやはり、関連付けがあるため削除できない
    image.png
  3. 会社の関連付け(Association)もろとも(社員すらも)削除 (CompanyForceDelete)
    - 関連していたUserもろとも消える
    Unscopedなし(CompanyInfo) Unscopedあり(CompanyInfoUnscoped)
    image.png image.png

image.png
ユーザーもろとも消える。
image.png

いわゆる夜逃げってやつかな?

まとめ

  • gorm.DeletedAtを使えば論理削除が実装される
    • (論理)削除するときはDelete()を使用
    • 物理削除するときはUnscoped()を併用
  • DBクエリ結果を受け取るために独自実装した構造体では、gorm.DeletedAtの恩恵がない
    • クエリに.Where("deleted_at IS NULL")を付与するか
    • 独自実装した構造体にDeletedAt gorm.DeletedAtフィールドを追加
  • 論理削除されたデータを見るときはUnscoped()を使用
  • 関連付けのあるデータは物理削除できない
    • 関連付けのみを削除するときはAssociation()Clear()を使用
    • 論理削除済みデータの関連付けも削除したいときはUnscoped()も併用
    • 関連付けデータもろとも一掃するときは、UnscopedSelect()Delete()を使用

おわりに

今回の検証を通して、gorm.DeletedAtDelete関連の挙動が何となくわかってきた気がします。

データを消す処理を実装する際の、一助になればと願っています。

それでは:wave:

参考

9
2
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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?