问题描述
现有一个实体类,内容如下:
kotlin
@Entity(name = "users")
class User: Base() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(
name = "id",
unique = true,
nullable = false
)
@SerializedName("user_id")
var id: Long = 0
@Column(
name = "custom_id",
unique = true,
nullable = false
)
@SerializedName("custom_id")
var customId: String = ""
@Column(
name = "username",
unique = true,
nullable = false
)
@SerializedName("username")
var username: String = ""
/** 其他字段 **/
}
现有一个业务设计如下:在用户首次注册时, custom_id
字段的内容与 user_id
的数值相同。但是显然, user_id
是一个由数据库顺序生成的字段,在首次插入数据库时其数值对于我们来说是未知的。经查阅资料,我们得知, MySQL 中的触发器功能可以实现这一需求:
MySQL 中的触发器(Trigger)是一种数据库对象,它在特定的数据库事件发生时自动执行。触发器可以在以下事件中触发:
- INSERT - 当一条新记录插入表时触发。
- UPDATE - 当表中的现有记录被更新时触发。
- DELETE - 当表中的记录被删除时触发。
触发器可以在事件发生的之前(BEFORE)或之后(AFTER)执行,具体取决于业务需求。例如,可以使用
BEFORE INSERT
触发器为某个字段设置默认值,或者使用AFTER UPDATE
触发器进行日志记录。主要用途包括:
- 数据完整性:确保在插入、更新或删除时满足特定条件。
- 自动化操作:如自动填充字段、生成日志、更新相关表等。
- 审计与日志:记录对数据库的更改历史。
创建触发器时需要注意性能和管理问题,因为复杂的触发器可能对数据库性能产生影响。此外,每张表每个事件类型只能定义一个触发器。
示例语法:
sqlCREATE TRIGGER trigger_name BEFORE INSERT ON table_name FOR EACH ROW BEGIN -- 触发器逻辑 END;
在开发中,触发器经常用于处理复杂的业务逻辑,保证数据的一致性和完整性。
需要用到的触发器代码如下:
sql
DROP TRIGGER IF EXISTS set_custom_id_after_insert;
CREATE TRIGGER set_custom_id_after_insert
AFTER INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.custom_id IS NULL OR NEW.custom_id = '' THEN
UPDATE users
SET custom_id = NEW.id
WHERE id = NEW.id;
END IF;
END;
我们知道,SpringBoot Application 在启动过程中会扫描 resources
文件夹下的 schema.sql
和 data.sql
并执行,因此理论上我们可以将这段 SQL 丢到这两个文件的任意一个里面,让 Spring 框架去处理,但是有些事情是超出理论之外的🤣。
我们先了解一下前面提到的几个文件在启动流程中的执行先后顺序(当然这跟本次问题没什么关系(虽然我一开始错误的以为这引发了问题),就当科普了):
schema.sql
> data.sql
> 初始化 JPA,开始扫描实体类和关联的 @Entity 注解,基于实体类的定义来检查或自动生成数据库表结构
根据这个顺序,我们可以想一下一个问题:如果这个项目是在一个全新的机器上运行(先前没配置过数据库表结构)(我这个项目是依赖 JPA 自动生成数据库表结构的),那就会出现触发器始终注册不上的问题。
但是在我的开发环境中,我是先在我本地数据库中创建过数据库表了的 ,也就是说不应该存在因为先后顺序问题导致触发器无法注册的问题的。但是如你所见,执行查询操作发现触发器没有正确注册:
也就是说,通过使用 Spring 框架提供的自动扫描执行功能不能正常注册触发器(此时我还是认为是先后顺序导致的问题)。
随后我考虑了一些数据库迁移工具(如 Flyway
、Liquibase
等)。(具体操作步骤在此处不赘述,用过的应该了解)
直接说结果:
Flyway 不支持 MySQL 8.4 (我一开始用的是 MySQL 9.0,确实挺高的,结果降级了也不支持)
Liquibase 会报一个 SQL Syntax Error
:
plaintext
Caused by: liquibase.exception.DatabaseException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 9 [Failed SQL: (1064) CREATE TRIGGER set_custom_id_after_insert
AFTER INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.custom_id IS NULL OR NEW.custom_id = '' THEN
UPDATE users
SET custom_id = NEW.id
WHERE id = NEW.id;
END IF]
at liquibase.executor.jvm.JdbcExecutor$ExecuteStatementCallback.doInStatement(JdbcExecutor.java:473)
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:80)
at liquibase.executor.jvm.JdbcExecutor.execute(JdbcExecutor.java:182)
at liquibase.executor.AbstractExecutor.execute(AbstractExecutor.java:141)
at liquibase.database.AbstractJdbcDatabase.executeStatements(AbstractJdbcDatabase.java:1176)
at liquibase.changelog.ChangeSet.execute(ChangeSet.java:764)
... 58 common frames omitted
但是实际上我的语法是没问题的(因为我在手动执行 SQL 的时候是没有报错的)
最后,我尝试在 @PostConstruct 方法中执行 SQL 语句,也会报一个和上面 Liquibase 一样的错误。
其执行时机是这样的:
- Spring 容器创建 Bean 实例:Spring 容器首先根据配置创建 Bean 实例。
- 依赖注入:Spring 执行依赖注入,将通过 @Autowired 或构造函数注入的依赖注入到 Bean 中。
- 执行 @PostConstruct 方法:依赖注入完成后,Spring 容器会自动调用被 @PostConstruct 注解的方法,完成一些初始化逻辑。
- Bean 准备就绪:在 @PostConstruct 方法执行之后,Bean 已经完全初始化,可以开始处理请求。
在 @PostConstruct 方法中,我先后尝试了 JdbcTemplate
和 EntityManager
,均会触发报错。
也就是说,通过框架代码执行注册触发器的操作行不通,但是这个触发器还必须要在业务开始处理前完成注册。总不可能每次都叫人手动执行吧?求大佬指教,我是否漏掉了任何可行的自动处理的方案。
各依赖项的版本信息
SpringBoot
3.3.2
MySQL
8.4.2 Homebrew
Flyway
10.18.1
Liquibase
implementation("org.liquibase:liquibase-core")
(这个没看版本,因为试了一下也存在前面其他方法同样的问题)