こんにちは!フリーランスエンジニアのこたろうです。
GORMを使ったデータベース操作の基本について、学びで得た知見を共有します。
なぜGORMを使うの?
データベースを操作する方法は大きく分けて2つあります:
- 生のSQLを書く方法
-- 生のSQL
UPDATE users
SET name = 'John', age = 25, updated_at = CURRENT_TIMESTAMP
WHERE id = 1;
- ORMを使う方法(今回はGORM)
db.Model(&user).Updates(map[string]interface{}{
"name": "John",
"age": 25,
})
生のSQLを書く場合の問題点:
- SQLインジェクションの危険性
- タイプミスが起きやすい
- カラム名の変更時に全SQLを修正する必要がある
- プログラムとSQLが分離している
GORMを使うメリット:
- SQLインジェクションの心配なし
- コンパイル時にエラーを検出可能
- モデルの変更が容易
- Goのコードとして書ける
Model()メソッドの詳しい解説
Model()とは?
Model()
は、どのテーブルのどのレコードを操作するかを指定するメソッドです。
基本的な使い方
// ユーザーのモデル定義
type User struct {
ID uint `gorm:"primarykey"`
Name string
Email string
Age int
CreatedAt time.Time
UpdatedAt time.Time
}
// 使用例1:特定のレコードを指定
db.Model(&User{ID: 1}) // ID=1のユーザーを操作
// 使用例2:テーブル全体を指定
db.Model(&User{}) // usersテーブル全体を操作
Model()の動作の詳細
// 例:ユーザーの年齢を更新
user := User{ID: 1}
db.Model(&user).Update("age", 25)
この1行で行われていること:
-
Model(&user)
でusersテーブルを特定- 構造体名からテーブル名を推測(User → users)
- IDフィールドから対象レコードを特定
-
プライマリーキーの扱い
- IDフィールドが自動的にWHERE句の条件になる
-
WHERE id = 1
が自動生成される
-
更新時のタイムスタンプ
-
UpdatedAt
フィールドがある場合、自動的に現在時刻が設定される
-
Updates()メソッドの詳細解説
更新方法の種類
- 構造体を使う方法:
user := User{
Name: "John", // 更新したい
Age: 25, // 更新したい
Email: "", // 空文字は無視される!
}
db.Model(&User{ID: 1}).Updates(user)
- mapを使う方法:
db.Model(&User{ID: 1}).Updates(map[string]interface{}{
"name": "John", // 更新される
"age": 25, // 更新される
"email": "", // 空文字でも更新される!
})
それぞれの特徴
構造体を使う場合:
- メリット
- タイプセーフ(コンパイル時にエラーを検出)
- IDEの補完が効く
- デメリット
- zero値(0や空文字)は更新されない
- 全フィールドが更新対象になる
mapを使う場合:
- メリット
- 必要なフィールドだけ更新可能
- zero値でも更新可能
- 動的に更新フィールドを決定可能
- デメリット
- タイプセーフではない
- キーのタイプミスに気づきにくい
実践的なUpdates()の使用例
- フォームからの更新処理:
func updateUserFromForm(db *gorm.DB, userID uint, form UserForm) error {
updates := map[string]interface{}{}
// フォームの値が設定されている場合のみ更新
if form.Name != "" {
updates["name"] = form.Name
}
if form.Age > 0 {
updates["age"] = form.Age
}
if form.Email != "" {
updates["email"] = form.Email
}
// 更新実行
result := db.Model(&User{ID: userID}).Updates(updates)
if result.Error != nil {
return fmt.Errorf("更新に失敗: %v", result.Error)
}
// 更新件数をチェック
if result.RowsAffected == 0 {
return fmt.Errorf("ユーザーID %d は存在しません", userID)
}
return nil
}
- 条件付き一括更新:
// 20歳未満のユーザーのステータスを一括更新
result := db.Model(&User{}).
Where("age < ?", 20).
Updates(map[string]interface{}{
"status": "junior",
"rank": 1,
})
// 更新件数を確認
fmt.Printf("%d件のレコードを更新しました\n", result.RowsAffected)
テストにおけるsqlmock.AnyArg()の詳細
sqlmock.AnyArg()とは?
テスト時に「値の存在は確認するが、具体的な値は問わない」場合に使用する特別な値です。
使用が推奨されるケース
- タイムスタンプフィールド(常に変化する値)
- UUIDやランダムな値
- 具体的な値が重要でない場合
実践的なテストコード
func TestUpdateUser(t *testing.T) {
// モックDBのセットアップ
db, mock := setupMockDB(t)
// テストケース
user := &User{
ID: 1,
Name: "John",
Age: 25,
}
// SQLの期待値を設定
mock.ExpectExec(`UPDATE "users" SET`).
// name, age, updated_atの順で引数を検証
WithArgs(
user.Name, // nameは具体的な値をチェック
user.Age, // ageも具体的な値をチェック
sqlmock.AnyArg(), // updated_atは任意の値を許容
).
WillReturnResult(sqlmock.NewResult(1, 1))
// テスト実行
err := updateUser(db, user)
// 検証
assert.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
このテストコードの解説:
-
ExpectExec
でSQLパターンを指定 -
WithArgs
で引数の検証ルールを設定 -
sqlmock.AnyArg()
で「任意の値」を許容 -
WillReturnResult
で期待する実行結果を設定 -
ExpectationsWereMet
で全ての期待値が満たされたか確認
GORMを使う際のベストプラクティス
-
モデルの定義
- 適切なタグを使用
- 必要なフィールドを明確に
- 命名規則の統一
-
更新操作
- 可能な限り構造体を使用
- 動的更新が必要な場合のみmapを使用
- 更新結果の確認を忘れずに
-
テスト
- sqlmockを活用
- 適切な期待値の設定
- エッジケースの考慮
まとめ
-
Model()
でテーブルと対象レコードを特定 -
Updates()
で効率的な更新処理 -
map[string]interface{}
で柔軟な更新 - テストでは
sqlmock.AnyArg()
を活用 - 適切な使い分けで保守性の高いコードに
これらの機能を理解し使いこなすことで、より堅牢なデータベース処理を実装できます。