Skip to content

協賛活動の更新APIを実装#1064

Merged
Wakai111 merged 4 commits intodevelopfrom
feat/kubosaka/update-sponsorship-activity
Mar 18, 2026
Merged

協賛活動の更新APIを実装#1064
Wakai111 merged 4 commits intodevelopfrom
feat/kubosaka/update-sponsorship-activity

Conversation

@Kubosaka
Copy link
Copy Markdown
Collaborator

対応Issue

resolve #0

概要

協賛活動(SponsorshipActivity)の更新API (PUT /sponsorship_activities/:id) を実装した。

  • SponsorshipActivity.Diff メソッドを追加し、既存の ActivitySponsorStyleLinks と新規リクエストの差分を計算(維持・追加・削除に分類)
  • UseCase に UpdateSponsorshipActivity を実装し、差分に基づいてリンクの追加・削除を実行
  • Repository に UpdateSponsorshipActivityCreateActivitySponsorStyleLinksDeleteActivitySponsorStyleLink を実装
  • Handler に UpdateSponsorshipActivity を実装

画面スクリーンショット等

なし(API実装のみ)

テスト項目

  • PUT /sponsorship_activities/:id で完全一致(変更なし)の場合、既存レコードのIDが維持されること
  • 全削除(sponsorStyleDetails=[])で sponsorStyles が空になること
  • 空状態から複数追加で新規IDが採番されること
  • 同一キーの重複追加で既存維持+新規追加が正しく動作すること
  • 重複→片方削除で正しく1件に絞られること

備考

なし

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

このプルリクエストは、協賛活動の更新APIを実装し、関連するスポンサースタイルリンクの管理を改善します。既存のリンクと新しいリクエストの差分を効率的に計算し、データベーストランザクションを通じて一貫性のある更新を保証することで、データの整合性を維持しながら柔軟な更新を可能にします。

Highlights

  • API実装: 協賛活動の更新API (PUT /sponsorship_activities/:id) が実装されました。
  • SponsorStyleLinksの差分計算: 既存のSponsorStyleLinksとリクエストされたリンクの差分(追加・削除)を計算する Diff メソッドが domain.SponsorStyleLinks に追加されました。
  • トランザクション処理: 協賛活動とその関連リンクの更新処理がデータベーストランザクション内で実行されるようになり、データの一貫性が向上しました。
  • リポジトリの機能拡張: 関連するスポンサースタイルリンクを取得する GetSponsorStyleLinksBySponsorshipActivityID、トランザクションを伴う Update、および特定のリンクを削除する DeleteSponsorStyleLinkByID メソッドがリポジトリに追加されました。
  • ユースケースロジックの強化: UpdateSponsorshipActivity ユースケースが、既存リンクの取得、差分計算、およびトランザクション内でのスポンサースタイルリンクの追加・削除を調整するようになりました。
Changelog
  • api/externals/handler/sponsorship_activity_handler.go
    • PutSponsorshipActivitiesId ハンドラが、更新リクエストを処理し、ユースケースを呼び出すように実装されました。
  • api/externals/repository/sponsorship_activity_repository.go
    • 関連するスポンサースタイルリンクを取得する GetSponsorStyleLinksBySponsorshipActivityID メソッドが追加されました。
    • Update メソッドのシグネチャがトランザクションを受け入れるように変更されました。
    • 特定のスポンサースタイルリンクをIDで削除する DeleteSponsorStyleLinkByID メソッドが追加されました。
  • api/internals/domain/sponsorship_activity.go
    • SponsorStyles の型が []SponsorStyleDetail から []SponsorStyleLink に変更されました。
    • SponsorStyleLinkSponsorStyleLinks の型が定義されました。
    • NewSponsorStyleLinkNewSponsorStyleLinks ヘルパー関数が追加されました。
    • 既存リンクとリクエストされたリンクを比較し、差分を検出する Diff メソッドが SponsorStyleLinks に実装されました。
  • api/internals/domain/sponsorship_activity_test.go
    • SponsorStyleLinks.Diff メソッドの単体テストが追加されました。
  • api/internals/usecase/sponsorship_activity_usecase.go
    • UpdateSponsorshipActivity メソッドのシグネチャが generated.UpdateSponsorshipActivityRequest を受け入れるように更新されました。
    • UpdateSponsorshipActivity のロジックが、既存リンクの取得、差分計算、およびスポンサースタイルリンクの作成/削除操作を含むトランザクション処理を伴うように書き換えられました。
Activity
  • このプルリクエストには、人間による活動(コメント、レビューなど)は記録されていません。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

このプルリクエストは、協賛活動の更新APIを実装するもので、ドメインロジック、ユースケース、リポジトリ、ハンドラ層にわたる変更が含まれています。全体的に、トランザクションを利用した堅牢な更新処理が実装されており、ドメインロジックの差分計算もテストで十分にカバーされています。いくつかの改善点として、パフォーマンス向上のためのN+1問題の解消、トランザクション管理のコードの簡潔化、テストコードの未使用フィールドの削除を提案します。これらの点を修正することで、より保守性が高く効率的なコードになるでしょう。

existing SponsorStyleLinks
requested SponsorStyleLinks
wantDeleteIDs []int
wantCreateCount int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

wantCreateCount フィールドがテストケース内で定義されていますが、使用されていません。コードの可読性を保つために、この未使用のフィールドは削除することをお勧めします。

Comment on lines +185 to +215
if err := u.repo.Update(ctx, tx, activity); err != nil {
u.transactionRepo.RollBack(ctx, tx)
return generated.SponsorshipActivity{}, err
}

// NOTE: N+1問題はあるが、協賛スタイルは件数が多くない想定のため、許容 by わかぺ
// 5. 削除
for _, linkID := range toDeleteIDs {
if err := u.repo.DeleteSponsorStyleLinkByID(ctx, tx, linkID); err != nil {
u.transactionRepo.RollBack(ctx, tx)
return generated.SponsorshipActivity{}, err
}
}

// 6. 追加
for _, link := range toCreate {
category := generated.SponsorStyleCategory(link.Category)
newLink := generated.ActivitySponsorStyleLink{
SponsorStyleId: &link.Style.ID,
Category: &category,
}
if err := u.repo.CreateSponsorStyleLink(ctx, tx, newLink, id); err != nil {
u.transactionRepo.RollBack(ctx, tx)
return generated.SponsorshipActivity{}, err
}
}

// 7. コミット
if err := u.transactionRepo.Commit(ctx, tx); err != nil {
return generated.SponsorshipActivity{}, err
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

トランザクション内の各エラー処理で u.transactionRepo.RollBack(ctx, tx) を呼び出しており、コードが冗長になっています。トランザクションを扱う処理をクロージャ(無名関数)にまとめることで、エラーハンドリングとロールバック処理を1箇所に集約でき、コードがよりクリーンになります。

err = func(tx *sql.Tx) error {
	// 4. メイン情報更新
	if err := u.repo.Update(ctx, tx, activity); err != nil {
		return err
	}

	// NOTE: N+1問題はあるが、協賛スタイルは件数が多くない想定のため、許容 by わかぺ
	// 5. 削除
	for _, linkID := range toDeleteIDs {
		if err := u.repo.DeleteSponsorStyleLinkByID(ctx, tx, linkID); err != nil {
			return err
		}
	}

	// 6. 追加
	for _, link := range toCreate {
		category := generated.SponsorStyleCategory(link.Category)
		newLink := generated.ActivitySponsorStyleLink{
			SponsorStyleId: &link.Style.ID,
			Category:       &category,
		}
		if err := u.repo.CreateSponsorStyleLink(ctx, tx, newLink, id); err != nil {
			return err
		}
	}

	// 7. コミット
	return u.transactionRepo.Commit(ctx, tx)
}(tx)

if err != nil {
	u.transactionRepo.RollBack(ctx, tx)
	return generated.SponsorshipActivity{}, err
}

Comment on lines +192 to +210
for _, linkID := range toDeleteIDs {
if err := u.repo.DeleteSponsorStyleLinkByID(ctx, tx, linkID); err != nil {
u.transactionRepo.RollBack(ctx, tx)
return generated.SponsorshipActivity{}, err
}
}

// 6. 追加
for _, link := range toCreate {
category := generated.SponsorStyleCategory(link.Category)
newLink := generated.ActivitySponsorStyleLink{
SponsorStyleId: &link.Style.ID,
Category: &category,
}
if err := u.repo.CreateSponsorStyleLink(ctx, tx, newLink, id); err != nil {
u.transactionRepo.RollBack(ctx, tx)
return generated.SponsorshipActivity{}, err
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

// NOTE にも書かれている通り、ここの削除と追加のループはN+1問題を引き起こします。協賛スタイルの数が少ないうちは問題になりにくいですが、将来的なパフォーマンスを考慮し、バルク操作にまとめることをお勧めします。

具体的には、以下のような変更が考えられます。

  • 削除: DeleteSponsorStyleLinkByID の代わりに DeleteSponsorStyleLinksByIDs(ctx, tx, toDeleteIDs) のようなメソッドをリポジトリに作成し、DELETE FROM ... WHERE id IN (...) を一度に実行します。
  • 追加: CreateSponsorStyleLink の代わりに CreateSponsorStyleLinks(ctx, tx, toCreate, id) のようなメソッドを作成し、INSERT INTO ... VALUES (...), (...), ... のように一括で登録します。

これにより、データベースへのラウンドトリップが削減され、パフォーマンスが向上します。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そんな件数行かないから一旦大丈夫

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

協賛活動(SponsorshipActivity)の更新 API(PUT /sponsorship_activities/:id)を実装し、協賛スタイルリンク(activity_sponsor_style_links)を「差分(維持/追加/削除)」にもとづいて更新できるようにするPRです。

Changes:

  • SponsorStyleLinks.Diff を追加し、既存リンクと更新リクエストの差分を計算
  • UseCase に更新処理(トランザクション内で活動本体更新+リンクの追加/削除)を実装
  • Repository に既存リンク取得・更新・リンク削除のSQL実装を追加し、Handler の PUT を実装

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
api/internals/usecase/sponsorship_activity_usecase.go 更新ユースケースを実装(差分計算→トランザクション更新)
api/internals/domain/sponsorship_activity.go SponsorStyleLinks と Diff ロジックを追加
api/internals/domain/sponsorship_activity_test.go Diff のユニットテストを追加
api/externals/repository/sponsorship_activity_repository.go 既存リンク取得・Update・リンク削除を追加
api/externals/handler/sponsorship_activity_handler.go PUT /sponsorship_activities/:id のハンドラを実装

Comment on lines +151 to +156
cat := ""
if detail.Category != nil {
cat = string(*detail.Category)
}
requestedLinks = append(requestedLinks, domain.NewSponsorStyleLink(*detail.SponsorStyleId, cat))
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detail.Category が nil の場合に cat を空文字にしているため、PUT で category を省略すると DB に空文字が保存される(または enum/制約で失敗する)挙動になります。CreateSponsorshipActivity では Category をそのまま(nil のまま)渡しているので、更新系も nil を保持して NULL として更新/挿入できるようにするか、category を必須入力としてバリデーションしてください。

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +176
remarks := ""
if req.Remarks != nil {
remarks = *req.Remarks
}
activity := domain.SponsorshipActivity{
ID: id,
YearPeriodsID: req.YearPeriodsId,
SponsorID: req.SponsorId,
UserID: req.UserId,
ActivityStatus: string(req.ActivityStatus),
FeasibilityStatus: string(req.FeasibilityStatus),
DesignProgress: string(req.DesignProgress),
Remarks: remarks,
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req.Remarks が nil の場合に空文字へ変換して更新しているため、リクエストで remarks を省略すると既存の NULL/値が空文字で上書きされます(Create は nil のまま保存できる実装)。PUT の仕様として省略時はエラーにするか、nil は NULL 更新できるように(sql.NullString や *string を利用する等)扱いを揃えるのが望ましいです。

Copilot uses AI. Check for mistakes.
}

// 7. コミット
if err := u.transactionRepo.Commit(ctx, tx); err != nil {
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit に失敗した場合に RollBack を呼んでいないため、トランザクションが未クローズのまま残りロック/コネクション枯渇の原因になります。他の UseCase では commit 失敗時に rollback しているので、この箇所も同様に rollback してからエラーを返すようにしてください。

Suggested change
if err := u.transactionRepo.Commit(ctx, tx); err != nil {
if err := u.transactionRepo.Commit(ctx, tx); err != nil {
u.transactionRepo.RollBack(ctx, tx)

Copilot uses AI. Check for mistakes.
return generated.SponsorshipActivity{}, err
}

// NOTE: N+1問題はあるが、協賛スタイルは件数が多くない想定のため、許容 by わかぺ
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

コメントで「N+1問題」とありますが、ここは1リクエスト内での複数 DELETE/INSERT のループであり一般的な N+1(親取得→子を件数分SELECT)とは異なります。また「by 個人名」の記述はコードベース内の恒久コメントとしては情報価値が低いので、コスト/許容理由を技術的に記述する形に修正するのがよいです。

Suggested change
// NOTE: N+1問題はあるが、協賛スタイルは件数が多くない想定のため、許容 by わかぺ
// NOTE: 協賛スタイルの削除・追加は1リクエスト内で複数回の DELETE/INSERT をループで発行している。
// 想定される協賛スタイル数は多くなく、現状のパフォーマンスコストは許容範囲と判断している。
// 今後スタイル数が増加しボトルネックとなる場合は、バルク操作等による最適化を検討すること。

Copilot uses AI. Check for mistakes.
Comment on lines +213 to +214
Where(goqu.I("activity_sponsor_style_links.sponsorship_activity_id").Eq(sponsorshipActivityID))

Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetSponsorStyleLinksBySponsorshipActivityID のクエリに ORDER BY が無いため、同一キー(sponsor_style_id + category)が複数存在するケースで「どの既存 link ID を維持するか」がDBの返却順に依存して不安定になります(UseCase の Diff は最初にマッチした既存を維持する実装)。ID維持の要件があるなら、activity_sponsor_style_links.id 等で ORDER BY を付けて決定的にしてください。

Suggested change
Where(goqu.I("activity_sponsor_style_links.sponsorship_activity_id").Eq(sponsorshipActivityID))
Where(goqu.I("activity_sponsor_style_links.sponsorship_activity_id").Eq(sponsorshipActivityID)).
Order(goqu.I("activity_sponsor_style_links.id").Asc())

Copilot uses AI. Check for mistakes.
return nil, err
}
links = append(links, link)
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rows.Next() のループ後に rows.Err() を確認していないため、イテレーション中に発生した遅延エラー(ネットワーク/ドライバ都合など)を見逃す可能性があります。ループ終了後に rows.Err() をチェックしてエラーを返すようにしてください。

Suggested change
}
}
if err := rows.Err(); err != nil {
return nil, err
}

Copilot uses AI. Check for mistakes.
existing SponsorStyleLinks
requested SponsorStyleLinks
wantDeleteIDs []int
wantCreateCount int
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テストケース struct に wantCreateCount フィールドがありますが、どのケースでも値がセットされずアサーションにも使われていません。未使用フィールドは誤解を生むので削除するか、意図があるなら実際に検証に使うようにしてください。

Suggested change
wantCreateCount int

Copilot uses AI. Check for mistakes.
Comment on lines +148 to +150
if detail.SponsorStyleId == nil {
continue
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req.SponsorStyleDetails の要素で SponsorStyleId が nil の場合に continue しているため、クライアントの不正/欠損データを静かに無視してしまい、結果として requestedLinks が空になって既存リンクが全削除される等の事故が起きえます。SponsorStyleId が nil の要素が来た時点で 4xx を返すなど、入力として明確に弾く方が安全です。

Copilot uses AI. Check for mistakes.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Mar 12, 2026

Deploying finansu with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5a4b97e
Status: ✅  Deploy successful!
Preview URL: https://49308346.finansu.pages.dev
Branch Preview URL: https://feat-kubosaka-update-sponsor.finansu.pages.dev

View logs

Base automatically changed from feat/wakape/create-sponsorship-activity to develop March 18, 2026 04:30
@Wakai111 Wakai111 merged commit 45d9c23 into develop Mar 18, 2026
3 checks passed
@Wakai111 Wakai111 deleted the feat/kubosaka/update-sponsorship-activity branch March 18, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants