Как стать автором
Обновить

Комментарии 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 никуда не годится (наоборот, мне нравится), но свои ограничения у этого подхода тоже есть.

У вас нет скрипта нактывающего чистую базу? Мы например для генерации 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 сразу.
Хотя.. хозяин - барин.

Spring Data JDBC не использует Hibernate. И reflection, вроде, тоже

nextval(

Если конечно заведомо знать, что работаешь с конкретной БД, то можно и хардкодить синтаксис.
Но может оказаться что в других БД (Oracle, в частности) другой синтаксис получения значения из sequence

Если использовать SQL для запросов, то, конечно, придется затачиваться под конкретную базу данных. Но если при этом все запросы собраны в одном месте, а не разбросаны по коду, то миграция на другую базу не занимает много времени.
Зато полная свобода в оптимизации запросов с учетом специфики конкретной базы

public record MyEntity

Выглядит как оксюморон в контексте Domain-Driven Design.

DDD различает и противопоставляет объекты-значения (value objects), которые иммутабельны и не имеют идентичности, и сущности (entity), которые могут быть различными при одинаковых значениях полей, и поэтому имеют идентификаторы.

Насколько я понимаю, record в java - это для объектов-значений. У них не может быть идентификатора (у них нет идентичности: 2 record с одинаковыми полями не могут различаться), и нет проблем с его установкой.

В концепции DDD и гексагональной архитектуры entity запросто может быть особым типом dto для взаимодействия с внешним хранилищем. А для dto рекорды вполне себе удобны, при остуствии дата-классов. Хотя, согласен, что, могут быть сценарии, когда это может привести к проблемам

Есть ощущение что вы пытаетесь сидеть на двух стульях - с одной стороны хочется чтобы как 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 становится не особо нужна, даже наоборот начинает мешать. А если не пользуешься основным функционалом большой библиотеки, то как-то избыточно ее в проект затаскивать ради каких-то второстепенных удобств, которая она может дать

Позволю себе небольшой совет - попробуйте сделать пет проект на JPA с чистого листа, используя рекомендуемые сообществом best practice и не пытаясь принести туда привычные вам наработки в виде кастомных sequence и record в виде entity.
Это позволит продвинуться дальше и понять, к примеру, что entity по задумке фреймворка мутабельный объект и генерация ID это еще малая беда. Дальше можно словить проблемы с Lazy Loading, апдейту связей итд.

Этим всем конечно можно не пользоваться, но тогда встает вопрос нужен ли вам этот JPA.

Я вот им пользуюсь много лет в проде, но до сих пор для себя не ответил однозначно на этот вопрос.

Я очень много использовал JPA и использую, когда это оправдано. JPA это идеальное решение для CRUD сервисов.
Но когда нужны сложные запросы к базе, то даже на голом jdbc получается проще писать и поддерживать. Именно поэтому Spring Data JDBC манит своей идеей получить преймущества из обоих миров

Там еще веселье будет, если нужно много записей вставлять. Hibernate может делать что-то типа кэша ID, так что не бегает за каждым ID в базу. Если такого добиваться вручную, придется немного повозиться.

А еще с batch операциями придется повозиться, кстати.

да для батчей тут совсем примитивные вещи только, приходится на обычный jdbc переключаться

Как раз таки 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
Представьте, что база не ваша, и вообще легаси. Какие 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)

Да, но как отметил в ps, в этом конкретном случае на старой версии Oracle средствами базы не удалось найти способа id вытащить из insert запроса. Если только процедуру в базе писать

Да, видел, как пример написал, возможно кому-то будет полезно.

Извините, но я никак не пойму в чём у вас проблема. В соответствии с документацией, для авто-инкрементных полей не нужно писать дополнительного кода вообще. У вас ведь 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.

oracle 8-ка

O, тогда отлично все делаете - default seq.nextval не работает - только с Oracle 12c, jooq может даже не знать про особенности и ограничения синтаксиса sql Oracle 8, uuid не поддерживается как тип и т.д.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории