undeexcepturi
Version:
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.
185 lines (144 loc) • 8.28 kB
text/typescript
import { MikroORM, Entity, PrimaryKey, Property, OptimisticLockError } from '@mikro-orm/core';
import { mockLogger } from '../../helpers';
import { PostgreSqlDriver } from '@mikro-orm/postgresql';
@Entity()
export class ConcurrencyCheckUser {
@PrimaryKey({ length: 100 })
firstName: string;
@PrimaryKey({ length: 100, concurrencyCheck: true })
lastName: string;
@Property({ concurrencyCheck: true })
age: number;
@Property({ nullable: true })
other?: string;
constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
}
describe('optimistic locking - concurrency check (postgres)', () => {
let orm: MikroORM;
let mock: jest.Mock;
beforeAll(async () => {
orm = await MikroORM.init({
entities: [ConcurrencyCheckUser],
dbName: `mikro_orm_test_concurrency_check`,
driver: PostgreSqlDriver,
});
await orm.schema.refreshDatabase();
mock = mockLogger(orm, ['query', 'query-params']);
});
beforeEach(async () => {
await orm.em.nativeDelete(ConcurrencyCheckUser, {});
orm.em.clear();
mock.mockReset();
});
afterAll(async () => orm.close(true));
test('should compare original to database value on entity update', async () => {
const test = new ConcurrencyCheckUser('Jakub', 'Smith', 20);
test.other = 'dsa';
await orm.em.persistAndFlush(test);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('insert into "concurrency_check_user" ("first_name", "last_name", "age", "other") values (\'Jakub\', \'Smith\', 20, \'dsa\')');
expect(mock.mock.calls[2][0]).toMatch('commit');
mock.mockReset();
test.age = 30;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('update "concurrency_check_user" set "age" = 30 where "first_name" = \'Jakub\' and "last_name" = \'Smith\' and "age" = 20');
expect(mock.mock.calls[2][0]).toMatch('commit');
mock.mockReset();
test.age = 40;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('update "concurrency_check_user" set "age" = 40 where "first_name" = \'Jakub\' and "last_name" = \'Smith\' and "age" = 30');
expect(mock.mock.calls[2][0]).toMatch('commit');
mock.mockReset();
test.other = 'asd';
await expect(orm.em.flush()).rejects.toThrow(`The optimistic lock on entity ConcurrencyCheckUser failed`);
mock.mockReset();
test.age = 41;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch('update "concurrency_check_user" set "age" = 41, "other" = \'asd\' where "first_name" = \'Jakub\' and "last_name" = \'Smith\' and "age" = 40');
expect(mock.mock.calls[2][0]).toMatch('commit');
});
test('throws when someone changed the state in the meantime', async () => {
const test = new ConcurrencyCheckUser('Jakub', 'Smith', 20);
test.other = 'dsa';
await orm.em.fork().persistAndFlush(test);
const test2 = await orm.em.findOneOrFail(ConcurrencyCheckUser, test);
await orm.em.nativeUpdate(ConcurrencyCheckUser, test, { age: 123 }); // simulate concurrent update
test2!.age = 50;
test2!.other = 'WHATT???';
try {
await orm.em.flush();
expect(1).toBe('should be unreachable');
} catch (e: any) {
expect(e).toBeInstanceOf(OptimisticLockError);
expect(e.message).toBe(`The optimistic lock on entity ConcurrencyCheckUser failed`);
expect((e as OptimisticLockError).getEntity()).toBe(test2);
}
});
test('should compare original to database value on entity update (batch update)', async () => {
const test1 = new ConcurrencyCheckUser('Jakub', 'Smith', 20);
test1.other = 'dsa';
const test2 = new ConcurrencyCheckUser('John', 'Smith', 25);
test2.other = 'lol';
await orm.em.persistAndFlush([test1, test2]);
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch(`insert into "concurrency_check_user" ("first_name", "last_name", "age", "other") values ('Jakub', 'Smith', 20, 'dsa'), ('John', 'Smith', 25, 'lol')`);
expect(mock.mock.calls[2][0]).toMatch('commit');
mock.mockReset();
test1.age = 30;
test2.age = 35;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch(`select "c0"."first_name", "c0"."last_name", "c0"."age" from "concurrency_check_user" as "c0" where (("c0"."first_name" = 'Jakub' and "c0"."last_name" = 'Smith' and "c0"."age" = 20) or ("c0"."first_name" = 'John' and "c0"."last_name" = 'Smith' and "c0"."age" = 25))`);
expect(mock.mock.calls[2][0]).toMatch(`update "concurrency_check_user" set "age" = case when ("first_name" = 'Jakub' and "last_name" = 'Smith') then 30 when ("first_name" = 'John' and "last_name" = 'Smith') then 35 else "age" end where ("first_name", "last_name", "age") in (('Jakub', 'Smith', 20), ('John', 'Smith', 25))`);
expect(mock.mock.calls[3][0]).toMatch('commit');
mock.mockReset();
test1.age = 40;
test2.age = 45;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch(`select "c0"."first_name", "c0"."last_name", "c0"."age" from "concurrency_check_user" as "c0" where (("c0"."first_name" = 'Jakub' and "c0"."last_name" = 'Smith' and "c0"."age" = 30) or ("c0"."first_name" = 'John' and "c0"."last_name" = 'Smith' and "c0"."age" = 35))`);
expect(mock.mock.calls[2][0]).toMatch(`update "concurrency_check_user" set "age" = case when ("first_name" = 'Jakub' and "last_name" = 'Smith') then 40 when ("first_name" = 'John' and "last_name" = 'Smith') then 45 else "age" end where ("first_name", "last_name", "age") in (('Jakub', 'Smith', 30), ('John', 'Smith', 35))`);
expect(mock.mock.calls[3][0]).toMatch('commit');
mock.mockReset();
test1.other = 'asd';
test2.other = 'lololol';
await expect(orm.em.flush()).rejects.toThrow(`The optimistic lock on entity ConcurrencyCheckUser failed`);
mock.mockReset();
test1.age = 41;
test2.age = 46;
await orm.em.flush();
expect(mock.mock.calls[0][0]).toMatch('begin');
expect(mock.mock.calls[1][0]).toMatch(`select "c0"."first_name", "c0"."last_name", "c0"."age" from "concurrency_check_user" as "c0" where (("c0"."first_name" = 'Jakub' and "c0"."last_name" = 'Smith' and "c0"."age" = 40) or ("c0"."first_name" = 'John' and "c0"."last_name" = 'Smith' and "c0"."age" = 45))`);
expect(mock.mock.calls[2][0]).toMatch(`update "concurrency_check_user" set "age" = case when ("first_name" = 'Jakub' and "last_name" = 'Smith') then 41 when ("first_name" = 'John' and "last_name" = 'Smith') then 46 else "age" end, "other" = case when ("first_name" = 'Jakub' and "last_name" = 'Smith') then 'asd' when ("first_name" = 'John' and "last_name" = 'Smith') then 'lololol' else "other" end where ("first_name", "last_name", "age") in (('Jakub', 'Smith', 40), ('John', 'Smith', 45))`);
expect(mock.mock.calls[3][0]).toMatch('commit');
});
test('throws when someone changed the state in the meantime (batch update)', async () => {
const test1 = new ConcurrencyCheckUser('Jakub', 'Smith', 20);
test1.other = 'dsa';
const test2 = new ConcurrencyCheckUser('John', 'Smith', 25);
test2.other = 'lol';
await orm.em.fork().persistAndFlush([test1, test2]);
const tests = await orm.em.find(ConcurrencyCheckUser, {}, { orderBy: { age: 1 } });
await orm.em.nativeUpdate(ConcurrencyCheckUser, tests[0], { age: 123 }); // simulate concurrent update
await orm.em.nativeUpdate(ConcurrencyCheckUser, tests[1], { age: 124 }); // simulate concurrent update
tests[0].age = 50;
tests[0].other = 'WHATT???';
tests[1].age = 51;
tests[1].other = 'WHATT???';
try {
await orm.em.flush();
expect(1).toBe('should be unreachable');
} catch (e: any) {
expect(e).toBeInstanceOf(OptimisticLockError);
expect(e.message).toBe(`The optimistic lock on entity ConcurrencyCheckUser failed`);
expect((e as OptimisticLockError).getEntity()).toBe(tests[0]);
}
});
});