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