entでINNER JOINを行う
entでのINNER JOINの実装方法がいまいち正確に載っていなかったので、頑張って調べてみました。
ER図
スキーマ定義
package schema
// Fields of the UploadedContent.
// Edges of the UploadedContent.
func (UploadedContent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("contents", Content.Type),
}
}
package schema
// Fields of the Content.
// Edges of the Content.
func (Content) Edges() []ent.Edge {
return []ent.Edge{
edge.From("uploaded_content", UploadedContent.Type).
Field("uploaded_content_filename").
Ref("contents").
Unique().
Required(),
edge.To("content_movie_metadata", ContentMovieMetadata.Type).
StorageKey(edge.Column("filename")).
Unique(),
}
}
package schema
// Fields of the ContentMovieMetadata.
// Edges of the ContentMovieMetadata.
func (ContentMovieMetadata) Edges() []ent.Edge {
return []ent.Edge{
edge.From("content", Content.Type).
Ref("content_movie_metadata").
Required().
Unique(),
}
}
スキーマを定義したら、以下のコマンドでコードを生成します。
$ go generate ./ent
INNER JOINを実行する
The sql/modifier option lets add custom SQL modifiers to the builders and mutate the statements before they are executed.
This option can be added to a project using the --feature sql/modifier flag.
"sql/modifier オプションを使用すると、ビルダーにカスタム SQL 修飾子を追加し、実行前にステートメントを変更することができます。
このオプションは --feature sql/modifier フラグを使用してプロジェクトに追加できます。"
よく分かりませんが、INNER JOINを実行するために必要な関数を使えるようにするために、以下のコマンドを実行します。
$ go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/modifier ./ent/schema
package main
import (
"context"
"database/sql"
"log"
"entgo.io/ent/dialect"
entsql "entgo.io/ent/dialect/sql"
"github.com/ent-join/ent"
"github.com/ent-join/ent/content"
"github.com/ent-join/ent/uploadedcontent"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
Query()
}
// Open new connection
func Open(databaseUrl string) *ent.Client {
db, err := sql.Open("pgx", databaseUrl)
if err != nil {
log.Fatal(err)
}
// Create an ent.Driver from `db`.
drv := entsql.OpenDB(dialect.Postgres, db)
return ent.NewClient(ent.Driver(drv))
}
func Query() {
client := Open("postgresql://postgres:postgres@127.0.0.1/postgres")
defer client.Close()
// migrate
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatal(err)
}
// INNER JOIN
cnt, err := client.Debug().UploadedContent.Query().
Where(uploadedcontent.ID("sample1.mp4")).
Modify(func(s *entsql.Selector) {
t := entsql.Table(uploadedcontent.ContentsTable)
s.Join(t).
On(
s.C(uploadedcontent.FieldID),
t.C(content.FieldUploadedContentFilename),
)
}).
Count(ctx)
if err != nil {
log.Fatal(err)
}
log.Println(cnt)
}
以下のように、Debug()メソッドを使用することで、実行されたSQLをログに出力することができます。
client.Debug().UploadedContent.Query()
% go run main.go
2024/01/15 16:37:56 driver.Query: query=SELECT COUNT("uploaded_content"."filename") FROM "uploaded_content" JOIN "content" AS "t1" ON "uploaded_content"."filename" = "t1"."uploaded_content_filename" WHERE "uploaded_content"."filename" = $1 args=[sample1.mp4]
2024/01/15 16:37:56 3
ちなみに、以下のコードでも、同様の結果を得ることができます。
// サブクエリ
count, err := client.Debug().UploadedContent.Query().
Where(uploadedcontent.ID("sample1.mp4")).
QueryContents().
Count(ctx)
if err != nil {
log.Fatal(err)
}
log.Println(count)
しかしこちらは、サブクエリを使ってJOINしていることがわかります。
% go run main.go
2024/01/15 22:06:54 driver.Query: query=SELECT COUNT(DISTINCT "content"."filename") FROM "content" JOIN (SELECT "uploaded_content"."filename" FROM "uploaded_content" WHERE "uploaded_content"."filename" = $1) AS "t1" ON "content"."uploaded_content_filename" = "t1"."filename" args=[sample1.mp4]
2024/01/15 22:06:54 3
ベンチマークテスト
せっかくなので、ベンチマークでスコアを比較してみましょう。
INNER JOIN
% go test -count 5 -benchmem -bench . 2>&1 | tee old.log
goos: darwin
goarch: arm64
pkg: github.com/ent-join
BenchmarkQuery-8 43 27041099 ns/op 126797 B/op 1928 allocs/op
BenchmarkQuery-8 44 26996943 ns/op 127110 B/op 1928 allocs/op
BenchmarkQuery-8 44 26523704 ns/op 126839 B/op 1927 allocs/op
BenchmarkQuery-8 45 26957560 ns/op 126908 B/op 1927 allocs/op
BenchmarkQuery-8 44 26818100 ns/op 126443 B/op 1926 allocs/op
PASS
ok github.com/ent-join 6.879s
Sub Query
% go test -count 5 -benchmem -bench . 2>&1 | tee new.log
goos: darwin
goarch: arm64
pkg: github.com/ent-join
BenchmarkQuery-8 45 27307084 ns/op 130155 B/op 1966 allocs/op
BenchmarkQuery-8 45 27357605 ns/op 129911 B/op 1964 allocs/op
BenchmarkQuery-8 45 26745164 ns/op 129761 B/op 1965 allocs/op
BenchmarkQuery-8 42 26974969 ns/op 129763 B/op 1965 allocs/op
BenchmarkQuery-8 48 27060554 ns/op 129865 B/op 1965 allocs/op
PASS
ok github.com/ent-join 7.040s
比較
2つのベンチマークをbefore/afterとして比較するためにbenchstatをインストールします。
$ go install golang.org/x/perf/cmd/benchstat@latest
% benchstat old.log new.log
goos: darwin
goarch: arm64
pkg: github.com/ent-join
│ old.log │ new.log │
│ sec/op │ sec/op vs base │
Query-8 26.96m ± ∞ ¹ 27.06m ± ∞ ¹ ~ (p=0.222 n=5)
¹ need >= 6 samples for confidence interval at level 0.95
│ old.log │ new.log │
│ B/op │ B/op vs base │
Query-8 123.9Ki ± ∞ ¹ 126.8Ki ± ∞ ¹ +2.39% (p=0.008 n=5)
¹ need >= 6 samples for confidence interval at level 0.95
│ old.log │ new.log │
│ allocs/op │ allocs/op vs base │
Query-8 1.927k ± ∞ ¹ 1.965k ± ∞ ¹ +1.97% (p=0.008 n=5)
¹ need >= 6 samples for confidence interval at level 0.95
1opあたりの実行時間は+0.1ms、メモリアロケーションサイズが+2.39%、メモリアロケーション回数が+1.97%と、サブクエリを使用する方がほんの少しだけ大きいことがわかりました。
結果的には、どちらを使ってもあまり変わらないということですね。
3つのテーブルを結合する
最後に、3つのテーブルを結合したいと思います。
count, err := client.Debug().UploadedContent.Query().
Where(uploadedcontent.ID("sample1.mp4")).
Modify(func(s *entsql.Selector) {
t := entsql.Table(uploadedcontent.ContentsTable)
s.Join(t).
On(
s.C(uploadedcontent.FieldID),
t.C(content.FieldUploadedContentFilename),
)
u := entsql.Table(content.ContentMovieMetadataTable)
s.Join(u).
On(
t.C(content.FieldID),
u.C(contentmoviemetadata.FieldID),
)
}).
Count(ctx)
if err != nil {
log.Fatal(err)
}
log.Println(count)
t
はcontent
テーブルを、u
はcontent_movie_metadata
テーブルを参照しています。
% go run main.go
2024/01/16 14:19:03 driver.Query: query=SELECT COUNT("uploaded_content"."filename") FROM "uploaded_content" JOIN "content" AS "t1" ON "uploaded_content"."filename" = "t1"."uploaded_content_filename" JOIN "content_movie_metadata" AS "t2" ON "t1"."filename" = "t2"."filename" WHERE "uploaded_content"."filename" = $1 args=[sample1.mp4]
2024/01/16 14:19:03 3
参考資料
Discussion