Spring Data JDBC и генерация ID

от автора

Каждый раз, когда необходимо сделать сервис на Java, работающий с реляционной базой, я не могу определиться, прямо как та обезъяна, которая хотела быть и умной, и красивой. Хочется делать запросы на обычном SQL, по-минимуму обкладываясь различными «магическими» аннотациями, но при этом лень самому писать RowMapper’ы, готовить PreparedStatement’ы или JdbcTemplate, и тому подобное, за что любят обзывать Java многословной. И каждый раз руки тянутся к Spring Data JDBC, который, вроде как, и был задуман как нечто среднее. Но с ним тоже, зачастую, можно вляпаться в какую-то ерунду на ровном месте.

Потребовалось мне сохранять новые записи в таблицу. Казалось бы, в чем вопрос — берешь CrudRepository и все у тебя работает из коробки. Но на практике возникло несколько нюансов, например:

  • прежде всего, надо теперь активно использовать целую пачку аннотаций для разметки энтити (@Id, @Table, @Column, @InsertOnlyProperty, и т.д.)

  • я предпочитаю использовать records для хранения данных, а в этом случае получается, что надо для поля id делать отдельный метод withId, котрый вернет новую рекорду с заполненым id.

  • хочется получать id из соответствующей sequence в базе данных

Вот на последнем пункте я и хотел бы более детально остановиться. Приученный к плохому хорошему в JPA, я ожидал, что настройка генерации поля id в Spring Data JDBC делается такими же настройками стратегии генерации. Но нет, читаем документацию и выясняем, что Spring Data JDBC умеет работать только со столбцами с автоинкрементом. Для всего остального предлагается использовать BeforeConvert listener. За деталями пришлось идти к всезнайке Google.

Google первой строкой выдал мне ссылку на блог некоего Thorben Janssen (заранее извиняюсь, если это кто-то известный, а я его не знаю — у меня плохая память на имена). И посмотрев на пример кода, я, если честно, немного офи.. удивился. До этого, все запросы в SpringData JDBC выглядели чистенько и аккуратненько, а тут снова JdbcTemplate и ручной парсинг результата.

Я не поверил и полез смотреть примеры от самого Spring в GitHub. Их пример, хоть и выглядит чуть чище, но все равно это ручная работа с JDBC :

@Bean BeforeConvertCallback<Customer> idGeneratingCallback(DatabaseClient databaseClient) {      return (customer, sqlIdentifier) -> {  if (customer.id() == null) {  return databaseClient.sql("SELECT primary_key.nextval") // .map(row -> row.get(0, Long.class)) // .first() // .map(customer::withId); }  return Mono.just(customer); }; }

Возникает вопрос — если уж все равно надо самому писать дополнительный запрос к базе, чтоб получить значени для id и вставлять его в энтити перед сохранением, то почему бы не сделать все это более явно и в едином стиле с другими запросами? В итоге у меня получился вот такой вариант:

public record MyEntity(long id, String someData, ...) {}   @org.springframework.stereotype.Repository public interface MyRepository extends Repository<MyEntity, Long> {      @Query("SELECT nextval('myentity_seq')")     long getNextMyEntityId();      @Modifying     @Query("INSERT INTO my_entities (id, some_data) VALUES (:#{newEntity.id}, :#{newEntity.someData})")     boolean insert(@Param("newEntity") MyEntity newEntity); }   @Service public class MyService {     private final MyRepository repository;              public long saveNewEntity(String someData) {         var entity = new MyEntity(                 repository.getNextMyEntityId(),                 someData         );         if (repository.insert(entity)) {             return entity.id();         }         throw new RuntimeException("Can't save");     } }

Мне кажется, такой вариант лучше, поскольку

  • логика по формированию новой энтити в одном месте, а не разнесена по разным бинам;

  • все запросы находятся в одном месте и выполнены в едином стиле;

  • все аннотации, относящиеся к фреймворку тоже собраны в одном месте (в репозитории), а сам класс с данными абсолютно чистый.

А вы что думаете?

PS:

Изначально я этим всем заморочился только из-за того, что мне требовалось вернуть id созданной записи. Потому как иначе, запрос на вставку превращался бы в что-то типа:

INSERT INTO my_entities (id, some_data) VALUES (nextval('myentity_seq'), :someData)

И первая мысль была — воспользоваться средствами СУБД с помощью insert with returning. Но у нас легаси база и там это не заработало.


ссылка на оригинал статьи https://habr.com/ru/post/709848/