LoginSignup
19
12

More than 3 years have passed since last update.

GormのDELETEで誤爆しないために

Last updated at Posted at 2020-05-07

はじめに

Gormが本番テーブルの数億件のデータを消そうとした話 が盛り上がってますね。

ゼロ値に関していうとGoの仕様上structの0空文字が未定義(ゼロ値)なのか検索条件にゼロ値の値を設定しているのか判断出来ず
Gorm v1では取り敢えずゼロ値ならWhere句を生成しないという判断にしているため全件削除するような動きになっています。
sliceのlengthが0件の場合は流石にerrorにせいよと思う部分はありますが…

この件はGormのWhere句を生成する処理に関する話なのでUpdateの際にも当てはまります。
Updateの件は以前検証した記事があるのでそちらを参照してください。

Gorm v2のリリースを待つか、安全な別のlibraryを使用すれば良いのですが
とはいえGorm v1を使ってるアプリケーションのlibraryを切り替えるのは結構大きなリファクタになるので
Gorm v1使って安全なDeleteするためにどうすればいいのか検証していきます。

前提

本記事は以下の環境で検証しています。

  • MySQL: 5.7.12
  • gorm: v1.9.12
go.mod
    github.com/go-sql-driver/mysql v1.5.0
    github.com/jinzhu/gorm v1.9.12

結論

先に結論から言ってしまうと安全にDeleteをするには
条件に struct を使用しない
ようにしないといけません。それ以外は全部ダメです。

ダメな例1:slice 0件、ゼロ値を指定したstructを条件に指定する

まずは全件削除されてしまうパターンを検証。
検証の為に以下のようなテーブルを作成して
Debug()で実際に実行されるSQLを見てみましょう。

Num (int) Bool (bool) Str(varchar)
package main

import (
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

type Sample struct {
    Num  int64
    Bool bool
    Str  string
}

func main() {
    db, err := gorm.Open("mysql", "test:password@/sample")
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // Migrate the schema
    if err := db.AutoMigrate(&Sample{}).Error; err != nil {
        panic(err.Error())
    }

    // Delete - initialization struct
    fmt.Print("Delete - initialization struct")
    db.Debug().Delete(Sample{})

    // Delete - slice size 0
    fmt.Print("Delete - slice size 0")
    db.Debug().Delete([]*Sample{})

    // Delete - zero value
    fmt.Print("Delete - zero value")
    db.Debug().Delete(&Sample{Num: 0})      // int(0)
    db.Debug().Delete(&Sample{Bool: false}) // bool(false)
    db.Debug().Delete(&Sample{Str: ""})     // string("")
}
Delete - initialization struct
DELETE FROM `samples`    

Delete - slice size 0
DELETE FROM `samples`    

Delete - zero value
DELETE FROM `samples`    
DELETE FROM `samples`    
DELETE FROM `samples`    
  • 初期化structを渡すパターン
  • size 0 の sliceを渡すパターン
  • ゼロ値を渡すパターン

は全件削除されます。
例の記事は2番目の罠に引っかかって全件DELETEのSQLを投げてしまったパターンですね。

ダメな例2: struct の値を pointer で定義する

Updateの際は上手くいったパターンでstructの定義をpointerで指定する場合です。

package main

import (
    "fmt"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

type Sample struct {
    Num  *int64
    Bool *bool
    Str  *string
}

func main() {
    db, err := gorm.Open("mysql", "root:root@/sample")
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // Migrate the schema
    if err := db.AutoMigrate(&Sample{}).Error; err != nil {
        panic(err.Error())
    }

    // Delete - slice size 0
    fmt.Print("Delete - slice size 0")
    db.Debug().Delete([]*Sample{})

    // Delete - zero value
    fmt.Print("Delete - zero value")
    queryInt := int64(0)
    db.Debug().Delete(&Sample{Num: &queryInt})
    queryBool := false
    db.Debug().Delete(&Sample{Bool: &queryBool})
    queryString := ""
    db.Debug().Delete(&Sample{Str: &queryString})
}
Delete - slice size 0
DELETE FROM `samples`

Delete - zero value
DELETE FROM `samples`
DELETE FROM `samples`
DELETE FROM `samples`

slice size 0の場合はともかく
ゼロ値のWhere句生成はUpdateの時上手く動いたので正常に動作しそうですが
結果はWhere句が生成されずに正常に動作しませんでした。

ダメな例3: sql.Null typeを使用する

Go の database packageで定義されている sql.NullBool sql.NullString sql.NullInt64 を使う方法です。

package main

import (
    "database/sql"
    "fmt"

    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

type Sample struct {
    Num  sql.NullInt64
    Bool sql.NullBool
    Str  sql.NullString
}

func main() {
    db, err := gorm.Open("mysql", "root:root@/sample")
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // Migrate the schema
    if err := db.AutoMigrate(&Sample{}).Error; err != nil {
        panic(err.Error())
    }

    // Delete - slice size 0
    fmt.Print("Delete - slice size 0")
    db.Debug().Delete([]*Sample{})

    // Delete - zero value
    fmt.Print("Delete - zero value")
    db.Debug().Delete(&Sample{Num: sql.NullInt64{Int64: 0, Valid: true}})
    db.Debug().Delete(&Sample{Bool: sql.NullBool{Bool: false, Valid: true}})
    db.Debug().Delete(&Sample{Str: sql.NullString{String: "", Valid: true}})
}
Delete - slice size 0
DELETE FROM `samples`

Delete - zero value
DELETE FROM `samples`
DELETE FROM `samples`
DELETE FROM `samples`

pointerでうまく動作しなかったのでsql.Null typeでも駄目だろうなと思ってましたが案の定ダメでした。

結局、Deleteで誤爆しないためには解決策としては条件に struct を使用しない事しかなさそうです。

解決策: 条件に struct を使用しない

解決策として有効なものはDeleteのWhere句でstructを使わない事です。

package main

import (
    _ "github.com/go-sql-driver/mysql"
    "github.com/jinzhu/gorm"
)

type Sample struct {
    Num  int64
    Bool bool
    Str  string
}

func main() {
    db, err := gorm.Open("mysql", "root:root@/sample")
    if err != nil {
        panic("failed to connect database")
    }
    defer db.Close()

    // Migrate the schema
    if err := db.AutoMigrate(&Sample{}).Error; err != nil {
        panic(err.Error())
    }

    // Delete list
    fmt.Print("Delete - slice size 0")
    db.Debug().Delete(&Sample{}, "Num IN (?)", []int{})

    // Delete
    fmt.Print("Delete - zero value")
    db.Debug().Delete(&Sample{}, "Num = ?", 0)
    db.Debug().Delete(&Sample{}, "Bool = ?", false)
    db.Debug().Delete(&Sample{}, "Str = ?", "")
}
Delete - slice size 0
DELETE FROM `samples`  WHERE (Num IN (NULL))  

Delete - zero value
DELETE FROM `samples`  WHERE (Num = 0)  
DELETE FROM `samples`  WHERE (Bool = false)  
DELETE FROM `samples`  WHERE (Str = '')  

カラム名指定しないといけないのが割とめんどくさいですが全件DELETEされることはなくなります。
sliceのlength=0の場合、妙なquery投げてますがMySQLの場合errorにはなりませんでした。

感想

今回検証で初めて知ったこともありました。
Gormを使用する場合はWhere句の生成周りのテストケースをしっかり網羅する必要がありそうです。

例の記事では恐らくテストパターンが足りていなかったので本番のコードにGormに起因するバグを埋め込んでしまったのかなと思います。
既にGormを使っている方はテストでしっかりと動作を検証する事をお勧めします。

Gormと仲良くなりたいというタイトルで幾つか記事を書いていますが
新規に開発する場合はGormを使うという選択肢はないかなと個人的には思っていたりします。

19
12
1

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
19
12