🏃

【Go】GORM v1.30 ジェネリクスAPIで型安全にUpsert

に公開

はじめに

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

今回は、同じくジェネリクスAPIを使って、型安全にUpsertする方法を見ていきます!

この記事でわかること

  • Upsertの概要
  • PostgreSQLでUpsertをする方法
  • GORMのジェネリクスAPIを用いたUpsertの実装方法

前提

ここでは、ユーザーを管理するusersテーブルが存在し、以下のようなデータが登録されているものとします。

usersテーブル
-- select * from users;

 id |  name  |  birthday  
----+--------+------------
  1 | Tomoya | 2025-10-12
  2 | Alice  | 2025-10-13
  3 | Bob    | 2025-10-14
(3 rows)

PostgreSQLにおけるUpsert

Upsertとは、Update(更新)またはInsert(挿入)のどちらかを自動的に選ぶ操作のことを指します。
既存データの有無によって、以下のように操作の振り分けを行うことができます。

  • 既にデータが存在している場合→Update
  • まだデータが存在してない場合→Insert

わざわざInsert文とUpdate文を作らなくても、Upsert用の構文を使えば一回で更新と挿入の振り分けができます。重複したデータの作成を防ぎつつ、効率的に操作することができるのが特徴です。

さて、PostgreSQLでUpsertをする方法は2つあります。

  1. ON CONFLICT句
  2. MERGE文

今回はその中でも、ON CONFLICT句 について見ていきましょう!

ON CONFLICT句

PostgreSQLでは、Insert文のオプションとして ON CONFLICT句 を用いることでUpsertを行うことができます。(2016年1月7日リリースされたPostgreSQL 9.5.0で導入されました。)

構文は以下のとおりです。

構文
INSERT INTO テーブル名(列名1, 列名2, ...)
VALUES (挿入データ1, 挿入データ2, ...)
ON CONFLICT (列名) または ON CONFLICT ON CONSTRAINT 制約名
DO NOTHING または DO UPDATE SET 更新する列名 = EXCLUDED.更新する列名

ここで使われているオプション・キーワードには、以下の意味や役割があります。

キーワード 意味・役割
ON CONFLICT 一意制約またはインデックスに違反したときの処理を指定
制約がある列名 衝突を検出する列(通常は主キーやUNIQUE列)
ON CONSTRAINT 制約名 特定の制約名を直接指定して衝突を判定
DO NOTHING 何もしない(挿入をスキップ)
DO UPDATE 衝突時に更新する
EXCLUDED 挿入しようとした新しいデータを参照する仮想テーブル名

それでは、実際にON CONFLICT句を用いてUpsertを実行してみましょう!

既存データが存在する場合

既にデータが存在する場合、ON CONFLICT句を用いるとUpdateが適用されます。

既存データが存在する場合
-- nameを変更
INSERT INTO users (id, name, birthday)
VALUES (1, 'Tomoyan', '2025-10-12')
ON CONFLICT (id)
DO UPDATE SET name = EXCLUDED.name;

これを実行すると、以下のようにnameが更新されていることがわかります。

実行結果
-- INSERT文を実行
INSERT 0 1

-- SELECT * FROM users;
 id |  name  |  birthday  
----+--------+------------
  1 | Tomoyan | 2025-10-12
  2 | Alice  | 2025-10-13
  3 | Bob    | 2025-10-14
(3 rows)

また、DO NOTHINGを使うと以下のようになります。

DO NOTHINGを使う
-- nameを変更
INSERT INTO users (id, name, birthday)
VALUES (1, 'Tomoya', '2025-10-12')
ON CONFLICT (id)
DO NOTHING;

-- INSERT文を実行
INSERT 0 0

-- SELECT * FROM users;
id |  name  |  birthday  
----+--------+------------
  1 | Tomoyan | 2025-10-12 -- 更新されていない
  2 | Alice  | 2025-10-13
  3 | Bob    | 2025-10-14
(3 rows)

既にデータが登録されているものに対し DO NOTHINGを指定したことで、処理がスキップされていることがわかります。

既存データが存在しない場合

データが存在しない場合、ON CONFLICT句を用いるとInsertが適用されます。

既存データが存在しない場合
INSERT INTO users (id, name, birthday)
VALUES (4, 'Cindy', '2025-10-15')
ON CONFLICT ON CONSTRAINT users_pkey
DO UPDATE SET name = EXCLUDED.name;

これを実行すると、以下のように新たにデータが登録されていることがわかります。

実行結果
-- INSERT文を実行
INSERT 0 1

-- SELECT * FROM users;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
  2 | Alice   | 2025-10-13
  3 | Bob     | 2025-10-14
  4 | Cindy   | 2025-10-15
(4 rows)

また、DO NOTHINGを使った場合でも、新たにデータが登録されます。

DO NOTHINGを使う
INSERT INTO users (id, name, birthday)
VALUES (5, 'David', '2025-10-16')
ON CONFLICT ON CONSTRAINT users_pkey
DO NOTHING;

-- INSERT文を実行
INSERT 0 1

-- SELECT * FROM users;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
  2 | Alice   | 2025-10-13
  3 | Bob     | 2025-10-14
  4 | Cindy   | 2025-10-15
  5 | David   | 2025-10-16
(5 rows)

ジェネリクスAPIを用いたUpsert

ここからはGORMのジェネリクスAPIを用いたUpsertの方法を、従来の実装と比較しながら見ていきましょう!

既存データが存在する場合

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

main.go
// Upsertに関わる部分のみ抽出
type User struct {
    ID       int       `gorm:"primaryKey"`
    Name     string    `gorm:"not null"`
    Birthday time.Time `gorm:"type:date"`
}

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

// nameを変更
db.Clauses(clause.OnConflict{
    Columns: []clause.Column{{Name: "id"}},
    DoUpdates: clause.Assignments(map[string]interface{}{"name": "Tomoya"}),
}).Create(&user)

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

実行結果
-- 実行結果
[1.205ms] [rows:1] INSERT INTO "users" ("name","birthday","id") VALUES ('Tomoya','2025-10-12 00:00:00',1) ON CONFLICT ("id") DO UPDATE SET "name"='Tomoya' RETURNING "id"

-- SELECT * FROM users WHERE id = 1;
-- nameが変更されている
 id |  name  |  birthday  
----+--------+------------
  1 | Tomoya | 2025-10-12
(1 row)

また、DO NOTHINGを使いたい場合は以下のように実装することができます。

main.go
birthday, _ := time.Parse("2006-01-02", "2025-10-12")
user := User{ID: 1, Name: "Tomoyan", Birthday: birthday}

// nameを変更
db.Clauses(clause.OnConflict{
    Columns: []clause.Column{{Name: "id"}},
    DoNothing: true,
}).Create(&user)

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

実行結果
-- 実行結果
[1.418ms] [rows:0] INSERT INTO "users" ("name","birthday","id") VALUES ('Tomoyan','2025-10-12 00:00:00',1) ON CONFLICT ("id") DO NOTHING RETURNING "id"

-- SELECT * FROM users WHERE id = 1;
-- nameが変更されていない
 id |  name  |  birthday  
----+--------+------------
  1 | Tomoya | 2025-10-12

一方で、ジェネリクスAPIを用いてUpsertを用いる場合は以下のように実装することができます。

main.go
// Upsertに関わる部分のみ抽出
type User struct {
    ID       int       `gorm:"primaryKey"`
    Name     string    `gorm:"not null"`
    Birthday time.Time `gorm:"type:date"`
}

ctx := context.Background()

birthday, _ := time.Parse("2006-01-02", "2025-10-12")
user := User{ID: 1, Name: "Tomoyan", Birthday: birthday}

// nameを変更
err = gorm.G[User](db, clause.OnConflict{
    Columns: []clause.Column{{Name: "id"}},
    DoUpdates: clause.Assignments(map[string]interface{}{"name": "Tomoyan"}),
}).Create(ctx, &user)
if err != nil {
    log.Fatal(err)
}

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

実行結果
-- 実行結果
[2.171ms] [rows:1] INSERT INTO "users" ("name","birthday","id") VALUES ('Tomoyan','2025-10-12 00:00:00',1) ON CONFLICT ("id") DO UPDATE SET "name"='Tomoyan' RETURNING "id"

-- SELECT * FROM users WHERE id = 1;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
(1 row)

また、DO NOTHINGを使いたい場合は以下のように実装することができます。

main.go
// Upsertに関わる部分のみ抽出
ctx := context.Background()
	
birthday, _ := time.Parse("2006-01-02", "2025-10-12")
user := User{ID: 1, Name: "Tomoya", Birthday: birthday}

err = gorm.G[User](db, clause.OnConflict{
    Columns: []clause.Column{{Name: "id"}},
    DoNothing: true,
}).Create(ctx, &user)
if err != nil {
    log.Fatal(err)
}

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

実行結果
-- 実行結果
[1.075ms] [rows:0] INSERT INTO "users" ("name","birthday","id") VALUES ('Tomoya','2025-10-12 00:00:00',1) ON CONFLICT ("id") DO NOTHING RETURNING "id"

-- SELECT * FROM users WHERE id = 1;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
(1 row)

既存データが存在しない場合

既存データが存在しない場合のUpsertは、既存データが存在する場合と構文自体は変わりません。
従来の実装方法は以下のとおりです。

main.go
// Upsertに関わる部分のみ抽出
type User struct {
    ID       int       `gorm:"primaryKey"`
    Name     string    `gorm:"not null"`
    Birthday time.Time `gorm:"type:date"`
}

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

// 新しいデータを登録
db.Clauses(clause.OnConflict{
    OnConstraint: "users_pkey",
    DoUpdates: clause.Assignments(map[string]interface{}{"name": "Fabi", "birthday": "2025-10-17"}),
}).Create(&user)

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

実行結果
-- 実行結果
[2.050ms] [rows:1] INSERT INTO "users" ("name","birthday") VALUES ('Fabi','2025-10-17 00:00:00') ON CONFLICT ON CONSTRAINT users_pkey DO UPDATE SET "birthday"='2025-10-17',"name"='Fabi' RETURNING "id"

-- SELECT * FROM users;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
  2 | Alice   | 2025-10-13
  3 | Bob     | 2025-10-14
  4 | David   | 2025-10-15
  5 | Eva     | 2025-10-16
  6 | Fabi    | 2025-10-17 -- データが追加されている

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

main.go
// Upsertに関わる部分のみ抽出
ctx := context.Background()

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

// 新しいデータを登録
err = gorm.G[User](db, clause.OnConflict{
    OnConstraint: "users_pkey",
    DoNothing: true,
}).Create(ctx, &user)
if err != nil {
    log.Fatal(err)
}

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

実行結果
-- 実行結果
[7.296ms] [rows:1] INSERT INTO "users" ("name","birthday") VALUES ('Gabriel','2025-10-18 00:00:00') ON CONFLICT ON CONSTRAINT users_pkey DO NOTHING RETURNING "id"

-- SELECT * from users;
 id |  name   |  birthday  
----+---------+------------
  1 | Tomoyan | 2025-10-12
  2 | Alice   | 2025-10-13
  3 | Bob     | 2025-10-14
  4 | David   | 2025-10-15
  5 | Eva     | 2025-10-16
  6 | Fabi    | 2025-10-17
  7 | Gabriel | 2025-10-18

まとめ

今回は、GORM v1.30で追加されたジェネリクスAPIを使って、Upsertする方法を見ていきました。

clauseパッケージを使うことで、簡単にUpsertを実装することができます。
OnConflict構造体のフィールドを活用することで、柔軟にクエリを作成できるので、様々な活用方法をとることができます。

ここまで、GORM v1.30で追加されたジェネリクスAPIを使ったさまざまな操作を見ていきました。
次回以降は特にシリーズとかは設けず、実務で気になったことやためになったことを記事にする感じでやっていこうと思います!

参考

Discussion