UNIQUE制約
DBの特定の列に重複した値が入力されないように制限を掛けることができます。
複数列に1つのUNIQUE制約を掛けることも可能です。(複合UNIQUE制約)
論理削除 (Soft Delete)
DB上の値を実際には削除せずに、deleted_at
などの「削除フラグ」を立てることで擬似的に削除を実現することを論理削除といいます。
UNIQUE制約と論理削除の両立
例えば、username
カラムに対してUNIQUE制約を掛ける場合、論理削除と併用して使うと、ユーザを削除してもDB上では実際に削除されず残っているので、制約に引っかかってしまい、新しく同じusername
のユーザを作成できなくなってしまいます。
deleted_at
とusername
に複合UNIQUE制約を掛けても、deleted_at
が NULL
の場合、NULL ≠ NULL と見なされ、制約に引っかからず思ったように動いてくれません。
この場合は、deleted_at
などの削除フラグにdefault値を入れ、username
と deleted_at
に複合UNIQUE制約を掛けることで解決することができます。
GormでのUNIQUE制約と論理削除の両立
Gormは、Find
や Scan
をする際に、自動で WHERE deleted_at IS NULL
を追加してくれるため、論理削除をあまり意識しなくとも利用することができます。
しかし、UNIQUE制約と論理削除を両立させたい場合、WHERE deleted_at = default value
の条件で値を取得してほしいので、これはあまり嬉しくありません。
DBにUNIQUE制約を書けずに、コード側で対処することもできますが、どこかで破綻しそうで好ましくない気がします。
論理削除をやめて物理削除にすれば解決しますが、削除されたデータを追いたい場合もあるので、これも今回の場合は好ましくありません。
Gormのカスタマイズ
GormでUNIQUE制約と論理削除の両立は厳しいのか...? と思いましたが、カスタマイズするための機能が存在しました。(Write Plugins)
deleted_at
に default値として、'1970-01-01 00:00:01'
を与え、
Find
や Scan
が実行されたときに走るCallbackの前に、WHERE deleted_at = '1970-01-01 00:00:01'
などの独自のロジックを追加して、通常のWHERE deleted_at IS NULL
が走らないようにすれば、UNIQUE制約と論理削除を両立させられそうです。
以下、それを実装したものです。(一部省略しています)
package main
import (
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
)
type User struct {
Username string `json:"username" gorm:"unique_index:unique_username"`
DeletedAt time.Time `json:"deleted_at" gorm:"default:'1970-01-01 00:00:01';unique_index:unique_username"`
}
var DB *gorm.DB
func main() {
DB = newDB
DB.Callback().Query().Before("gorm:query").Register("new_before_query_callback", newBeforeQueryFunction)
}
func newBeforeQueryFunction(scope *gorm.Scope) {
var (
quotedTableName = scope.QuotedTableName()
deletedAtField, hasDeletedAtField = scope.FieldByName("DeletedAt")
defaultTimeStamp = "1970-01-01 00:00:01"
)
if !scope.Search.Unscoped && hasDeletedAtField {
scope.Search.Unscoped = true
sql := fmt.Sprintf("%v.%v = '%v'", quotedTableName, scope.Quote(deletedAtField.DBName), defaultTimeStamp)
scope.Search.Where(sql)
}
}
後は、通常どおり使うことができます。
最後に
間違っている箇所や、もっといい方法などがあれば是非教えてください。