【gorm】データの登録・更新・論理削除時にユーザIDを自動で登録・更新する

# はじめに

GORMを通して登録・更新・論理削除すると、登録日時・更新日時・削除日時を自動的に登録・更新してくれます。

一方、今開発中のシステムでは登録・更新・削除を実施したユーザのIDも保持する仕様としているため、日時と同様に自動で登録・更新してくれると便利だなと思っていたのでやってみました。

# 方針

ユーザ情報は構造体に持たせておく形とします。 
以下の例でいうと、``RequestUser``にユーザ情報を入れた状態で登録・更新・削除を実施すると、自動的に``CreateUserID``・``UpdateUserID``・``DeleteUserID``にユーザIDがセットされ、登録・更新・削除されるようにします。

```go
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には登録・更新・削除の前後にコールバックを挟むことができるため、登録・更新・削除前にコールバックを使用して実装します。

# やり方

まずはコールバックを登録するところから。登録・更新・削除前にコールバックを実行したいので以下の通りになります。

```go
// 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型のフィールドがある

```go

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が実行されるのであまり褒められたものではありませんが…)
以下実装例です。

```go
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型のフィールドがある

なお、更新の実装は[実際に論理削除を行っているソース](https://github.com/jinzhu/gorm/blob/v1.9.1/callback_delete.go)を参考にしました。

ゴリ押しになってしまいましたが、これで削除時に``DeleteUserID``を自動で更新することもできました。

# 終わりに

これで、何も考えずに ``RequestUser``にデータを入れておけば、登録・更新・削除時にうまいことユーザIDを更新してくれるようになりました。楽チンです。