JOOQ 乐观锁定和 DataChangedException:数据库记录已在 record.update 上更改
Posted
技术标签:
【中文标题】JOOQ 乐观锁定和 DataChangedException:数据库记录已在 record.update 上更改【英文标题】:JOOQ optimistic locking and DataChangedException: Database record has been changed on record.update 【发布时间】:2020-10-02 09:11:40 【问题描述】:问题总结:
我有一个 case class Test04(...)
,其值来自例如移动客户端更新到数据库。表使用timestamptz
类型的tSt
字段进行乐观锁定,tSt
值是数据库中的当前值。
所以我有唯一的 id 和最新的tSt
。所以应该可以更新数据库中的值。
case 类转换为 RecordTest04
以缩短数据库操作:record.update()
而不是 DSL 语句,每次在表中添加/删除新字段时我都必须手动修改。
由于某种原因,record.update()
抛出 org.jooq.exception.DataChangedException: Database record has been changed
详情:
我有以下 sql:
-- for table04
create or replace function createUuid() returns uuid
as 'SELECT md5(random()::text || clock_timestamp()::text)::uuid;'
language sql
stable;
create or replace function insertUuidT()
returns trigger as
$BODY$
begin
if new.id is null then
new.id = createUuid();
end if;
new.tSt = now();
return new;
end
$BODY$
language 'plpgsql';
create or replace function updatePreventT()
returns trigger as
$BODY$
begin
if new.id <> old.id then
raise exception 'You cannot modify id. Current id: % Proposed id: %', OLD.id, NEW.id; -- USING ERRCODE='123';
end if;
new.tSt = now();
return new;
end
$BODY$
language 'plpgsql';
drop table if exists test04 cascade;
create table test04 (
id uuid not null, -- Unique, link to other tables
intNotNull int not null,
dateNotNull date not null,
dateNull date,
timestamptzNotNull timestamptz not null,
timestamptzNull timestamptz,
tSt timestamptz not null, -- timestamp for optimistic locking support
primary key ( id )
);
create unique index test04_id on test04( id );
drop trigger if exists test04_insert ON test04;
create trigger test04_insert before insert on test04 for each row execute procedure insertUuidT(); --setUuid();
drop trigger if exists test04_update ON test04;
create trigger test04_update before update on test04 for each row execute procedure updatePreventT(); --preventIdChange();
数据库设置为:
val settings = new Settings()
.withExecuteWithOptimisticLocking(true) // Defaults to false
.withUpdatablePrimaryKeys(true) // Defaults to false, primary keys are not always internal
.withReturnAllOnUpdatableRecord(true) // Defaults to false, return all db/JOOQ generated values.
.withMapJPAAnnotations(false) // Defaults to true, annotations are not used
//.withExecuteWithOptimisticLockingExcludeUnversioned(true) // Defaults to false
val sqlDialect = SQLDialect.POSTGRES
val jdbcDriverClass = Class.forName("org.postgresql.Driver")
代码是:
val connection = JooqTestConnectionPool.dataSource.getConnection
val db = DBSettings.getDSLContext(connection) // Using Conf to create DSL.
val uuid1 = UUID.fromString("0c6e629b-aa0c-43eb-82fd-645c565a689a")
val t04_1 = Test04(uuid1, 1230, LocalDate.now(), Some(LocalDate.now().minusDays(100)), OffsetDateTime.now(), Some(OffsetDateTime.now().minusMinutes(10)),OffsetDateTime.now())
db.deleteFrom(TEST04).where(TEST04.ID.eq(uuid1)).execute()
val t04_1RecA = db.newRecord(TEST04, t04_1)
val t04_1RecB = db.newRecord(TEST04, t04_1)
println(s"t04_1RecA before insert\n$t04_1RecA")
val iResA = t04_1RecA.insert()
println(s"t04_1RecA after insert\n$t04_1RecA")
if (iResA != 1) throw new RuntimeException(s"Invalid insert res A: $iResA")
t04_1RecB.changed("id",false)
t04_1RecB.setIntnotnull(800)
t04_1RecB.setTst(t04_1RecA.getTst)
println(s"t04_1RecB before update\n$t04_1RecB")
t04_1RecB.update() // -> This will cause org.jooq.exception.DataChangedException: Database record has been changed
println(s"t04_1RecB after update\n$t04_1RecB")
connection.close()
运行时输出为:
Thank you for using jOOQ 3.12.4
Executing query : delete from "public"."test04" where "public"."test04"."id" = cast(? as uuid)
-> with bind values : delete from "public"."test04" where "public"."test04"."id" = '0c6e629b-aa0c-43eb-82fd-645c565a689a'
Affected row(s) : 1
t04_1RecA before insert
+-------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull |tst |
+-------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
|*0c6e629b-aa0c-43eb-82fd-645c565a689a| *1230|*2020-06-12|*Some(2020-03-04)|*2020-06-12T15:59:39.655505+03:00|*Some(2020-06-12T15:49:39.655508+03:00)|*2020-06-12T15:59:39.655514+03:00|
+-------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
Executing query : insert into "public"."test04" ("id", "intnotnull", "datenotnull", "datenull", "timestamptznotnull", "timestamptznull", "tst") values (cast(? as uuid), ?, cast(? as date), cast(? as date), cast(? as timestamp with time zone), cast(? as timestamp with time zone), cast(? as timestamp with time zone)) returning "public"."test04"."id", "public"."test04"."intnotnull", "public"."test04"."datenotnull", "public"."test04"."datenull", "public"."test04"."timestamptznotnull", "public"."test04"."timestamptznull", "public"."test04"."tst"
-> with bind values : insert into "public"."test04" ("id", "intnotnull", "datenotnull", "datenull", "timestamptznotnull", "timestamptznull", "tst") values ('0c6e629b-aa0c-43eb-82fd-645c565a689a', 1230, date '2020-06-12', date '2020-03-04', timestamp with time zone '2020-06-12 15:59:39.655505+03:00', timestamp with time zone '2020-06-12 15:49:39.655508+03:00', timestamp with time zone '2020-06-12 15:59:39.655514+03:00') returning "public"."test04"."id", "public"."test04"."intnotnull", "public"."test04"."datenotnull", "public"."test04"."datenull", "public"."test04"."timestamptznotnull", "public"."test04"."timestamptznull", "public"."test04"."tst"
Fetched result : +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
: |id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull |tst |
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
: |0c6e629b-aa0c-43eb-82fd-645c565a689a| 1230|2020-06-12 |Some(2020-03-04)|2020-06-12T15:59:39.655505+03:00|Some(2020-06-12T15:49:39.655508+03:00)|2020-06-12T15:59:40.181434+03:00|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
Fetched row(s) : 1
t04_1RecA after insert
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull |tst |
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
|0c6e629b-aa0c-43eb-82fd-645c565a689a| 1230|2020-06-12 |Some(2020-03-04)|2020-06-12T15:59:39.655505+03:00|Some(2020-06-12T15:49:39.655508+03:00)|2020-06-12T15:59:40.181434+03:00|
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
t04_1RecB before update
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull |tst |
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
|0c6e629b-aa0c-43eb-82fd-645c565a689a| *800|*2020-06-12|*Some(2020-03-04)|*2020-06-12T15:59:39.655505+03:00|*Some(2020-06-12T15:49:39.655508+03:00)|*2020-06-12T15:59:40.181434+03:00|
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+---------------------------------+
Executing query : select "public"."test04"."id", "public"."test04"."intnotnull", "public"."test04"."datenotnull", "public"."test04"."datenull", "public"."test04"."timestamptznotnull", "public"."test04"."timestamptznull", "public"."test04"."tst" from "public"."test04" where "public"."test04"."id" = cast(? as uuid) for update
-> with bind values : select "public"."test04"."id", "public"."test04"."intnotnull", "public"."test04"."datenotnull", "public"."test04"."datenull", "public"."test04"."timestamptznotnull", "public"."test04"."timestamptznull", "public"."test04"."tst" from "public"."test04" where "public"."test04"."id" = '0c6e629b-aa0c-43eb-82fd-645c565a689a' for update
Fetched result : +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
: |id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull |tst |
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
: |0c6e629b-aa0c-43eb-82fd-645c565a689a| 1230|2020-06-12 |Some(2020-03-04)|2020-06-12T15:59:39.655505+03:00|Some(2020-06-12T15:49:39.655508+03:00)|2020-06-12T15:59:40.181434+03:00|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+--------------------------------+
Fetched row(s) : 1
Concurrent update 1 1677ms
org.jooq.exception.DataChangedException: Database record has been changed
org.jooq.impl.UpdatableRecordImpl.checkIfChanged(UpdatableRecordImpl.java:427)
org.jooq.impl.UpdatableRecordImpl.storeUpdate0(UpdatableRecordImpl.java:247)
问题是t04_1RecB.update()
抛出异常,尽管用于乐观锁定的tst
字段是在更新之前从数据库中复制的。
如果我使用.withExecuteWithOptimisticLockingExcludeUnversioned(true)
,则更新不会失败,但更新不会使用tst
列来检测过期更新。
【问题讨论】:
有一个与时间戳精度有关的问题:github.com/jOOQ/jOOQ/issues/9933 可以吗? 【参考方案1】:伟大的调查!不确定这是否是预期的语义 - timestampz
数据类型不应该有任何特别之处。您能否为此打开GitHub issue,我们会看看?
【讨论】:
【参考方案2】:我用两张表对这个问题做了一些进一步的测试。第一个表 (test07) 具有用于乐观锁定的 timestamp
类型列。 JOOQ 文档提到了乐观锁定字段的 int
和 timestamp
类型。在我原来的环境中,乐观锁列的类型是timestamptz
。
然而这并没有解决问题。抛出完全相同的异常。
第二个表 (test03) 有 int
列用于乐观锁定:
-- for table 03
create or replace function insertUuidVerT()
returns trigger as
$BODY$
begin
if new.id is null then
new.id = createUuid();
end if;
new.ver = 0;
return new;
end
$BODY$
language 'plpgsql';
create or replace function updatePreventVerT()
returns trigger as
$BODY$
begin
if new.id <> old.id then
raise exception 'You cannot modify id. Current id: % Proposed id: %', OLD.id, NEW.id; -- USING ERRCODE='123';
end if;
new.ver = old.ver + 1;
return new;
end
$BODY$
language 'plpgsql';
drop table if exists test03 cascade;
create table test03 (
id uuid not null, -- Unique, link to other tables
intNotNull int not null,
dateNotNull date not null,
dateNull date,
timestamptzNotNull timestamptz not null,
timestamptzNull timestamptz,
ver int not null default 0, -- version for optimistic locking support
primary key ( id )
);
drop trigger if exists test03_insert ON test03;
create trigger test03_insert before insert on test03 for each row execute procedure insertUuidVerT(); --setUuid();
drop trigger if exists test03_update ON test03;
create trigger test03_update before update on test03 for each row execute procedure updatePreventVerT(); --preventIdChange();
JOOQ 设置与以前相同,scala 代码为:
val connection = JooqTestConnectionPool.dataSource.getConnection
val db = DBSettings.getDSLContext(connection) //conf) // Using Conf to create DSL.
val t03_1 = Test03(UUID.fromString("0bec53ff-91cb-47be-8318-14dd298e1d00"), 1230, LocalDate.now(), Some(LocalDate.now()), OffsetDateTime.now(), Some(OffsetDateTime.now()), null)
db.deleteFrom(TEST03).where(TEST03.ID.eq(t03_1.id)).execute()
val t03_1RecA = db.newRecord(TEST03, t03_1)
val t03_1RecB = db.newRecord(TEST03, t03_1) // Identical id values
println(s"t03_1RecA before insert\n$t03_1RecA")
val iResA = t03_1RecA.insert()
println(s"t03_1RecA after insert\n$t03_1RecA")
if (iResA != 1) throw new RuntimeException(s"Invalid insert res A: $iResA")
t03_1RecB.changed("id",false) // Does not work without this
t03_1RecB.setIntnotnull(800)
t03_1RecB.setVer(t03_1RecA.getVer)
t03_1RecB.changed("ver",false) // Does not work without this
println(s"t03_1RecB before update\n$t03_1RecB")
t03_1RecB.update()
println(s"t03_1RecB after update\n$t03_1RecB")
connection.close()
运行时输出:
Executing query : insert into "public"."test03" ("id", "intnotnull", "datenotnull", "datenull", "timestamptznotnull", "timestamptznull", "ver") values (cast(? as uuid), ?, cast(? as date), cast(? as date), cast(? as timestamp with time zone), cast(? as timestamp with time zone), ?) returning "public"."test03"."id", "public"."test03"."intnotnull", "public"."test03"."datenotnull", "public"."test03"."datenull", "public"."test03"."timestamptznotnull", "public"."test03"."timestamptznull", "public"."test03"."ver"
-> with bind values : insert into "public"."test03" ("id", "intnotnull", "datenotnull", "datenull", "timestamptznotnull", "timestamptznull", "ver") values ('0bec53ff-91cb-47be-8318-14dd298e1d00', 1230, date '2020-06-13', date '2020-06-13', timestamp with time zone '2020-06-13 16:12:49.455824+03:00', timestamp with time zone '2020-06-13 16:12:49.455849+03:00', 1) returning "public"."test03"."id", "public"."test03"."intnotnull", "public"."test03"."datenotnull", "public"."test03"."datenull", "public"."test03"."timestamptznotnull", "public"."test03"."timestamptznull", "public"."test03"."ver"
Fetched result : +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
: |id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull | ver|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
: |0bec53ff-91cb-47be-8318-14dd298e1d00| 1230|2020-06-13 |Some(2020-06-13)|2020-06-13T16:12:49.455824+03:00|Some(2020-06-13T16:12:49.455849+03:00)| 0|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
Fetched row(s) : 1
t03_1RecA after insert
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull | ver|
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
|0bec53ff-91cb-47be-8318-14dd298e1d00| 1230|2020-06-13 |Some(2020-06-13)|2020-06-13T16:12:49.455824+03:00|Some(2020-06-13T16:12:49.455849+03:00)| 0|
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
t03_1RecB before update
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+----+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull | ver|
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+----+
|0bec53ff-91cb-47be-8318-14dd298e1d00| *800|*2020-06-13|*Some(2020-06-13)|*2020-06-13T16:12:49.455824+03:00|*Some(2020-06-13T16:12:49.455849+03:00)| 0|
+------------------------------------+----------+-----------+-----------------+---------------------------------+---------------------------------------+----+
Executing query : update "public"."test03" set "intnotnull" = ?, "datenotnull" = cast(? as date), "datenull" = cast(? as date), "timestamptznotnull" = cast(? as timestamp with time zone), "timestamptznull" = cast(? as timestamp with time zone), "ver" = ? where ("public"."test03"."id" = cast(? as uuid) and "public"."test03"."ver" = ?) returning "public"."test03"."id", "public"."test03"."intnotnull", "public"."test03"."datenotnull", "public"."test03"."datenull", "public"."test03"."timestamptznotnull", "public"."test03"."timestamptznull", "public"."test03"."ver"
-> with bind values : update "public"."test03" set "intnotnull" = 800, "datenotnull" = date '2020-06-13', "datenull" = date '2020-06-13', "timestamptznotnull" = timestamp with time zone '2020-06-13 16:12:49.455824+03:00', "timestamptznull" = timestamp with time zone '2020-06-13 16:12:49.455849+03:00', "ver" = 1 where ("public"."test03"."id" = '0bec53ff-91cb-47be-8318-14dd298e1d00' and "public"."test03"."ver" = 0) returning "public"."test03"."id", "public"."test03"."intnotnull", "public"."test03"."datenotnull", "public"."test03"."datenull", "public"."test03"."timestamptznotnull", "public"."test03"."timestamptznull", "public"."test03"."ver"
Fetched result : +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
: |id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull | ver|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
: |0bec53ff-91cb-47be-8318-14dd298e1d00| 800|2020-06-13 |Some(2020-06-13)|2020-06-13T16:12:49.455824+03:00|Some(2020-06-13T16:12:49.455849+03:00)| 1|
: +------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
Fetched row(s) : 1
t03_1RecB after update
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
|id |intnotnull|datenotnull|datenull |timestamptznotnull |timestamptznull | ver|
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
|0bec53ff-91cb-47be-8318-14dd298e1d00| 800|2020-06-13 |Some(2020-06-13)|2020-06-13T16:12:49.455824+03:00|Some(2020-06-13T16:12:49.455849+03:00)| 1|
+------------------------------------+----------+-----------+----------------+--------------------------------+--------------------------------------+----+
因此,当使用int
类型乐观锁定字段时,似乎不会发生此异常,但如果使用timestamptz
或timestamp
字段,则会由于某种原因发生。
用int
替换timestamptz
是一种解决方案,但我仍然想了解timestamptz
有什么问题。
【讨论】:
以上是关于JOOQ 乐观锁定和 DataChangedException:数据库记录已在 record.update 上更改的主要内容,如果未能解决你的问题,请参考以下文章