По умолчанию 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)


  1. GaDzik
    09.09.2022 19:19

    Клева. А что лучше JDBC или Hibernate?


  1. ProstakovAlexey
    09.09.2022 19:24

    Отличное решение для jdbc. Но можно ли его улучшить, чтобы не каждый раз последовательность дергать, а получить штук 100 ключей за раз и использовать по необходимости? И интересно какие еще callback есть.


    1. feoktant
      09.09.2022 23:15

      Описано в PoEAA, Chapter 12, Example: Using a Key Table (Java). Примерно так.


  1. feoktant
    09.09.2022 23:12
    +1

    Максимально странный пример. Для Postgres автоинкрементная колонка - это и есть созданная под капотом sequence, из которой берется значение. Логичнее было бы взять Oracle/SQL Server.

    Вариант 2 примера - использовать одну последовательность для всех ключей в БД, но зачем же так делать.

    В MySQL последовательностей нет, поэтому говорить о БД агностик технике нельзя.