Liferay Service Builder未获取最新自增ID致主键冲突问题
这个问题的核心原因是Service Builder的id-type="increment"是在应用层面(通过Liferay的Counter表)维护自增序列,而非依赖数据库的AUTO_INCREMENT机制。当Module2直接用原生SQL插入数据时,数据库的AUTO_INCREMENT值会自动增长,但Service Builder缓存的序列值(存在Counter表)并没有同步更新,导致两边的主键序列脱节,最终触发重复主键异常。
下面是几种可行的解决方案,按推荐优先级排序:
1. 修改Service Builder的主键类型为identity(推荐)
这是最彻底的解决方式,让Service Builder完全依赖数据库的自增主键机制,和Module2的原生SQL插入行为保持一致。
修改service.xml中xyzId列的配置:
<entity name="XYZ" local-service="true" remote-service="false" cache-enabled="false" > <column name="xyzId" type="long" primary="true" id-type="identity"></column> <column name="x" type="long"></column> <column name="y" type="long"></column> <column name="z" type="long"></column> </entity>
修改完成后,重新执行Service Builder的构建命令(比如ant build-service或Maven对应构建目标),生成新的Service层代码。
为什么这能解决问题?
当id-type="identity"时,Service Builder在插入数据时不会提前生成主键,而是让数据库通过AUTO_INCREMENT自动生成主键,插入后再将生成的ID返回给实体对象。这样不管是Module1通过Service API插入,还是Module2通过原生SQL插入,都共用数据库的自增序列,不会出现冲突。
2. 手动同步Service Builder的Counter缓存(兼容旧代码场景)
如果因为历史原因无法修改id-type,可以在Module1插入数据前,手动将Service Builder的Counter值同步到数据库最新的AUTO_INCREMENT值。
实现步骤:
- 先查询数据库中XYZ表当前的AUTO_INCREMENT值:
private long getCurrentAutoIncrement() throws SQLException { String sql = "SELECT AUTO_INCREMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'myDatabase' AND TABLE_NAME = 'XYZ'"; try (Connection conn = DriverManager.getConnection(dbUrl, dbUser, dbPass)) { try (Statement stmt = conn.createStatement()) { ResultSet rs = stmt.executeQuery(sql); if (rs.next()) { return rs.getLong("AUTO_INCREMENT"); } } } return 0; }
- 更新Liferay的Counter值,同步到数据库的自增值:
// 获取数据库最新的自增值 long currentAutoInc = getCurrentAutoIncrement(); // 更新Counter表中对应实体的序列值 CounterLocalServiceUtil.setCounter(XYZ.class.getName(), currentAutoInc); // 然后执行插入操作 XYZ xyz = XYZLocalServiceUtil.createXYZ(0); xyz.setX(x); xyz.setY(y); xyz.setZ(z); xyz = XYZLocalServiceUtil.addXYZ(xyz);
注意事项:
- 这种方式存在并发风险,如果在查询和更新Counter之间有其他操作插入数据,可能还是会出现冲突。
- 需要确保Module1有数据库查询的权限,并且处理好JDBC连接的资源释放。
3. 统一数据插入入口(理想场景)
如果Module2可以调整,建议统一通过Module1的Service Builder API来插入数据,避免直接操作数据库。这样所有的插入操作都走同一个序列维护逻辑,从根源上杜绝冲突。但如果Module2无法依赖Liferay,这个方案就不可行。
内容的提问来源于stack exchange,提问作者Navankur Chauhan

