Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion model/respondents_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"gopkg.in/guregu/null.v4"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)

// Respondent RespondentRepositoryの実装
Expand Down Expand Up @@ -99,6 +100,18 @@ func (*Respondent) InsertRespondent(ctx context.Context, userID string, question
}

if !questionnaire.IsDuplicateAnswerAllowed {
// Lock the questionnaire row to serialize concurrent insert attempts.
// SELECT ... FOR UPDATE on respondents would not lock non-existent rows,
// allowing both transactions to pass the duplicate check simultaneously.
// By locking the parent questionnaire row (which always exists), we ensure
// only one transaction can proceed through the check+insert at a time.
err = db.
Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", questionnaireID).
First(&Questionnaires{}).Error
if err != nil {
return 0, fmt.Errorf("failed to lock questionnaire row: %w", err)
}
err = db.
Where("questionnaire_id = ? AND user_traqid = ?", questionnaireID, userID).
First(&Respondents{}).Error
Comment on lines 108 to 117
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

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

SELECT ... FOR UPDATE here may not reliably provide the intended per-(questionnaire_id,user_traqid) serialization because the respondents table has no index/unique constraint on these columns (see Respondents struct), so this query will degenerate into a full table scan and can lock far more than intended (or behave DB/optimizer-dependent when no row exists). Consider instead locking a stable row that always exists (e.g., questionnaires row WHERE id = ? FOR UPDATE) before checking/inserting, and/or add a composite index (and preferably a uniqueness constraint if semantics allow) on (questionnaire_id, user_traqid) so the lock is scoped and deterministic.

Copilot uses AI. Check for mistakes.
Expand All @@ -108,7 +121,6 @@ func (*Respondent) InsertRespondent(ctx context.Context, userID string, question
if !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, fmt.Errorf("failed to check duplicate answer: %w", err)
}

}

err = db.Create(&respondent).Error
Expand Down
Loading