Rules Hub
Coding Rules Library
Rule priority, scope & exceptions
Use this to align rules with the senior-level structure (P0/P1/P2, scope, exceptions/tradeoffs).
Make check-then-create atomic with a transaction and row lock
Wrap look-up-then-insert-if-absent logic in a single transaction with a pessimistic row lock to avoid concurrent duplicate-creation races.
Bad example
| 1 | async addItem(cartId: number, productId: number, dto: ItemDto) { |
| 2 | // Read outside any transaction |
| 3 | let item = await this.itemRepo.findOne({ where: { cartId, productId } }); |
| 4 |
|
| 5 | if (!item) { |
| 6 | item = await this.itemRepo.save({ cartId, productId }); |
| 7 | } |
| 8 |
|
| 9 | // ... continue with item |
| 10 | } |
Explanation (EN)
Two concurrent requests both read item === null, then both insert. The second insert collides with the unique constraint and fails, surfacing as a spurious error to the user.
Objašnjenje (HR)
Dva istovremena zahtjeva oba procitaju item === null, zatim oba umetnu. Drugi insert sudari se s unique ogranicenjem i ne uspije, sto se korisniku prikaze kao laznu gresku.
Good example
| 1 | async addItem(cartId: number, productId: number, dto: ItemDto) { |
| 2 | return this.dataSource.transaction(async (manager) => { |
| 3 | let item = await manager |
| 4 | .getRepository(Item) |
| 5 | .createQueryBuilder('i') |
| 6 | .setLock('pessimistic_write') |
| 7 | .where('i.cart_id = :cartId AND i.product_id = :productId', { cartId, productId }) |
| 8 | .getOne(); |
| 9 |
|
| 10 | if (!item) { |
| 11 | item = await manager.getRepository(Item).save({ cartId, productId }); |
| 12 | } |
| 13 |
|
| 14 | // ... rest of the work shares the same transaction |
| 15 | return item; |
| 16 | }); |
| 17 | } |
Explanation (EN)
Doing the lookup, the absence check, and the insert inside one transaction with a pessimistic_write lock serializes concurrent callers so only one creates the row and the other reads it, eliminating the duplicate-create race.
Objašnjenje (HR)
Obavljanjem pretrage, provjere odsutnosti i inserta unutar jedne transakcije s pessimistic_write zakljucavanjem serijaliziraju se istovremeni pozivi pa samo jedan kreira redak, a drugi ga procita, cime se uklanja utrka na duplikatima.
Exceptions / Tradeoffs (EN)
Alternatively use an idempotent upsert (INSERT ... ON CONFLICT DO NOTHING / UPDATE) where the database guarantees atomicity; the key point is the check and create must not be two separate non-atomic round-trips.
Iznimke / Tradeoffi (HR)
Alternativno koristi idempotentni upsert (INSERT ... ON CONFLICT DO NOTHING / UPDATE) gdje baza jamci atomicnost; bit je da provjera i kreiranje ne smiju biti dva odvojena ne-atomicna poziva.