🏃‍♂️

【Go】GORM v1.30 ジェネリクスAPIで型安全にトランザクション

に公開

はじめに

前回は、GORM v1.30で追加されたジェネリクスAPIで型安全にRaw SQLを使う方法を見ていきました。

今回は、同じくジェネリクスAPIを使って、型安全にトランザクションを張る方法を見ていきます!

この記事でわかること

  • 従来のトランザクションの実装方法
  • 型安全にトランザクションを扱う方法

前提

ここでは、ユーザーを管理するusersテーブルと、ユーザーのプロフィールを管理するprofilesというテーブルが存在するものとします。

-- \d users
  Column  |  Type  | Collation | Nullable |              Default              
----------+--------+-----------+----------+-----------------------------------
 id       | bigint |           | not null | nextval('users_id_seq'::regclass)
 name     | text   |           | not null | 
 birthday | date   |           | not null | 
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)

-- \d profiles

 Column  |  Type  | Collation | Nullable |               Default                
---------+--------+-----------+----------+--------------------------------------
 id      | bigint |           | not null | nextval('profiles_id_seq'::regclass)
 user_id | bigint |           | not null | 
 address | text   |           | not null | 
 phone   | text   |           | not null | 
Indexes:
    "profiles_pkey" PRIMARY KEY, btree (id)

ジェネリクスAPIを用いたトランザクションの張り方

ここでは、ジェネリクスAPIを用いたトランザクションの張り方を、従来の実装と比較しながら見ていきましょう。

従来では、以下のように実装することができます。

main.go
// トランザクションに関わる部分のみ抽出
type User struct {
    ID       int
    Name     string
    Birthday time.Time
}

type Profile struct {
    ID      int
    UserID  int
    Address string
    Phone   string
}

birthday, _ := time.Parse("2006-01-02", "2025-10-07")
user := User{Name: "Tomoya", Birthday: birthday}

err = db.Transaction(func(tx *gorm.DB) error {
    // ユーザー情報を登録
    if err := tx.Create(&user).Error; err != nil {
        return err
    }

    // プロフィール情報を登録
    profile := Profile{UserID: user.ID, Address: "Example", Phone: "09012345678"}
    if err := tx.Create(&profile).Error; err != nil {
        return err
    }
    return nil
})
if err != nil {
    log.Fatal(err)
}

実行すると、以下のような出力結果となります。

ターミナル
# ユーザ情報を登録
[1.396ms] [rows:1] INSERT INTO "users" ("name","birthday") VALUES ('Tomoya','2025-10-07 00:00:00') RETURNING "id"

# プロフィール情報を登録
[0.942ms] [rows:1] INSERT INTO "profiles" ("user_id","address","phone") VALUES (1,'Example','09012345678') RETURNING "id"

また、データベースへの登録状況は以下のとおりです。

登録状況
-- select * from users;
 id |  name  |        birthday        
----+--------+------------------------
  1 | Tomoya | 2025-10-07 00:00:00+00
(1 row)

-- select * from profiles;
 id | user_id | address |    phone    
----+---------+---------+-------------
  1 |       1 | Example | 09012345678
(1 row)

トランザクションなので、一方で処理が失敗すると両テーブルで登録されません。

ターミナル
# ユーザー情報を登録
INSERT INTO "users" ("name","birthday") VALUES ('Alice','2025-10-08 00:00:00') RETURNING "id"

# プロフィール情報を登録
ERROR: duplicate key value violates unique constraint "profiles_pkey" (SQLSTATE 23505)
[0.667ms] [rows:0] INSERT INTO "profiles" ("user_id","address","phone","id") VALUES (2,'Example','09012345678',1) RETURNING "id"
2025/10/06 22:09:49 ERROR: duplicate key value violates unique constraint "profiles_pkey" (SQLSTATE 23505)
exit status 1
登録状況
-- select * from users;
 id |  name  |        birthday        
----+--------+------------------------
  1 | Tomoya | 2025-10-07 00:00:00+00
(1 row)

-- select * from profiles;
 id | user_id | address |    phone    
----+---------+---------+-------------
  1 |       1 | Example | 09012345678
(1 row)

一方、ジェネリクスAPIを使うと、以下のように実装することができます。

main.go
// トランザクションに関わる部分のみ抽出
type User struct {
    ID       int
    Name     string
    Birthday time.Time
}

type Profile struct {
    ID      int
    UserID  int
    Address string
    Phone   string
}

ctx := context.Background()

birthday, _ := time.Parse("2006-01-02", "2025-10-08")
user := User{Name: "Alice", Birthday: birthday}

err = db.Transaction(func(tx *gorm.DB) error {
    // ユーザー情報を登録
    if err := gorm.G[User](tx).Create(ctx, &user); err != nil {
        return err
    }

    // プロフィール情報を登録
    profile := Profile{UserID: user.ID, Address: "Example", Phone: "09012345678"}
    if err := gorm.G[Profile](tx).Create(ctx, &profile); err != nil {
        return err
    }
    return nil
})
if err != nil {
    log.Fatal(err)
}

実行すると、以下のような出力結果となります。

ターミナル
# ユーザー情報を登録
[0.836ms] [rows:1] INSERT INTO "users" ("name","birthday") VALUES ('Alice','2025-10-08 00:00:00') RETURNING "id"

# プロフィール情報を登録
[0.636ms] [rows:1] INSERT INTO "profiles" ("user_id","address","phone") VALUES (2,'Example','09012345678') RETURNING "id"

また、データベースへの登録状況は以下のとおりです。

登録状況
-- select * from users;
 id |  name  |        birthday        
----+--------+------------------------
  1 | Tomoya | 2025-10-07 00:00:00+00
  2 | Alice  | 2025-10-08 00:00:00+00
(2 row)

-- select * from profiles;
 id | user_id | address |    phone    
----+---------+---------+-------------
  1 |       1 | Example | 09012345678
  2 |       2 | Example | 09012345678
(2 row)

繰り返しですが、トランザクションなので、一方で処理が失敗すると両テーブルで登録されません。

ターミナル
# ユーザー情報を登録
[0.721ms] [rows:1] INSERT INTO "users" ("name","birthday") VALUES ('Bob','2025-10-09 00:00:00') RETURNING "id"

# プロフィール情報を登録
ERROR: duplicate key value violates unique constraint "profiles_pkey" (SQLSTATE 23505)
[1.903ms] [rows:0] INSERT INTO "profiles" ("user_id","address","phone","id") VALUES (3,'Example','09012345678',2) RETURNING "id"
2025/10/06 22:21:37 ERROR: duplicate key value violates unique constraint "profiles_pkey" (SQLSTATE 23505)
exit status 1
登録状況
-- select * from users;
 id |  name  |        birthday        
----+--------+------------------------
  1 | Tomoya | 2025-10-07 00:00:00+00
  2 | Alice  | 2025-10-08 00:00:00+00
(2 row)

-- select * from profiles;
 id | user_id | address |    phone    
----+---------+---------+-------------
  1 |       1 | Example | 09012345678
  2 |       2 | Example | 09012345678
(2 row)

ここまで、従来のトランザクションとジェネリクスAPIを用いたトランザクションを見ていきました。
比較してみると、ジェネリクスAPIを使ってもトランザクションの張り方そのものは変わらないことがわかります。
違いがあるのは、トランザクション内での「登録」や「更新」などの処理の書き方です。

ジェネリクスAPIによるCRUD処理の基本については、以前の記事で詳しく解説していますので、ぜひそちらもあわせてご覧ください。

まとめ

今回は、GORM v1.30で追加されたジェネリクスAPIを使って、型安全にトランザクションを張る方法を見ていきました。

実際に試してみると、ジェネリクスAPIを使ってもトランザクションの流れ自体は従来と変わりません。
それでも、型安全に扱えることでコード補完が効きやすくなり、より安心して開発できるようになります。

次回は「ジェネリクスAPIを使ったUpsert」について見ていきます!

参考

Discussion