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. Но у нас легаси база и там это не заработало.