I have some logic in my application which I think results in the following MySQL calls, however when I get two of these run within a couple of milliseconds, I get two incompatible child rows.
- Start transaction with Repeatable Read isolation.
- Fetch and lock Account object
SELECT ... FROM Account WHERE id = ? FOR UPDATE
- Read existing Addressees linked to the Account
SELECT ... FROM Address WHERE account_id = ?
- If existing address is marked as primary, update it.
UPDATE Addresses SET primary = false WHERE id = ?
- Insert new address with primary = true.
- Complete transaction.
If this process is run twice quickly, I get two Address rows with primary = true and account_id set to the account which I was surprised by because I thought that locking the account in step 2 would prevent multiple transactions from running simultaneously. I don't want to change the behaviour in a way that would limit my throughput too much.
I wondered if I need to switch the isolation level to "Serializable" [sic] but I'm not clear whether a "FOR SHARE" read on step 3 would actually fix the issue.
My data structure is likely obvious from the above but looks like this:
Account
:
id
PK
other irrelevant account data
Address
:
id
PK,
account_id
- not an actual foreign key, just an id that matches the Accout PK.
primary
- boolean, should be a maximum of one primary address per account but this is not enforced in MySQL because it would complicate the DB a little to have a generated column to enable this since MySQL doesn't support partial indexes.
other irrelevant address data
SELECT ... FROM Account acc LEFT JOIN Address addr ON acc.id = addr.account_id