はじめに
GORMを通して登録・更新・論理削除すると、登録日時・更新日時・削除日時を自動的に登録・更新してくれます。
一方、今開発中のシステムでは登録・更新・削除を実施したユーザのIDも保持する仕様としているため、日時と同様に自動で登録・更新してくれると便利だなと思っていたのでやってみました。
方針
ユーザ情報は構造体に持たせておく形とします。
以下の例でいうと、RequestUser
にユーザ情報を入れた状態で登録・更新・削除を実施すると、自動的にCreateUserID
・UpdateUserID
・DeleteUserID
にユーザIDがセットされ、登録・更新・削除されるようにします。
type User struct {
ID int
// ... 何かしらのデータ
}
type TestData struct {
ID int
// ... 何かしらのデータ
CreatedAt time.Time
CreateUserID int
UpdatedAt time.Time
UpdateUserID int
DeletedAt mysql.NullTime
DeleteUserID sql.NullInt64
RequestUser User
}
また、GORMには登録・更新・削除の前後にコールバックを挟むことができるため、登録・更新・削除前にコールバックを使用して実装します。
やり方
まずはコールバックを登録するところから。登録・更新・削除前にコールバックを実行したいので以下の通りになります。
// ConnectDB DBに接続する
func ConnectDB(databaseName string) *gorm.DB {
db, err = gorm.Open("mysql", "...")
db.Callback().Create().Before("gorm:create").Register("create_user", createUserCallback)
db.Callback().Update().Before("gorm:update").Register("update_user", updateUserCallback)
db.Callback().Delete().Before("gorm:delete").Register("delete_user", deleteUserCallback)
return db
}
次に、登録・更新のコールバックをみていきます。
下記の条件を満たす場合に、ID
フィールドからデータを取得し、CreateUserID
、UpdateUserID
にセットしています。
- 更新対象の構造体に
RequestUser
の構造体がある - 構造体の中に
ID
というint型のフィールドがある
func createUserCallback(scope *gorm.Scope) {
u, ok := scope.FieldByName("RequestUser")
if ok && u.Field.Type().Kind() == reflect.Struct {
v := u.Field.FieldByName("ID")
if v.IsValid() && v.Type().Kind() == reflect.Int {
userID := v.Int()
if createUserIDField, ok := scope.FieldByName("CreateUserID"); ok {
createUserIDField.Set(userID)
}
if updateUserIDField, ok := scope.FieldByName("UpdateUserID"); ok {
updateUserIDField.Set(userID)
}
}
}
}
func updateUserCallback(scope *gorm.Scope) {
u, ok := scope.FieldByName("RequestUser")
if ok && u.Field.Type().Kind() == reflect.Struct {
v := u.Field.FieldByName("ID")
if v.IsValid() && v.Type().Kind() == reflect.Int {
userID := v.Int()
if updateUserIDField, ok := scope.FieldByName("UpdateUserID"); ok {
updateUserIDField.Set(userID)
}
}
}
}
問題は削除の場合です。
登録・更新の場合と違って、構造体内のDeleteUserID
を事前に変更しても論理削除のUpdateには反映されないため、論理削除のSQLとは別のSQLを実行します。(2回SQLが実行されるのであまり褒められたものではありませんが…)
以下実装例です。
func deleteUserCallback(scope *gorm.Scope) {
if !scope.HasError() && !scope.Search.Unscoped {
u, ok := scope.FieldByName("RequestUser")
if ok && u.Field.Type().Kind() == reflect.Struct {
v := u.Field.FieldByName("ID")
if v.IsValid() && v.Type().Kind() == reflect.Int {
userID := v.Int()
if deleteUserIDField, ok := scope.FieldByName("DeleteUserID"); ok {
var extraOption string
if str, ok := scope.Get("gorm:delete_option"); ok {
extraOption = fmt.Sprint(str)
}
scope.Raw(fmt.Sprintf(
"UPDATE %v SET %v=%v%v%v",
scope.QuotedTableName(),
scope.Quote(deleteUserIDField.DBName),
scope.AddToVars(userID),
addExtraSpaceIfExist(scope.CombinedConditionSql()),
addExtraSpaceIfExist(extraOption),
)).Exec()
scope.SQLVars = []interface{}{}
}
}
}
}
}
下記の条件を満たす場合に、ID
フィールドからデータを取得し、DeleteUserID
にセットし、更新を実行しています。
- 更新対象の構造体に
RequestUser
の構造体がある - 構造体の中にIDというint型のフィールドがある
なお、更新の実装は実際に論理削除を行っているソースを参考にしました。
ゴリ押しになってしまいましたが、これで削除時にDeleteUserID
を自動で更新することもできました。
終わりに
これで、何も考えずに RequestUser
にデータを入れておけば、登録・更新・削除時にうまいことユーザIDを更新してくれるようになりました。楽チンです。