По умолчанию Spring Data JDBC ожидает, что первичные ключи сущностей генерируются на стороне базы данных. В статье Introduction to Spring Data JDBC (Введение в Spring Data JDBC) мы использовали вариант с автоинкрементной колонкой, а в этой статье рассмотрим другой способ – использование последовательностей (sequence).
Spring Data JDBC, конечно, справится и с этим, но придется написать чуть больше кода: получить из базы данных очередное значение последовательности и установить первичный ключ перед сохранением сущности в базе данных. Это можно сделать, реализовав BeforeConvertCallback
.
Реализация BeforeConvertCallback
Возможно, вы уже знакомы с механизмом колбэков (callback) в других модулях Spring Data. Entity Callback API появился в Spring Data Commons 2.2 и это официально рекомендуемый способ модификации объектов до или после определенных событий жизненного цикла. В 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
. Этот колбэк выполнится перед сохранением 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
. Полученный JdbcTemplate
мы можем использовать в реализации метода onBeforeConvert
.
Spring Data JDBC выполняет BeforeConvertCallback
как для операций INSERT, так и для UPDATE. Поэтому в методе onBeforeConvert
следует проверить первичный ключ на null. В случае null присваиваем новое значение первичному ключу. Значение ключа получаем выполнив SQL-запрос через JdbcTemplate
.
Это все, что нужно сделать. При повторном запуске примера, приведенного выше, в логе вы увидите сообщения от 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 по умолчанию ожидает, что первичный ключ генерируется на стороне базы данных. Часто для этого используется столбец с автоинкрементом.
В этой статье мы рассмотрели, как создать собственный генератор значений первичного ключа, реализовав BeforeConvertCallback
. Spring Data JDBC автоматически вызывает его при сохранении и обновлении агрегата, поэтому необходимо проверять необходимость генерации нового значения первичного ключа. Получить очередное значение последовательности из базы данных можно, выполнив SQL-запрос используя JdbcTemplate
.
Приглашаем всех желающих на открытое занятие «Сборщик мусора в Java», на котором обсудим:
- Java Memory Model;
- 3 стадии и 2 поколения сборки мусора;
- Карьера и гибель объектов.
Регистрация на урок.
Комментарии (4)
ProstakovAlexey
09.09.2022 19:24Отличное решение для jdbc. Но можно ли его улучшить, чтобы не каждый раз последовательность дергать, а получить штук 100 ключей за раз и использовать по необходимости? И интересно какие еще callback есть.
feoktant
09.09.2022 23:15Описано в PoEAA, Chapter 12, Example: Using a Key Table (Java). Примерно так.
feoktant
09.09.2022 23:12+1Максимально странный пример. Для Postgres автоинкрементная колонка - это и есть созданная под капотом sequence, из которой берется значение. Логичнее было бы взять Oracle/SQL Server.
Вариант 2 примера - использовать одну последовательность для всех ключей в БД, но зачем же так делать.
В MySQL последовательностей нет, поэтому говорить о БД агностик технике нельзя.
GaDzik
Клева. А что лучше JDBC или Hibernate?