I've come to an issue when handling concurrency.
In the example below, two users A and B edit the same invoice and make different changes to it. If both of them click save at the same time I would like one of them to succeed, and the other one to fail. Otherwise the resulting invoice would be an undesired "merged invoice".
Here's the example, tested in PostgreSQL (but I think this question should be database agnostic):
create table invoice (
id int primary key not null,
created date
);
create table invoice_line (
invoice_id int,
line numeric(6),
amount numeric(10,2),
constraint fk_invoice foreign key (invoice_id) references invoice(id)
);
insert into invoice(id, created) values (123, '2018-03-17');
insert into invoice_line (invoice_id, line, amount) values (123, 1, 24);
insert into invoice_line (invoice_id, line, amount) values (123, 2, 26);
So the initial rows of the invoice are:
invoice_id line amount
---------- ---- ------
123 1 24
123 2 26
Now, user A edits the invoice, removes line 2 and clicks SAVE:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #1 will it block the other thread?
delete invoice_line where invoice_id = 123 and line = 2;
commit; -- User A would expect the invoice to only include line 1.
At the same time user B edits the invoice and adds line 3, and clicks SAVE:
-- transaction begins
set transaction isolation level serializable;
select * from invoice where id = 123; -- #2 will this wait the other thread?
insert into invoice_line (invoice_id, line, amount) values (123, 3, 45);
commit; -- User B would expect the invoice to include lines 1, 2, and 3.
Unfortunately both transactions succeed, and I get the merged rows (corrupted state):
invoice_id line amount
---------- ---- ------
123 1 24
123 3 45
Since this is not what I wanted, what options do I have to control concurrency?