违反约束异常处理
基本概念
在日常项目开发中,有一个非常头疼的问题,就是将因违反数据库约束的错误信息翻译为对用户友好的错误信息。
以无法简单地通过输入数据验证为准,数据库存在如下三种约束,一旦被违背难以处理:
-
主键约束
-
唯一约束 (或唯一索引)
-
外键约束
为了统一不同的数据库产品,SQL标准规定因违反约束而导致的错误类别码为23 (integrity constraint violation),
所有于此相关的错误state都以23
开头。
但是,标准的规范化也仅限于此。至于更细节的问题,譬如:
-
被违背的约束是什么
-
如何按照约束名从数据库字典中获取发生错误的表名和列名
-
最重要的问题,究竟是哪条数据的修改行为导致了异常
不同数据库产品行为并不统一,并缺乏清晰的API获取这些信息。
为了给予用户清晰的错误描述,很多业务项目采用一种简单粗暴方法,在执行修改前,
稍后即将执行的操作是否安全。然而,这种这种事先预判行为的缺点很多:
-
开发人员需要不厌其烦地编写各种事先预判代码,开发成本居高不下
-
仅适合简单的单条数据DML修改行为,对保存指令这种递归批量保存深层次数据结构的高级行为完全不适合
-
性能低,发生错误是小概率事件,每次都事先预判是一种浪费
因此,Jimmer采用如下策略
-
采用事后调查的方式,先直接修改数据库,如果事后发现数据库报告了约束被违背的错误,再调查错误原因
-
在错误调查过程中,利用查询获取尽可能多的信息,将
-
导致非法操作的对象在被保存数据结构中的路径
-
导致错误的实体类型和相关属性名
-
导致错误的具体对象和相关数据
全部报告给用户
-
-
允许用户自定义异常翻译器,将Jimmer通过调查得到的异常进一步翻译成对最终用户友好的信息
调查数据库错误这个行为,既可以因根对象的保存失败而发生,也可以因关联对象的保存失败而发生。Jimmer对二者一视同仁,没有差异。
然而,为了简化文档,本文的例子尽量示范保存相对简单的对象,避免保存过深的数据结构 (事实上,如果保存深层次的关联对象失败,一样会被调查)。
检查Id是否冲突
- Java
- Kotlin
List<Book> books = Arrays.asList(
Immutables.createBook(draft -> {
draft.setId(100L);
draft.setName("SQL in Action");
draft.setEdition(1);
draft.setPrice(new BigDecimal("59.9"));
draft.setStoreId(2L);
}),
Immutables.createBook(draft -> {
draft.setId(7L); // Exists
draft.setName("LINQ in Action");
draft.setEdition(3);
draft.setPrice(new BigDecimal("49.9"));
draft.setStoreId(2L);
})
);
sqlClient.insertEntities(books);
val books = listOf(
Book {
id = 100L
name = "SQL in Action"
edition = 1
price = BigDecimal("59.9")
storeId = 2L
},
Book {
id = 7L // Exists
name = "LINQ in Action"
edition = 3
price = BigDecimal("49.9")
storeId = 2L
}
)
sqlClient.insertEntities(books)
这段代码会生产如下两条SQL
-
批量插入数据
- 绝大部分数据库
- Mysql
insert into BOOK(
ID, NAME, EDITION, PRICE, STORE_ID
) values(?, ?, ?, ?, ?)
/* batch-0: [100, SQL in Action, 1, 59.9, 2] */
/* batch-1: [7, LINQ in Action, 3, 49.9, 2] */警告默认情况下,MySQL的批量操作不会被采用,而采用多条SQL。具体细节请参考MySQL的问题
-
insert into BOOK(
ID, NAME, EDITION, PRICE, STORE_ID
) values(
? /* 100 */,
? /* SQL in Action */,
? /* 1 */,
? /* 59.9 */,
? /* 2 */
) -
insert into BOOK(
ID, NAME, EDITION, PRICE, STORE_ID
) values(
? /* 7 */,
? /* LINQ in Action */,
? /* 3 */,
? /* 49.9 */,
? /* 2 */
)
其中,插入第二条数据会导致
id
发生冲突 -
调查违反约束的原因
- 场景1
- 场景2
Purpose: COMMAND(INVESTIGATE_CONSTRAINT_VIOLATION_ERROR)
SQL: select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.ID = ? /* 7 */Purpose: COMMAND(INVESTIGATE_CONSTRAINT_VIOLATION_ERROR)
SQL: select
tb_1_.ID
from BOOK tb_1_
where
tb_1_.ID = any(? /* [100, 7] */)