🐭
GoのBunでFunctional Option patternで少しだけクエリを書きやすくした
GoのORMライブラリBunを使用する際、複雑なリレーションを持つモデルを扱うと、 以下のように同じようなリレーションを持つクエリを繰り返し書くことがあります。
// 例:特定のチームメンバーをチーム・メンバー情報・チームの作成者を一緒に持ってくるクエリ
func (d *MemberDriver) FindTeamMembers(ctx context.Context, exec bun.IDB, memberID string) ([]*models.TeamMember, error) {
var ms []*models.TeamMember
err := exec.NewSelect().
Model(&ms).
Relation("Team").
Relation("Team.Detail").
Relation("Team.CreatedBy").
Relation("Team.CreatedBy.Profile").
Relation("Team.CreatedBy.Role").
Relation("Team.CreatedBy.Account").
Relation("Member").
Relation("Member.Profile").
Relation("Member.Role").
Relation("Member.Account").
Where("team_members.member_id = ?", memberID).
Scan(ctx)
if err != nil {
return nil, err
}
return ms, nil
}
今回はFunctional Option patternを利用して、より柔軟にクエリを構築する方法を紹介します。
実装
まず、以下のようなヘルパー関数とオプション型を定義します:
package models
import "github.com/uptrace/bun"
type QueryOption func(*bun.SelectQuery)
// WithPrefix returns a QueryOption that applies the given prefix to each relation in the query.
func WithPrefix(prefix string, relations ...string) QueryOption {
return func(q *bun.SelectQuery) {
p := func(suffix string) string {
if prefix == "" {
return suffix
}
return prefix + "." + suffix
}
for _, relation := range relations {
q.Relation(p(relation))
}
}
}
func WithNoPrefix(relations ...string) QueryOption {
return WithPrefix("", relations...)
}
// GenBaseQuery constructs a base query with the given options applied.
func GenBaseQuery(exec bun.IDB, m any, opts ...QueryOption) *bun.SelectQuery {
q := exec.NewSelect().Model(m)
for _, opt := range opts {
opt(q)
}
return q
}
この実装を使用すると、先ほどのようなクエリを以下のように簡潔に書くことができます:
// Memberに関するリレーションを分離
var MembersRelations = []string{
"Profile",
"Role",
"Account",
}
func (d *MemberDriver) FindTeamMembers(ctx context.Context, exec bun.IDB, memberID string) ([]*models.TeamMember, error) {
var ms []*models.TeamMember
err := models.GenBaseQuery(exec, &ms,
models.WithNoPrefix([]string{"Team", "Team.Detail", "Team.CreatedBy", "Member"}...),
models.WithPrefix("Member", MembersRelations...),
models.WithPrefix("Team.CreatedBy", MembersRelations...),
).
Where("team_members.member_id = ?").
Scan(ctx)
if err != nil {
return nil, err
}
return ms, nil
}
このように、WithPrefixとを使用することで、異なる深さのリレーションを簡単に指定できます。
またリレーションのつけ忘れもなくなり、共通のリレーション設定を別の場所で定義し、再利用することができます。
ただし、複雑なリレーションを持つクエリを簡潔に書くのに役立ちますが、過度に使用すると逆に可読性が低下する可能性があるので、クエリがある程度長くなってきて、他の箇所でも利用できる場合に使用することをお勧めします
Discussion