やる気がストロングZERO

やる気のストロングスタイル

重複Insertを防ぐ(postgreSQL)

例えばユーザー名の重複登録をNGとする場合、こんな感じでバリデーションと登録処理を書くことが多い。

※疑似コードです

begin
name = "山田太郎"

// dbからデータを取ってみて存在しないことを確認する
user = getUserByName(name)
if user != null {
    return false 
}

createUser(name)
commit

return true

でもこれだと名前が重複したユーザーが登録されてしまう可能性がある。

理由はこちら:

yaruki-strong-zero.hatenablog.jp

防ぐには

テーブルにユニーク制約をつけて、再実行処理を組み入れる

これが一番確実。
ユニーク制約があることで重複データが登録されようとしてもエラーになり絶対に入らない。

重複エラーでアプリが落ちるのを避けたければ再実行処理を入れる。

name = "山田太郎"

execCount = 0
maxTryCount = 2 
while {
    execCount++
    if execCount > maxTryCount {
        raise("許容再実行回数を超えました")        
    }

    begin

    user = getUserByName(name)
    if user != null {
        return false 
    }

    try {
        createUser(name)
    } catch(e) {
        // 重複登録エラーが出たら再実行する
        if execCount < maxTryCount {
            continue
        } else {
            raise e
        }
    }
    commit
}
return true

テーブルにユニーク制約をつけられない場合はトランザクション分離レベルをSERIALIZABLEにして再実行処理を入れる。

delete flagで運用しているなど、ユニーク制約をつけられない場合(削除ユーザーで使用されていた名前は再度使えるようになる等)はトランザクション分離レベルをSERIALIZABLEにして再実行処理を入れる。
SERIALIZABLEだと、仮にバリデーションをすり抜けて重複データのInsert処理が実行されてもcommit時にエラーが出る。
※どの程度パフォーマンスに影響が出るのかは調べてない。
※REPEATABLE READではエラーが出ず、重複登録されてしまう。

name = "山田太郎"

execCount = 0
maxTryCount = 2 
while {
    execCount++
    if execCount > maxTryCount {
        raise("許容再実行回数を超えました")        
    }

    begin SERIALIZABLE // シリアライザブル指定

    user = getUserByName(name)
    if user != null {
        return false 
    }

    
    createUser(name)

    try {
        commit
    } catch(e) {
        // エラーが出たら再実行する
        if execCount < maxTryCount {
            continue
        } else {
            raise e
        }
    }
}
return true

追記: このスライドの中でも触れられている。

https://speakerdeck.com/saiya_moebius/rdbms-in-action