【Go】GORM v1.30 ジェネリクスAPIで型安全にJoins・Preload
はじめに
前回は、GORM v1.30で追加されたジェネリクスAPIで型安全にレコードを削除する方法について見ていきました。
今回は、同じくジェネリクスAPIを使って、型安全にJoinメソッドやPreloadメソッドを使う方法を見ていきます!
この記事でわかること
- Joinsメソッドの使い方
- Preloadメソッドの使い方
- JoinsメソッドとPreloadメソッドのレスポンスオブジェクトの違い
前提
前提として、以下のようなテーブルとデータが存在するものとします。
SELECT * FROM users;
id | name
----+--------
1 | tomoya
2 | Alice
3 | Bob
4 | Cindy
SELECT * FROM posts;
# tomoyaとAliceは1つずつ、Cindyは2つ投稿があり、Bobは一つも投稿がない
id | user_id | title | body
----+---------+---------+-------
1 | 1 | テスト1 | 本文1
2 | 2 | テスト2 | 本文2
3 | 4 | テスト3 | 本文3
4 | 4 | テスト4 | 本文4
ジェネリクスAPIをJoin
ここでは、ジェネリクスAPIを使ったレコードのJoin
メソッドの使い方を、従来の実装と比較しながら見ていきましょう。
// 結果を格納するためのカスタム構造体
type UserPost struct {
UserID uint
Name string
PostID uint
Title string
Body string
}
var userPosts []UserPost
db.Table("users").
Select("users.id as user_id, users.name, posts.id as post_id, posts.title, posts.body").
Joins("inner join posts on posts.user_id = users.id").
Scan(&userPosts)
for _, userPost := range userPosts {
fmt.Printf("Name:%s, PostID:%d, Title:%s, Body:%s\n", userPost.Name, userPost.PostID, userPost.Title, userPost.Body)
}
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[1.209ms] [rows:4] SELECT users.id as user_id, users.name, posts.id as post_id, posts.title, posts.body FROM "users" inner join posts on posts.user_id = users.id
# 標準出力結果
Name:tomoya, PostID:1, Title:テスト1, Body:本文1
Name:Alice, PostID:2, Title:テスト2, Body:本文2
Name:Cindy, PostID:3, Title:テスト3, Body:本文3
Name:Cindy, PostID:4, Title:テスト4, Body:本文4
Joins
メソッドでINNER JOINを指定しているので、両方のテーブルに存在するデータのみ抽出するようになっています。
Bobは投稿が0件なので抽出されていないのが確認できます。
一方、ジェネリクスAPIを使うと、以下のように実装することができます。
type User struct {
ID uint
Name string
Posts Post // スライスにしない
}
type Post struct {
ID uint
UserID uint
Title string
Body string
}
users, err := gorm.G[User](db).Joins(clause.Has("Posts"), nil).Find(ctx)
if err != nil {
log.Fatal(err)
}
for _, user := range users {
fmt.Printf("Name:%s, PostID:%d, Title:%s, Body:%s\n", user.Name, user.Posts.ID, user.Posts.Title, user.Posts.Body)
}
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[0.518ms] [rows:4] SELECT "users"."id","users"."name","Posts"."id" AS "Posts__id","Posts"."user_id" AS "Posts__user_id","Posts"."title" AS "Posts__title","Posts"."body" AS "Posts__body" FROM "users" INNER JOIN "posts" "Posts" ON "users"."id" = "Posts"."user_id"
# 標準出力結果
Name:tomoya, PostID:1, Title:テスト1, Body:本文1
Name:Alice, PostID:2, Title:テスト2, Body:本文2
Name:Cindy, PostID:3, Title:テスト3, Body:本文3
Name:Cindy, PostID:4, Title:テスト4, Body:本文4
clauseパッケージのHas
メソッドでは、デフォルトでINNER JOINをするように定義されています。
従来の場合同様、Bobは投稿が0件なので抽出されていないのが確認できます。
ジェネリクスAPIをPreload
Preloadについて、公式ドキュメントには以下のように記載されています。
GORM allows eager loading relations in other SQL with Preload
Preload
メソッドを使用することで、リレーション関係にあるテーブルのデータを事前に読み込むことが可能です。
ここでは、ジェネリクスAPIを使ったレコードのPreload
メソッドの使い方を、従来の実装と比較しながら見ていきましょう。
従来のPreload
type User struct {
ID uint
Name string
Posts []Post // UserとPostは1対多の関係にある
}
type Post struct {
ID uint
UserID uint
Title string
Body string
}
var users []User
db.Preload("Posts").Find(&users)
for _, user := range users {
fmt.Printf("User:%s\n", user.Name)
for _, post := range user.Posts {
fmt.Printf("PostID:%d, Title:%s, Body:%s\n", post.ID, post.Title, post.Body)
}
}
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[1.734ms] [rows:4] SELECT * FROM "users"
[0.663ms] [rows:4] SELECT * FROM "posts" WHERE "posts"."user_id" IN (1,2,3,4)
# 標準出力結果
User:tomoya
PostID:1, Title:テスト1, Body:本文1
User:Alice
PostID:2, Title:テスト2, Body:本文2
User:Bob
User:Cindy
PostID:3, Title:テスト3, Body:本文3
PostID:4, Title:テスト4, Body:本文4
ここでは、
- usersテーブルから全データを抽出
-
postsテーブルからuser_idが1〜4に当てはまるデータを抽出
この2つのクエリが実行されていることがわかります。
一方、ジェネリクスAPIを使うと、以下のように実装することができます。
ctx := context.Background()
users, err := gorm.G[User](db).Preload("Posts", nil).Find(ctx)
if err != nil {
log.Fatal(err)
}
for _, user := range users {
fmt.Printf("Name:%s", user.Name)
for _, post := range user.Posts {
fmt.Printf("PostID:%d, Title:%s, Body:%s\n", post.ID, post.Title, post.Body)
}
}
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[0.614ms] [rows:4] SELECT * FROM "users"
[0.266ms] [rows:4] SELECT * FROM "posts" WHERE "posts"."user_id" IN (1,2,3,4)
# 標準出力結果
Name:tomoya
PostID:1, Title:テスト1, Body:本文1
Name:Alice
PostID:2, Title:テスト2, Body:本文2
Name:Bob
Name:Cindy
PostID:3, Title:テスト3, Body:本文3
PostID:4, Title:テスト4, Body:本文4
こちらも従来の実装と同じく、以下の2つのクエリが実行されていることがわかります。
- usersテーブルから全データを抽出
- postsテーブルからuser_idが1〜4に当てはまるデータを抽出
また、Preloadを使う上で条件設定を行いたい場合、従来は以下のように実装することができます。
// postsテーブルのidを降順で並べるよう設定
db.Preload("Posts", func(db *gorm.DB) *gorm.DB {
return db.Order("id DESC")
}).Find(&users)
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[1.512ms] [rows:4] SELECT * FROM "users"
[0.573ms] [rows:4] SELECT * FROM "posts" WHERE "posts"."user_id" IN (1,2,3,4) ORDER BY id DESC
# 標準出力結果
User:tomoya
PostID:1, Title:テスト1, Body:本文1
User:Alice
PostID:2, Title:テスト2, Body:本文2
User:Bob
User:Cindy
## PostIDが降順になっている
PostID:4, Title:テスト4, Body:本文4
PostID:3, Title:テスト3, Body:本文3
一方、ジェネリクスAPIでは以下のように実装することができます。
users, err := gorm.G[User](db).Preload("Posts", func(db gorm.PreloadBuilder) error {
db.Order("id DESC")
return nil
}).Find(ctx)
実行すると、以下のような出力結果となります。
# 実行されるクエリ
[0.813ms] [rows:4] SELECT * FROM "users"
[0.337ms] [rows:4] SELECT * FROM "posts" WHERE "posts"."user_id" IN (1,2,3,4) ORDER BY id DESC
# 標準出力結果
Name:tomoya
PostID:1, Title:テスト1, Body:本文1
Name:Alice
PostID:2, Title:テスト2, Body:本文2
Name:Bob
Name:Cindy
## PostIDが降順になっている
PostID:4, Title:テスト4, Body:本文4
PostID:3, Title:テスト3, Body:本文3
JoinsメソッドとPreloadメソッドのオブジェクトの違い
ここまでジェネリクスAPIにおけるJoins
メソッドとPreload
メソッドの使い方を、従来の実装と比較しながら見ていきました。
最後に、Joins
メソッドとPreload
メソッドそれぞれの抽出データ構造を確認します。
Joinsメソッドの場合
Joinsメソッドの場合、関連するテーブルを結合し抽出するのでフラットな構造になります。
[
{
"ID": 1,
"Name": "tomoya",
"Posts": {
"ID": 1,
"UserID": 1,
"Title": "テスト1",
"Body": "本文1"
}
},
{
"ID": 2,
"Name": "Alice",
"Posts": {
"ID": 2,
"UserID": 2,
"Title": "テスト2",
"Body": "本文2"
}
},
{
"ID": 4,
"Name": "Cindy",
"Posts": {
"ID": 3,
"UserID": 4,
"Title": "テスト3",
"Body": "本文3"
}
},
{
"ID": 4,
"Name": "Cindy",
"Posts": {
"ID": 4,
"UserID": 4,
"Title": "テスト4",
"Body": "本文4"
}
}
]
Preloadメソッドの場合
一方で、Preload
メソッドはリレーション関係にあるデータを、それぞれクエリを発行して抽出するのでネスト構造になります。
[
{
"ID": 1,
"Name": "tomoya",
"Posts": [
{
"ID": 1,
"UserID": 1,
"Title": "テスト1",
"Body": "本文1"
}
]
},
{
"ID": 2,
"Name": "Alice",
"Posts": [
{
"ID": 2,
"UserID": 2,
"Title": "テスト2",
"Body": "本文2"
}
]
},
{
"ID": 3,
"Name": "Bob",
"Posts": []
},
{
"ID": 4,
"Name": "Cindy",
"Posts": [
{
"ID": 4,
"UserID": 4,
"Title": "テスト4",
"Body": "本文4"
},
{
"ID": 3,
"UserID": 4,
"Title": "テスト3",
"Body": "本文3"
}
]
}
]
まとめ
今回は、GORM v1.30で追加されたジェネリクスAPIを使って、型安全にJoinメソッドとPreloadメソッドを使う方法を見ていきました!
以下、JoinsメソッドとPreloadメソッドの特徴や利用シーンをまとめております。
項目 | Joinsメソッド | Preloadメソッド |
---|---|---|
クエリ発行回数 | 1回(結合して取得) | 複数回(親と子で別々に発行) |
データ構造 | フラット(UserとPostの組み合わせごとに行が増える) | ネスト(User の中にPostsのスライスが入る) |
抽出されるデータ | 両方のテーブルに存在するデータのみ(今回はINNER JOINのため、Postがないユーザーは除外) | User全件を対象にし、Postがない場合は空スライスとして返却 |
利用シーン | 一覧表示や集計処理など、結合結果を直接扱いたい場合 | ユーザーとその投稿一覧のように、リレーションを保持したまま扱いたい場合 |
ここまで見てきたように、Joins
メソッドとPreload
メソッドはどちらも「関連データを取得する」という目的を持ちながらも、クエリの発行方法や返却されるデータ構造が大きく異なります。
用途に応じて適切に使い分けることが重要です。
次回は、「ジェネリクスAPIのRaw SQL」について見ていきます!
Discussion