Каждый раз, когда необходимо сделать сервис на 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/
Добавить комментарий