【Go/GORM】データベース操作の基本：Model()とUpdates()の使い方


こんにちは！フリーランスエンジニアのこたろうです。
GORMを使ったデータベース操作の基本について、学びで得た知見を共有します。

## なぜGORMを使うの？

データベースを操作する方法は大きく分けて2つあります：

1. 生のSQLを書く方法
```sql
-- 生のSQL
UPDATE users 
SET name = 'John', age = 25, updated_at = CURRENT_TIMESTAMP 
WHERE id = 1;
```

2. ORMを使う方法（今回はGORM）
```go
db.Model(&user).Updates(map[string]interface{}{
    "name": "John",
    "age":  25,
})
```

生のSQLを書く場合の問題点：
- SQLインジェクションの危険性
- タイプミスが起きやすい
- カラム名の変更時に全SQLを修正する必要がある
- プログラムとSQLが分離している

GORMを使うメリット：
- SQLインジェクションの心配なし
- コンパイル時にエラーを検出可能
- モデルの変更が容易
- Goのコードとして書ける

## Model()メソッドの詳しい解説

### Model()とは？
`Model()`は、どのテーブルのどのレコードを操作するかを指定するメソッドです。

### 基本的な使い方
```go
// ユーザーのモデル定義
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()の動作の詳細
```go
// 例：ユーザーの年齢を更新
user := User{ID: 1}
db.Model(&user).Update("age", 25)
```

この1行で行われていること：
1. `Model(&user)`でusersテーブルを特定
   - 構造体名からテーブル名を推測（User → users）
   - IDフィールドから対象レコードを特定

2. プライマリーキーの扱い
   - IDフィールドが自動的にWHERE句の条件になる
   - `WHERE id = 1`が自動生成される

3. 更新時のタイムスタンプ
   - `UpdatedAt`フィールドがある場合、自動的に現在時刻が設定される

## Updates()メソッドの詳細解説

### 更新方法の種類

1. 構造体を使う方法：
```go
user := User{
    Name: "John",    // 更新したい
    Age:  25,        // 更新したい
    Email: "",       // 空文字は無視される！
}
db.Model(&User{ID: 1}).Updates(user)
```

2. mapを使う方法：
```go
db.Model(&User{ID: 1}).Updates(map[string]interface{}{
    "name": "John",  // 更新される
    "age":  25,      // 更新される
    "email": "",     // 空文字でも更新される！
})
```

### それぞれの特徴

構造体を使う場合：
- メリット
  - タイプセーフ（コンパイル時にエラーを検出）
  - IDEの補完が効く
- デメリット
  - zero値（0や空文字）は更新されない
  - 全フィールドが更新対象になる

mapを使う場合：
- メリット
  - 必要なフィールドだけ更新可能
  - zero値でも更新可能
  - 動的に更新フィールドを決定可能
- デメリット
  - タイプセーフではない
  - キーのタイプミスに気づきにくい

### 実践的なUpdates()の使用例

1. フォームからの更新処理：
```go
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
}
```

2. 条件付き一括更新：
```go
// 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()とは？
テスト時に「値の存在は確認するが、具体的な値は問わない」場合に使用する特別な値です。

### 使用が推奨されるケース
1. タイムスタンプフィールド（常に変化する値）
2. UUIDやランダムな値
3. 具体的な値が重要でない場合

### 実践的なテストコード
```go
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())
}
```

このテストコードの解説：
1. `ExpectExec`でSQLパターンを指定
2. `WithArgs`で引数の検証ルールを設定
3. `sqlmock.AnyArg()`で「任意の値」を許容
4. `WillReturnResult`で期待する実行結果を設定
5. `ExpectationsWereMet`で全ての期待値が満たされたか確認

## GORMを使う際のベストプラクティス

1. **モデルの定義**
   - 適切なタグを使用
   - 必要なフィールドを明確に
   - 命名規則の統一

2. **更新操作**
   - 可能な限り構造体を使用
   - 動的更新が必要な場合のみmapを使用
   - 更新結果の確認を忘れずに

3. **テスト**
   - sqlmockを活用
   - 適切な期待値の設定
   - エッジケースの考慮

## まとめ
- `Model()`でテーブルと対象レコードを特定
- `Updates()`で効率的な更新処理
- `map[string]interface{}`で柔軟な更新
- テストでは`sqlmock.AnyArg()`を活用
- 適切な使い分けで保守性の高いコードに

これらの機能を理解し使いこなすことで、より堅牢なデータベース処理を実装できます。