默认情况下,弹簧数据 JDBC 期望数据库为每个新记录提供主键值。实现此目的的最简单方法是使用自动增量列。我们在春季数据简介 JDBC 指南中使用了这一点。但是,如果您的表模型使用数据库序列,该怎么办?
当然,春季数据JDBC也可以处理这个问题。但它需要一些额外的代码。在将实体写入数据库之前,您需要从数据库序列中获取值并设置主键属性,而不是依赖于默认处理。最好的方法是实现一个转换前回调。
实现转换前回调以获取序列值
您可能已经知道其他Spring数据模块的回调机制。实体回调API在2.2版的Spring Data Commons中引入,它是官方推荐的在某些生命周期事件之前或之后修改实体对象的方法。使用 Spring 数据 JDBC 时,可以使用该机制在持久保存新实体对象时自动检索序列值。
让我们使用此方法在持久化 ChessGame 聚合之前自动从数据库序列中获取主键值。
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ChessGame { @Id private Long id; private String playerWhite; private String playerBlack; private List<ChessMove> moves = new ArrayList<>(); ... } |
如果不进行任何其他更改,以下测试用例将保留 ChessGame 聚合,并期望数据库提供主键值。如前所述,这通常是通过将主键列建模为自动增量列来实现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | ChessGame game = new ChessGame(); game.setPlayerWhite( "Thorben Janssen" ); game.setPlayerBlack( "A strong player" ); ChessMove move1white = new ChessMove(); move1white.setMoveNumber( 1 ); move1white.setColor(MoveColor.WHITE); move1white.setMove( "e4" ); game.getMoves().add(move1white); ChessMove move1Black = new ChessMove(); move1Black.setMoveNumber( 1 ); move1Black.setColor(MoveColor.BLACK); move1Black.setMove( "e5" ); game.getMoves().add(move1Black); gameRepo.save(game); |
如果要使用其他方法来生成主值,可以使用“转换前回调”进行设置。春季数据JDBC将在将国际象棋游戏聚合转换为数据库更改之前执行回调。
正如您在以下代码片段中看到的,这种回调的实现很简单。实现“转换前回调”接口,并将聚合根的类作为类型参数提供。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @Component public class GetSequenceValueCallback implements BeforeConvertCallback<ChessGame> { private Logger log = LogManager.getLogger(GetSequenceValueCallback. class ); private final JdbcTemplate jdbcTemplate; public GetSequenceValueCallback(JdbcTemplate jdbcTemplate) { this .jdbcTemplate = jdbcTemplate; } @Override public ChessGame onBeforeConvert(ChessGame game) { if (game.getId() == null ) { log.info( "Get the next value from a database sequence and use it as the primary key" ); Long id = jdbcTemplate.query( "SELECT nextval('chessgame_seq')" , rs -> { if (rs.next()) { return rs.getLong( 1 ); } else { throw new SQLException( "Unable to retrieve value from sequence chessgame_seq." ); } }); game.setId(id); } return game; } } |
在实现接口时,您应该定义一个需要 Jdbc 模板的构造函数。Spring将使用与当前事务关联的模板来调用它。然后,您可以在“前转换”方法的实现中使用该 Jdbc 模板。
弹簧数据 JDBC 为所有插入和更新操作触发转换前回调。因此,在实现 onBefore 方法时,应检查主键属性是否为空。如果是这种情况,我们将保留一个新的聚合,并且需要生成一个唯一的主键值。为此,可以使用 JdbcTemplate 执行从数据库序列中获取下一个值的 SQL 语句,并将该值设置为主键。
这就是您需要做的所有事情。如果重新运行相同的测试用例,则可以看到由 GetSequenceValueCallback 编写的消息和 SQL 语句,用于从日志输出中的数据库序列中获取值。
1 2 3 4 5 6 7 8 | 16:00:22.891 INFO 6728 - – [ main] c.t.j.model.GetSequenceValueCallback : Get the next value from a database sequence and use it as the primary key 16:00:22.892 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL query [ SELECT nextval( 'chessgame_seq' )] 16:00:22.946 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL update 16:00:22.947 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [ INSERT INTO "chess_game" ( "id" , "player_black" , "player_white" ) VALUES (?, ?, ?)] 16:00:22.969 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys 16:00:22.970 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [ INSERT INTO "chess_move" ( "chess_game" , "chess_game_key" , "color" , "move" , "move_number" ) VALUES (?, ?, ?, ?, ?)] 16:00:22.979 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing SQL update and returning generated keys 16:00:22.980 DEBUG 6728 - – [ main] o.s.jdbc.core.JdbcTemplate : Executing prepared SQL statement [ INSERT INTO "chess_move" ( "chess_game" , "chess_game_key" , "color" , "move" , "move_number" ) VALUES (?, ?, ?, ?, ?)] |
结论
默认情况下,弹簧数据 JDBC 期望数据库为每个聚合提供唯一的主键值。大多数 DBA 为此使用自动增量列。
正如您在本文中所看到的,您可以通过实现之前转换回调来轻松提供自己的主键生成。春季数据 JDBC 在保留或更新聚合时会自动调用它。因此,您需要检查是否需要生成主键值。如果是这种情况,则可以使用 Jdbc 模板执行一个简单的 SQL 语句,该语句从数据库序列中获取下一个值。