I am using CTE to check database rows, delete rows by filtering and finally to insert row into the database.
Below is the go function to Insert a row:
func (vm VoteModel) Insert(vote *Vote) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// begin atomic transaction
tx, err := vm.DB.BeginTx(ctx, nil)
if err != nil {
return err
}
// Use a CTE to check if the poll_choice_id belongs to the poll_id and to delete any existing vote
insertVoteQuery := `
WITH valid_choice AS (
SELECT 1
FROM poll_choices
WHERE id = $1 AND poll_id = $2
LIMIT 1
),
delete_existing_vote AS (
DELETE FROM votes
WHERE user_id = $3 AND poll_id = $2
RETURNING 1
)
INSERT INTO votes (user_id, poll_id, poll_choice_id)
SELECT $3, $2, $1
FROM valid_choice
RETURNING id, added_at, version
`
insertArgs := []interface{}{vote.PollChoiceID, vote.PollID, vote.UserID}
err = tx.QueryRowContext(ctx, insertVoteQuery, insertArgs...).Scan(
&vote.ID,
&vote.AddedAt,
&vote.Version,
)
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return rbErr
}
switch {
case errors.Is(err, sql.ErrNoRows):
return ErrRecordNotFound
default:
return err
}
}
// commit
err = tx.Commit()
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
return rbErr
}
return err
}
return nil
}
Suppose I have these rows of votes table:
id | user_id | poll_id | poll_choice_id | added_at | version
----+---------+---------+----------------+---------------------------+---------
10 | 11 | 12 | 5 | 2024-05-21 13:39:58+05:30 | 1
16 | 3 | 18 | 34 | 2024-05-22 21:57:44+05:30 | 1 <-- existing user vote
24 | 9 | 18 | 35 | 2024-05-24 10:54:47+05:30 | 1
(3 rows)
When I try to vote to a poll which I already voted with another poll choice like and my user id is 3
:
{
"poll_id": 18,
"poll_choice_id": 31
}
I get error:
votes_user_id_poll_id_key : "violates unique constraint"
But I thought I deleted any user and poll related votes with models go Insert()
function in this line:
delete_existing_vote AS (DELETE FROM votes WHERE user_id = $3 AND poll_id = $2 RETURNING 1 )
What am I missing? I could have separated the query transactions and make extra db calls, but I want to minimize number of db calls.
2
Answers
the answer comes from the manual :
It is not necessary to delete the existing row just Insert it. All supported versions of Postgres support an
insert ... on conflict ...
clause (see Insert documentation). With that you handle the existing row with an Update. In this case something like:Alternately, with version 15 or greater you can use the
merge
statement. So (see Merge documentation):See Demo (for each) here.
NOTE: For demo I have also updated columns, if for no other reason than visualization,
added_at
andversion
.