默认情况下,Spring Data JDBC希望数据库为每个新记录提供一个主键值。实现这一目标的最简单方法是使用一个自动递增的列。我们在《Spring Data JDBC简介》指南中使用了这个方法。但是,如果你的表模型使用了数据库序列,你该怎么办呢?
当然,Spring Data JDBC也可以处理这个问题。但这需要一些额外的代码。你需要从数据库序列中获取值,并在实体被写入数据库之前设置主键属性,而不是依赖默认的处理方式。做到这一点的最好方法是实现BeforeConvertCallback。
实现BeforeConvertCallback 来获取序列值
你可能已经知道其他Spring Data模块的回调机制。实体回调API在2.2版本中被引入Spring Data Commons,它是官方推荐的在某些生命周期事件之前或之后修改实体对象的方式。在使用Spring Data JDBC时,你可以使用该机制在持久化一个新实体对象时自动检索序列值。
让我们使用这种方法,在持久化ChessGame 聚合之前,从数据库序列中自动获取一个主键值:
public class ChessGame {
@Id
private Long id;
private String playerWhite;
private String playerBlack;
private List<ChessMove> moves = new ArrayList<>();
...
}
在没有任何额外改动的情况下,下面的测试案例会持久化ChessGame聚合,并期望数据库提供一个主键值。如前所述,这通常是通过将主键列建模为一个自动递增的列来实现:
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);
如果你想使用不同的方法来生成主键值,你可以使用BeforeConvertCallback来设置它。Spring Data JDBC在将ChessGame聚合转换为数据库变化之前会执行该回调。
正如你在下面的代码片段中看到的,这样的回调的实现很简单。你实现BeforeConvertCallback 接口并提供你的聚合根的类作为类型参数:
@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;
}
}
实现该接口时,你应该定义一个期待JdbcTemplate的构造函数。Spring会用一个与当前事务相关的模板来调用它。然后你可以在onBeforeConvert 方法的实现中使用该JdbcTemplate。
Spring Data JDBC会对所有的插入和更新操作触发BeforeConvertCallback。因此,在实现onBeforeConvert 方法时,你应该检查主键属性是否为空。如果是这样的话,我们要持久化一个新的聚合,需要生成一个唯一的主键值。你可以通过使用JdbcTemplate来执行一个SQL语句,从数据库序列中获取下一个值,并将该值设置为主键。
这就是你需要做的一切。如果你重新运行同一个测试案例,你可以在日志输出中看到由GetSequenceValueCallback 和从数据库序列中获取值的SQL语句编写的消息。
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 (?, ?, ?, ?, ?)]
结论
默认情况下,Spring Data JDBC期望数据库为每个聚合提供一个唯一的主键值。大多数DBA为此使用了一个自动递增的列。
正如你在这篇文章中所看到的,你可以通过实现BeforeConvertCallback轻松地提供你自己的主键生成。Spring Data JDBC在持久化或更新聚合时自动调用它。由于这个原因,你需要检查你是否需要生成主键值。如果是这样的话,你可以使用JdbcTemplate 来执行一个简单的SQL语句,从数据库序列中获取下一个值。