🔍
GoのORMであるentを用いて検索機能を実装してみる
はじめに
Java開発者である私がGo言語にスキルチェンジするために小さいチームプロジェクトでアプリを作りながら学んだことを伝える記事になります。誤ったことがある場合コメントいただけると嬉しいです。
開発環境
- Go
- Fiber
- ent
- MySQL
何を作るのか
- 技術(
skills
, 複数可) - ポジション(
positions
, 複数可) - 進め方(
online
,offline
,all
) - 検索キーワード(検索欄に指定したキーワードでタイトルと内容検索可能)
- お気に入り、募集中はスコープアウト
四つの条件をつけて検索したい
リレーション
ゴール
検索機能の要件は以下になりそうです。
-
skills
を指定すれば関連づいている応募が検索できます -
skills
とpositions
を指定すれば両方の条件を満たす応募が検索できます -
skills
とpositions
とkeyword
を指定すれば三つの条件を満たす応募が検索できます -
keyword
はtitle
とcontent
どちらかがヒットある場合検索できます
実装
Controller
検索条件はクエリパラメターで受け取る想定です。
/recruitments?skills=go,java&positions=backend&keyword=ECサイト
では、Controllerから実装していきます。
検索条件として何も指定しなかった場合nil
ではなく空のstringスライス([]string
)になりますので注意です。
type RecruitmentController interface {
...
FindAll(c *fiber.Ctx) error
...
}
type recruitmentController struct {
service service.RecruitmentService
}
func NewRecruitmentController(service service.RecruitmentService) RecruitmentController {
return &recruitmentController{service: service}
}
...
func (r *recruitmentController) FindAll(c *fiber.Ctx) error {
queries := c.Queries()
keyword := queries["keyword"]
querySkill := queries["skills"]
queryPosition := queries["positions"]
var skills []string
if querySkill != "" {
skills = strings.Split(querySkill, ",")
}
var positions []string
if queryPosition != "" {
positions = strings.Split(queryPosition, ",")
}
recruitments, err := r.service.FindAll(keyword, skills, positions)
if err != nil {
return c.Status(http.StatusInternalServerError).
JSON(fiber.NewError(http.StatusInternalServerError, err.Error()))
}
return c.Status(http.StatusOK).JSON(recruitments)
}
...
Service
service レイヤではrepositoryから返り値をoutputにマッピングしているだけです。
type RecruitmentService interface {
...
FindAll(keyword string, skills []string, positions []string) ([]*domain.RecruitmentOutput, error)
...
}
type recruitmentService struct {
repo repository.RecruitmentRepository
skillRepo repository.SkillRepository
userRepo repository.AuthRepository
positionRepo repository.PositionRepository
}
func NewRecruitmentService(
repo repository.RecruitmentRepository,
skillRepo repository.SkillRepository,
userRepo repository.AuthRepository,
positionRepo repository.PositionRepository,
) RecruitmentService {
return &recruitmentService{
repo: repo,
skillRepo: skillRepo,
userRepo: userRepo,
positionRepo: positionRepo,
}
}
...
func (r *recruitmentService) FindAll(keyword string, skills []string, positions []string) ([]*domain.RecruitmentOutput, error) {
recruitments, err := r.repo.FindAll(keyword, skills, positions)
if err != nil {
return nil, err
}
var outputs []*domain.RecruitmentOutput
for _, re := range recruitments {
var skills []string
for _, s := range re.Edges.Skills {
skills = append(skills, s.Name)
}
var positions []string
for _, p := range re.Edges.Positions {
positions = append(positions, p.Name)
}
output := &domain.RecruitmentOutput{
ID: re.ID,
UserID: re.UsersID,
Positions: positions,
Skills: skills,
Proceed: re.Proceed,
Category: re.Category,
Contact: re.Contact,
Deadline: re.DeadLine,
Period: re.Period,
Members: re.Members,
Title: re.Title,
Content: re.Content,
}
outputs = append(outputs, output)
}
return outputs, nil
}
...
Repository
Controller レイヤで説明した検索条件が空のスライスになる場合があるので
ここでは空のスライスの場合は検索条件として含まれないようにしております。
各条件ごとのpredicateを作り最後に結合する形です。
JavaのQueryDSLとかの書き方とすごく似ていますね。
type RecruitmentRepository interface {
...
FindAll(keyword string, skills []string, positions []string) ([]*ent.Recruitment, error)
...
}
type recruitmentRepository struct {
orm *ent.Client
}
func NewRecruitmentRepository(orm *ent.Client) RecruitmentRepository {
return &recruitmentRepository{orm: orm}
}
...
func (r *recruitmentRepository) FindAll(keyword string, skills []string, positions []string) ([]*ent.Recruitment, error) {
query := r.orm.Recruitment.Query().WithSkills().WithPositions()
var conditions []predicate.Recruitment
if len(skills) > 0 {
conditions = append(conditions, recruitment.HasSkillsWith(skill.NameIn(skills...)))
}
if len(positions) > 0 {
conditions = append(conditions, recruitment.HasPositionsWith(position.NameIn(positions...)))
}
if keyword != "" {
conditions = append(conditions, recruitment.
Or(
recruitment.TitleContains(keyword),
recruitment.ContentContains(keyword),
),
)
}
if len(conditions) > 0 {
query.Where(recruitment.And(conditions...))
}
recruitments, err := query.All(context.Background())
if err != nil {
log.Println(err)
return nil, err
}
return recruitments, nil
}
...
これで簡単な検索機能の実装が終わりました。
結論
Go言語初心者ですが、すごくFiberとentの開発経験がよく惚れました。
ドキュメントもすごく整理されていてレシピもあったりしますのですごく参考になりました。
またentの場合はFacebook(meta)で作ったもので実際使われているので信頼ができますね。
Discussion