【Go初学】gormでjoinしてgroup byした総countを取得する
概要
gormでjoinしてcountを取得する処理を記述する際、何パターンもあり何が正しいのかよく分からなくなったので失敗例と成功例を記述する。
前提
上記のような関係のテーブルと幾つかのレコードが存在する。
SQLとしてはあまり意味がない例になるが、タスクをユーザーID毎にまとめ、その総件数を取得する。
gorm.DBの接続処理は以下のように行った。
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func main() {
DB, err := connectDB()
if err != nil {
panic("failed to connect database")
}
if err := testGorm(DB); err != nil {
fmt.Errorf(err.Error())
}
}
func connectDB() (*gorm.DB, error) {
user := "testuser"
pass := "testpass"
protocol := "tcp(127.0.0.1:23306)"
dbName := "test"
path := fmt.Sprintf("%s:%s@%s/%s?charset=utf8&parseTime=true&loc=Local",
user, pass, protocol, dbName)
return gorm.Open(mysql.Open(path))
}
検証
gorm.DBオブジェクトで実行する際、Debug()を付加することで実行したいSQL文を見られるため、どこが間違っているか確認できる。
// 各エンティティ
type (
Task struct {
Id string
Name string
UserId string
gorm.Model
}
User struct {
Id string
Name string
Email string
gorm.Model
}
)
// 一部実行処理で用いるSQLやテーブル名の定数定義
const (
// Distinctを利用するSQL
countDistinctSql = `
select count(distinct(u.id))
from tasks t
inner join users u on u.id = t.user_id;`
// Group Byを利用するSQL
countGroupSql = `
select
count(*)
from(
select u.id
from tasks t
inner join users u on u.id = t.user_id
group by u.id
) ids;`
usersTableName = "users"
tasksTableName = "tasks"
)
func testGorm(db *gorm.DB) error {
var count int64
// テーブル自動作成(先に実行してテーブル作成するとCreate文書かなくて済む)
//db.AutoMigrate(&User{})
//db.AutoMigrate(&Task{})
// 以下 Debug() で実行SQL文を確認
//
// ▼失敗例
//
// joinが機能していない
db.Debug().Table("tasks").
Select("count(*)").
Joins("users").
Group("users.id").
Scan(&count)
// -> SELECT count(*) FROM `tasks` users GROUP BY `users`.`id`;
// Count()を使うようにしたが、上記同様にjoinが機能していない
db.Debug().Table("tasks").
Joins("users").
Group("users.id").
Count(&count)
// -> SELECT count(*) FROM `tasks` users GROUP BY `users`.`id`;
// Groupでusers.id毎にまとめられた件数の和が取得できていない
// (1行目のusers.idの件数を取得している)
db.Debug().Table("tasks").
Select("count(*)").
Joins("inner join users on users.id = tasks.user_id").
Group("users.id").
Scan(&count)
// -> SELECT count(*) FROM `tasks` inner join users on users.id = tasks.user_id GROUP BY `users`.`id`;
//
// ▼成功例
//
// GroupもCountも機能し、正しく取得できる
db.Debug().Table("tasks").
Joins("inner join users on users.id = tasks.user_id").
Group("users.id").
Count(&count)
// -> SELECT count(*) FROM `tasks` inner join users on users.id = tasks.user_id GROUP BY `users`.`id`;
/*
リポジトリ内で何度もテーブル名を記述する際は以下のように定数を利用するのもよい
(可読性考えるとC#のように "inner join {usersTableName} on {usersTableName}.id 〜〜" と記述したい)
Joins(fmt.Sprintf("inner join %s on %s.id = %s.user_id", usersTableName, usersTableName, tasksTableName)).
*/
// 略称も機能する
db.Debug().Table(fmt.Sprintf("%s T", tasksTableName)).
Joins(fmt.Sprintf("inner join %s U on U.id = T.user_id", usersTableName)).
Group("U.id").
Count(&count)
// Distinctが機能し、正しく取得できる
db.Debug().Table("tasks").
Distinct("users.id").
Joins("join users").
Count(&count)
// -> SELECT COUNT(DISTINCT(`users`.`id`)) FROM `tasks` join users;
// Distinctを利用したSQL文が機能し、正しく取得できる
db.Debug().Raw(countDistinctSql).Scan(&count)
// ->
// select count(distinct(u.id))
// from tasks t
// inner join users u on u.id = t.user_id;
// Groupを利用したSQL文が機能し、正しく取得できる
db.Debug().Raw(countGroupSql).Scan(&count)
// ->
// select
// count(*)
// from(
// select u.id
// from tasks t
// inner join users u on u.id = t.user_id
// group by u.id
// ) ids;
return nil
}
備考
何となくGoLandでER図を書いてみたくてPlantUMLを利用するにあたりPlantUML Integrationのプラグインを利用した。その際「Can not find Graphviz」とモジュール不足エラーになったためこちらを参考にインストールを行った。インストールでもエラーになったため `brew doctor` 診断の指示に従いディレクトリの作成やアクセス権限の設定などを行った。
あとがき
DBオブジェクトのメソッドを用いて操作する際、全てDBオブジェクトをリターンしてメソッドチェーンで書けるメリットの反面、SELECTを使うのか、Scanを使うのか、Countを使うのか、Rawを使うのか、選択肢が閉じていないため悩めてしまう。
Gormメソッドを使うか生SQLを書くかも悩みどころで、Gormメソッドの組み合わせによる記述はIDEのフォントカラーの効果もあると思うが形が把握しやすい気がする。将来的にMySQLから別のものに移行するなどある場合は全部Gormで書いておいて構文の差を吸収するなどもメリットだろうか。ただ複雑になると学習コストが増えると思われる。gorm一強であればまだよいが、別のライブラリ、また別のライブラリ、となるとコストも無視できない。
Rawで生SQLを記述する方はシンプルに書きやすい。SequelProなどで記述したものも当然そのまま利用出来る。が、ただの文字列のためIDEのチェック機構が働かずタイプミスのリスクがある。
個人的には、単純なSQL操作はGormメソッドを使い、サブクエリ使うなどになってきたらRawで記述するのがよい印象(もしくはgorm調べるの面倒なのでRawに全振り)。