🔍

GoのORMであるentを用いて検索機能を実装してみる

2025/02/13に公開

はじめに

Java開発者である私がGo言語にスキルチェンジするために小さいチームプロジェクトでアプリを作りながら学んだことを伝える記事になります。誤ったことがある場合コメントいただけると嬉しいです。

開発環境

  • Go
  • Fiber
  • ent
  • MySQL

何を作るのか

  • 技術(skills, 複数可)
  • ポジション(positions, 複数可)
  • 進め方(online,offline,all)
  • 検索キーワード(検索欄に指定したキーワードでタイトルと内容検索可能)
  • お気に入り、募集中はスコープアウト

四つの条件をつけて検索したい

リレーション

ゴール

検索機能の要件は以下になりそうです。

  • skillsを指定すれば関連づいている応募が検索できます
  • skillspositionsを指定すれば両方の条件を満たす応募が検索できます
  • skillspositionskeywordを指定すれば三つの条件を満たす応募が検索できます
  • keywordtitlecontentどちらかがヒットある場合検索できます

実装

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