Комментарии 38
Обратите внимание на JOOQ в следующий раз. Ноль магии аннотаций, максимум помощи строгой типизации. Вот код, который инсертит и возвращает ID новых строк. Это Котлин, но разницы никакой.
val newIds: List<Long> = dsl.insertInto(TABLE_NAME)
.columns(COLUMN1, COLUMN2, ...)
.values(value1, value2, ...)
.onConflictDoNothing()
.returning(ID)
.fetch()
.map{it[ID]}
К слову, можно возвращать строки целиком, если есть задача сразу же вернуть новые энтити. И классы энтити аннотировать никак не надо.
Упустил момент про легаси базу. Но тогда можно вызвать сначала nextval. В общем в итоге получается ровно то же, что у вас.
val newId = dsl.nextval(MY_TABLE_SEQ)
dsl.insertInto(TABLE_NAME)
.columns(ID, COLUMN2, ...)
.values(newId, value2, ...)
.execute()
return newId
JOOQ пока ни разу нигде не попадался, потому не могу его сравнить со спринговым JDBC. Но в приведенном примере не увидел, какое преймущество он добавляет. При этом запрос на SQL, на мой взгляд, проще поддерживать, т.к. SQL больше людей знает, чем api JOOQ
Сила JOOQ в том, что синтаксис запроса к БД условно проверяет компилятор. При рефактоинге модели данных у вас код не скопилируется.
У вас нет скрипта нактывающего чистую базу? Мы например для генерации jooq классов просто написали небольшой скриптик, который поднимает чистую бд в докере через testcontainers, накатывает flyway миграции, и запускает генерилку jooq'а.
Попробуйте чисто ради тяги к открытиям. Я для себя такие плюсы JOOQ отметил:
Нет ничего неявного. Ты четко видишь запрос и четко понимаешь, когда он выполнится и что будет потом
Нет рефлексии и программирования на аннотациях
При некоторой помощи Котлина можно очень красиво оформлять билдинг сложных кондишнов, когда множество клауз опциональны и зависят от чего-то
Помощь от компилятора, как отметили в соседнем комменте
Полный контроль над маппингом из кортежа в джава-объект
Когда совсем уж хитровылизанный запрос, то можно в обход DSL записать его как обычный SQL-квери
Недостатки:
Писать вычурные аналитические запросы с CTE и оконками неудобно. Больше воюешь с DSL, а не с самой задачей. Но в то же время я еще ни разу не сдавался и таки писал запрос на DSL.
Раньше джук писал запросы, которые ок для Пострегрса, но не ок для Кликхауса. Буквально лишняя пара круглых скобок - и Кликхаус не может распарсить вопрос. Но это наверное уже исправили или к Клике или в Джуке.
хочется получать id из соответствующей sequence в базе данных
я ожидал, что настройка генерации поля id в Spring Data JDBC делается такими же настройками стратегии генерации.
Я наверное не понял в чем сложности.
Вот кусок кода в SbpingBoot приложении (аналогичный в без Spring c чистым hibernate).
Работающий кусок
работает и с Oracle и PostgreSQL базой.
Обычный SEQUENCE "CREATE SEQUENCE SBPAY_SEQ.."
Правда не совсем стандартно (вызов hibernate) для Spring.
@Entity
@Table(name = "SBPAY_C2B")
@Setter
@Getter
@ToString
public class C2BTable implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seq_gen")
@SequenceGenerator(name = "seq_gen", sequenceName = "SBPAY_SEQ", allocationSize = 1)
@Column(name = "ID", updatable = false, nullable = false)
private Long id;
......
@Service
public class C2BTableService {
@PersistenceContext private EntityManager em;
@Transactional
public void save(C2BTable table) {
if (table.getId() == null) {
log.trace("save new:" + table);
em.persist(table);
} else {
log.trace("update:" + table);
em.merge(table);
}
}
Как раз jpa (hibernate) и не хочется использовать..
я могу и ошибаться, но мне казалось, что Repository автоматически тянет за собой hibernate (код, инициализация и пр.). Использовать hibernate (через Repository) исключительно для native запросов - это как то наплевать на потребляемы программой ресурсы. Лучше уж голый JDBC сразу.
Хотя.. хозяин - барин.
nextval(
Если конечно заведомо знать, что работаешь с конкретной БД, то можно и хардкодить синтаксис.
Но может оказаться что в других БД (Oracle, в частности) другой синтаксис получения значения из sequence
Если использовать SQL для запросов, то, конечно, придется затачиваться под конкретную базу данных. Но если при этом все запросы собраны в одном месте, а не разбросаны по коду, то миграция на другую базу не занимает много времени.
Зато полная свобода в оптимизации запросов с учетом специфики конкретной базы
public record MyEntity
Выглядит как оксюморон в контексте Domain-Driven Design.
DDD различает и противопоставляет объекты-значения (value objects), которые иммутабельны и не имеют идентичности, и сущности (entity), которые могут быть различными при одинаковых значениях полей, и поэтому имеют идентификаторы.
Насколько я понимаю, record в java - это для объектов-значений. У них не может быть идентификатора (у них нет идентичности: 2 record с одинаковыми полями не могут различаться), и нет проблем с его установкой.
Есть ощущение что вы пытаетесь сидеть на двух стульях - с одной стороны хочется чтобы как ORM - Repository, Entity и работа на уровне объектов, а с другой чтоб было видно конкретный sql и писать по-сути его. Если хочется именно sql то для вашей задачи у Connection (https://docs.oracle.com/en/java/javase/19/docs/api/java.sql/java/sql/Connection.html) есть метод prepareStatement(String sql, int autoGeneratedKeys), он похоже что делает то что вам нужно. Подозреваю что все развитые библиотеки-помошники в работе с JDBC вкурсе об этой возможности и скорее всего у них есть более удобный способ утилизировать эти возможности.
> вы пытаетесь сидеть на двух стульях
именно ) И пока Spring Data JDBC мой основной кандидат на средство, которое позволит этого достичь.
> Подозреваю что все развитые библиотеки-помошники в работе с JDBC вкурсе об этой возможности
Собственно удивление, что Spring Data JDBC это не умеет и предлагает это решать, на мой взгляд, кривовато, и стало причиной этой статьи
А чем вас не устраивает использование JPA? Хочется на уровне энтити работать - пожалуйста, хочется запрос какой-то особенный - есть native query
Все ради того чтобы экономить строчки на аннотациях?
Позволю себе небольшой совет - попробуйте сделать пет проект на JPA с чистого листа, используя рекомендуемые сообществом best practice и не пытаясь принести туда привычные вам наработки в виде кастомных sequence и record в виде entity.
Это позволит продвинуться дальше и понять, к примеру, что entity по задумке фреймворка мутабельный объект и генерация ID это еще малая беда. Дальше можно словить проблемы с Lazy Loading, апдейту связей итд.
Этим всем конечно можно не пользоваться, но тогда встает вопрос нужен ли вам этот JPA.
Я вот им пользуюсь много лет в проде, но до сих пор для себя не ответил однозначно на этот вопрос.
Я очень много использовал JPA и использую, когда это оправдано. JPA это идеальное решение для CRUD сервисов.
Но когда нужны сложные запросы к базе, то даже на голом jdbc получается проще писать и поддерживать. Именно поэтому Spring Data JDBC манит своей идеей получить преймущества из обоих миров
Там еще веселье будет, если нужно много записей вставлять. Hibernate может делать что-то типа кэша ID, так что не бегает за каждым ID в базу. Если такого добиваться вручную, придется немного повозиться.
А еще с batch операциями придется повозиться, кстати.
Как раз таки JPA абсолютно избыточное решение для CRUD. Если вы используете ORM для CRUD, значит вы не поняли суть и философию ORM, как и суть Entity.
На мой взгляд, задача классического ORM (и JPA в частности) - "синхронизировать" состояние объектов приложения (энтити) с соответствующими записями в таблицах базе, учитывая все их связи между собой. А это, по сути, и есть CRUD операции.
Поэтому очень хотелось бы, чтоб вы как-то более детально раскрыли ваше видение философии ORM и суть энтити
Spring Data Jdbc, Jooq, MyBatis, ... Все они тоже "синхронизируют" состояние объектов с записями в таблицах, учитывая их связи. Но они не ORM!
Иногда встречаю людей, которые считают Jooq/MyBatis ORM библиотеками, только потому что ORM это про маппинг, а они как раз таки автоматически конвертируют resultset в entity объект, да ещё и с поддержкой связей.
Суть ORM в реализации паттерна Unit Of Work, в object-level транзакционности (бизнес транзакция). ORM позволяет реализовывать бизнес-логику (не путать с use-case логикой приложения) без переживаний "а как и когда это сохранится в базу". ORM позволяет выстраивать логику так, словно никакого хранилища нет вовсе. Это кардинально отличается от CRUD.
Понятное дело, что при комите транзакции в БД уйдут insert/update/select/delete запросы, но с таким подходом можно любую библиотеку для доступа к данным называть ORM!
CRUD сервисы, как правило, это сервисы с очень простой бизнес логикой или вовсе без неё. Классическая слоенка контроллер-сервис-репозиторий, где даже сервисный слой зачастую лишний. Такие сервисы настолько просты, что придумали даже Spring Data Rest. Само собой, и в таких сервисах можно ORM использовать, но тогда ORM будет использоваться исключительно как маппер с автоматическим билдингом запросов. Минимум телодвижений для реализации персистентности в сервисе.
Если же мы говорим о приложениях со сложной размашистой бизнес логикой (привет монолитам и микромонолитам), в которых граф сущностей не просто удобный доступ к данным в таблицах БД, а необходимость для реализации всех бизнес правил с сохранением консистентности данных на уровне бизнес транзакции (не путать с БД транзакцией), то добро пожаловать в гости, ORM.
Касательно сущностей, я всегда сталкиваюсь с мнением, что "entity = таблица". Почему-то разработчики даже не задумываются о том, что к одной и той же таблице можно сделать разные Entity под разные контексты бизнес логики. А когда их сущности разростаются десятками связей, начинаюи ругать Hibernate за тормознутость.
Наверное, для данной дискуссии нужно больше предметности, примеры приложений.
Спасибо, теперь я понял в чем различие наших взглядов. Просто я к CRUD отношу еще сервисы, которые что-то берут из базы, меняют и сохраняют изменения обратно. Т.е. когда у нас объекты-энтити мутабельные. Для таких сценариев да, ORM идеальное решение.
А вот Spring Data Jdbc как раз не про "синхронизацию" с базой. Это просто более простой способ делать jdbc запросы и мапить результаты на объекты. Тут нет энтити в понимании ORM, т.к. у них нет жизненного цикла. Поэтому для сервисов, в которых база, это лишь как еще одна внешняя система (например, некий аналитический сервис), я и предпочитаю использовать немутабельные ентити и что-то полегче, чем полноценный ORM.
Представьте, что база не ваша, и вообще легаси. Какие sequence дали, такими и приходится пользоваться. Ваш подход вполне осмысленный, но не универсальный (как наверное и любой другой).
В postgres можно делая INSERT вернуть значение последовательности:
@Data
@Accessors(chain = true)
public static class Note {
private Long id;
private String data;
}
public Note save(Note entity) {
var sql = """
INSERT INTO notes (id, data)
VALUES (default, :data)
RETURNING id
""";
var params = new MapSqlParameterSource().addValue("data", entity.getData());
return namedParameterJdbcTemplate.query(sql, params, x -> {
if (x.next()) {
entity.setId(x.getLong("id"));
}
return entity;
});
}
Таким образом, сделать все за один запрос.
Если необходим батч, то прийдется заранее получить IDки:
SELECT nextval('notes_id_seq') FROM generate_series(1, :size)
Извините, но я никак не пойму в чём у вас проблема. В соответствии с документацией, для авто-инкрементных полей не нужно писать дополнительного кода вообще. У вас ведь Postgres, почему вы не можете просто объявить поля соответственно, используя легаси SERIAL
или современный GENERATED ... AS IDENTITY
?
Сиквенс у вас уже есть, и вы его пытаетесь использовать. А тип SERIAL
это просто синтаксический сахар, который создаёт сиквенс и объявляет поле как NOT NULL DEFAULT nextval('seq_name')
Плюс этого в том, что вам не нужно делать дополнительный запрос к БД для получения ID.
Можно оптимизировать, как делают многие JPA: сделайте SQL sequence сразу с шагом 10, и кешируйте промежуточные значения. Однако, в этом случае могут появляться "дырки" и будет нарушен порядок по Id в случае нескольких инстансов.
Почему бы просто не использовать UUID и генерировать его прямо на клиенте? Или, если хочется последовательные id, то можно и ULID.
а какую БД используете? и наскольког Legacy?
default nextval('seq') для id должно бы помочь, если используете sequence.
Spring Data JDBC и генерация ID