.NET Fest: полная программа конференции на сайте. Присоединяйся к самому большому .NET ивенту
×Закрыть

Как использовать Hibernate: основные проблемы и их решения

Меня зовут Андрей Слободяник, я уже более 10 лет работаю в Java enterprise проектах. В каждом из них были данные в базе, а доступ к ним осуществлялся с помощью JPA/Hibernate. При этом фреймворк использовался, как мне кажется, не совсем правильно: код мог быть компактнее, а производительность выше.

Эта статья — о наболевшем: основных проблемах, способах их исправления, и, главное, подходу, где уместен Hibernate.

Слова «JPA» как стандарт и «Hibernate» как реализация используются как синонимы.

Проверочный вопрос

@Column(name = "name", nullable = false, length = 32)
private String name;
  • Для чего нужен атрибут nullable?
  • Можно ли создать и сохранить entity, если в поле name будет значение null?
  • Проверяется ли длина поля?
  • В чем отличие этих атрибутов @Column и аннотаций @javax.validation.constraints.NotNull и @Size?

Если вы не уверены в ответах, добро пожаловать в статью.

Подход

Hibernate не нужен автоматически везде, где есть БД. Начинать следует не с неё. JPA по самому своему определению применяется, когда оказывается, что объекты Java нужно где-то хранить между выключениями приложения. Один из возможных вариантов — реляционная база данных.

Поэтому создайте удобные классы, описывающие вашу доменную модель так, как будто JPA у вас нет. Используйте всю выразительность Java: различные типы, композицию, наследование, коллекции, Maps, Enums. Только потом переведите её на JPA: добавьте ID, отношения и каскады. Проверьте созданные Hibernate таблицы, если необходимо, поправьте их.

Звучит слишком широко и абстрактно, поэтому давайте разберём на конкретном примере.

Техническое задание

Предположим, мы разрабатываем магазин с заказами.

У заказа есть:

  • дата создания;
  • статус — новый, в обработке и т. д.;
  • заказ может быть «срочный»;
  • адрес доставки (каждый раз разный, поэтому смысла в нормализации пока нет);
  • в заказ входят товары в каком-то количестве;
  • у товара есть неизменная цена с валютой;
  • заказ относится к определённому клиенту.

Обычный подход

Дальше почему-то происходит следующее. Вначале создаются таблицы, что-то вроде:

А потом для них — соответствующие entities. В зависимости от опыта разработчиков, в самом тяжелом случае получается такое.

@Entity
@Table(name = "orders")
public class OrderEntity {
   @Id
   private Long id;

   private LocalDateTime created;
   private String status; // see StatusConstants for available values
   private Integer express;

   private String addressCity;
   private String addressStreet;
   private String addressBuilding;

   private Long clientId;

   public static class StatusConstants {

       public static final String NEW = "N";
       public static final String PROCESSING = "P";
       public static final String COMPLETED = "C";
       public static final String DEFERRED = "D";

       public static final List<String> ALL = Arrays.asList(NEW, PROCESSING, COMPLETED);
       // oops, forgot to add "deferred" to the list
   }
}

@Entity
@Table(name = "items")
public class Item {
   @Id
   private Long id;

   private String name;
   private BigDecimal priceAmount;
   private String priceCurrency;
}

Это вполне рабочие entities, сделанные по принципу «поле в БД — такое же поле в классе», но далеко не лучшие. Из всех возможностей Hibernate мы используем только одну — маппинг между таблицами и классами. Фактически это работа в JDBC режиме.

Проблемы:

  • адрес и цена «размазаны» по нескольким полям;
  • целостность и операции с полем client делаются вручную;
  • легко ошибиться со значениями поля status;
  • нужно помнить, что флажок express представлен числом: 1 — true, 0 — false;

С некоторым опытом можно создавать более удобный маппинг, но сама идея — подгонять entities под таблицы — в случае JPA не верна.

К слову, подход строить приложение от базы данных тоже имеет место быть, но инструменты для него другие, например:

  • база данных;
  • сложные запросы, написанные вручную со всеми возможными оптимизациями;
  • маппер в Java-классы типа MyBatis.

JPA же предлагает другой принцип: вначале Java-классы, потом их сохранение в БД.

Поэтому давайте отложим Hibernate в сторону и создадим объектную модель для исходной задачи.

Очевидно, что:

  • для статуса заказа удобно использовать Enum;
  • флажок «срочный» по самой своей сути — boolean;
  • для адреса будет отдельный класс;
  • для цены — тоже, причём уже есть готовый — Money из JavaMoney;
  • для товаров и их количества подойдёт Map;
  • клиент заказа — это поле типа Client, а не числовой указатель на него.

В результате получается следующее:

public class Order {
   private LocalDateTime created;
   private Status status;
   private boolean express;
   private Address address;
   private Map<Item, Integer> items;
   private Client client;
}

public class Address {
   private String city;
   private String street;
   private String building;
}

public class Item {
   private String name;
   private Money price;
}

public class Client {
   private String firstName;
   private String lastName;
}

Коллекции

Поскольку JPA умеет сам инициализировать коллекции, разработчики часто ограничиваются их объявлением, как в примере выше, а дальнейшие модификации осуществляют через геттер.

С учетом базового принципа «Класс должен уметь работать без JPA» лучше:

  • инициализировать коллекцию сразу: в месте объявления либо в конструкторе;
  • убрать сеттер;
  • в геттере возвращать не модифицируемую копию;
  • добавить модифицирующие методы.

Для нашего примера может быть так:

private Map<Item, Integer> items = new HashMap<>();

public Map<Item, Integer> getItems() {
   return Collections.unmodifiableMap(items);
}

public void addItem(Item item) {
   items.merge(item, 1, (v1, v2) -> v1 + v2);
}

public void removeItem(Item item) {
   items.computeIfPresent(item, (k, v) -> v > 1 ? v - 1 : null);
}

Также уместно напомнить, что хотя Hibernate требует для своей работы пустой конструктор, для инициализации объектов можно и нужно использовать конструкторы с параметрами.

Подключаем JPA

Для этого требуются:

  • Если хотим, чтобы класс хранился в отдельной таблице — аннотации @Entity и @Id, если в таблице другого класса — @Embeddable.
  • Примитивы, числа, строки и даты Hibernate умеет сохранять сам.
  • Для Enum-ов указываем @Enumerated и тип: хранить либо порядковый номер — EnumType.ORDINAL, либо строковое представление — EnumType.STRING.
  • Ещё для Enum-ов и других объектов, которым достаточно одного поля в БД, удобно использовать AttributeConverter.
  • Классы из нескольких полей и не соответствующие конвенциям Java Bean — Money в нашем случае — требуют @Type с описанием преобразования аналогично AttributeConverter-у. Для Money нужный класс уже написан.
  • Для классов и коллекций указываются соответствующие отношения (ManyToOne, OneToMany, ElementCollection и т.д.).

После этого Hibernate вполне может создать необходимые таблицы.

Возможно, для кого-то эта информация будет новой, но @Table и различные @Column:

  • не обязательны и содержат лишь уточняющие DDL атрибуты;
  • не являются валидацией.

Ответ на вступительный вопрос.

@Column(name = "name", nullable = false, length = 32)
private String name;

... конвертируется в часть инструкции «create table»

name varchar(32) not null

В runtime Java никаких проверок на null и длину поля не происходит.

Поскольку при разработке приложения структура классов меняется, созданная JPA схема — это, скорее, заготовка для flyway/liquibase и/или in-memory БД.

Null

Null и в Java, и в базе данных следует использовать только тогда, когда нам действительно нужно значение «не определено». Во многих случаях такой необходимости нет. Ленясь инициализировать поля, мы либо подкладываем себе грабли в виде NPE, либо осыпаем код ненужными проверками на null.

Срочность заказа (поле express) на первый взгляд имеет три состояния — «да», «нет», «не указано». На практике, нам, скорее всего, будет достаточно двух — срочные и обычные заказы. Поэтому используйте примитивы (boolean) вместо классов (Boolean) там, где это возможно.

Важно помнить, что для поля id примитив использовать нельзя, поскольку для новых (transient) entities оно не определено. И, к сожалению, по техническим причинам (чтобы Hibernate мог создавать прокси) указывать модификатор final невозможно.

Именование полей

По умолчанию JPA использует такой naming convention для полей и классов fieldName (в java) -> field_name (в БД).

Поэтому указывать в @Column(name = «another_name») имеет смысл, если это не так. В нашем примере «Order» — служебное слово в SQL, поэтому я назвал таблицу «Orders», и остальные в множественном числе — для однообразия.

Ключи

Бывают естественные и суррогатные. Суррогатные ключи обладают ощутимыми преимуществами — удобством и производительностью, поэтому будем использовать именно их.

Для всех entities (кроме Address) добавляем:

@Id
private Long id;

(Не)использование Id

При написании бизнес-логики объекты сравнивают между собой. Некоторые пишут так:

boolean equals = order.getId().equals(anotherOrder.getId());

... а принадлежность объекта к коллекции проверяют с помощью стрима

boolean contains = orders.stream().anyMatch(o -> o.getId().equals(someOrder.getId());

Это не компактно и не совсем верно. Лучше:

boolean equals = order.equals(anotherOrder);

boolean contains = orders.contains(someOrder);

Суррогатные ключи — IDs — не должны фигурировать ни в бизнес-логике, ни в запросах. Всегда работайте с объектами. Вместо параметра clientId

select o from Order o where o.client.id = :clientId

... должен быть объект

select o from Order o where o.client = :client

Позвольте JPA построить запрос самостоятельно.

Если в контексте нет объекта client и целиком он не нужен, достаточно использовать его reference. То есть вместо

Client client = em.find(Client.class, clientId);

... используем

Client client = em.getReference(Client.class, clientId);

Эквивалентность (методы equals/hashCode)

Чтобы сравнивать объекты, как было указано выше,

boolean equals = order.equals(anotherOrder);

... нужно определить методы equals и hashCode. Часто реализуют их через id:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Order that = (Order) o;
   return Objects.equals(id, that.id);
}

@Override
public int hashCode() {
   return Objects.hash(id);
}

... но это не вполне корректно.

Из нашего принципа — не привязываться к JPA — следует, что суррогатный id не должен фигурировать в equals/hashCode. Важно помнить, что для новых (transient) entities поле id еще не инициализировано (равно null). Тем не менее, эквивалентность должна работать корректно независимо от состояния entity.

Объекты должны сравниваться по бизнес-ключу, а не по id.

Для класса Order в качестве бизнес-ключа напрашивается пара полей дата-клиент.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (!(o instanceof Order)) return false;
   Order order = (Order) o;
   return Objects.equals(created, order.created)
           && Objects.equals(client, order.client);
}

@Override
public int hashCode() {
   return Objects.hash(created, client);
}

Обратите внимание, что Hibernate может создавать прокси-объекты и проверять класс следует не методом getClass(), а через instanceOf. К счастью, в Lombok этот момент учтен.

Только в исключительных случаях, если у класса нет ничего, что может быть использовано в качестве бизнес-ключа кроме поля id, equals() проверяем через тождество (==) и эквивалентность id, а hashCode() без полей вырождается в константу.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (!(o instanceof Foo)) return false;
   Foo that = (Foo) o;
   return Objects.equals(id, that.id);
}

@Override
public int hashCode() {
   return 31;
}

Более детально тему раскрывает Влад Михальча:

Cascades

JPA призвано всячески упрощать рутинные операции. Для сохранения и удаления сложных объектов нет необходимости «пробегать» по структуре и повторять операции для вложенных объектов, достаточно указать каскады.

В нашем примере при создании нового заказа для нового пользователя не нужно сохранять их по отдельности, это сделает каскад.

Очевидный нюанс: прежде чем указывать CascadeType.ALL, подумайте, нужен ли включенный в него CascadeType.REMOVE. Опять же, для нашего примера — нет, при удалении заказа, клиент не удаляется, поэтому ALL не применяем.

Entity и DTO

Если мы разрабатываем Web-приложение, нам не обойтись без передачи entities на Front и обратно. Теория учит, что для передачи следует использовать отдельные DTO классы. Часто, в случае простых entities типа Client и Address в нашем примере, поля ClientDto и AddressDto будут точно такие же. Возникает соблазн не создавать отдельные классы, а использовать существующие. Это неверный подход.

Могут появиться поля, нужные только для DTO. Приходится маскировать их от сохранения в БД с помощью @Transient. Возможны изменения значений полей перед отправкой на UI. Чтобы эти модификации не отразились в БД, начинаются вызовы entityManager.detach().

Коллекции по умолчанию работают в режиме lazy loading и уходят на UI пустыми. Изменение режима на FetchType.EAGER закладывает серьёзную мину под производительность. Загрузка элементов коллекции теперь будет происходить во всех вопросах и создавать N+1 проблему. Не делайте так. У Entity и DTO разная ответственность. Валидацию введенных пользователем данных — проверки @NotNull, @Size и т. д. — делает DTO.

Правильный подход — всегда создавать отдельные DTO классы. Чтобы сократить написание boilerplate кода по перекладыванию полей используйте MapStruct или аналоги.

FetchType.EAGER

Исторически Hibernate по умолчанию использует режим EAGER загрузки в отношении ManyToOne и OneToOne, а во всех остальных случаях — LAZY. Рекомендуется использовать LAZY во всех случаях. Указать в запросе делать join вместо нескольких select-ов всегда возможно, а обратно — отключить EAGER для определённых случаев — нет.

Опять же, слово Владу — «EAGER fetching is a code smell when using JPA and Hibernate».

EntityManager.flush() и clear()

Ещё один тревожный маркер — это многочисленные вызовы flush() и clear(). Почему-то вместо того, чтобы доверить управление entities фреймворку, разработчики начинают вмешиваться в этот процесс.

Навскидку приходят в голову только две исключительные ситуации, когда нужны эти методы:

  • обработка очень большого количества данных (репорты), переполнения кеша 1-го уровня;
  • вызов бизнес-логики из хранимых процедур в процессе транзакции, в таком случае нужен flush().

Во всех остальных случаях эти вызовы, скорее всего, лишние и только ухудшают производительность.

Project Lombok

Отличная штука, сокращает количество boilerplate кода. С JPA, однако, необходимо учитывать нюанс. @Data по умолчанию включает в себя @EqualAndHashCode и @ToString по всем полям, что в свою очередь может порождать каскад ненужных загрузок полей, игнорируя старательно указанный FetchType.LAZY, и зацикленные вызовы для bi-directional отношений.

Поэтому рекомендуется не использовать @Data, а в @EqualsAndHashCode и @ToString указывать только нужные поля.

Правильные entities

Применяя всё вышеизложенное к нашим классам, получаем:

@Getter
@EqualsAndHashCode(of = {"firstName", "lastName"})
@ToString(of = {"firstName", "lastName"})
@NoArgsConstructor
@Entity
@Table(name = "clients")
public class Client {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Column(length = 32, nullable = false)
   private String firstName;

   @Column(length = 32, nullable = false)
   private String lastName;

   public Client(String firstName, String lastName) {
       this.firstName = firstName;
       this.lastName = lastName;
   }
}

@Getter
@EqualsAndHashCode(of = "name")
@ToString(of = {"name", "price"})
@NoArgsConstructor
@Entity
@Table(name = "items")
public class Item {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Column(nullable = false, length = 32)
   private String name;

   @Columns(
           columns = {
                   @Column(name = "price_currency", length = 3, nullable = false),
                   @Column(name = "price_amount", precision = 7, scale = 2, nullable = false)
           }
   )
   @Type(type = "org.jadira.usertype.moneyandcurrency.moneta.PersistentMoneyAmountAndCurrency")
   private Money price;

   public Item(String name, Money price) {
       this.name = name;
       this.price = price;
   }
}

@Getter
@EqualsAndHashCode(of = {"city", "street", "building"})
@ToString(of = {"city", "street", "building"})
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class Address {
   private String city;
   private String street;
   private String building;
}

@AllArgsConstructor
public enum Status {
   NEW("N"),
   PROCESSING("P"),
   COMPLETED("C"),
   DEFERRED("D");

   @Getter
   private final String code;
}

@Converter(autoApply = true)
public class StatusConverter implements AttributeConverter<Status, String> {
   @Override
   public String convertToDatabaseColumn(Status status) {
       return status.getCode();
   }

   @Override
   public Status convertToEntityAttribute(String code) {
       for (Status status : Status.values()) {
           if (status.getCode().equals(code)) {
               return status;
           }
       }
       throw new IllegalArgumentException("Unknown code " + code);
   }
}

@Getter
@EqualsAndHashCode(of = {"created", "client"})
@ToString(of = {"created", "address", "express", "status"})
@NoArgsConstructor
@Entity
@Table(name = "orders")
public class Order {
   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   private Long id;

   @Column(nullable = false)
   private LocalDateTime created = LocalDateTime.now();

   @AttributeOverrides({
           @AttributeOverride(name = "city", column = @Column(name = "address_city", nullable = false, length = 32)),
           @AttributeOverride(name = "street", column = @Column(name = "address_street", nullable = false, length = 32)),
           @AttributeOverride(name = "building", column = @Column(name = "address_building", nullable = false, length = 32))
   })
   private Address address;

   @Setter
   private boolean express;

   @Column(length = 1, nullable = false)
   @Setter
   private Status status = Status.NEW;

   @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST, optional = false)
   private Client client;

   @ElementCollection
   @Column(name = "quantity", nullable = false)
   @MapKeyJoinColumn(name = "item_id")
   private Map<Item, Integer> items = new HashMap<>();

   public Order(Address address, Client client) {
       this.address = address;
       this.client = client;
   }

   public Map<Item, Integer> getItems() {
       return Collections.unmodifiableMap(items);
   }

   public void addItem(Item item) {
       items.merge(item, 1, (v1, v2) -> v1 + v2);
   }

   public void removeItem(Item item) {
       items.computeIfPresent(item, (k, v) -> v > 1 ? v - 1 : null);
   }
}

Многочисленные аннотации @Table и @Column присутствуют только для того, чтобы сгенерировать точно такую же схему, как на диаграмме. Пример учебный, на практике для генерации id лучше использовать sequence.

Внимательный читатель должен заметить, что использование поля client c lazy-загрузкой в Order.equals() противоречит рекомендациям раздела Lombok. Если бизнес-логика позволяет, лучше реализовать эквивалентность без него.

Заключение

JPA достаточно обширная тема, а Hibernate, к сожалению, содержит большое количество «gotchas», чтобы рассмотреть все нюансы в одной статье.

Много полезного и интересного в блоге Влада Михальча.

За кадром остались:

  • наследование;
  • uni- и bi-directional отношения;
  • стратегии работы с коллекциями;
  • criteriaBuilder;
  • batching;
  • QueryDSL;
  • и многие другие темы.

При встрече с ними я бы рекомендовал придерживаться основного посыла, который всячески старался проиллюстрировать — вначале полностью рабочая объектная модель, потом вопросы, как сохранить её в БД, не наоборот.

Резюмируя, составим check list потенциальных проблем, которые освещены в статье:

  • entities состоят из большого количества примитивных полей, а не из классов;
  • вместо enum-ов используются строковые константы;
  • методы equals/hashCode не определены;
  • в бизнес-логике и запросах фигурируют id;
  • происходит смешение Entity и Dto в одном классе;
  • использование FetchType.EAGER, в том числе по умолчанию в @ManyToOne;
  • ненужные вызовы flush() и clear();
  • неаккуратное использование Lombok.


За рецензию материала и дельные замечания благодарю Игоря Дмитриева.

LinkedIn

219 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Спасибо большое за доклад на Logeek Night. Во время доклада мы представили что DB нет, а есть только java, и у нас получились идеальные условия для разработки! А теперь представьте что DB всё-таки есть, и её охраняют 3 опасных DBA.
Как везти разработку в таких условиях?
Было бы очень интересно услышать ваше виденье inheritance, и тот случай когда нужно строить сложные структуры с разворотом таблицы?

Спасибо за комментарий.

А теперь представьте что DB всё-таки есть, и её охраняют 3 опасных DBA.
Как везти разработку в таких условиях?

Прочитал вопрос как «Что делать, если схема БД уже создана?» Два варианта: возможно Hibernate не нужен вообще, см. MyBatis. Либо JPA и «дизассемблировать» таблицы, большое количество полей «сгруппировать» с помощью @Embeddable, использовать enum и т.д.

ваше виденье inheritance

Я не сторонник использования наследования, это специфичный приём и должен применять аккуратно, вот моя старая, несколько наивная статья dou.ua/...​n-vs-inheritance-in-java

Если наследование всё таки нужно, и проектировать от Java, то Hibernate прекрасно его поддерживает и предоставляет три стратегии для этого.

Если «дизассемблировать» таблицы, то @MappedSuperClass для повторяющихся полей (например, в каждой таблице есть аудит createdBy, createdAt, modifiedBy, modifiedAt) мне не кажется хорошей идеей. Я предолжил бы использовать композицию и @Embeddable.

Є декілька різних позицій по цьому питанню.

1. Proxy задає життєвий цикл, здавалося б data об’єкта.

У вашій логіці є одне протиріччя. З однієї сторони, ви будуєте класи OOP-way — ціль достойна. Але з іншої, використовуєте для цього досить сильну магію.

В магії, як такій, нічого поганого немає, але в цьому випадку, кінець-кінцем, доведеться працювати з proxy об’єктами, а не з чистими data класами. І це в багато чому множить на нуль всі старання (так як ви вже не можете з цим об’єктом працювати як з data, він прив’язаний до storage/session, управляється ним та без нього не існує).

2. Ви натягуєте data об’єкт на базу.

Ви згадали про DTO. І вірно зазначили, що DTO часто відрізняються. Більше того, є сенс також розрізняти DTO (структури в запитах/відповідях api), DAO(структури для роботи з storage) та data-класи (структури для роботи в пам’яті). В більшості випадків, як мінімум з початку розробки, всі ці три набори класів сутностей тупо ідентичні. І в усіх цих випадках є сенс НЕ плодити сутності і взяти, наприклад, DAO класи і використовувати їх всюди. Як тільки десь щось змінюється — це все рефакториться та сутність розділяється.

Так ось, на вашому прикладі, перша модель — це для DAO, а друга — це для data. І мапити їх через ORM — це костиль, який спрацює лише в такому випадку як ви описали. Додайте сюди, скажем searchString в DAO (який генериться з інших полів не очевидним чином, щоб потім з нього пошук LIKE-ом робити). Та додайте в data-класі, замість Map items якусь специфічно оптимізовану колекцію (уявімо, для прикладу, що Item не товар, а щось, що має якісь координати, і колекція — не Map, а якийсь geo індекс). Також в data можна додати пару atomic примітивів та lock-ів для роботи в synchrinized. А тепер зліпіть ці дві сутності в одну і зробіть марінг в базу.

Суть не в тому, щоб довести вашу ідею до абсурду, а в тому, щоб показати, що ваш спосіб «пофіксити» «звичайний підхід» ламається, якщо рухатися в тому ж напрямку «покращення» стуктури, в якому ви кажете, що він круто працює.

3. Базу даних потрібно «поважати» ... вона все-таки зберігає для вас ваші дані :)

БД — не проста штука. І коли даних багато і складних, треба вирішити як з ними швидко працювати. І тут логіка починає тягнутися: в sql створюються денормалізовані таблиці та хитрі індекси, щось виноситься в nosql, щось в elastic search, а шось взагалі в щось схоже на annoy індекс чи in-memory структури.

Hibernate-like ORM ж кажуть: «та забийте, ми все зробимо ... ви тільки гляньте що ми можем і розберіться», але в результаті, по факту, там закрито дуже мало тих методів, що можна використати для оптимізації роботи з БД і зроблено це так, що ще можна пальці повідстрілювати.

4. В коментарях згадувався аудит...

Я не працював з аудитом в Hibernate, але з того, що доводилось робити, хуки на базу — було б дуже грубим рішенням.

В тих проектах, де ми робили аудит-логи, ці логи були на рівні чуть вище бази. Наприклад лог міг виглядати як: «[req:12355][user:45][ip:127.0.0.1][agent:android/chrome] Авторизація Петро Петро Петрович»,
а не " Петро Петро Петрович lastLogin changed from XXX to YYY«.

5. Неочевдний життєвий цикл

Можливо, це все Hibernate і вміє, але є сенс звертати увагу на наступне.

Сутності бувають різні. Крім класичних строго транзакційних (користувач змінив name чи створив замовлення), ще є словники (які вигідно записати в map в пам’ять і оновлювати кожні 10хв або по зовнішньому trigger-у), логи (append-only, можна писати bulk-ом), статистика (можна накопичувати і писати агреговані зміни раз в хвилину)

Кожен з цих випадків працює по різному і, в цілому, ломає роботу з одним «графом» об’єктів.

Окремо є сенс згадати транзакційність...
Я не знаю як в кого, але в нас з нею є вже традиційні особливості. І поки не видно яка може бути альтернатива retry-циклу з ідемпотентним try/catch-ем з явним контролем того які запити і в якому пооядку робляться.

А то буває тут kill −9 прилітає (чи щось типу пропало з’єднання до бази, якийсь lock wait exception чи disk is full, або, взагалі dead lock) а потім шукай що там де не так.

------
В цілому, для ясності, я не є великий противник ORM. ORM — це часто зручно. Але від ORM я чекаю простий і швидкий mapper на data клас, простий crud api, зручний api для raw sql, optional query builder та утиліти для крайніх випадків (mass select/insert/update, управління транзакціями, тощо).

Робота ж з DDL, каскадний select/update/delete, детектор змін в об’єктах, кеш об’єктів з БД, автоматичні join-и, автоматична підтримка купи БД одночасно, та своя мова запитів — це все тільки ускладнює роботу.

Вопрос не троллинга ради, а чтобы понять для себя: зачем вообще использовать Хибернейт?

Пока вижу две ситуации:
1) демо/пилотный проект. Работа с БД не важна, в простейшем случае не нужно об этом думать — все запустит Spring/JPA/Hibernate.
2) коробочный продукт реально поддерживающий работу с разными базами данных.

В остальных случаях — какие проблемы решает Хибернейт не создавая больших, когда есть вменяемый jOOQ, Spring JDBC templates, etc?

какие проблемы решает Хибернейт

Проблемы данных с большими графами связей. Если вам нужно вытащить сущность, которая является корнем графа на 20-30 узлов, и вы решите делать это плейн олд ждбц, вы это сделаете (возможно), но это замедлит разработку вашего проекта в десятки раз.

Проблемы данных с большими графами связей.

ОК, в целом пункт принимаю. Спасибо!

Если вам нужно вытащить сущность, которая является корнем графа на 20-30 узлов, и вы решите делать это плейн олд ждбц, вы это сделаете (возможно), но это замедлит разработку вашего проекта в десятки раз.

В десятки раз? Вытащить? Та вроде не, операция довольно тривиальная: выборка по ключу парента, результат пропустить через маппер. Повторить 20-30 раз. Будет чуть сложнее если нужно оптимизировать производительность, но Хибернейт тут тоже не блещет с постоянными муками выборы между fetch lazy v eager на проектах что мне приходилось поддерживать. Лучше расписано тут: blog.jooq.org/tag/entitygraph

Другой вопрос, почему граф оказался таким разветвленным, и нет ли частичной вины Хибернейта в этом, но оставим этот вопрос на потом.

результат пропустить через маппер. Повторить 20-30 раз.

Это значит, что надо написать 20-30 мапперов, да? Агонь решение ) про то, сколько багов будет создано в процессе и какая будет стоимость разработки имеет смысл говорить? Это при том, что существенного плюса финальный результат не даст.

blog.jooq.org/tag/entitygraph

jooq вообще проприетарная штука, или частично проприетарная, точно не помню. Странно продвигать его как серьезную замену свободным решениям.

Другой вопрос, почему граф оказался таким разветвленным, и нет ли частичной вины Хибернейта в этом

Постановка вопроса вообще некорректна. Хиб это лишь инструмент по маппингу таблиц на объекты, как он может диктовать архитектуру графа данных? Она диктуется исключительно бизнес-логикой. Разветвленная логика, большая бизнес-структура данных, вот и большой граф.

В десятки раз? Вытащить?

Десятки это образно, я не мерял. Но предполагаю, что близок к правде. Каждые мелкий апдейт, селект и прочие будет превращаться в отдельную, забагованную вручную написанную километровую sql-jdbc процедуру. Да, стоимость разработки всего этого по сравнению с em.find(BusinessGraphRoot.class, id); — десятки раз, если не сотни.

Это значит, что надо написать 20-30 мапперов, да? Агонь решение ) про то, сколько багов будет создано в процессе и какая будет стоимость разработки имеет смысл говорить? Это при том, что существенного плюса финальный результат не даст.

откуда баги в мапперах? Оттуда же, откуда в маппинге хибернейта. Принципиальных различий нет, особенно если следовать правильному совету топик кастера:

Правильный подход — всегда создавать отдельные DTO классы.
blog.jooq.org/tag/entitygraph
jooq вообще проприетарная штука, или частично проприетарная, точно не помню. Странно продвигать его как серьезную замену свободным решениям.

в данном конкретном случае я ссылался на полезный разбор из их блога, где нечего про собственно jooq нет.
Отдельный вопрос открытости и свободности jOOQ для открытых баз, но для простоты можно ограничиться Spring JDBC Templates или apache commons-dbutils.

Другой вопрос, почему граф оказался таким разветвленным, и нет ли частичной вины Хибернейта в этом
Постановка вопроса вообще некорректна. Хиб это лишь инструмент по маппингу таблиц на объекты, как он может диктовать архитектуру графа данных? Она диктуется исключительно бизнес-логикой. Разветвленная логика, большая бизнес-структура данных, вот и большой граф.

Бизнес логика редко диктует выбор корней графов, полной нормализации, забивания статических словарных значений в базу или выборки по типу «верни мне все, а я там разберусь что мне нужно». Так же смотри на максиму топиккастера:

JPA же предлагает другой принцип: вначале Java-классы, потом их сохранение в БД.
В десятки раз? Вытащить?
Десятки это образно, я не мерял. Но предполагаю, что близок к правде.

первый спринт/два будет Хибернейтом быстрее, потом чем дальше в лес тем чаще он будет становиться обузой. Из моего лично опыта по крайней мере.

Каждые мелкий апдейт, селект и прочие будет превращаться в отдельную, забагованную вручную написанную километровую sql-jdbc процедуру. Да, стоимость разработки всего этого по сравнению с em.find(BusinessGraphRoot.class, id); — десятки раз, если не сотни.

если ваши разработчики не могут написать простой Insert или Update без драмы, то сильно подозреваю что Хайбернейт ваших проблем не решит. Максимум — перенесет из репозиториев в сервисы.

откуда баги в мапперах?

Мы по маппером имеем в виду класс, который из сиквеля делает бизнес-объект, да? Даошку грубо говоря.

первый спринт/два будет Хибернейтом быстрее, потом чем дальше в лес тем чаще он будет становиться обузой.

Нет. Уже больше двух лет как мы идем дальше в лес, а проблем все нету. Видимо, мы чтото делаем не так. Ведь тру-разработчики имеют с хибом только проблемы, а мы — преимущества. Что-то с нами не так.

Из моего лично опыта по крайней мере.

Это только значит, что не смогли правильно работать с хибом. Делать отсюда вывод, что в этом виноват сам хиб это самомнение.

Бизнес логика редко диктует выбор корней графов

Бизнес логика диктует логику работу приложения, структуру данных создает программист, да. Если вы не усматриваете влияния предметной области на структуру данных в бд, или отрицаете, что увеличение предметной области ведет за собой разростание структуры данных, то говорить не о чем.

если ваши разработчики не могут написать простой Insert или Update без драмы

Мои разработчики могут, и я об этом написал в еще одном комментарии. А еще мои разработчики могут и в хибернейт, и он для них — полезный инструмент, которым они умеют пользоваться, а не источник проблем, как в вашей практике. А еще мои разработчики умеют определять, когда хибернейт нужен, а когда — нет.

Мы по маппером имеем в виду класс, который из сиквеля делает бизнес-объект, да? Даошку грубо говоря.

думаю да. На подобие такого (kotlin):

private val claimableRowMapper: (rs: ResultSet, _: Int) -> Claimable = { rs, _ ->
        Claimable(
            id = rs.getLong("id"),
            version = rs.getInt("version"),
            warehouseId = rs.getInt("warehouse_id"),
            supplierId = rs.getLong("supplier_id"),
            segments = fromPgJson(rs.getString("segments"), segmentListType),
            mutationTimestamp = rs.getTimestamp("mutation_timestamp").toInstant(),
            orderType = OrderType.valueOf(rs.getString("order_type")),
            closureDate = rs.getDate("closure_date")?.toLocalDate()
        )
    }

fun findOne(claimableId: Long): Claimable? = jdbcTemplate.query(
    // OK, we're cheating with *, not good for every context but we're 100% resilient
    "SELECT * FROM claimables WHERE id = ?", arrayOf(claimableId), claimableRowMapper
).singleOrNull()

Да, это именно то, о чем я говорил. Очень удобно.
Гораздо удобнее, чем
Claimable c = em.find(...);

Вы сами хоть понимаете абсурдность утверждения, что вот эту вот всю героическую галиматью писать якобы быстрее и удобнее чем ем.файнд?

Claimable c = em.find(...);

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

правила о том, как пользовать это чтобы не выстрелить себе в ногу

Да вы знаете, программировать вообще нелегко. Правила соблюдать там разные нужно, использовать чтото сложнее блокнота, и т.д.

как много девелоперов это правильно выполняют.

Кто это правильно выполняет, у тех хиб не является источником проблем. А у вас какоето странное желание требовать от сложного инструмента чтобы его использование не нуждалось в знании.

А у вас какоето странное желание требовать от сложного инструмента чтобы его использование не нуждалось в знании.

 у меня требование чтобы сложные инструменты не использовались без нужды. Пока реальных доводов использовать хибернейт кроме как то что так все делают, мы любим аннотационное программирование и «ой мне сложно пару лишних строчек написать» я тут не увидел. Но это мое личное мнение, я его никому пока здесь не навязываю.

и да, я таки не сильно понимаю чем принципиально

@Column(name = "name")
лучше, чем
name = rs.getString("name")
. Но знаю много причин почему второе лучше первого.

Тем, что name = «name» опционально
Как и сама аннотация @Column вообще.
Это возможность, а не требование.
Любое не-транзиент поле класса @Entity уже по-умолчанию является полем таблицы с таким же именем, как само поле класса. @Column позволяет вам уточнить или скорректировать метаданные таблицы.
Теперь lдолжно быть понятнее, чем

private String name;
лучше чем
name = rs.getString("name")
Более того, он еще и тип сам определит. Со стрингом то не фокус, а вот с LocalDateTime, вот это да.

Блин, ну я вам сейчас основы JPA преподаю. Может легче просто выделить некоторое время на изучение JPA? Много вопросов отпадет

Я вот тут еще об одной интересности подумал.
Вот вы такой олдскульно-красивый (в кавычках) код написали, и инсерты-апдейты у вас красивые, скл-ждбц онли, никакой фигни вроде этих наших глупых орм.

Но тут вот вопросец. Завтра нагрузка на ваше приложение повысилась, а послезавтра оно вообще станет распределенным, и внезапно, необходима синхронизация транзакций на данных.

Самый просто способ, которым это сделаю я — это
em.find(id, timeout, PESSIMISTIC_READ);

А вы? Ой, это ж надо везде начинать сиквель переписывать на разные SELECT FOR UPDATE, да? Потом коммититься явно, да? А если у нас логика в разных методах, то Connection из метода в метод передавать, да? А там же везде чекед SQLException и вавилонские зиккураты из try { try { try { } catch catch catch. Это ведь очень удобно и разумно, примерно так же как отмораживать уши назло бабушке.

Завтра нагрузка на ваше приложение повысилась, а послезавтра оно вообще станет распределенным, и внезапно, необходима синхронизация транзакций на данных.

уже вчера

Самый просто способ, которым это сделаю я — это
em.find(id, timeout, PESSIMISTIC_READ);

Оптимистические локи все-ж предпочтительнее будут при повышенной нагрузке. Разве что у вас высокий уровень contention по одному ключу. Но допустим.

А вы? Ой, это ж надо везде начинать сиквель переписывать на разные SELECT FOR UPDATE, да?

Да. Соглашусь, может быть неудобством. Хотя при переходе между типами локов в существующем высоконагруженном приложении — это будет самой маленькой проблемой.

Потом коммититься явно, да?

Коммит это проблема? или диплоймент? У вас уровень изоляции в конфиге лежит? Нас с вами явно не попути :)

А если у нас логика в разных методах, то Connection из метода в метод передавать, да?

Spring. @Transactional. Походу тоже самое будет с Хибернейтом — от транзакции никуда не денешься с пессимистическим локом, поэтому и мене желательный подход. Старайтесь держать эту транзакцию как можно уже, очень нежелательно вшеншие вопросы.

А там же везде чекед SQLException и вавилонские зиккураты из try { try { try { } catch catch catch.

про работу с сырым JDBC — это ты сам себе придумал. В спринге с эксепшинами проблем нет. В котлине и подавно.

Оптимистические локи все-ж предпочтительнее будут при повышенной нагрузке.

Это скользкий вопрос. Да, оптимистические локи менее требовательны к базе, но уродуют логику.
Мне не нужно начинать процедуру заново, словив OptmisticLockException и стартовать каждый раз заново в высококонкуррентном окружении, это экспешн драйвен девелопмент.
Я лучше залочусь и подожду. Это в базовом варианте.
А вообще у нас локи идут на критические операции а не на базу, они вне скоупа транзаций и построены на распределенных мапах хазлкаста, поэтому даже под нагрузками база курит.

Разве что у вас высокий уровень contention по одному ключу.

Да не скажу чтобы сильно. Проблема которую мы точно смогли детектировать — это когда та часть функционала, которая напрямую доступна человеку, получает дабл-клики, которые прилетают из интерфейсов интеграторов. Т.е. взял человек и настрочил кнопочку три раза. Полетело три идентичных запроса с разницей в несколько мс.

Соглашусь, может быть неудобством

Ну то есть колемся, но едим кактус :)

Spring. @Transactional.

Ааа, я думал у вас совсем хардкорненько, ждбц онли. Вы с транзакшн менеджером который прямо к дата сорсу?

Посмотрите на MyBatis, всяко легче будет, чем руками jdbc-маппинг писать.
Я упоминал о нём в статье.

Ребята, маппинги — это не проблема, честно. И это не то, в чем сила Хибернейта.

По батису — пользуем. Не очень. Зачем еще лишнюю xml прослойку, когда можно прямо в коде репозитория держать?

Плюс у него прокси глюкавые.

Хозяин — барин. Как по мне, сила Хибернейта в state. Если вам stateful не нужно, то и JPA скорее всего не нужно. Делайте маппинги как душа пожелает.

Поделитесь, пожалуйста, как аудит делать будете?
Тут dou.ua/...​to-use-hibernate/#1581886 спрашивал

Что значит

stateful

в данном случае? У меня сервисы stateless, state — он в базе.

В данном контексте — держать состояние объектов

dou.ua/...​to-use-hibernate/#1581886

Вы в другой ветке ломаете копья, какой инструмент лучше, и вполне может быть, что для ваших задач stateless jooq, jdbc templates и т.д. предпочтительнее.

doesn’t compute to me :) у вас что, хибернейт объекты между запросами в памяти висят?

Именно так. Кеш 1-ого уровня называется.

1й уровень это сессионный. Он по идее между запросами не шарится? Я имею ввиду внешние запросы, скажем веб.

Поскольку внешние запросы, скажем веб, каждый в своей сессии, то нет, не шарится.

Это называется 2й уровень кеша. По дефолту он отключен, но его можно настроить.

Если вам stateful не нужно, то и JPA скорее всего не нужно.

Жпа не про стейтфул. Жпа имеет широкие возможности вообще избегать выгрузки менеджед ентити из базы. Всякие там конструктор квери и резалт сет маппинг про это. Если бы базовой идеей был именно стейтфул, то этого бы не было.
Но стейтфул тоже важная часть.

Жпа не про стейтфул

Я в том смысле, что stateless mapper-ов много разных есть, а stateful — кроме провайдеров JPA не знаю

Если в таком смысле, то да, верно.

И это не то, в чем сила Хибернейта.

Вообщето сила жпа именно в этом.
ORM stands for ORMapping на мысли не наталкивает? :)

Остальное все — плюшки к маппингам. Там внизу Андрей еще про стейтфул говорит, но если сравнивать стейтфул и маппинг, то маппинг более изначальная идея любого орм. Все стейтфулы флаши и прочее это дополнение.

какие проблемы решает Хибернейт

Проблемы данных с большими графами связей.

И да, хочу заметить, что в нескольких проектах, где граф связей невелик, я сам использую plain old jdbc и не парюсь, поскольку до определенного порога действительного легче в даошках накидать скриптов через ждбц, чем подключать хиб, расставлять жпа аннотации, и думать о транзакционности методов.
Каждому подходу своя специфика.

Дмитрий, пора перейти на монгу уже :D

Да ну, Ростислав, скл и носкл предназначены для решения разных задач же :)

Классический ответ таков — JPA отражает доменную область на БД без написания sql вручную.
Т.е. можно свободно оперировать объектами:

        Order order = new Order(
                new Address("Kiev", "Shevchenko av.", "1"),
                new Client("Mykola", "Gurov")
        );
        Item item1 = new Item("aaa", Money.of(100, "USD"));
        order.addItem(item1);
        em.persist(order);
Потом делать
        Item item2 = new Item("bbb", Money.of(50, "EUR"));
        order.addItem(item2);
        order.addItem(item1);
... и Hibernate выполнит все нужные insert-ы и update-ы и расставит id-шники.

* совсем маленький нюанс stackoverflow.com/...​ate-using-jpa-annotations

Классический ответ таков — JPA отражает доменную область на БД без написания sql вручную.

Доменная область с любыми репозиториями будет нормально выглядеть:

    Order persistedOrder = orderRepository.persist(order) //can also update the original order instance, but that's bit of a smelly practice
    orderRepository.addOrderitem(
      persistedOrder, // often simply persistedOrder.id
      new Item("bbb", Money.of(50, "EUR"))
    )

в репозиториях да, будет SQL или еще что, но если содержать минималистично и без логики — то не будет принципиально сложнее Хибернейтовских маппингов и костылей.

Потом делать
Item item2 = new Item("bbb«, Money.of(50, «EUR»));
order.addItem(item2);
order.addItem(item1);

Я вижу то, что думаю? Обновление order без явного вызова репозитория или entityManager, расчитывая на autoflush ?
Вижу такое очень часто, дико сложно поддерживать в комбинации с привычкой Hibernate разработчиков использовать глубоко мутирующие объекты.
Это не антипаттерн? Как бороться? Для меня лично такая фича одна из основных мотиваций рефакторинга от Хайбернейта на альтернативы даже если пока вроде работает без проблем.

Разница между объектом (из доменной области) и репозиторием

order.addItem(item1);

и

orderRepository.addItem(...)
Я вижу то, что думаю? Обновление order без явного вызова репозитория или entityManager, расчитывая на autoflush ?

Да, именно так.

Я понимаю, конечно, что практика вносит свои стереотипы, и обычно либо приложение разрабатывается одновременно с БД, либо БД уже есть, да ещё с хранимыми процедурами, например, и нужно разработать приложение. Реализовывать можно разными инструментами, во втором случае для вызова ХП напрашивается, Spring JDBC Template, как вариант.

JPA же о случае, когда доменная область уже сформирована и возникла необходимость хранить её в БД (а до этого, гипотетически, файлы устраивали). В этом случае достаточно описать маппинг, а Хибернейт и схему может создать, и данные туда положить, и изменения данных бизнес-логикой — проапдейтить.

Что смущает в мутирующих объектах? О каких костылях Хибернейта идёт речь?

Да, именно так.

 в моем мирке side-effects недолюбливают, особенно такие здоровые как флуш в базу графа сущности. Так что это как раз ИМХО не плюс, а здоровый минус Хибернейта.

JPA же о случае, когда доменная область уже сформирована и возникла необходимость хранить её в БД (а до этого, гипотетически, файлы устраивали). В этом случае достаточно описать маппинг, а Хибернейт и схему может создать, и данные туда положить, и изменения данных бизнес-логикой — проапдейтить.

Это гипотетически или реальный кейс? Сложно представить развитый проект с файловым персистенсом. Даже если и есть такое, то надеюсь файлы не в сеттерах пишутся?

в моем мирке side-effects недолюбливают, особенно такие здоровые как флуш в базу графа сущности. Так что это как раз ИМХО не плюс, а здоровый минус Хибернейта.

Полагаю, есть разница между side-effects POJO и модификация объекта через бизнес-методы, как в моём примере? Flush — одна из основных фич Хибернейта, и если вы строго за иммутабельность, то он вам вряд ли нужен.

Это гипотетически или реальный кейс? Сложно представить развитый проект с файловым персистенсом.

Гипотетический. Я не застал, но возможно были джава-проекты без БД?

Я не застал, но возможно были джава-проекты без БД?

Здесь на ДОУ есть пользователь Дмитрий Думанский. Он реально толкал тему о том, что файловую систему можно использовать вместо бд на проде.

файловую систему можно использовать вместо бд на проде.

 ну типа наверное можно если не важно, что данные могут навернутся. Или распределенная файловая система с гарантиями.

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

в моем мирке side-effects недолюбливают...Так что это как раз ИМХО не плюс, а здоровый минус Хибернейта.

Я не вижу здесь аргументов.
Я вижу только «мне не нравится хибернейт потому что просто потому».
Если не нравится — не используйте. Но это не аргумент о том, что хиб — плохой инструмент.

сайдэффекты — это хорошо? Ну тогда говорить особо не о чем по этому пункту :)

Если не нравится — не используйте.

не все проекты начинаются с нуля. Те что с нуля — не используем. И опытные девелоперы в моей среде не советуют. Вот и интересуюсь, зачем другие пользуют, что находят.

сайдэффекты — это хорошо?

в зависимости от того что именно вы имеете в виду под сайд эффектом.

Те что с нуля — не используем.

Потому что не умеете пользоваться. Я бы тоже не пользовался тем, чем не умею.

опытные девелоперы в моей среде не советуют

Ключевые слова — ваша среда. Исходя из ваших комментариев, ваша среда не репрезентативна и состоит из кружка хибернейт-хейтеров которые не умеют его использовать, без обид.

Потому что не умеете пользоваться. Я бы тоже не пользовался тем, чем не умею.

логично. Вот я и интересуюсь зачем людям еще одна довольно непростая абстракция, тянущая в прошлое программирования на Джаве. Каждому свое.

кружка хибернейт-хейтеров которые не умеют его использовать, без обид.

обид нет. Пользовать его действительно мало кто умеет хорошо, и я тоже не в этом числе.

Вот я и интересуюсь зачем людям еще одна довольно непростая абстракция

Потому что она упрощает очень многое.

тянущая в прошлое программирования на Джаве.

Прошлое — это как раз написанные вами маппер резалтсета на объект. Прошлое я говорю без негатива, потому что это действительное самое-самое прошлое, исторически первый способ перевести данные из субд в объект. И он же самый примитивный и громоздкий из доступных на сегодня способов решить ту же задачу.

И он же самый примитивный и громоздкий из доступных на сегодня способов решить ту же задачу

в чем громоздкость? В том, что маппинг не аннотацией статически на доменном объекте, а потенциально динамически также простой строчкой в БД маппере, где ему и положено по-хорошему быть?

в чем громоздкость?

Посчитайте количество строчек в своем маппере и прикиньте количество багов.
А количество работы, которую вам придется делать каждый при изменении класса или ренейму поля вообще исчислению не поддается.

Немного лирики — вы так говорите, будто я не знаю, что такое работа с jdbc. Я знаю, поверьте. Я накушался ее еще когда был пре-джуном дома в селф-емплойед ну вы понели :)
У меня есть свой проект, немалый, как по меркам домашнего проекта на 1 человека. Так вот, я не хотел тянуть туда хиб, и писал сначала на jdbc. А потом знаете что случилось? Я настолько заебался писать те мапперы, которые вы хвалите, и тысячекратно их править от каждого чиха в моделях, что написал собственную обертку над jdbc. С маппингами, логированием, транзакционностью и возможность открыть стрим прямо из абстракции над резалт сетом. С тех пор я больше не страдаю иллюзиями простых решений про «раньше было лучше». Я полностью прошел этот путь и понял, что он тяжелый и неприятный. И что есть лучшие пути. Они разнообразны и обсуждаемы, но прямые мапперы в стиле jdbc-архаики — худшая из дорог.

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

одна строчка на изменение одного поля. Ничего для полей и сущностей уходящих в jsonb или xml, но за это своя цена поддержки схемы в коде.

А как вы миграции к базе делаете? Из хибернейтной схемы генерите? Данных наверное не очень много?

одна строчка

Я закончил обсуждать jdbc маппер, мне больше нечего добавить, я все сказал по этой теме. Хотите, можете хоть на нейтив си методы перейти.

А как вы миграции к базе делаете?

Скрипты миграций пишем руками, ничего сложного. Скрипты накатывает флайвей. Хиб к DDL не допущен.

Из хибернейтной схемы генерите?

Нет, чтобы делать ddl под те изменения, что нам нужны, хибернейт не нужен. Любой разработчик может сам описать все изменения, которые внес в модель.
Только пару раз нам понадобилась помощь DBA, когда модель менялась очень радикально. Там скрипт составлялся недели две и занял несколько сотен строк PLSQL магии. Но там была суть не в самом ддл, а в переносе старых данных но новый вариант структуры.

Данных наверное не очень много?

Да нет, не очень, где десятки миллионов строк, а гдето сотни миллионов, а гдето может по несколько тысяч, по разному. Миграции по полчаса могут идти или дольше, если большую таблицу цепляет.

Что смущает в мутирующих объектах?

мутации сложно отслеживать в нетривиальных проектах.

О каких костылях Хибернейта идёт речь?

На каждое отступление от идей ORM начала 2000х костыль нужен. Захотел в постгресе jsonb заиспользовать, чтобы ненужные джойны не плодить — добро пожаловать свой диалект писать. Нужно обновить версию parent по обновлению child? Добро пожаловать в прекрасный мир Event Listeners. Которых в свою очередь есть разные наборы с разными ограничениями.

мутации сложно отслеживать в нетривиальных проектах.

Повторюсь, мы о сеттерах или бизнес методах-мутаторах?

Захотел в постгресе jsonb заиспользовать, чтобы ненужные джойны не плодить — добро пожаловать свой диалект писать.

А если ещё и от провайдера JPA абстрагироваться, то даже возможности писать диалект не будет.

Нужно обновить версию parent по обновлению child? Добро пожаловать в прекрасный мир Event Listeners.

А вот тут, при всём уважении к Владу, давайте с моей максимой зайдём.

JPA же предлагает другой принцип: вначале Java-классы, потом их сохранение в БД.

Видимо таки придётся во всех мутаторах child-а ручками дописать обновление версии parent-а. А то в EclipseLink лисенеры другие.

Повторюсь, мы о сеттерах или бизнес методах-мутаторах?

о вот таком:

order.addItem(item1);

Что при более внимательном рассмотрении действительно не так страшно — отследить изменения легко.

public Map<Item, Integer> getItems() {
   return Collections.unmodifiableMap(items);
}

^^ если бы все пользователи Хибернейт так делали, моя жизнь была бы лучше. К сожалению, в живой природе попадается все чаще такое (кстати не от индусов каких, а от вполне таких наших синьоров с энтерпрайзных галер):

public List<Item> getItems() {
   return items;
}
... 

doSomeFancyStuffWithLotsOfStreamingAndMaybeBitOfReactiveProgrammingAndSneakilyModifyCollectionInASubtleWay(myOrder.getItems())

что становится сложнее отследить для частоиспользуемых сущностей, особенно если нет явных repository.save(), что и вызвало мою бурную реакцию.

А если ещё и от провайдера JPA абстрагироваться, то даже возможности писать диалект не будет.

Зачем?

Видимо таки придётся во всех мутаторах child-а ручками дописать обновление версии parent-а.

Я бы с удовольствием, только из-за того, что нету явного момента сохранения — это становится немного проблематично. Очень. Кроме того заносить такие детали в каждый доменный объект:

public void setStatus(Status newStatus) {
    this.status = newStatus;
    this.increaseMyAndParentVersion();
}

не всегда желательно. И наверное будет не очень приятно если под Лобоком сидите.

А то в EclipseLink лисенеры другие.

тут у меня 100% YAGNI.

что становится сложнее отследить для частоиспользуемых сущностей, особенно если нет явных repository.save(), что и вызвало мою бурную реакцию.
что нету явного момента сохранения

А и их и не должно быть. Зачем делать save персиснтнутого объекта?

Вы в другой ветке ломаете копья, какой инструмент лучше, и вполне может быть, что для ваших задач stateless jooq, jdbc templates и т.д. предпочтительнее.

Можно хитрый batch update написать, пока глупый хибернейт будет сто раз селекты делать, а потом сто апдейтов за ними.

Я с задачей обновить версию parent-а при изменении child-а не сталкивался, поэтому, каюсь, вчера не вдумался. Зато вам спасибо — напомнили важное.

Абсолютно во всех проектах, когда все сервисы, дао и т.д. написаны, приходил бизнес и говорил: «А теперь надо сделать аудит. Какой пользователь, когда, что и на что поменял. В какую коллекцию что добавили, а что удалили. Складывайте это всё в отдельную табличку, мы будем на неё смотреть».

И тут внезапно я готов и принципом «доменная область должна сама всё уметь делать» поступиться, и state во благо, и селекты перед апдейтами нужны, и EventListener-ы — не костыли, а единственный выход. Несмотря на то, что в Hibernate одни нюансы, а в EclipseLink-е другие, успешно справлялся.

Поделитесь, пожалуйста, как бы вы со stateless репозиториями эту задачу решали.
Наворачивать руками dirty checking в джаве? Который уже сделан и отлажен в хибере?
Или триггерами в базе?

Потому как аудит бизнесу удобный нужен, не «orderId: 235, clientId: 111 -> 222», а понятный «Заказ 235, клиент: Петя -> Вася», плюс иногда понятность для бизнеса может отличаться от объектной, т.е. технически это изменение child-а, но записываем его как изменение parent-а.

ОК, аудит — хороший вопрос.

Потому как аудит бизнесу удобный нужен, не «orderId: 235, clientId: 111 -> 222», а понятный «Заказ 235, клиент: Петя -> Вася», плюс иногда понятность для бизнеса может отличаться от объектной, т.е. технически это изменение child-а, но записываем его как изменение parent-а.

 не очень понимаю в чем для визуализации Хибернейт выигрывает когда аудит уже сохранен. Он же тоже айдишки будет в _aud хранить, которые ризолвить нужно.

Теперь по делу. Когда нужно отслеживать изменения, то таки да — лучше это делать явно на уровне домена. сильное ИМХО.

Хибернейтовский @Audited — штука полезная, но больше для прикрытия формальных требований аудита, чем бизнес требований. Работать с его данными не удобно. ИМХО. Могу ошибаться, может можно оттюнить.

В моей практике изменения приходят трекаемыми и линкуемыми командами, большая часть данных append only, немногочисленные изменяемые поля проверяются на изменение еще бизнес-логикой, до персистентности, и трекание их не составляет проблем. Плюс внешние системы типа Кафки для хранения и поглощения истории всеми заинтересованными.

Но это специфика, засчитываю себе аудит как потенциальный плюс Хибернейта, особенно для более CRUD приложений.

не очень понимаю в чем для визуализации Хибернейт выигрывает когда аудит уже сохранен. Он же тоже айдишки будет в _aud хранить, которые ризолвить нужно.

Не факт, что айдишки, может натурально строки, как в log-файле, foreign key все равно не будет. И не все сразу приходят к идее ставить флаг удаления, некоторые физически записи удаляют.

Когда нужно отслеживать изменения, то таки да — лучше это делать явно на уровне домена.

В теории — да. Когда выше речь шла о изменении версии parent-а, я тоже так думал. Но в случае аудита — сразу сдаюсь, не буду я каждый сеттер править.

Хибернейтовский @Audited — штука полезная, но больше для прикрытия формальных требований аудита, чем бизнес требований

Так и есть. Из Envers только идею инструмента — eventlistner — можно взять, тюнинг там примитивный и потому недостаточный. А в тех двух аудитах, которые я написал, бизнес был весьма изобретателен, казалось бы в такой самоочевидной задаче.

немногочисленные изменяемые поля проверяются на изменение еще бизнес-логикой, до персистентности, и трекание их не составляет проблем.

А теперь давайте возьмём мой пример из статьи: Order-Client-Item-Quanity. И представим, что трекать нужно каждый чих.

Но это специфика, засчитываю себе аудит как потенциальный плюс Хибернейта, особенно для более CRUD приложений.

Ага

Не факт, что айдишки, может натурально строки, как в log-файле, foreign key все равно не будет.

Хибернейт тут роли не играет.

И не все сразу приходят к идее ставить флаг удаления, некоторые физически записи удаляют.

Хибернейт тут тоже не поможет, пока не задумаются. Ну разве что доку читать будут и озадачатся.

В теории — да. Когда выше речь шла о изменении версии parent-а, я тоже так думал. Но в случае аудита — сразу сдаюсь, не буду я каждый сеттер править.

В то время как с явными репозиториями задача решается легко добавлением сравнения с сохраненной версией. Это к вопросу зачем явное сохранение может быть удобно.

А теперь давайте возьмём мой пример из статьи: Order-Client-Item-Quanity. И представим, что трекать нужно каждый чих.

такое бывает? Никогда не видел чтобы созданный ордер менялся / удалялся после того как засубмишен. Это по законодательству недопустимо. Отменяться / статус меняться может, но это другие транзакции. ИМХО пример очень плохой.

Никогда не видел чтобы созданный ордер менялся / удалялся после того как засубмишен.

Пффф. Аж бигом. Мы же понимаем, что ордер-клиент-итем в данном контексте это пример, а не жесткая привязка?

Бизнес такой, да. Если есть объект, то с вероятностью, близкой к 1, бизнес захочет его редактировать, удалять, восстанавливать после удаления, снова удалять после восстановления, знать, кто и когда редактировал/удалял, запрещать комуто редактировать/удалять, вызывать сайдэффект логики если чтото из вышеперечисленного произошло. Потом бизнес придет к вам и скажет, что вообще-то им нужно, чтобы ордер можно было удалять, если у клиента больше 10 ордеров, не менее 5 из которых уже выполнены, но есть и невыполненные, каждый на сумму не менее Х, при этом клиент зарегистрирован в системе дольше чем У, у клиента есть машина (ват, но у нас же нет этого в модели — добавьте) и при этом ФОТ предприятия больше Z.
И это не то чтобы анекдот.
Мне приходится регулярно заворачивать хотелки бизнеса потому что «сколько лет бабушке если цена гвоздей 5 метров» это терпимо по сравнению с той ахинеей которую мне иногда дают на оценку тз.

Это по законодательству недопустимо.

Пффф лол.

Хибернейт тут роли не играет.

Я переформулирую, расклад такой. Меняется и удаляется вообще всё, как расписал Dmtry Bugay. Поэтому аудит — это незатейливая табличка из трёх столбцов: кто-когда-что сделал (в текстовом виде). Никаких референсов на другие таблицы у неё нет.

Бизнес-логика в таком стиле

<blockquote>public List<Item> getItems() {
   return items;
}
... 

doSomeFancyStuffWithLotsOfStreamingAndMaybeBitOfReactiveProgrammingAndSneakilyModifyCollectionInASubtleWay(myOrder.getItems());

Лично я предпочитаю бизнес-мутаторы, но Хибернейту, по большому счёту, всё равно, кто и как объект поменял.

Явного сохранения нет, и быть не должно по самому определению персистент объектов.

А теперь делаем аудит. И с EventListener-ами это как раз не проблема. Есть предыдущий state и текущий. Смотрим разницу и пишем в audit table.

явными репозиториями задача решается легко добавлением сравнения

Так а что вообще в CRUD задачах сложного? C хибернейтом просто кода меньше писать, а так всё то же самое.

Я вижу то, что думаю? Обновление order без явного вызова репозитория

Хибернейт это позволяет, и это не антипаттерн. Нужно только определиться с политикой каскадов операций по связям. Мое имхо что каскады лучше не использовать вообще.

Мое имхо что каскады лучше не использовать вообще.

 в смысле каскады на обновление / удаление не использовать? И на хибернейт только для вычитки длинных графов полагаться?

в смысле каскады на обновление / удаление не использовать?

Да. Мое имхо, чисто имхо, что каскады — это тот самый револьвер, стреляющий в ногу. Но т.к. это только мое мнение, то не стану говорить, что оно правильное. Использование каскадов вполне легально, и не апнтипаттерн просто мне не очень нравится. Особенно каскад=делит.

И на хибернейт только для вычитки длинных графов полагаться?

Нет. Я вот сейчас не уверен, что ты понимаешь, что имеется в виду под каскадом. Каскад — это передача операции во вложенные сущности.

Думаю, что понимаю. У меня все ноги в крови от 

public List<Item> getItems() {
   return items;
}
... 

doSomeFancyStuffWithLotsOfStreamingAndMaybeBitOfReactiveProgrammingAndSneakilyModifyCollectionInASubtleWayMaybeCompletelyDifferentThread(myOrder.getItems())

а что, когда удаляется родитель — удалить детей?

Да.
В том числе, если добавиь несохраненное дитя в коллекцию несохраненного родителя, а потом сохранить только родителя, это вызовет каскадное сохранение и детей.

1) демо/пилотный проект. Работа с БД не важна, в простейшем случае не нужно об этом думать — все запустит Spring/JPA/Hibernate.

Все-таки не «демо/пилотный», а проект где нужно часто менять требования/структуру БД. То есть большенство проектов в первые 0,5-1 год своей жизни. Плюс всякие админки и тд КРУД приложения. В теории с появлением spring.io/...​projects/spring-data-jdbc хибер может уйти на ... второй план.

какие проблемы решает Хибернейт не создавая больших, когда есть вменяемый jOOQ, Spring JDBC templates, etc?

Spring JDBC templates? Рылли? Даже в инфраструктуре спринга репозитории на ДжДБС теплейте занимают больше времени на разработку чем с хибером (это по опыту последних проектов). Сюда же количество багов, особенно когда люди не включат БД в воркфлоу тестов.
Сюда же надо не забывать о том что существуют джуниоры, которые на ДжДБС теплейте такого трешака могут натворить (чтобы код был типобезопасным и переиспользуемым :) )

jOOQ как бы платный. Это шоустопер для энтерпрайзов.

№ 3: оказывается все еще есть много проектов где не используют спринг :) А по «альтернативным JPA» СО ответы сложнее найти.

Но в общем Хибернейт — гуано. По статье тоже есть вопросы, так сказать, но обсуждать мне их не хочетсо.

Spring JDBC templates? Рылли? Даже в инфраструктуре спринга репозитории на ДжДБС теплейте занимают больше времени на разработку чем с хибером (это по опыту последних проектов).

Да? А че так? У меня пятый год активного использования в разных проектах — полет вполне нормальный. Особенно с переходом на Котлин, так можно даже сказать приятно. Но все с минимальной магией, без стандартных Спринговых репозиториев — простые Jdbc Templates, Insert Templates или как их там зовут.

Сюда же количество багов, особенно когда люди не включат БД в воркфлоу тестов.

Это в любом случае. docker + полноценная база без всяких компромиссов типа H2. Но это к хибернейтному коду тоже относится. Даже еще больше, в нем одними репозиториями точно не обойдешься.

Сюда же надо не забывать о том что существуют джуниоры, которые на ДжДБС теплейте такого трешака могут натворить (чтобы код был типобезопасным и переиспользуемым :) )

Так эти же джуниоры такого же или еще худшего с Хибернейтом вытворят если оставить без присмотру. И хорошо если только с моделью, а то как-то свой Entity Traverser написали — то вообще так и не получилось выковырять.

jOOQ как бы платный. Это шоустопер для энтерпрайзов.

Платный для платных БД вроде? Но понимаю, Оракл корпоративно закупили, а на эту лицинзию уже денег не получить.

№ 3: оказывается все еще есть много проектов где не используют спринг :) А по «альтернативным JPA» СО ответы сложнее найти.

Apache commons db? Использовал до Спринга — вроде тоже терпимо было.

Но в общем Хибернейт — гуано.

Где ж выход?

Но понимаю, Оракл корпоративно закупили, а на эту лицинзию уже денег не получить.

Корпоративный оракл продали/втюхали. А как вы буде обосновывать необходимость лицензии jOOQ?

Это в любом случае. docker + полноценная база без всяких компромиссов типа H2

А теперь представляем что билд-агенты не имеют доступ к докерхабу и вообще вам не подконтрольны.

Да? А че так?

Потому что нужно написать запросы, добавить все поля в маперы, не забыть как из резалтсета получить нулл значение, для интов/лонгов например. А еще нужно трекать состояние «сессии» чтобы знать какой элемент вложенной коллекции надо инсертить, а какой обновлять/игнорировать. Если у вас делит — это именно делит, а не апдейт, тут тоже надо думать.

Корпоративный оракл продали/втюхали. А как вы буде обосновывать необходимость лицензии jOOQ?

ok, ok, you won this round.

А теперь представляем что билд-агенты не имеют доступ к докерхабу и вообще вам не подконтрольны.

А чего тут представлять, они у меня и не имеют доступа к докерхабу, и мне не подконтрольны. Но имеют доступ ко внутреннему докер репозиторию и там лежат базовые имиджи. Нету возможности запускать контейнеры для CI в 2019? Ну, надеюсь вы с них рейтами хорошо за это дерете. Можно локально гонять тогда за такие деньге :)

Потому что нужно написать запросы, добавить все поля в маперы, не забыть как из резалтсета получить нулл значение, для интов/лонгов например. А еще нужно трекать состояние «сессии» чтобы знать какой элемент вложенной коллекции надо инсертить, а какой обновлять/игнорировать. Если у вас делит — это именно делит, а не апдейт, тут тоже надо думать.

Ха, дяденька, так вы это хибернейт заново переписывали? Так лучше уж его натуральный наверное пользовать. Нужно удалить элемент из коллекции? entityRepository.delete(child, parent); — в таком ключе я имею ввиду использование Spring JDBC и подобного. Явно, просто, понятно, и не так сложно как адепты ORM пугают.

Ха, дяденька, так вы это хибернейт заново переписывали?

Да, потому что архитектор уверен что «хибернейт тормозит» :)

Нету возможности запускать контейнеры для CI в 2019?

Угу, нет возможности запускать на билд-машинах. Секурити-шмакурити.

Да, потому что архитектор уверен что «хибернейт тормозит» :)

и чем закончился эксперимент? Опередили?

Выучил пару-тройки прикольных штук о том как разруливать то что делает хибер :)
Когда код был соизмерим по функционалу, то перформанс мы не меряли ибо все знают что «хибернейт тормозит».

Но в общем Хибернейт — гуано.

Это как демократия, которая тоже говно, но ничего лучшего не придумали :)

Но в общем Хибернейт — гуано.

(голосом Подервянского) И яка розумная цьому stateful-альтернатива?

Там выше за stateless топят, а я нет — мне аудит нужен.

Такое чувство что автор не знает о чем пишет

«Любая дискуссия в интернете постепенно скатывается на личности, поэтому чтобы сэкономить время, начну сразу с них»?

не в этом дело, а в достаточно очевидных вещах.

Дальше почему-то происходит следующее. Вначале создаются таблицы, что-то
вроде:
*рисунок спрятан*
А потом для них — соответствующие entities

Специально прошелся по тренировочным курсам, ни в одном не видел обучение такому подходу. назвать это «обычным» весьма странно. пример: java brains: www.youtube.com/...​w&list=PL4AFF701184976B25

public static class StatusConstants
Очевидно, что: для статуса заказа удобно использовать Enum;

Какое это имеет отношение к JPA. Оно выглядит будто подходит, но на деле оно независимо.

нужно помнить, что флажок express представлен числом: 1 — true, 0 — false;

PostgreSQL, к примеру, как и другие SQL, имеет тип boolean, так что проблема не в переносе в джаву.

легко ошибиться со значениями поля status;

каким образом?

для цены — тоже, причём уже есть готовый — Money из JavaMoney;

добавление фреймворка (ненужного, для денег вполне подходит BigDecimal, если на слово не верите... stackoverflow.com/a/8148773/5070158), не добавляет статье\туториалу ценности. Только усложняет понимание.

JPA же предлагает другой принцип: вначале Java-классы, потом их сохранение в БД.

JPA позволяет отделить\разделить создание БД и создание сущностей и их последующий маппинг. т.е. описывать связь сущности и ее представление в БД можно после того как уже отдельно создана БД, отдельно созданы сущности, можно с помощью JPA сделать разметку что-куда.

это подводит к куда более серьезному вопросу:

конвертируется в часть инструкции «create table»

name varchar(32) not null

созданная JPA схема — это, скорее, заготовка для flyway/liquibase и/или in-memory БД.

Зачем учить тому, что нужно потом отбросить. Да и не указано как это отбрасывать, что из этого правильно делать в продакшене, а что нет.
(ответ: создавать схему которую автоматически генерируют на продакшене — не лучшая идея)

отсюда и самый первый комментарий: возникает чувство что автор не знает о чем пишет. Почему? потому что непонятно кому нужна статья. Кто целевая аудитория? Новичкам вредна, опытным не нужна.

Идем дальше, скачки в логике:

хотя Hibernate требует для своей работы пустой конструктор, для инициализации объектов можно и нужно использовать конструкторы с параметрами.
С учетом базового принципа «Класс должен уметь работать без JPA» лучше:

Почему?

Ответ на вступительный вопрос.

@Column(name = «name», nullable = false, length = 32)
private String name;
... конвертируется в часть инструкции «create table»

name varchar(32) not null

Учитывая, что автоматическое создание таблиц противопоказано в продакшене, то это не делает этот вариант лучше. Хуже, правда, тоже не делает. Но отговаривать пользоваться отдельной аннотацией я бы не стал.

При написании бизнес-логики объекты сравнивают между собой. Некоторые пишут так:

boolean equals = order.getId().equals(anotherOrder.getId());

Не описано почему. Опять таки, непонятно для кого статья. Я, допустим, понимаю, что там прокси и при прямом сравнении может оказаться, что объекты разные, потому что две разных прокси указывающие на одну и ту же сущность. А те кто хотят понять работу с Hibernate?

EntityManager

Самое странное для меня. Уже давно есть Spring Data JPA. Если об этом не указано, поскольку идет обучение конкретно Hibernate, то еще ладно. Но ведь даже не указано в

За кадром остались:

а тема полезна для тех кто работает с orm.

Должен сказать что разбора как именно работает Hibernate нет. Объяснения чем есть прокси, для чего нужна и как это сказывается. К тому же не указываются те минусы, которые неизбежны при использовании Hibernate. Разобрано на примере «абстрактной DB» в вакууме. К тому же ньюансы работы с hibernate обычно спрятаны за другими фреймворками (тем же Spring Data JPA)

Вотъ, теперь когда прошелся по статье, могу сказать что «о чем пишете» знаете, но выразить это не получилось. На мой взгляд.

— Надо делать вот так!
— Почему?
— Потому что...
— Так почему?
— Потому что....
— Ну?
— Потому что.

PS: у Вас были проекты где нельзя было использовать Spring Data?

добавление фреймворка (ненужного, для денег вполне подходит BigDecimal, если на слово не верите... stackoverflow.com/a/8148773/5070158)

Вот ту не соглашусь.
У нас коммерческое апи, и мы используем БигДецимал для денег.
Это ахуеть как неудобно, и мне пофиг на ссылки на соф.
Математические операции с деньгами на БигДецимал — это АдЪ и Изгаиль, врагу не пожелать.

(ответ: создавать схему которую автоматически генерируют на продакшене — не лучшая идея)

Совершенно верно. Для прода есть миграторы вроде флайвея, где скрипты пишутся руцями, форматируются, и сто раз тестируются. Автор как я понял, это и имел в виду, упомянув флайвей.

А в целом да, критика имеет место быть.

1. Для статьи ценности не имеет, учиться хибернейт.
Для продакшена — да, имеет, но тогда мы попадаем на 2...
2. Упоминание одной строкой, против статьи где две страницы примеров как делать с авто генерацией.

Ну и, 1 и 2 два противоположных подхода. Почему не следовать одному принципу. Либо давать оптимальные фреймворки, либо уменьшить их количество объясняя чётко hibernate.

И вообще неплохо было бы описать место hibernate в мире, когда нужен, как и чем его применять. Ведь чистый hibernate никто не использует

место hibernate в мире

А что тут описывать, тотальное доминирование :)

Ведь чистый hibernate никто не использует

Ну да, обычно через фасад жпа как минимум.

PostgreSQL, к примеру, как и другие SQL, имеет тип boolean, так что проблема не в переносе в джаву.

А Oracle, к примеру — не имеет. Но это не значит, что в entity следует использовать Integer.

легко ошибиться со значениями поля status;
каким образом?

Если для поля status использовать String, то можно приосвоить произвольное значение, если enum — только определённые.

для денег вполне подходит BigDecimal,
у товара есть неизменная цена с валютой;

Вы невнимательно читали статью? Или хотите подискуссировать на тему класс vs отдельные поля? Свои аргументы я привёл в этой ветке dou.ua/...​to-use-hibernate/#1581031

С учетом базового принципа «Класс должен уметь работать без JPA» лучше:
Почему?

Знакомы с понятием инкапсуляции?

Опять таки, непонятно для кого статья.
поскольку идет обучение конкретно Hibernate

Это не обучение. Делюсь опытом и проблемами с коллегами. В этой ветке dou.ua/...​to-use-hibernate/#1580909, например, обсуждалась эквивалентность объектов.

Самое странное для меня. Уже давно есть Spring Data JPA.

Все описанные проблемы (см. список в конце статьи) имеют место быть в Java Persistence API независимо от реализации.

могу сказать что «о чем пишете» знаете, но выразить это не получилось.

В комуникации всегда есть две стороны.

Давайте я вам в качестве ответной любезности верну — такое чувство, что комментатор не понимает о чём статья, потому что работает только со Spring Data и POJO не зная об ООП.

PS: у Вас были проекты где нельзя было использовать Spring Data?

Да, например, JEE + Glassfish + EclipseLink. Ещё раз — всё в статье о JPA, независимо(*) от провайдера.

* — в статье есть один Hibernate-специфичный нюанс

А Oracle, к примеру — не имеет. Но это не значит, что в entity следует использовать Integer.

Если в примере абстрактная БД это Oracle, то это лучше упомянуть. В статье.

Если для поля status использовать String, то можно приосвоить произвольное значение, если enum — только определённые.

Если значения вводит пользователь, то программист забыл сделать проверку в программе.
Если вводит программист, то почему использует стринги вместо ссылок из Static класса.
Если это нуб, кто доверил ему писать этот код, и как он прошел код ревью?
Т.е. по сути проблема не в Hibernate, и возращаемся к нашим баранам: для кого статья написана? Для опытных (тех кто умеет пользоваться hibernate) или для новичков?

у товара есть неизменная цена с валютой;
Вы невнимательно читали статью?

Как сказал начальник борьбы с наркотиками: «это был мой косяк».

Знакомы с понятием инкапсуляции?

Что возращает меня к вопросу: почему? Точнее, в чем преимущества перед привносимыми недостатками: boilerplate code:

public Map getItems() {
return Collections.unmodifiableMap(items);
}

public void addItem(Item item) {
items.merge(item, 1, (v1, v2) -> v1 + v2);
}

public void removeItem(Item item) {
items.computeIfPresent(item, (k, v) -> v > 1 ? v — 1 : null);
}

Это не обучение. Делюсь опытом и проблемами с коллегами.

Спасибо. Без сарказма, вещь нужная.

Это не обучение
Как использовать Hibernate

Статья идет с объяснениями и правильными идеями, но с другой стороны есть вещи которые не стоит использовать в продакшене. Зачем на них объяснять? Чтобы упростить? Ок, но почему тогда другие моменты не упростить. Получается одно упрощается, приводиться в упрощенном варианте, другое наоборот — в усложненном варианте который можно нести в продакшн. В чем подвох?

Давайте я вам в качестве ответной любезности верну

Без проблем, я не против критики

такое чувство, что комментатор не понимает о чём статья

К сожалению, не понимаю на кого она направлена и чему призвана научить. Если, как Вы говорите:

Делюсь опытом и проблемами с коллегами

непонятна первая часть статьи. Вторая действительно делиться опытом (Правила переопределения equals&hashcode для hibernate, lombok), первая же часть статьи — каламбур. Описание того что сделает «самый странный нуб» если вдруг решит что всё умеет и возьмется за джаву. Как будто два разных человека писали. Или в два разных дня а потом склеили.

Опять таки, не указан стэк, приходиться догадываться что:

JEE + Glassfish + EclipseLink
Oracle

Но в статье приводиться что оно как бы и абстрактно, но есть технические ньюансы которые на деле подходят только некоторым технологиям. Далее...

потому что работает только со Spring Data и POJO

Не только, но предпочитаю. ИМХО, быстрее.

не зная об ООП.

Знаю. Нужно ли она для статьи? Указать что-ли это стоило в статье. ООП, как и другие принципы, не панацея, надо использовать когда оно подходит. Здесь нет наследования, инкапсуляция порождает boilerplate. Мы о каких-то разных ООП говорим?

А Oracle, к примеру — не имеет. Но это не значит, что в entity следует использовать Integer.
Если в примере абстрактная БД это Oracle, то это лучше упомянуть. В статье.

Ок. Я объясню. В программировании предпочтительнее оперировать стандартами (интерфейсами), а не конкретными реализациями. Поэтому, какой бы провайдер JPA вы не выбрали (Hibernate, EclipseLink и т.д.), и какая бы реляционная БД не была (Oracle, Postgres, MySql и т.д.) есть такая фича. Можно указать поле private boolean express; и его значение правильно сохраниться в таблице, вне зависимости, есть в базе такой тип или нет.

Если значения вводит пользователь, то программист забыл сделать проверку в программе.
Если вводит программист, то почему использует стринги вместо ссылок из Static класса.

Немного растерялся, вы знакомы с понятием Enum? docs.oracle.com/...​ial/java/javaOO/enum.html

Программист имеет возможность ошибиться и «использовать стринги вместо ссылок из Static класса», а enum не позволит скомпилировать такой код.

Что возращает меня к вопросу: почему? Точнее, в чем преимущества перед привносимыми недостатками: boilerplate code:
Здесь нет наследования, инкапсуляция порождает boilerplate.

Вы просите объяснить преимущества инкапсуляции перед POJO?
И давайте уточним, одинаково ли мы понимаем слова boilerplate code. Вы что в них вкладываете в этом примере?

есть вещи которые не стоит использовать в продакшене.

Какие?

Опять таки, не указан стэк, приходиться догадываться что:

Зачем указывать стэк?! Повторюсь,

Все описанные проблемы (см. список в конце статьи) имеют место быть в Java Persistence API независимо от реализации.
Мы о каких-то разных ООП говорим?

Судя по всему, да. Java — объектно-ориентированный язык программирования. Java Persistence API — о том, как эти самые объекты хранить в БД.

для кого статья написана?

Для тех, кто понимает суть поднятых вопросов.

Ок. Я объясню. В программировании предпочтительнее оперировать стандартами (интерфейсами), а не конкретными реализациями. Поэтому, какой бы провайдер JPA вы не выбрали (Hibernate, EclipseLink и т.д.), и какая бы реляционная БД не была (Oracle, Postgres, MySql и т.д.) есть такая фича. Можно указать поле private boolean express; и его значение правильно сохраниться в таблице, вне зависимости, есть в базе такой тип или нет.

Напомню, мы говорим об вот этом:

А потом для них — соответствующие entities. В зависимости от опыта разработчиков, в самом тяжелом случае получается такое.

@Entity
@Table(name = «orders»)
public class OrderEntity {
...
private Integer express;

Я конечно понимаю что случай «самый тяжелый», но это вещь специфична. Как бы здесь камень преткновения между абстрактной БД и ньюансами реализации в, к примеру, Oracle.

Мы не говорим за то что я такой бездарь чего-то там не знаю :D

Немного растерялся

Сам в шоке. Можно мы это забудем? Пытался объяснить что программисты не дураки, но потом понял, что зря это делаю.

Вы просите объяснить преимущества инкапсуляции перед POJO?

Прошу объяснить откуда она прилетела в статью. Но я так понимаю, что из того же:

Это не обучение.

Как итог: статья в начале идет под видом обучения. Но позиционирует себя как проблема-решение, но иногда не указывает проблему, а иногда не указывает каким путем пришли к решению.
Абзац выше — личное мнение.

Я конечно понимаю что случай «самый тяжелый», но это вещь специфична. Как бы здесь камень преткновения между абстрактной БД и ньюансами реализации в, к примеру, Oracle.

Здесь нет камня преткновения для абстрактного JPA, который работает с абстрактной БД.
Тяжелым случаем его делает разработчик, который глядя на Oracle с полем Number(1), пишет
private Integer express;
вместо
private boolean express;

Вы просите объяснить преимущества инкапсуляции перед POJO?
Прошу объяснить откуда она прилетела в статью.

en.wikipedia.org/...​on_(computer_programming

Т. Е. Все таки глядя на оракл?

Насчёт инкапсуляции говорить не буду, эта беседа пошла в уж очень странном направлении

Т.е. если так сложилось, что entities «угадываются» для существующей таблицы, будь то Oracle с NUMBER(1), или Postgres с BOOLEAN, или MySql с BIT(1) — во всех случаях удобнее, если в джаве будет

private boolean express;

А этот момент давайте отдельно.

(ответ: создавать схему которую автоматически генерируют на продакшене — не лучшая идея)

Тут я спрошу «Почему?» Какие аргументы против?

Сразу за скобки вынесем, что:
1) приложению права на DDL в БД обычно не дают,
2) последующие изменения структуры классов/таблиц тоже к вопросу не относятся.

вот тут великий срач: stackoverflow.com/...​auto-update-in-production

сразу скажу, что ответ(ссылка) подходит для более развернутых сущностей с их взаимотношениями. Для данного примера подойдет и автогенерация, но это не значит что с ней лучше работать или лучше на ней объяснять.

Вы снова невнимательны.
Я же два раза уточнял

Поскольку при разработке приложения структура классов меняется, созданная JPA схема — это, скорее, заготовка для flyway/liquibase и/или in-memory БД.
2) последующие изменения структуры классов/таблиц тоже к вопросу не относятся.

Я говорю о create, а не о update.

docs.jboss.org/...​tml#configurations-hbmddl

none
No action will be performed.

create-only
Database creation will be generated.

create
Database dropping will be generated followed by database creation.

имееться ввиду create-only, так?
раньше, если правильно помню, этой опции не было и create банально мог затереть существующие данные. Для разработки — норм, для проды...

допустим, что create-only.

Увеличивает время на загрузку приложения (слабый аргумент)
аргумент два: когда добавим новый код, как это поддерживать? Раньше мы предпологали что используем:

flyway/liquibase и/или in-memory БД.

Три: придеться разбираться с hibernate ньюансами для создания индексов, связей и ограничений, которые могут интересовать нас только в контексте DB
DB-specific limitations. Если есть «тонкости работы» с БД, то придеться их учить дважды. сначала с базой, потом с hibernate. Вообще с ORM это великая беда, но ведь к hibernate относиться, верно?

Попробую перефразировать начало статьи другими словами.

Речь вообще не о том, чтобы позволить приложению создавать схему на проде.
Речь о том, что маппинг можно прокладывать с двух сторон.
Либо создавать джава классы и

разбираться с hibernate ньюансами для создания индексов, связей и ограничений,

(их немного и они очевидны)
... смотреть на созданную схему и использовать её в flyway/liquibase.

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

Так это ж совсем другое дело...
Проблема только в том что это не в статье (или недостаточно явно)

Из комментариев можно отдельную статью создать, ибо хватает поправок от читателей. И как я уже уточнял, дело не в незнании автора(он то знает) а в подаче материала.

Тут я спрошу «Почему?» Какие аргументы против?

Аргументов будет ОЧЕНЬ много. На самом деле просто лениво писать целую статью на ваше вопрос почему, с примерами из жизни, если и так почти все об этом знают.

Я в этой ветке dou.ua/...​to-use-hibernate/#1581522 привёл свои аргументы

Фреймворки не нужны.

Языки программирования не нужны.

Только машинные коды, только хардкор

Только алгебра.

Найголовніше шо має мати будь-яка ORM — це логування всіх реальних запитів у базу з таймінгами виконання і змінними які передаються. Бо як би нам не хотілося, абстрагуватися від бази, але все одно там чи сям протікає і треба дивитися чому твій запит робить N+1 або чому довго виконується і так далі.

І з цим у хібера серйозні проблеми. Дефолтне логування просто нікуди не годиться, воно зроблено на диво погано для такого популярного проекту.

Для порівняння подивіться на логи ActiveRecord, та це просто небо і земля.

Дефолтне логування просто нікуди не годиться, воно зроблено на диво погано для такого популярного проекту.

Вот тут согласен полностью. Логи хиба отвратные. С форматом логов еще можно было бы мириться, если бы он умел вставлять в лог параметры вместо ?, ?, ?, ?.

на логи ActiveRecord

АР это концепция а не имплементация. С имплементациями в джаве не очень. А причина одна — безблагодатность Мартин теоретик. АР как идея сама по себе в реализации превращается в костыль. Даже теоретически АР означает что внутри объекта должна содержаться ссылка на провайдер какойнибудь персистентности, что убивает все влажные мечтания о поджо, объектности, слабой связности и теде и тепе.

АР это концепция

Я мав наувазі лібу для Ruby. Вона так і називається — ActiveRecord. Дивно пропонувати подивитися на те як організовані логи в концепції, егеж?)

С имплементациями в джаве не очень.

Це проблеми джави і тугої спільноти. Рубі-пани пишуть user.save вже 10 год як і у вуса не дують.

Рубі-пани пишуть user.save вже 10 год

Расскажи, как персистенс провайдер инжектится при
User user = new User();
user.blaBlaBlaSet
user.save();
офигеть как интересно.

Я мав наувазі лібу для Ruby. Вона так і називається — ActiveRecord.

Понятно. Тема то про хиб, и я изначально думал что ты имеешь в виду какуюто джава реализацию.

Расскажи, как персистенс провайдер инжектится

Факторі метод :) Ізі.
User user = users.create();

я изначально думал что ты имеешь в виду какуюто джава реализацию.

Все шо на джаві надристанописано — купа енетпрайзного смердячого лайна, яка ні разу не поважає ергономіку користування та зручність для програміста. Так і будемо сидіти з геттерами і сеттерами ажно до кінця кар’єри.

Факторі метод :) Ізі.

Да, именно это я и хотел услышать. Т.е. вы в принципе отказались от чистых объектов, молодцы. Стильно-модно-маладьожно. Вы прибили гвоздями доменную область прямо к провайдеру персистентности.

Говорить про говнистость джавы после такого это просто лол :)

купа енетпрайзного смердячого лайна

Сорян, но с точки зрения дизайна и ооп, это относится как раз не к джаве, а к фактори методам для получения плейн обжект

Вы прибили гвоздями доменную область прямо к провайдеру персистентности.

Топ кек. Ви так пишете ніби це щось погане. Добре що вам не треба вручную істанціювати entityManager бо спрінг-дата-жпа вже для вас подумав що робити з implements CrudRepository, так?)))

с точки зрения дизайна и ооп

А мені по барабану дизайн та ооп якщо цим користуватися неможливо.
Rails написаний людьми для людей. Те що всередині там їхав макрос через метапрограмування мене мало турбує аж поки я маю можливість писати код з реактивною швидкістю.
А от всякі хібернейти написані астронавтами для мавп.

Ви так пишете ніби це щось погане.

Таки да, это плохо. Примерно как если бы прибить гвоздями ди контейнер к платформе логгирования. Не знаю зачем, звучит бредово, но так и задумано :)

Добре що вам не треба вручную істанціювати entityManager бо спрінг-дата-жпа

Не фанат спринг дата жпа, кстати. С ентитиМенеджером както приятнее еси честно. Но это персонально мои тараканы.

А мені по барабану дизайн та ооп якщо цим користуватися неможливо.

Можно, и очень хорошо. Просто ктото не умеет или не въехал в концепцию :) тогда конечно, пользоваться невозможно.

Rails написаний людьми для людей.

Так прикольно, когда пытаются натянуть сову на глобус «а мне удобно», пытаясь оправдать говноидеи, положенные в основу.

Не фанат спринг дата жпа, кстати. С ентитиМенеджером както приятнее еси честно. Но это персонально мои тараканы.

Из двух зол всегда можно выбрать третье — QueryDSL

Таки да, это плохо.

Чим?

ктото не умеет или не въехал в концепцию

Дядь я як гавнокодер з 8+ років сраної джавки можу тебе запевнити — коли спробуєш свої круди на фреймворку зробленому для людей (Rails наприклад, ще от Phoenix хвалять але я ще туди не дійшов), то назад вже не захочеш повертатися. Я от не хочу. Але треба. І тоді страждаю і нию яке у нас тут все гавно з гетерами і сетерами.

Всі ці срані концепції які попридумували люди, що не мають стосунку до щоденного кодінгу швиденько ламаються об реальне життя і в результаті чомусь з’являється культ entityManager.save(crap) можна а crap.save() не можна, хоча по факту воно одно й те саме.

Коротше кажучи рекомендую на вихідних зробити круд на rails, базарю, ще захочеш.

Для геттерів і сеттерів є Lombok.
Дозволь поцікавитись: що в Rails придумали на тему lazy, eager, fetch join, entityGraph і всього іншого лайна, про яке ми тут сперечаємось?

є Lombok

А пацани-то не знали!

що в Rails придумали на тему

Все це закривається. Поки сам не спробуєш, не відчуєш. Переказувати доку (guides.rubyonrails.org/...​ager-loading-associations) і розписувати було-стало довго і ліниво, але я хочу робити доповідь на якомусь мітапчику на цю тему, так що в тебе буде шанс :)

але я хочу робити доповідь на якомусь мітапчику на цю тему, так що в тебе буде шанс :)

Позовешь :)
Троллинг из зала допустим?

clients = Client.includes(:address)

SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))

Ті самі яйця, вид сбоку — lazy + а-ля fetch join/entityGraph. Плюс, так розумію, можлива додаткова проблема через ліміт кількості в IN.

Ті самі яйця, вид сбоку

Насправді ні, тому що ActiveRecord це те що нижче назвали query dsl і ваші проблеми з тим що в одному випадку треба половину графу витягнути, а в іншому — увесь, вирішуються ізі, без необходіности городити сотні анотацій. Якщо дуже хочеться реюзати запити то пишуться скоупи які хош. Я так розумію що найближчий аналог в java-світі це jOOQ.

Плюс, так розумію, можлива додаткова проблема через ліміт кількості в IN.

Я не впевнений, але думаю що пацани передбачили і на великих лістах конвертують то все в арраї. Лібі вже 15 год скоро буде.

в одному випадку треба половину графу витягнути, а в іншому

Це якось фреймоворку все одно вказати треба, може в ActiveRecord, дивлячись на пройдений хібером шлях, зробили очевидніше.
У хібері від @ManyToOne(EAGER) із-за сумісності, мабуть, відмовитись не можуть.

Я не впевнений, але думаю що пацани передбачили і на великих лістах конвертують то все в арраї.

Арраї не в усіх БД є, скоріше temporary table. Алгоритм для eager-а частини графа, гадаю, у всіх один:

  • якщо можна, робимо join
  • якщо ні — передаємо ліст айдішніків. IN() тут здивував через ліміт, temporary table більш надійніше, хоча, певне, ламається якщо DDL заборонені
  • оптимізації не вдались — N+1

І все-одно треба або дивитись за логами кожного запиту, бо після нової колекції може погіршитись, або плюнути і хай робить N+1, як йому треба.

Я згоден припустити, може в AR це позручніше, але сумніваюсь, что принципово інакше.

IN() тут здивував через ліміт,

в мене до речі жодного разу не виникало такої потреби (грузити більше ліміту в in). На старій ентерпрайз роботі так, були такі штуки, нам навіть тренінги хлопці з перформенс команди влаштовували, як правильно запити писати. Але з того часу я трошки переосмислив розклади і тепер вважаю що якщо вам треба в ін передавати 1000 штук то явно якісь проблеми є з загальним дизайном рішення.

8+ років сраної джавки

Вот знаешь, ни о чем не говорит, честно :)
Только, возможно, о том, что ты 8 лет писал говнокруды. Возможно, но здесь не джава виновата.

Вот знаешь, ни о чем не говорит, честно :)

Твої тєлєги теж ні про що не говорять, чесно, ми ж не знайомі особисто, не працювали разом і ти для мене (як і я для тебе) просто рандом з інтернету.

Да всем

Ясно.

Ну ты понимаешь, что мне просто впадло писать телеги аргументации,? Особенно ввиду того, что ты все равно будешь все отрицать ссылаясь на говнокодинг на руби, поэтому нет смысла :)

Мені теж впадло сперечатися. Тим більше що наш діскач пропаде даремно. Треба писати велику статтю і там сратися.

Изменение режима на FetchType.EAGER закладывает серьёзную мину под производительность. Загрузка элементов коллекции теперь будет происходить во всех вопросах и создавать N+1 проблему.

В мене від цієї штуки спершу серйозно бомбануло.
Чому там відразу не зроблено по-нормальному, без N+1?

Треба думати про це два рази — перший раз коли пишеш EAGER і другий раз коли пишеш join fetch. Бісить.

Навіщо писати EAGER?
По-нормальному зробити не вийде, одночасно можуть бути сценарії, коли треба діставати вкладені колекції і коли ці дані не потрібні

Навіщо писати EAGER?

Шоб зразу весь граф завантажити.

Якщо є Dto, то весь граф буде завантажений у будь-якому випадку: чи то одним запитом, чи то подслідовними.

Якщо ти написав EAGER але не написав join fetch а просто join то в тебе буде N+1. Не знаю як кому, але це мене дуже здивувало.

Я назубок не пам’ятаю всі комбінації jpql/hql/criteriaBuilder на fetchType/fetchStrategies. Кожен раз тре дивитись в лог. Тяжка спадщина наворотів слоїв.

Якщо ти написав EAGER але не написав join fetch а просто join то в тебе буде N+1.

Открою тайну, если написал eager, то join в jpql можно вообще не писать. Хиб загрузит игер-связи и без твоих ценных указаний. Твой кеп, не благодари.

В мене це не працювало. Перевіряв спеціально. Може була багнута версія чогось, не знаю.

Эээ... загрузить-то загрузит, вопрос каким способом. И с fetch можно N+1 получить, если там граф дальше.

Конечно, в реальном приложении Н+1 будет, везде и всегда.
Красивым борцуном с Н+1 можно быть с примитивным проектом с плоскенькими модельками и 1 вложенной коллекцией. И если делать нечего и таски закрыты, можно изучать борьбу с системой и даже статьи об этом писать, как вот я мол в 100500й раз поборол Н+1 селект в приложении на двадцать классов включая контроллеры.

Особенно если граф на листах, то и обычная связь а — > б — > с N+1 дёргает

Статья неплоха, но по сути, является компиляцией бессчетного количества других статей, например того же Влада.

в бизнес-логике и запросах фигурируют id;

В реальных приложениях идентификаторы всегда фигурируют. Давайте это признаем, вместо того чтобы реплицировать теоретический идеализм. В тех же внешних запросах вида clients/123/orders/456/items/789/price.
Ту да же относится и порицаемое автором еквалс и хешкод по ид. В теории да, все красиво очень.
На практике же, есть у вас миллион ордеров с полем name="shirt", color="BLUE«. И все еквивалентны, поздравляю :) Но все принадлежат разным лицам, и являются различными записями, а у вас они все будут равны. Какой практический смысл вы получите с такого равенства?
Мы давно перешли на еквалс по айдишнику (если быть точным, то по ууидам), и это упростило все. Потому что на практике важна идентичность, а не равенство по бизнес-параметрам. С вероятностью 99,99% бизнес-идентичные данные, принадлежащие разным графам, никогда не пересекутся в одной логической операции и/или транзакции и вам нечего будет сравнивать. В остальных случаях бизнес-равенство является более сложным понятием, оно определяется конкретной потребностью и для него пишется отдельный компаратор или несколько.
Помимо этого, как вы смотрите на то, что бизнес-логика очень часто должна позволять хранить бизнес-дубликаты в коллекциях? Вам нужны в коллекциях разные объекты которые еквалс друг другу? Точно?

entities состоят из большого количества примитивных полей, а не из классов;

В самом факте того, что сущность состоит из большого количества примитивных полей, нет ничего страшного, в ней просто может быть много плоских данных.

и многие другие темы.

...например вот о чем. Развесили вы по всем бест-практисам лейзи лоадинга, все вроде красиво. Но тут у вас есть запрос который чтото достает, и отдает в жсон. А это чтото — глубокой вложенности, и все на лейзи. Привет, Hibernate.initialize(graphRootEntity); В JPA нет такой функции, привет платформ-депендент апи.
А потом оказывается, что отдельные компоненты графа нужны, отдельные нет. А у вас уже хибернейт.инишлайз стоит. В результате появляется сущность, описывающая все нужные данные с расставленным везде игером.

А еще есть такой вариант, когда бизнес-идентификатор хранится отдельным полем как ентити, и нужен чуть более чем вообще всегда. Лейзи лоадинг такого объекта вообще будет ошибкой. При этом сделать его плоским нельзя, потому что таблицу идентификации используют многие разные сущности.
Хороший пример — хранилище ШКИ в логистике.

Я не порицаю лейзи лоадинг, мне просто не нравится тиражируемое заблуждение «лейзи = силвербуллет, игер = сжечьеретика», которая сквозит в большинстве статей про хиб/жпа.

Изменение режима на FetchType.EAGER закладывает серьёзную мину под производительность.

Это не совсем так. Зависит, внезапно, бадум-тссс от того, какие у вас сценарии работы с сущностями. Неожиданно, да? :)
Рассмотрим крайности.
Сценарий 1, примитивный.
У нас круд. GET cats/123 и так далее. 100% запрошенной из базы инфы конвертируется в жсон и отдается в веб. Так вот, в таком сценарии на фечтайп абсолютно пофиг, и лейзи будет даже вреден. Просто потому что вам все равно нужно отдать всю информацию, которая есть в базе. Игером или лейз, но надо ехать. Поэтому можно не парится и ставить игер везде, пускай хиб до итс бест и нафигачит джойнов, все равно вам все это вытягивать.
Сценарий 2, сложный.
У вас приложение, обратное круду. Т.е. по запросу вызывается каскад сложнейшей логики, которая будет доставать/апдейтить/создавать кучу данных через кучу джойнов и форейн кеев, при этом для большинства операций будет использован лишь факт связи и ограниченное количество данных, а обратно будет отдано очень ограниченное количество динамических сгенерированных данных. В таком случае, таки действительно необходим лейзи, поскольку хиб будет играть роль не столько собирателя данных сколько формирователя необходимого графа, где не все данные действительно нужны, а чать работы будет происходить вообще с одними лишь идентификаторами.

Загрузка элементов коллекции теперь будет происходить во всех вопросах и создавать N+1 проблему.

Н+1 не является такой уж серьезной проблемой. Более того, на практике он неизбежен.

Спасибо за развёрнутый коментарий.

В остальных случаях бизнес-равенство является более сложным понятием, оно определяется конкретной потребностью и для него пишется отдельный компаратор или несколько.

С этим, конечно, сложно спорить.

Помимо этого, как вы смотрите на то, что бизнес-логика очень часто должна позволять хранить бизнес-дубликаты в коллекциях? Вам нужны в коллекциях разные объекты которые еквалс друг другу? Точно?

А из-за этого, мне кажется, массово используется List вместо Set. Что в свою очередь приводит к невозможности отличить элементы в Cartesian product.

Развесили вы по всем бест-практисам лейзи лоадинга, все вроде красиво. Но тут у вас есть запрос который чтото достает, и отдает в жсон.

Json аннотации прямо на entity? Или есть Dto?

Я не порицаю лейзи лоадинг, мне просто не нравится тиражируемое заблуждение «лейзи = силвербуллет, игер = сжечьеретика», которая сквозит в большинстве статей про хиб/жпа.
Это не совсем так. Зависит, внезапно, бадум-тссс от того, какие у вас сценарии работы с сущностями. Неожиданно, да? :)

Я не хотел бы, чтобы меня приняли за фанатика lazy, но как быть, если eager устраивал до поры до времени, а потом внезапно появляется сценарий где эти данные не нужны, а их выборка тормозит запрос?

Н+1 не является такой уж серьезной проблемой. Более того, на практике он неизбежен.

Без конкрентного сценария и замеров невозможно сказать, насколько серьёзна проблема.

А из-за этого, мне кажется, массово используется List вместо Set.

Не изза этого. Лист банально удобнее, с ним меньше танцев с бубном. Дефолт коллекция в человеческом мозгу == лист. Сет — это если нужно оставить уникальные елементы. Это уже часть бизнес логики, а не персистентости. А там уже трисеты и прочее.

Базовый мой пойнт в том, что еквалс по ид не является плохим решением. Тем более когда сущность, например, в принципе не содержит данных, суть которых позволяет считать их хоть скольконибудь уникальными.

Что в свою очередь приводит к невозможности отличить элементы в Cartesian product.

Да, механизм хиба позволяет держать только 1 игер-коллекцию в сущности. Попытка добавить вторую игер приведет к картезиану. Это тот случай, когда лейзи вынужденный.

Я тоже не фанат игера как такового, просто иногда он более естественный, чем лейзи, который многими рассматривается «правильным по дефолту», вне зависимости от сценария. Я вот против такого подхода, а не за переход на игер.

Json аннотации прямо на entity? Или есть Dto?

Конечно дто+маппер. Но это вообще не важно. Важно то, что если вам надо отдать данные — их надо загрузить, неважно как. И если запрос на сущность == 100% загрузке вложенных объектов, т.к. они включены в данные, которые требуется отдать в жсоне, то игер лучше. Это конечно не касается случая из прошлого абзаца.

Я не хотел бы, чтобы меня приняли за фанатика lazy, но как быть, если eager устраивал до поры до времени, а потом внезапно появляется сценарий где эти данные не нужны, а их выборка тормозит запрос?

Как уже сказал, я тоже не фанат игера как такового, я просто против распространенных утверждение что переход с игера на лейзи всегда благо, или все на лейзи изначально — сильвербуллет.

где эти данные не нужны, а их выборка тормозит запрос?

Возможно, это причина пересмотреть саму структуру данных, выраженных в сущности и исключить поле, вызывающее тяжелый запрос, из сущности вообще, а для связи использовать новую сущность, которая мапится на ту же таблицу, но предназначена для конкретного сценария. Фактически, view-версия.

Без конкрентного сценария и замеров невозможно сказать, насколько серьёзна проблема.

Я вообще не сталкивался с ситуацией, когда Н+1 проблема.

Базовый мой пойнт в том, что еквалс по ид не является плохим решением. Тем более когда сущность, например, в принципе не содержит данных, суть которых позволяет считать их хоть скольконибудь уникальными.

Да, против практики не попрешь. Тогда с точки зрения джавы equals() равен ==, а JPA добавляет id.

Возможно, это причина пересмотреть саму структуру данных, выраженных в сущности и исключить поле, вызывающее тяжелый запрос, из сущности вообще, а для связи использовать новую сущность, которая мапится на ту же таблицу, но предназначена для конкретного сценария. Фактически, view-версия.

Не одобряю такое решение. Создаются две сущности, копи-паст либо «наследование». Зачем вообще исключать? Потому что то поле аннотировано eager, а менять на lazy — опасно?

Я вообще не сталкивался с ситуацией, когда Н+1 проблема.

Поиск по произвольным полям. CriteriaBuilder с like-ами. Хочется, чтобы он работал максимально быстро. А если у выдаваемой сущности есть eager поля или коллекции?

Тогда с точки зрения джавы equals() равен ==, а JPA добавляет id.

А это почему? С чего это вдруг рассматривать this.id.equals(other.id) как == ?
ПК может быть не только лонгом ведь? Ууид, стринг (в случае таблиц-справочников например). Мы полностью перешли на ууиды и очень этому рады. Забыли про хобота с хибовскими сиквенсами. При желании генерацию ууида можно вынести из хиба.

Не одобряю такое решение.

Не могу назвать его верхом элегантности, да. Но если есть например сущность, у которой десятка два вложенных объектов, почти все через лейзи. Это основная доменная модель, которая используется для логических операций, и в разных логических операциях используются разные участки графа — лейзи как раз для такого.
Но есть запрос, который хочет сразу часть графа. Зачем тянуть основную сущность, делая дофига лейзи-инициализаций, если можно создать частичную view-сущность со всеми нужными полями игер? Альтернатива — написание большого jpql-запроса с куче JOIN FETCH. На практике это еррор-прон.

Зачем вообще исключать?

Не говорю, что это нужно делать в каждом случае. Но появление такого кейса может означать, что эти данные в этой сущности не очень то и нужны. Это кейс из практики, кстати. Не подумав, включили в сущность коллекцию, которая доставалась из ооочень длинной таблицы, а-ля лог. Потом смотрим, чот селект сущности долгий. Убрали эту коллекцию (оказалось что для этой сущности она вообще нафиг была не нужна) и селект ускорился в 5 раз. И там, кстати, было лейзи. Мы же умные, ага. А на выдаче то оно все равно потом проходит черех хибернейт.инишлайз(). Т.е. возможно, что это изначально сигнал об ошибочном дизайне.

Поиск по произвольным полям. CriteriaBuilder с like-ами. ...А если у выдаваемой сущности eager поля или коллекции?

А вот тут я вообще логику не поймал? Как лайк соотносится с сущностью? Как условия запроса на 1 поле вообще связаны с представлением того, что нужно вернуть? Лайк работает на бд по 1 полю, ему пофиг на все остальное. Лайк или не лайк — вообще нет никакой разницы, по какой причине было решено выгрузить ту или иную сущность.

Тогда с точки зрения джавы equals() равен ==, а JPA добавляет id.

Имел в виду основной принцип, который иллюстрировал — не привязываться к JPA. Если сущность в принципе не содержит данных, суть которых позволяет считать их хоть скольконибудь уникальными, то equals() является ==. Переходим на JPA, добавляем PK, неважно Long или uuid, он один начинает фигурировать в equals().

Uuid-ами не пользовался, но идея нравится.

про хобота с хибовскими сиквенсами

Хибернейтовскими?! Я думал, мы про сиквенс из БД говорим. Там, правда, тоже есть нюансы. Мы о каких хоботах?

При желании генерацию ууида можно вынести из хиба.

Я вначале подумал, что генерация уудда в конструкторе. Иначе невозможно сравнивать transient и persisted объекты. Впрочем, вы избегаете таких ситуацих в бизнес логике.

Поиск по произвольным полям.

Имел в виду такой сценарий. Тот же Client с произвольным поиском по firstName и/или lastName и т.д. Если в него ранее включен List<Order> с eager, это его замедлит.

Хибернейтовскими?! Я думал, мы про сиквенс из БД говорим.

Конечно, про сиквесны в бд. Хибовские потому что они в бд так и называются, hibernate.sequence :)

Мы о каких хоботах?

Девелоперских. У нас были случаи, когда очереди айдишников доставляли очень много гемора. Мы из сендбокса копируем базу как образец для тестирования в девелоперскую базу. Были проблемы с копированием значений оракловских счетчиков. И получалось так, что в девелоперскую базу попадали сущности из сендбокса, а счетчики нет. И потом падают все тесты, потому что значение счетчика на сотни тысяч отстает от тех лонгов, которые уже реально лежат в праймари кеях. Еще какието приколы были, сейчас не вспомню.

Я вначале подумал, что генерация уудда в конструкторе.

Совершенно необязательно. Но возможно, как и просто
obj.setUuid(randomUUID());
save(obj);

Иначе невозможно сравнивать transient и persisted объекты.

Да, такой нюанс есть. Вопрос в том, в какой момент их нужно сравнивать. Но в целом, конечно, правильно чтобы в транзиент объекте ид было нулл.
Я лично не могу придумать ситуацию, в которой нужно будет делать .еквалс() на транзиент объекте. Если рассматривать еквалс как отображение идентичности, то это действительно, лишено смысла, у транзиент объекта нету идентичности.
Для бизнес-операций я предпочти использовать какой-нибудь кастомный компаратор или предикат.

Тот же Client с произвольным поиском по firstName и/или lastName и т.д. Если в него ранее включен List с eager, это его замедлит.

Каким образом это его замедлит? Общее время получения сущности из хибернейта? Конечно замедлит. Любая не-лейзи связь в теории замедлит. Но с поиском по имени это вообще никак не связано.
Вопрос в том, что «замедление» может быть столь ничтожным, что им можно пренебречь. Это уже зависит от конкретного сценария.

Конечно, про сиквесны в бд. Хибовские потому что они в бд так и называются, hibernate.sequence :)

Да, с сиквенсами есть проблемы при вставке большого количества данных.
Извлекать значения по одному — медленно.
Извлекать значения пачками — есть нюансы vladmihalcea.com/the-hilo-algorithm
Я поэтому и подумал, что uuid в конструкторе решает эти проблемы.

Но в целом, конечно, правильно чтобы в транзиент объекте ид было нулл.

Такого требования нет.

Вопрос в том, что «замедление» может быть столь ничтожным, что им можно пренебречь.

Что-то никак не получается объяснить, что я имею в виду. «Свободный» поиск по полям Сlient c помощью CriteriaBuilder-а выльется в запрос select * from client where first_name like '%str%' or last_name like '%str%'. Если у класса уже была eager-коллекция, то получаем N+1 проблему, которая замедлит этот поиск.

Такого требования нет.

Честно говоря, не помню, есть оно гдето в мануалах, практиках, или спецификациях, или нет.
Но если подумать, то необходимость такого правила будет очевидной.
В общем случае, транзиент объект с синтетическим, а не бизнес ид, не дожен иметь ид. Он еще не сохранен, у него нету и не может бд-идентити записи. Мы сейчас не говорим про мануальную генерацию ид в стиле
entity.setId(smthRandom());
em.save(entity);

Это не правило, это следствие использования суррогатного ключа. Тем не менее, проверять transient ли объект нужно не через id == null, а через entityManager.contains()

Тем не менее, проверять transient ли объект нужно не через id == null

Это школьная истина, и я не имел в виду, что проверка на нулл ключа является проверкой транзиент ли объект, но в нормально написанном приложении нулл в ид будет иметь только транзиент объект, и не-транзиент объект не будет иметь нулл в ключе.

Но есть запрос, который хочет сразу часть графа. Зачем тянуть основную сущность, делая дофига лейзи-инициализаций, если можно создать частичную view-сущность со всеми нужными полями игер? Альтернатива — написание большого jpql-запроса с куче JOIN FETCH.

EntityGraph?

И откуда всё таки хибернейт.инишлайз(), если строятся дто-шки?

И откуда всё таки хибернейт.инишлайз(), если строятся дто-шки?

А откуда построятся дтошки, если попытка гета поля вне сесии швырнет ЛейзиЛоадингЕксепшн: но сессион?
Чтобы отобразить чтото в дто, это чтото нужно загрузить.

EntityGraph?

Ну тоже вариант, да.

Дтошка строится вне сессии?

Конечно вне сессии. Преобразованию во вью нечего делать в сервисном слое.

Разрешите полюбопытствовать: почему так?
Где у вас рамки транзакции?
Как строится дто из нескольких entities?

почему так?

Потому что сервисы и представление должны быть разнесены. Если говорить о жсон дто, то маппинг сущности на дто происходит в контроллере. Маппингу бизнес ентити на форматы инпут-аутпут ваще нечего делать внутри сервисов. Я полагал, что это вполне очевидно. Я уделяю большое внимание дизайну компонентов, и поэтому сервисный слой у нас полностью декомпозирован от любых слоев представлений и передачи данных. Более того, ввиду сложной логики, сервисный слой у нас тоже так сказать, многоэтажный, есть сервисы, которые в свою очередь, управляют десятком других сервисов, скопмонованных для выполнения общих логических операций в разных сервисах более низкого порядка, но в одной транзакции.
А вот интеграционные тесты у нас, кстати, не покрыты глобальной транзакцией, мы от этого отказались.

Где у вас рамки транзакции?

Странный вопрос. Обычно там, где они нужны :) Если в общем, то обычно это просто транзакционные методы сервисов. Иногда это несколько транзакций. Иногда по запросу запускается тред, у которого своя транзакция, и т.д.

Как строится дто из нескольких entities?

Тоже странный вопрос. В чем проблема дернуть Х сущностей, потом слепить из них дто? Обычно сущности, нуждающиеся в общем дто както связаны, и у них почти всегда есть связь, которая в свою очередь, оформлена как сущность. И обычно она и маппится в дто.
А даже если нет такой сущности, можно сделать объект с конструктором, и делать жпкл селекты с конструктором. Или резалт маппинг. Да кучу всего можно придумать.
Народ почемуто панически боится писать жпкл-запросы.
А даже если не в сервисном слое, то госпаде,
getXandYBy(xId, yId) {
X x = xService.get(xId);
Y y = yService.get(yId);
XandYDto xy = xAndYMapper.map(x, y);
return xy;
}
у чем проблема? Вариантов просто море, реально странный вопрос.

Я задал эти вопросы, потому что у меня недоумение.

Выше выше утверждаете, что строите дтошку вне сессии. А границы транзакций на сервисах. Этим, мне кажется (поэтому я и переспрашиваю), создаются следущие проблемы:
— lazy-loading либо бросает LazyLoadingException либо вызывается вендор-специфичным Hibernate.initialize();
— для нескольких entities приведённый код
X x = xService.get(xId); Y y = yService.get(yId);
может вернуть неконсистентную дто из-за 2 транзакций.

Почему вы не строите дто в сессии?

Преобразованию во вью нечего делать в сервисном слое.
Потому что сервисы и представление должны быть разнесены.

Абсолютно верно. Но почему транзакции на сервисе, а не на фасаде?

создаются следущие проблемы:
— lazy-loading либо бросает LazyLoadingException либо вызывается вендор-специфичным Hibernate.initialize();

Первое является не проблемой а нормой. Если вы используете данные вне сервисов под маской сущности, вы должны позаботится об их явной загрузке.
Второе не является проблемой, поскольку тот же вызов хиба в случае гипотетического отказа от хиба как провайдера можно заменить на ручную загрузку лейзи связей. Что делает вызов хиба лишь удобной альтернативой, а значит, инициализацию можно вынести в абстракции уровня интерфейса дао
void initialize(MyEntity);

— для нескольких entities приведённый код
X x = xService.get(xId); Y y = yService.get(yId);
может вернуть неконсистентную дто из-за 2 транзакций.

А вот тут очень интересно :)
Если вкратце — то нет, вы ошибаетесь, угрозе консистентности здесь нет.
Я выше уже писал, отвечая на ваш вопрос, что если эти сущности Х и У связаны логически, то существует сущность связи, а значит ее можно вытянуть в рамках одной транзакции, одной сущностью, вместе с Х и У.
Участок кода, в котором вы подразумеваете нарушение консистентности, сам по себе подразумевает отсутствие какой-либо связи между состояними Х и У.

Но, даже если такая связь между Х и У существует, а общей сущности (с одновременным доступом и к Х и к У из нее) нет, что само по себе уже признак ужасной архитектуры данных и приложения, то это легка решается выносом этих двух вызовов в сервис более высокого уровня, который объединит эти два вызова в 1 транзакционном методе, и вернет в слой представления Pair(X, Y)

Если погружаться в вопросы консистентности еще глубже, то факт транзакций вообще не обеспечивает консистентности данных в графе, и рано или поздно вам придется начать играться с em.lock(x, timeout, pessimistic);
em.lock(y, timeout, pessimistic);
и так далее. Или же, в более сложном варианте, заняться синхронизацией вне транзакций на уровне операций в слое представления.

Кажется, мы пришли к религиозному расхождению во взглядах.

Я ратую за такой подход:

  • контроллер (отвечает, допустим, за REST);
  • фасад (границы транзакции и преобразование entities в dto);
  • сервис (бизнес-логика);
  • дао;

Таким образом, в фасаде при построении дто, если есть обращение к lazy-полю, оно будет инициализировано запросами в БД, и нет необходимости заботиться о явной их загрузке.

Вы же настаиваете, что ДТО должно строиться принципиально вне транзакции, хотя это требует ручной загрузки лейзи связей и диктует необходимость использования только одного сервиса.

Я ратую за такой подход:

У этого подхода есть концептуальные недостатки.
1. Слой фасада не нужен. Это не самостоятельный компонент. Это противоестественный компонент, который забирает часть ответственности у контроллера (создание дто) и часть ответственности сервиса (контроль транзакций). И все бы ничего, но это лишает сервис и контроллеры консистентности их функций. Перенося в фасад лейзи-инициализацию, вы делаете ошибку, лишая сервис независимости, делая его зависимым от внешнего применения, поскольку в вашей архитектуре сервисный вызов
Entity getBy(id);
не значит ничего конкретного. Ваш сервис сам по себе, как компонент, более не обладает гарантированным поведением. Возвращенные им данные — фикция, поскольку заведомо находятся в неопределнном состоянии. Фактически, надежность поведения вашего сервисного слоя разрезана на два под-слоя — сервис и фасад. При этом уже в фасаде вы взаимодействуете с дто. Вы ошибаетесь, если думаете, что не смешиваете зоны ответственности (а важность их разделения вы ранее признали), поскольку ваш фасад и является этим смешением.

Теперь касательно самой концепции фасадов. Это бессмысленная концепция. Поскольку сервис уже накрыт интерфейсом. Если вам нужна отдать куда-то лишь часть функционала данного сервиса, вы просто сегрегируете интерфейсы. Преобразование данных в транспортный конверт не является частью связи с базой данных. У вас же именно так и есть. Преобразование данных в/из транспортных конвертов является неотъемлемой частью слоя представления и конкретной реализации транспорта. Вы же отсекаете от представления его естественную часть функционала и отдаете эту часть в непонятный слой на стыке зон ответственности.

хотя это требует ручной загрузки лейзи связей

Нет. Это требует от пользователя интерфейса явного декларирования намерений использования.

interface AbstractEntity { }

class MyEntity implements AbstractEntity { }

interface CommonDao {

    void initialize(AbstractEntity e);
}

interface MyEntityDao extends CommonDao {
    ...
}

class MyEntityDaoImpl implements MyEntityDao {

    @Override
    public void initialize(AbstractEntity e) { 
        Hibernante.initialize(e) // or other solution
    }
}

interface DataIntegrity {
    boolean requiresFullyInitialized();
}

enum FixedDataIntegrity implements DataIntegrity {
    FULL_INTEGRITY,
    PARTIAL_INTEGRITY
    ...
}

interface MyEntityService {
    MyEntity getBy(long id, DataIntegrity requiredIntegrity);
}

class MyEntityServiceImpl implements MyEntityService {
    @Override
    @Transactional
    public MyEntity getBy(long id, DataIntegrity requiredIntegrity) {
        MyEntity e = dao.getBy(id);
        // null check bla bla bla
        if (requiredIntegrity.requiresFullyInitialized()) {
            dao.initialize(e);
        }
    }
}

class MyEntityRestController {
    @Autowired MyEntityToJsonMapper mapper;
    @Autowired MyEntityService service;
    getById(...) {
        MyEntity e = service.getBy(id, FULL_INTEGRITY);
        Json json = mapper.map(e);
        return json;
    }
}
Это заставляет потребителя интерфейса сервиса явно декларировать суть операции, явно перекладывает ответственность на пользователя и позволяет гибко управлять поведением сервиса, в зависимости от условий работы експлиситно, а не полагаясь на чьито предположения, что этот сервис-де должен работать только в условиях внешней жпа-транзакции.
Если вызов сервиса идет из другого сервиса, и транзакция уже открыта, мы передаем флаг частичной полноты данных, и продолжаем использовать лейзи инициализацию, полностью отдавая себе отчет в том, что делаем. Или же, если вызов сервиса является конечной операцией, и данные покинут персистентость, дизайн сервиса обязывает пользователя явно указать необходимость в полной загрузке данных.
диктует необходимость использования только одного сервиса.

Абсолютно нет.

Ваше приложение — ваши правила. Мне видятся два недостатка:

  • необходимость реализовывать инициализацию, либо привязкой к вендору Hibernante.initialize(e), либо руками
  • вы не гранулируете, если в сущности больше одного lazy графа, которые могут быть нужны/не нужны для разных сценариев, и вытягиваете их все в Dao.initialize(). Либо добавляете больше флажков в FixedDataIntegrity.
Но, как я понимаю, вы сознательно идёте на это ради соблюдения концепции.
необходимость реализовывать инициализацию, либо привязкой к вендору Hibernante.initialize(e), либо руками

Вы серьезно считаете недостатком вызов 1 строки? Даже если мы отказались от хибернейта, пройтись по лейзи-полям так сложно?

вы не гранулируете

Я специально написал DataIntegrity как интерфейс, это подразумевает возможность расширить его до грануляции. На практике это просто нам не было нужно, и мы этого не делали, двух вариантов нам хватает.

Но, как я понимаю, вы сознательно идёте на это ради соблюдения концепции.

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

Это как раз то, чего нету в вашем варианте. И вы так ничего на это и не ответили.

если в сущности больше одного lazy графа, которые могут быть нужны/не нужны для разных сценариев

Если мы отдаем сущность вовне персистентности, это значит, что все данные в этой сущности нужны для отображения, в 95% случаев. Обратный же кейс, когда часть данных не нужна, означает что с этой сущностью идет внутренняя работа в сервисном слое с логикой. Если у вас есть ситуации когда ни то и ни другое, и у вас реально есть необходимость маппить только половину графа в представление, я бы предположил проблемы в дизайне самих сущностей и самой модели предметной области.

Даже если мы отказались от хибернейта, пройтись по лейзи-полям так сложно?

Ну, не знаю... За полями свои графы вообще-то. Ладно, неважно делать это Hibernate.initialize() или другим deep clone.

Вы серьезно считаете недостатком вызов 1 строки?

А почему вы так легко отмахиваетесь от загрузки (не факт, что нужной) всего графа?

Это как раз то, чего нету в вашем варианте. И вы так ничего на это и не ответили.

Давайте я переформулирую, а то может дто и фасад ввели в заблуждение.

Мои поинт абзаца «Entity и DTO» в том, что entities не должны фигурировать вне границ транзакции.

Можем назвать дто — бизнес-объектом, а фасад — внешним-сервисом, если это принципиально. С таким подходом нет LazyLoadingExceptions(LLEs), зато есть преимущества lazy-loading.
Вы так тоже делаете, только в части случаев, а я всегда.

В остальных случаях зачем-то боретесь с LLEs Hibernate.initialize-ом, который по сути тот же EAGER.

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

Что непредсказуемого в построенном dtovalue object?

Если мы отдаем сущность вовне персистентности, это значит, что все данные в этой сущности нужны для отображения, в 95% случаев. Обратный же кейс, когда часть данных не нужна, означает что с этой сущностью идет внутренняя работа в сервисном слое с логикой. Если у вас есть ситуации когда ни то и ни другое, и у вас реально есть необходимость маппить только половину графа в представление, я бы предположил проблемы в дизайне самих сущностей и самой модели предметной области.

Ой ли? Как по мне, вполне валидно запросить Orders+Clients (без товаров) или Orders+Items (без клиентов).

В других ветках вы утверждаете, что N+1 проблема неизбежена. Согласен. Но на мало-мальски сложном графе она превращается в N*M*K*X*Y*Z...+1 проблему. Hibernate.initialize-ом вы собираете всё произведение, а я предпочёл бы ограничиться необходимыми множителями ради производительности.

Опять же, вы сталкивались с этим, иначе не предлагали бы view-entities.

Для меня польза от этой ветки в том, что в следующей редакции я переформулирую абзац «Entity и DTOBO» и предостерегу от Hibernate.initialize().

Мои поинт абзаца «Entity и DTO» в том, что entities не должны фигурировать вне границ транзакции.

Это почему вдруг? Ентити в детачед состоянии как была ентити так ей и осталась. В ней нет ничего нелегального. А вот раздвигание границ транзакций в космос как раз плохая идея.

В остальных случаях зачем-то боретесь с LLEs

Мы с ними не боремся, потому что в нашем дизайне их просто не возникает.

Hibernate.initialize-ом, который по сути тот же EAGER.

об этом ниже

Что непредсказуемого в построенном dtovalue object?

Если я захочу воспользоваться вашим сервисом, я не смогу этого сделать без внешней транзакционности, поскольку ваш сервис не возвращает мне данных, он возвращает сущность в неизвестном мне состоянии, что там загружено, или не загружено — никто не знает. Если же я захочу воспользоваться более готовым решеним, мне нужен будет ваш фасад, в котором, ой блин, вы навалили дтошек, которые мне не нужны.

Можем назвать дто — бизнес-объектом

Назвать можно что угодно и как угодно. Но если назвать жирафа слоном, слоном он не станет. Дто есть дто, и бизнес-объектом это их не делает. Суть ошибки, которую вы делаете с вашими фасадами, проста — вы формируете представление в слое сервисов, как бы вы не меняли терминологию.

Как по мне, вполне валидно запросить Orders+Clients (без товаров) или Orders+Items (без клиентов).

Вот тут много вопросов. Мне неизвестно, как у вас связаны, Orders-Clients-Items.
Схемы запросов на данные вполне легальны, конечно. Но в таком случае, я опять же не вижу никаких проблем в прямом специализированном запросе через jpql в стиле

List<Object[]> = SELECT o, i
FROM Orders o JOIN Items i ON... WHERE...
ну вы поняли.
Я так понял, вы говорите о ситуации, в которой у вас есть сущность, так сказать, супер-корень, которая объединяет в себе:
List<Client>
List<Order>
List<Item>
Мы интенсивно используем jpql, это позволяет нам не держать сущности супер-корни всего-всего бизнес-графа. И это приводит нас к ситуации, о которой я уже говорил — даже в случае очень сложной сущности, если уж она требуется на вывод в жсон, 99% что в ней нужны все данные, поэтому ей вполне адекватно сделать полный инишиалайз всех лейзи.
Тяжело говорить точно, но я не очень уверен в правильности дизайна, приведшего к существованию такой сущности супер-корня графа.
А теперь я ответил и на этот вопрос:
сложном графе она превращается в N*M*K*X*Y*Z...+1 проблему.

На самом деле вы делаете тоже самое. Просто думаете, что не делаете, или делаете меньше, но нет. Вы все равно загрузили то, что вам нужно, не важно сколько А*Б*В+1 это заняло. Как и мы. Разность подходов в том, как избежать загрузки лишнего. Вы избегаете лишней загрузки, расширив границы тразакционности в слой представления, нарушив независимость компонентов и зоны ответственности. Мы — дизайном сущностей, более детализированными операциями сервисом, вью-сущностями в случае крайней необходимости и явным контролем необходимости загрузки.

Опять же, вы сталкивались с этим, иначе не предлагали бы view-entities.

Уточню, мы сталкивались с ситуацией, когда часть графа не нужна, но не сталкивались с реальными проблемами по нагрузке, потому что для критических операций изначально написали вью-ентити.

Для меня польза от этой ветки в том, что в следующей редакции я переформулирую абзац «Entity и DTOBO» и предостерегу от Hibernate.initialize().

Как хотите :)
Следующий шаг на этом пути — инжект ентити менеджера прямо в контроллер и транзакция прямо на контроллере. Вы ведь тоже не видите проблем с таким подходом, да? :)

Вот тут много вопросов. Мне неизвестно, как у вас связаны, Orders-Clients-Items.

Я о примере из статьи :-) Там и entities, и таблицы.

Пожалуйста, поправте свой комментарий относительно примера, и мы продолжим дискуссию. Как бы вы запрашивали

Orders+Clients (без товаров) или Orders+Items (без клиентов)

?

Менять там нечего особо, я продолжу здесь.

Если рассматривать этот конкретный пример, то я согласен, ваш вариант, на первый взгляд, выглядит более естественным ввиду структуры данных, которые вы написали под себя. Дергая ордер, и подгружая по необходимости или клиента или айтемы, вы получаете что хотите.

Однако, мы обое понимаем, что ирл это не приложение, а пет-круд, в котором не хватает критически важных для нормального ентерпрайза моментов.
Как например:
— когда был добавлен айтем в ордер
— возможность переноса айтема из ордера А в ордер Б, с историей переноса: когда перенесен, откуда и куда
— когда айтем был удален
— сколько времени айтем может лежать в ордере, а когда он устареет от не-оформления ордера.
— обновление времени ожидания конкретного айтема при переносе айтема из ордера А в Б.
— софт делит. Не знаю, как у вас, а у нас в принципе ничего не удаляется. Любой делит на самом деле означает софт-делит. Терять данные недопустимо.
— время жизни ордера, до того, как он устареет при неактивности
— перенос ордеров от клиента А клиенту Б, вместе с айтемами, свежесть айтемов рефрешится. Специфическая операция, согласен, но бизнесу часто хочется странного.

Одна только реализация этого списка, не говоря же о возможном расширении, может подтолкнуть вас к мысли, что описанная вами в статье струткрура в принципе не является удобной для такого уровня подробности.

Мы изначально закладывали в приложение возможность такой расширенной логики, поэтому у нас все несколько иначе. Связи значимых сущностей сами являются сущностями.
Т.е. в моей версии картинка была бы такая (упрощенно):

@Entity
Client {
  // only client data
}

@Entity
Order {
  // only order data
}

@Entity
Item {
  // only item data
}

@Entity
OrderPerClient {
  @ManyToOne
  private Client;
  @ManyToOne
  private Order;
  // client-order particular relation metadata
}

@Entity
ItemPerOrder {
  @ManyToOne
  private Order;
  @ManyToOne
  private Item;
  // item-order parcticular relation metadata
}
XperY сущности выполняют роль одновременно исторического лога и хранилища метаданных связей между сущностями так сказать более высокого порядка.
Эта структура данных позволяет мне из коробки иметь весь тот функционал, о котором я написал в списке, ваша — нет. Помимо этого, она же позволяет мне запрашивать фактически любые варианты бизнес-графа, обращаясь к сущностям высшего порядка или к сущностям-связям, сохраняя при этом в сущностях связей лейзи инициализиацию для работы с связями внутри слоя логики, но не вызывая глубокой вложенности объектов друг в друга, вызывая инициализацию связей для отдачи их вовне.
У нас есть объекты, которые похожи на ваш дизайн, в тех областях, где нету таких требований. Но специфика в том, что вложенные чайлды в таком случае настолько зависимы от парента логически, что запрос чайлдов без парента лишен какого-либо серьезного бизнес-смысла, поэтому у нас и не возникает задачи получить из сущности, которая является графом с тремя узлами только два узла на выбор.
В остальных случаях зачем-то боретесь с LLEs
Мы с ними не боремся, потому что в нашем дизайне их просто не возникает

Либо у вас везде ManyToOne с eager-ом по умолчанию, как в коде примера, либо контрольный Hibernate.initialize в конце, который, по сути, то же самое. Либо просто по самому определению LLE вы можете обратиться к lazy-полю за рамками транзакции, забыв упомянуть его в fetch jpql.

не хватает критически важных для нормального ентерпрайза моментов.
Как например:
— когда был добавлен айтем в ордер

Думаете, я на языке общего назначания java не смогу удобно описать все эти вещи? :-)

Эта структура данных позволяет мне из коробки иметь весь тот функционал, о котором я написал в списке, ваша — нет.

Ну, так нечестно :-) Расширять ТЗ, а потом говорить, что мой код не соответствует.

Ладно, давайте по сути. Я что пытался донести в статье о JPA? Что можно строить доменную область, как удобно. Потом накидывать аннотаций, говорить персист — и хибернейт сам и DDL для схемы сгенерит, и данные туда положит-достанет. Да, у меня в Order-е одновременно и many-to-one (client) и one-to-many (item+quanity) за удобной мапой. А могут быть и ещё коллекции. Да, буду обязательные множители в N+1 собирать, когда представление в транзакции буду строить. Если совсем грустно, буду пытаться подсказать мега-джойн с помощью entityGraph-a, но это за рамками статьи.

Есть ограничения у этого подхода? Конечно есть! Может нагрузка такова, что не удобств будет.

А вы поправляете — реальность сурова, разобъётся. One-to-many нельзя, Влад опять же против. Не доменную область надо строить, а о будущих jpql-ях думать. Так чтобы не возникало

задачи получить из сущности, которая является графом с тремя узлами только два узла на выбор.
Мы интенсивно используем jpql,

а не просто сводно ходим по доменной модели?

Как вы их пишите, кстати? Руками в стрингах?

Следующий шаг на этом пути — инжект ентити менеджера прямо в контроллер и транзакция прямо на контроллере. Вы ведь тоже не видите проблем с таким подходом, да? :)

Нет, я избегаю

лишней загрузки, расширив границы тразакционности в слой представления,

Не контроллером единым, и из других точек бизнес-логика может вызываться. Но вы озвучьте, пожалуйста, проблемы этого подхода, второй раз предлагаете.

поскольку ваш сервис не возвращает мне данных, он возвращает сущность в неизвестном мне состоянии, что там загружено, или не загружено — никто не знает.

Если вы будете моим сервисом, а не фасадом, пользоваться, я специально для вас сделаю его safe-варинт: оберну АОП, транзакцией и пройдусь Hibernate.initialize() по возвращаемым entities, чтобы от LLE обезопасить.

Пожалуй, у меня больше нет аргументов. Боюсь, ситуация выглядит как с товарищем с ActiveRecord. Вы ему говорите — «Неправильно!», а он вам — «Удобно!» Наверное, все ваши усилия стоят

соблюдения независимости поведения компонентов от окружения и предсказуемости поведения.
Либо у вас везде ManyToOne с eager-ом по умолчанию

Я же написал, что с лейзи. Потому что связи используются в тысяче мест и кроме прямой отдачи данных вовне.

либо контрольный Hibernate.initialize в конце

Когда он нужен. Нужность чего пользователь интерфейса и контролирует.

Думаете, я на языке общего назначания java не смогу удобно описать все эти вещи? :-)

И тени сомнения не имел, что напишите. Вопрос в том, насколько вы перепишете изначальную концепцию, и к какому результату придете в конце, и насколько этот результат будет хорош.

Не доменную область надо строить, а о будущих jpql-ях думать. Так чтобы не возникало

задачи получить из сущности, которая является графом с тремя узлами только два узла на выбор.

Вы ошибаетесь, разделяя построение доменной области и структуры данных, в которой эта область будет храниться. Это суть одно и то же. Если вы примете это, у вас не будет вызывать баттхерта мысли о jpql. Продумать модель в классах и продумать запросы к бд, которые будут эту модель обслуживать — это одно и то же при нормальном дизайна домена.

Ну, так нечестно :-) Расширять ТЗ, а потом говорить, что мой код не соответствует.

Да, есть немного. Но я, как и вы, прочитал тыщу и одну статью о том, как сохранить @Entity Cat extends Pet в базу с помощью хиба, и знаю, что в блади интерпрайзе все шушуть сложнее, и поэтому статьи про @Entity Cat extends Pet вызывают у меня скепсис, поскольку раз за разом демонстрируют не подходы к решению реальных задач, а базовые функции жпа максимум.

One-to-many нельзя

Я нигде этого не написал, наоборот, я сказал, что у нас есть сущности, построенные по простому принципу, как ваш ордер.

Как вы их пишите, кстати? Руками в стрингах?

Ну да, а как их еще можно писать? Я не отношусь к тем, кто считает что орм должен полностью заменить написание sql.

и из других точек бизнес-логика может вызываться

Я потом и говорю «преставление» а не контроллер, потому что представление более широкое понятия, чем рест контроллер. Ну ок, жмс колл как пример. Я так понимаю, чтобы ваш фасад умел отправлять javax.jms.TextMessage вы заинжектите javax.jms.* прямо в свой фасад, и TextMessage будет собираться внутри транзакции. Ведь иначе вы не соберете сообщение, потому что нет данных. Итого — у вас есть точка, где пересекается и накладываются jms connection+session и бд транзакция. Это то, что видится мне грубой ошибкой.

Если вы будете моим сервисом, а не фасадом, пользоваться, я специально для вас сделаю его safe-варинт: оберну АОП, транзакцией и пройдусь Hibernate.initialize() по возвращаемым entities, чтобы от LLE обезопасить.

Спасибо. А чтобы вам воспользоваться моим сервисом, нужно всеголишь воспользоваться им as is, и вы уже в безопасности от LLE. Почувствуйте разницу.

Как вы их пишите, кстати? Руками в стрингах?
Ну да, а как их еще можно писать? Я не отношусь к тем, кто считает что орм должен полностью заменить написание sql.

Посмотрите на QueryDSL, должен подойти, получите compile-check.

Я так понимаю, чтобы ваш фасад умел отправлять javax.jms.TextMessage

Вы как будете TextMessage строить? Получать entity из своего сервиса, обёрнутого транзакцией, и маппером конвертить?
Вот и я так же — из своего фасада, который не знает о javax.jms.*

А чтобы вам воспользоваться моим сервисом

Я потерялся, в чём разница. У вас в сервисе транзакции, у меня в фасаде транзакции. Вы возвращаете entity

class MyEntityServiceImpl implements MyEntityService {
@Override
@Transactional
public MyEntity

Я из персистнутой entity возвращаю что угодно: для себя компактную дто с двумя графами из трёх, экономя лишние N+1, для вас — deep clone этой же самой энтити — или маппер-ом, или тем же самым Hibernate.initialize().

Кстати, оказывается ещё Hibernate.isInitialized() есть, наверное, чтобы на LLEs проверить.

статьи про @Entity Cat extends Pet вызывают у меня скепсис

Пытался, как мог, рассказать широкой аудитории, и преимущества, и подводные камни.
Мне видится, что основная беда, это бесконтрольное лечение LLEs eager-ом, а потом Хибернейт — гуано и тормозит, поэтому я вам оппонировал.

Вы как будете TextMessage строить?

Между сервисом и джмс лежит абстракция, которая находится вне скоупа как джмс, так и бд транзакции. В нее выходит ентити из сервиса, попадает в скоуп работы джмс, и там сторится меседж.

Вот и я так же — из своего фасада, который не знает о javax.jms.*

Вы серьезно? Вы построите javax.jms.Message в фасаде, не используя при этом в этом же фасаде импорт javax.jms классов? А вы в курсе, что javax.jms.Message создается из javax.jms.Session.createMessage()?

это бесконтрольное лечение LLEs eager-ом

У меня такое ощущение, что вы вообще не читали, что я писал, ну да ладно.

Я

Получать entity из своего сервиса, обёрнутого транзакцией, и маппером конвертить?

Вы

В нее выходит ентити из сервиса,

Я

Вот и я так же — из своего фасада, который не знает о javax.jms.*

Вот и я так же [буду получать entity] из своего фасада, который не знает о javax.jms.*

Опасно тире ставить, лучше полными предложениями писать.

Я уже детально описал вам разницу.
В моем случае ентити, возвращенное сервисом, находится в заведомо известном состоянии (загруженное илинет), которое определяет сам пользователь сервиса. В вашем случае — ентити из сервиса выходит в неизвестном состоянии. Часть данных в нем может быть не загружена, и мне неизвестно, какая часть. Я уже повторяю это раз третий. И я говорю именно про ваш сервис, ваш фасад меня не интересует, он мне не нужен, поскольку мне не нужны ваши маппинги и ваши дто.

Давайте дальше двигаться )
Вы не согласны с тем, что полностью загруженное энтити

MyEntity e = service.getBy(id, FULL_INTEGRITY);
Этот вызов — вне транзакции.

является своим собственным представлением.
Поделитесь, пожалуйста, в чём принципиальное отличие? Как вы его используете, кроме сорса для маппера?

Возможно, плохую роль играет моя узкопрофильность? Я никогда не писал em.detach(), чтобы продолжить работать с бизнес-объектом, ловя LLE. Тогда я бы сам пришёл к сервису с парт и фул-интегрити.

является своим собственным представлением.

Это уже казуистика.
Объекты, содержащие какие-то данные, делятся на 2 типа.
1) модель.
2) представление.
Упрощенно:
1)

@Entity
class Model {
}

2)

@Json
class ModelJsonDto {
}

@Xml
class ModelXmlDto {
}

@JmsMessage
class ModelJmsMessageDto {
}

и так далее. И нет, ентити не является собственным представлением, потому что тот же джексон не знает, как ее правильно замапить нужный форма жсона для отдачи.

в чём принципиальное отличие?

Уже ответил

Как вы его используете, кроме сорса для маппера?

Кроме сорса для маппера, ентитя участвует в 9000 логических внутрисервисных операций, которые от жсонов дальше чем от нас китайская стена. В одной логической операции может принимать участие десятка полтора одних только сервисов, которые будут друг другу доставать, перекидывать и обрабатывать состояние моделей внутри транзакции.

Я никогда не писал em.detach()

Аналогично. Вообще ни разу этого не делал.

А позвольте, я ещё так переформулирую.

Преобразованию во вью нечего делать в сервисном слое.
Суть ошибки, которую вы делаете с вашими фасадами, проста — вы формируете представление в слое сервисов, как бы вы не меняли терминологию.

Из какого источника вы взяли это утверждение?
Потому как я считаю, наоборот, ошибка не формировать представление в транзакции.

Более того, вы сами именно это и делаете.

MyEntity e = service.getBy(id, FULL_INTEGRITY);

Что вы сейчас вернули?
Persistence entity? Зачем же вы ей Hibernate.initialize() вызывали? Будете её модифицировать и flush-ить? Не похоже.

Вы вернули представление, потому как любой объект является представлением самого себя. Дальнейшее его использование — дергание геттеров — ничем принципиально не отличается от возврата аналогичного DTO с теми же полями.

Более того, вы сами именно это и делаете.
MyEntity e = service.getBy(id, FULL_INTEGRITY);

Вы вообще не поняли о чем я говорил все это время. Этот вызов — вне транзакции.

Вы вернули представление, потому как любой объект является представлением самого себя.

Нет.
Дальше уже не интересно.

Уф!

Ваш вариант

class MyEntityServiceImpl implements MyEntityService {
    @Override
    @Transactional
    public MyEntity getBy(long id, DataIntegrity requiredIntegrity) {
        MyEntity e = dao.getBy(id);
        // null check bla bla bla
        if (requiredIntegrity.requiresFullyInitialized()) {
            dao.initialize(e);
        }
   }
}

class MyEntityRestController {
    @Autowired MyEntityToJsonMapper mapper;
    @Autowired MyEntityService service;
    getById(...) {
        MyEntity e = service.getBy(id, FULL_INTEGRITY);
        Json json = mapper.map(e);
        return json;
    }
}

Мой вариант

class MyFacadeImpl implements MyFacade {
    @Override
    @Transactional
    public MyEntity getByIdForYou(long id) {
        MyEntity e = service.getBy(id);
        Hibernate.initialize(e);
        return e;
    }

    @Override
    @Transcational
    public SmallBO getByIdForMe(long id) {
       MyEntity e = service.getBy(id);
       smallBoMapper.toBo(e);
    }
}

// Controller is the same

В чём разница? Зачем выделять

Этот вызов — вне транзакции.

?
Я ваш код видел, о своём фасаде писал

— фасад (границы транзакции
В чём разница?

1) у вас путаница что называть фасадом а что сервисом, имхо. То, что у вас фасад — это сервис.
2) Ваш сервис (вы называете это фасадом, хотя это не фасад) делает работу, не характерную для сервиса, а именно — занимается маппингом бизнес модели на вью. Из контекста нашего разговора я рассматриваю ваш SmallBO как жсон. Носитель аннотаций джексона, например. Короче часть дто слоя.

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

Проблема в том, что в вашем видении мира допустима строчка

smallBoMapper.toBo(dao.getBy(id));
Эта строчка — гипотетический источник зла. Она гипотетически позволяет кому-нибудь прибивать гвоздями поля жсона к прямым вызовам базы без разделяющей абстракции. В моей архитектуре такая возможность физически отсутствует. Это превентивная защита от дурака.

От меня было предложение назвать фасад внешним сервисом, вы сказали, что суть не измениться, а теперь это всё таки сервис, ок, это не важно.

Я там выше правил код немного, но суть та же.
Итак, у нас есть
1. SmallBO — носитель аннотаций джексона.
2. Маппер из entity в smallBo
3. И строчка

smallBoMapper.toBo(entity);

Объясните про зло другими словами, пожалуйста. Я не понял про

гипотетически позволяет кому-нибудь прибивать гвоздями поля жсона к прямым вызовам базы без разделяющей абстракции.
Объясните про зло другими словами, пожалуйста.

pastebin.com/uCAuvESZ
Показал суть природы зла.

Я не понял про

Ваша архитектура позволяет реализовывать изменения формата представлений в слое логики, а не в слое представления. Это значит, что формат данных устанавливает диктат над вашей архитектурой сервисов, а не вы решаете как организовать сервисы, чтобы они предоставляли данные для сборки нужного формата (вдумайтесь в это предложение, в нем суть), и вы теряете над ней контроль.

Следуя пути наименьшего сопротивления, который вы проложили, менее квалифицированные программисты, которым таску закрыть на хуяк-хуяк, быстро раздуют вам граф зависимостей в слое сервисов, инжектя сервисы друг в друга без разбору по малейшей причине, создав вам такую сильную связность, что вы покроетесь холодным потом, когда решив заинжектить безобидный сервис А в Б, словите циклик депенденси, и увидите цепочку цикла зависимостей на 10 звеньев. И вот тогда, я гарантирую, вы меня поймете, но к этому моменту будет поздно устранять причину и вы будете долго ценой нервов устранять последствия.

Сначала хочу поблагодарить вас за проявленное терпение, что не прерываете дискуссию и иллюстрируете её кодом. Благодаря вашим замечания я чётче переформулирую абзац «Entity и RRO». А теперь к делу.

Полностью согласен с pastebin.com/uCAuvESZ. Также согласен с тем, что либо энтити связаны, либо нет и приемлемо их доставать в разных транзакциях.

@Request
JsonModel getBy(id) {
Model model = modelService.getBy(id, FULL);
SomeQty qty = qtyService.get(..., FULL); // 2.1
ModelJson json = mapper.toJson(model, qty); // 2.2
return json;
}

Поговорим о вашем решении с флажком DataIntegrity. Это вы придумали? Я нигде не встречал такого подхода. Его плюс — академическая правильность о разделении слоёв. Недостатки, на которые вы закрываете глаза — это необходимость в интенсивном написании jpql, Hibernate.initialize(), лишние N+1, потенциальные LLE, если в контроллере будет вызов modelService.getBy(id, PARTIAL) и накладывание ограничений на доменную модель, о которых ниже.

Вы это почувствовали, когда я предложил извлечь только Orders+Clients(без Items) на примере из статьи, и стали менять правила, дескать, в жизни всё сложнее, и два графа из трёх не бывает.

По уму, надо было сразу спросить, а как же циклические зависимости, когда вы первый раз упомянули Hibernate.initialize(), и мы бы сэкономили много времени.

Попробую угадать ответ. У вас их нет, потому что домен правильно спроектирован. Ок.

Теперь моё решение. Можете пропустить, и переходить сразу к ТЗ, если

. ваш фасад меня не интересует, он мне не нужен, поскольку мне не нужны ваши маппинги и ваши дто.

1. У меня совершенно классические сервисы, возвращающие энтити, без флажка integrity.
2. Дальше я говорю контроллерам — ребята, раньше вы обращались к сервису за энтити, а теперь новый договор — обращайтесь к фасаду.
3. Есть @Transactional-фасад, содержащий сервис(ы), перемапливающий их и возвращающий Raw Result Object (RRO) — чистые данные для контроллера.
4. Они raw — никакой привязки к DTO, Json, JMS мессадж и JMS транзакции — это просто классы с полями без единой аннотации и зависимости.
5. Контроллер маппит RRO в DTO.
6. Сервисы от контроллера полностью скрыты.

Очевидный недостаток этот решения

не вы решаете как организовать сервисы, чтобы они предоставляли данные для сборки нужного формата (вдумайтесь в это предложение, в нем суть),

... да, на фасад с RRO оказывается определённое влияние.

Недостатки:

1. Контроллер требует от фасада, какие данные ему предоставить. Некоторое логическое нарушение однонаправленности слоёв.
Это осознанный вынужденный компромисс для случаев, когда контроллер не является неизвестной третьей стороной, а разрабатывается тут же в приложении. Впрочем, для неизвестных третих сторон всегда можно создать RRO идентичные по структуре Entity.

Преимущества:
1. Я не накладываю вообще никаких ограничений на доменную модель. Любые ссылки, любые связи, хибернейт всё стерпит, только поменяйте на lazy в many-to-one.
2. Создавая компактные RRO с только необходимыми полями — я выигрываю в N+1.

Теперь специальный синтетический пример для сравнения подходов.

Дополняем ТЗ статьи: у клиента появляются друзья — такие же клиенты.

Тут вы снова можете захотеть изменить условия, в реальной жизни ещё нужны данные о том, когда познакомились, где, но суть не поменяется — это циклическая связь.

Теперь короткий пересказ статьи.

Создаём удобный класс не думая о JPA вообще. Добавляем в Client

class Client {
    // ... prev code
    private List<Client> friends = new ArrayList<>();

    public void addFriend(Client friend) {
        friends.add(friend);
        friend.addFriend(this);
    }

Ой, ловим StackOverflowException, добавляем проверку и всё же естественнее и логичнее перейти на set и задуматься о equals/hashCode.

Навешиваем аннотации, итого:

class Client {
    // ... prev code
    @ManyToMany(cascade = CascadeType.PERSIST)
    @JoinTable(
            name = "client_friends",
            joinColumns = @JoinColumn(name = "client_id"),
            inverseJoinColumns = @JoinColumn(name = "friend_id")
    )
    private Set<Client> friends = new HashSet<>();

    public Set<Client> getFriends() {
        return Collections.unmodifiableSet(friends);
    }

    public void addFriend(Client friend) {
        if (friends.add(friend)) {
            friend.addFriend(this);
        }
    }
}

Как ни крути, мы не может отдавать эту энтити в контроллер.
Либо LLE, либо OOO. Представление влияет даже на сигнатуру метода.

class Controller {
    ClientFacade clientFacade;
    int friendsLevel = 2;

    Json getClient (Long clientId) {
      ClientRro rro = clientFacade.getBy(clientId, friendsLevel);
      return mapper.toJson(rro);
    }
}

class ClientFacade {
    ClientService clientService;

    @Transactional
    ClientRro getBy(Long clientId, int friendsLevel) {
      Client client = clientService.getBy(clientId);
      return clientRro; // custom building of ClientRro by client of friends level
    }
}
необходимость в интенсивном написании jpql

У нас сложная логика. Зачастую, приходится искать логику, которая не выражена прямыми связями. Или эти связи очень громоздки. JPQL получается более простым вариантом. И, как я уже писал, я не считаю применение jpql антипаттерном, которого нужно почему-то избегать.

Hibernate.initialize()

О нем я уже все сказал, не считаю проблемой, учитывая сокрытие за абстракцией и потенциальную беспроблемную замену.

в контроллере будет вызов Model model = modelService.getBy(id, PARTIAL)

В нашей архитектуре, такой вызов возможен, если сущность не планируется к использованию. Если нет — это прямая ошибка (скорее всего просто невнимательность), и эта ошибка будет исправлена самим же разработчиком при написании юнит тестов (у нас это обязательно).

Ввиду этого,

потенциальные LLE

у нас не возникают, серьезно.

Вы это почувствовали, когда я предложил извлечь только Orders+Clients(без Items)

Конечно, почувствовал. Я ж не говорю, что вы в принципе неверно строите модель. Но я также понимаю, что ваш пример ближе к сферичновакуумному, поэтому обсуждать практические моменты принимая пример за нерушимый образец тоже неверно. Также я показал пример альтернативного дизайна моделей, при котором не возникает проблем, с которой мой подход сталкивает в вашей модели, в принципе не возникает.

По уму, надо было сразу спросить, а как же циклические зависимости, когда вы первый раз упомянули Hibernate.initialize(), и мы бы сэкономили много времени.

Если бы вы спросили, это ничего бы не изменило :)
Причина проста — мы избегаем бидирекшинал связей, поэтому у нас нет циклов в сущностях. Вот, так просто. И делаем мы так даже не для того, чтобы избежать циклов. Избегание циклов это последствие. Причина — мы приняли строгую иерархичность сущностей. Есть сущность порядком выше, есть сущность порядком ниже. Нижняя зависит от верхней, верхняя содержит нижнюю(-ие), без исключений. Это, в том числе, позволяет не решать дилемму какой сервис в какой инжектить для обработки логики каждой сущности. Сущности-связи только указывает на сущность, которая не связана хибом с указующей на нее связью. Нет циклов в сущностях — нет циклов в сервисах. Как бонус, имеем структуру зависимостей в сервисах — сервисы подчиненных сущностей зависят от сервисов высших сущностей, а не высшие сущности требуют сервисов нижних.

Попробую угадать ответ. У вас их нет, потому что домен правильно спроектирован. Ок.

Не знаю, угадали ли вы или нет :)

Поговорим о вашем решении с флажком DataIntegrity. Это вы придумали? Я нигде не встречал такого подхода.

Да, это наше внутреннее решение. Мы пришли к нему как раз в мучительном процессе разнесения представления, который был залит прямо в сервисы, и сервисов.

5. Контроллер маппит RRO в DTO.

Возможно, если бы вы с этого начали, дискуссия была бы короче. Фактически, вместо пары entity-DTO в вашей архитектуре есть entity-businessDTO-transportDTO. С таким вариантом я могу согласиться, поскольку вы сохраняете абстрагированность формата конверта от модели, пускай и с помощью еще одного промежуточного объекта.

2. Создавая компактные RRO с только необходимыми полями — я выигрываю в N+1.

Вот здесь вот принципиально не согласен. Я насчет выигрыша в Н+1. Любая лейзи инциаилизация == +1. Неважно, как вы ее делаете и где. fetch=LAZY -> entity.getLazyField() == +1 селект. В этом плане вы ничего не выигрываете перед моим подходом, поскольку, как я уже писал, наши модели изначально построены так, что если вы спросиили entity на вовне — то вам нужны все данные. В этом плане и вы и я делаем 100% необходимых на +1. Вы просто разрешаете модели иметь больше данных, чем нужно. Мы же стараемся этого не делать. Грубо говоря, мы более мелко фрагментируем граф, если узлы графа имеет не совсем железную связь.

class Client {
// ... prev code
private List friends = new ArrayList<>();
А вот так я бы не сделал никогда. Фактически, вы создали рекурсивную коллекцию. Думаю, можно даже не говорить о том, что вы держите в обоих карманах по револьверу и уже готовы стрелять себе по ногам :)

у клиента появляются друзья — такие же клиенты.

А теперь следите за руками:

@Entity
class Friendship {
   Client first;
   Client second;
   // + metadata that is NOT present in your model
   // moreover, your model does not even allow to have
   // really consistent metadata
}

interface FriendshipService {
   List<Friendship> friendshipsOf(Client);
   Friendship establishBetween(Client, Client);
   Optional<Friendship> findEstablishedBetween(Client, Client);
}

@Entity
class FriendshipGroup {
   Client establisher;
   @JoinTable
   List<Friendship> relations;
   // + metadata that is NOT present in your model

   Set<Client> members() {
       return relations
         .stream()
         .flatMap(relation::getBoth)
         .collect(toSet())
         .add(establisher);
   }
}

interface FriendshipGroupService {
   List<FriendshipGroup> groupsOf(Friendship);
   List<FriendshipGroup> groupsOf(Client);
   FriendshipGroup establishGroup(Client, List<Friendship>);   
}

Посмотрите, насколько гибче мой подход. Насколько он безопаснее, насколько более естественной получается доменная модель. Я создаю модель, которая гораздо ближе к реальному миру, чем ваша. Попробуйте-ка представить, дает ли ваш подход такую же степень свободы в дизайне.

Ой, ловим StackOverflowException

Как вы уже поняли, актуально только для вашего дизайна. В моем дизайне этого не случится.

Как ни крути, мы не может отдавать эту энтити в контроллер.

А вот я могу извлечь любую часть графа и отдать в любом формате. И! Обратите внимание, это, имхо, охренительно важно — моя реализация ТЗ не затронула а) ни класс Client б) ни класс ClientService. А что у вас? Вы залезли пальцами прямо в модель и увеличили ее сложность.

Либо LLE, либо OOO. Представление влияет даже на сигнатуру метода.

oh no, not on my watch

в вашей архитектуре есть entity-businessDTO-transportDTO. С таким вариантом я могу согласиться

Ок, с этим разобрались. Я всю ветку выше именно это говорил. Спасибо, что помогли четче сформулировать.

мы избегаем бидирекшинал связей, поэтому у нас нет циклов в сущностях.... Причина — мы приняли строгую иерархичность сущностей...имеем структуру зависимостей в сервисах

А то я такого не видел никогда :-)

Обратите внимание, это, имхо, охренительно важно — моя реализация ТЗ не затронула а) ни класс Client б) ни класс ClientService. А что у вас? Вы залезли пальцами прямо в модель и увеличили ее сложность.

Конечно, это охренительно важно. Я топлю за ООП, весь код, который я привёл — это объекты. Не POJO, у них нет сеттеров (Order.setStatus и Order.setExpress — это вполне валидные бизнес-мутаторы). В Order.removeItem есть защита от ухода в отрицительные значения и написан тест на этот метод. Я не ставлю никаких ограничений на иерархию и связи: надо bi-directional (в объектном, не БД смысле) — сейчас в Client добавим Set<Order>.

Всё это добро свободно работает без JPA. А JPA я воспринимаю именно как Persistance реально существующих объектов. Да, чтобы Money сохранить — надо конвертер написатьвсе уже в jadira сделали. И вообще @Type — это не JPA, а Hibernate-specific. Но я реальный класс сохраняю, а не модель.

Думаю, мы вполне можем с Дмитрием Думанским сработаться: домен у него, утверждалось, есть. Накидаем анноташекНапишем orm.xml и ACID будет вместо файловой системы.

Не возвражаете, если ваши сервисы мы назовём модулями с процедурами, а модель — структурами?

Мои аргументы в пользу ООП.

Сервис у меня выглядит так

class ClientService {
    ClientDao clientDao;

    @Transactional
    void addFriend(Long clientId, Long friendId) {
        Client client = clientDao.getBy(clientId);
        Client friend = clientDao.getBy(friendId);
        client.addFriend(friend);
    }
}

Unit-тест на такой метод я писать не буду: просто перепроверять вызовы — это не black-box тестирование.

А вот на Client.addFriend, наоборот, обязательно AAA-тест напишу: убедиться, что StackOverflowException не поймал, и дружба взаимная получается.

public class ClientTest {
    @Test
    public void addFriend() {
        // arrange
        Client andriy = new Client("Andriy", "Slobodyanyk");
        Client dmitry = new Client("Dmitry", "Bugay");

        // act
        andriy.addFriend(dmitry);

        // assert
        assertThat(andriy.getFriends(), hasItem(dmitry));
        assertThat(dmitry.getFriends(), hasItem(andriy));
    }
}

Я если честно, суть вашего последнего комента не понял. Вернее сам комент понятен, непонятна его связь с прошлыми постами.

Мои аргументы в пользу ООП.

А у меня не ООП? Смелое заявление, весьма смелое :)

Сервис у меня выглядит так

Да понятно, как он у вас выглядит. Вот только ООП как раз тут и нет почти.

andriy.addFriend(dmitry);

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

assertThat(andriy.getFriends(), hasItem(dmitry));

Суть вашей операции:
1) Селект все друзья вере 1 = Андрей.
2) Потом замапить всех на объекты
3) итерация по циклу фредны.гет(и) == Дмитрий
В пунктах 1 вы можете выбрать 5000 записей (а вдруг вы блоггер), потом создать 5000 объектов, и итерировать их циклом с целью найти одного меня.

Суть моей операции
1) СЕЛЕКТ ФРОМ дружбы ВЕРЕ друг1 = Андрей И друг2 = Дмитрий
2) Если резалт сет не пустой, создать 1 объект.

Сравните.

public class ClientTest {
    @Test
    public void addFriend() {
        // arrange
        Client andriy = new Client("Andriy", "Slobodyanyk");
        Client dmitry = new Client("Dmitry", "Bugay");

        // act
        FriendshipConditions conditions = conditions()
                .metAt(localDateTime(someTime))
                .metIn(someLocation());
                .having(Drink.BEER);
        Friendship f = friendshipService.establishBetween(andriy, dmitry, conditions);

        // assert
        assertThat(friendshipService.findEstablishedBetween().isPresent());
    }
}
А у меня не ООП? Смелое заявление, весьма смелое :)

Убедите в обратном, пожалуйста. У меня возникло ощущение, что вся логика у вас в сервисах, entity — POJO. Как у вас будет выглядеть метод FriendshipService.establishBetween? Тест на него? Почему вы считаете, что изменение класса Client для задачи «добавить друзей» — это

залезли пальцами прямо в модель и увеличили ее сложность

?

Дайте мне информацию о конкретно этой связи.

Извольте

class Client {
  ... // /previous code
   private Map<Client, Friendship> friends;
}
когда и при каких обстоятельствах мы с вами познакомились.

На ДОУ, в комментариях :-)

В пунктах 1 вы можете выбрать 5000 записей (а вдруг вы блоггер)

Конечно, с отношениями *-к-многим нужно быть аккуратным. Замечание Влада, что @OneToMany следовало бы назвать @OneToFew справедливо.

Map снимает этот вопрос?

Суть моей операции
1) СЕЛЕКТ ФРОМ дружбы ВЕРЕ друг1 = Андрей И друг2 = Дмитрий
2) Если резалт сет не пустой, создать 1 объект

Но делает это сервис. С помощью таблицы. Могут ли Андрей и Дмитрий быть друзьями без них?

У меня возникло ощущение, что вся логика у вас в сервисах, entity — POJO.

Логика по работе с персистентностью не обязательно, но в основном да, в сервисах. В ентитях тоже много логики, и там в том числе бывает и персистентность. В том числе через бизнес-мутаторы, я их тоже люблю.

Как у вас будет выглядеть метод FriendshipService.establishBetween?
Friendship establishBetween(client1, client2) {
   Optional<Friendship> existing = find(client1, client2);
   if (existing.isPresent()) {
      return existing.get();
   }
   Friendship friendship = new Friendship(client1, client2);
   em.persist(friendship);
   eventPublisher.publish(new FriendshipEstablishedEvent(friendship));
}
Тест на него?

Я уже его написал в предыдущем посте.

Извольте

Итоги падведьом ©
В попытке держать метаданные дружбы клиентов и сохранить свою модель вы создали структуру, в которой клиент А содержит мапу из клиентов и объектов, у которых тоже по 2 клиента, причем в каждой паре 1 из клиентов — тот же А? У вас 4-уровневая (ой-вей!) структура из клиентов.
Вам не кажется эта структура офигеть каким искусственным, громоздким, излишним, error-prone и вообще неудачным решением?

Конечно, с отношениями *-к-многим нужно быть аккуратным.

Будете вы аккуратным или нет — не важно. Если в вашей модели реально есть клиент с 5000 друзей, то чтобы установить факт наличия у него в друзьях другой персоны, вам нужно будет сделать ровно то, что я сказал — селект и маппинг 5000 записей, после чего цикл по 5000 объектов.

Не хотите? Добро пожаловать в мою модель.

Map снимает этот вопрос?

Никоим образом. Чтобы узнать, есть ли среди ваших 5000 друзей я, вы все равно должны загрузить в мапу 5000 объектов, вычитыва их ВСЕ из базы. Да, цикл вы заменили хештаблицей (построив попутно 4-уровневую пирамиду), но решению от этого не похорошело.

Но делает это сервис.

А таки что ви имеете пготив сегвисов? Логика расположена там, где ее естественнее держать. Если ентити обладает всей полнотой данных, то в ентити. Если нужно подключение внешней (по отношению к ентити) логики, то в сервисах. То, что вы типа «держите логику в ентити», называя логикой каскадный персист который полетит в чайлдов ну это вообще ни о чем.

С помощью таблицы.

Мы с вами вообще все храним с помощью таблиц, даже небо, даже Аллаха.

Итоги падведьом ©

Да, мы вроде уже возле финиша.

Люблю Optional, не люблю isPresent(), написал бы через orElse.
С другой стороны, вижу if — пишу тест, а orElse в глаза бросается только JaCoCo.

Map снимает этот вопрос?
Никоим образом.

Извините, поспешил

class Client {
  ... // /previous code
   @LazyCollection(LazyCollectionOption.EXTRA)
   private Map<Client, Friendship> friends;
}
В попытке держать метаданные дружбы клиентов и сохранить свою модель вы создали структуру, в которой клиент А содержит мапу из клиентов и объектов, у которых тоже по 2 клиента, причем в каждой паре 1 из клиентов — тот же А? У вас 4-уровневая (ой-вей!) структура из клиентов.
Вам не кажется эта структура офигеть каким искусственным, громоздким, излишним, error-prone и вообще неудачным решением

Неа, Map кажется мне простым и понятным решением, не зависящим от JPA и сервисов.

Но спасибо, что подняли эту тему, допишу абзац про *-to-many.

С одной стороны, я понимаю, что любое ТЗ вы можете решить в рамках своего подхода и с помощью сервисов, дробления графа и jpql не проиграть в N+1.

С другой стороны, может правы и те, кто к вашим strict rules дополнительно запрещает *-to-many вообще, потому что сегодня там три записи, а завтра три миллиона, а код уже задеплоен.

Правильно понимаю, что отличие наших подходов в том, что я свободно отношусь к модели и играю DTORRO-шками, а вы достигаете того же вводя правила и платя строками кода?

Так вот, итоги.

А таки что ви имеете пготив сегвисов?

Таки имею. Что мой, что ваш сервисы — процедуры.

ООП как раз тут и нет почти

... и поэтому на них нет юнит-тестов, сразу интеграционные.

Не хотите? Добро пожаловать в мою модель.

Я наконец-то понял о чём мы спорим. Это ORM vs Native SQLИнтенсивный JPQL, где на стороне ORM в плюсах полная свобода в модели и отсутсвие sql, когда можно просто пройтись по ней (что я и демонстрирую с помощью Entity->RRO). Только в случае Native SQL (+MyBatis) преимущество легче продемонстировать на каком-то аналитическом запросе, где ORM «вытянет несколько мегабайт данных» (цитата из другого топика), то c JPQL это невозможно неочевидно.

Мне кажется, пока вы ограничены JPQL, в N+1 выиграть сложно, максимум паритет, и то за счёт написания запросов руками.

Вы ближе ко второму подходу, поэтому пишите JPQL, накладываете ограничения (нет би-дирекшнл связям, иерархия и т.д.), но от вытягивания графа с помощью Hibernate.initialize() не отказываетесь.

Вам не кажется эта структура офигеть каким искусственным, громоздким, излишним, error-prone и вообще неудачным решением?
Извините, не удержался :-)

Не то чтобы я явной благодарности прошу, скорее просто какой-то реакции, но, кажется, для интенсивного jpql вам нужен QueryDSL.

Но почему транзакции на сервисе, а не на фасаде?

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

*Под запросом я понимаю не только хттп реквест, а любое внешнее взаимодействие — RPC/RMI колл, JMS вызов, и т.д.

Если вы все это покрываете транзакциями, то следующий вопрос, который вы могли бы задать (надеюсь не зададите) а почему б не инжектить дао прямо в контроллер? В контроллере ведь уже есть транзакция.

Ответил в другой ветке своими словами, тут приведу ссылочку dzone.com/...​implementation-patterns-3

То, что этот подход где-то описан в статье, или то, что вы им пользуетесь не оправдывает и не исправляет фатальные недостатки этого подхода.

Симметрично. Впрочем, если вы укажете дополнительные материалы в пользу вашего подхода, я с интересом ознакомлюсь.

хорошо написал. к сожалению на практике приходится иметь с легаси проектами, где структура базы уже спроектирована (и часто не лучшим образом)

Спасибо.
Да, обычно структура БД уже есть и в своей практике не припомню с ней каких-то особых проблем. А вот как её потом в entities «дизассемблировать» — тут самое веселье и начинается.
Например: в каких-то таблицах есть createdBy, createdOn, modifiedBy, modifiedOn. Почему их выносят в супер-класс? Почему не композиция с @Embeddable?

Почему не композиция с @Embeddable?

KISS.
user.getCreatedTime();
лучше чем
user.getMetaData().getCreatedTime();

А как же инкапсуляция? Я специально привел примеры Address и особенно Money

А в чем тут особое инкапсулирование? Тогда можно идти еще дальше, например
client.getName();
client.getSurName();
заменить на
client.getPersonalData().getName();
client.getPersonalData().getSurName();
в чем смысл объектного объединения полей, которые все равно нужно получить из того же client? Имхо — это прямое нарушение кисс.
Допускаю, что вы бы хотели сделать чтото вроде
List clients = ...
List personals = clients
.stream()
.map(Client::getPersonalData)
.collect(toList());
и вернуть их кудато, чтобы не светить весь объект клиента.
Ок. Обжект вей, одобряю.
Но. Зачем вся эта сложнота и анотота с @Embeddable если они могут быть просто полями, а getPersonalData() возвращать имутабл дто объект создаваемый через нью? Хотите закрыть остальной объект оставив поля мутабельными? Закройте объект интерфейсом:
class Client implements PersonalData
Короче говоря, я не вижу реальных преимуществ фрагментирования плоских данных именно через @Embedded-объекты. Мне видится это нарушением кисс.

client.getPersonalData().getName();
client.getPersonalData().getSurName();

Если в client содержится разнообразная информация о клиенте, то именно так и имеет смысл группировать.

Хотите закрыть остальной объект оставив поля мутабельными?

Поинт в том, что возможно лучше не мутабельные поля, а бизнес методы модификаторы?

Давайте продолжать на примере Money. Имхо, класс лучше двух произвольно модифицируемых полей.

Если в client содержится разнообразная информация о клиенте, то именно так и имеет смысл группировать.

Нет. Хотябы погуглите правило деметры. Оно несколько упоротое, если формализировать, но идея такая, что ваш вариант — нарушение кисс.

Поинт в том, что возможно лучше не мутабельные поля, а бизнес методы модификаторы?

Эммм. Ну ок, сделайте бизнес методы модификаторы, я только за. Но как это относится к искусственной и излишней группировке полей в объекты, Никак. Те же бизнес-методы можно иметь на уровне основной сущности.
В вашем примере нету никаких методов модификаторов, вы просто усложнили доступ к полям, заставляя вызывать 2 метода вместо 1. При этом никакой реальной инкапсуляции это все равно не несет.

Давайте продолжать на примере Money. Имхо, класс лучше двух произвольно модифицируемых полей.

Не работал с этим апи, увы. Глянул очень бегло.
Между вашим примером с емеддабл адрес и мани есть две большие разницы. Ваш класс — излишний враппер над 3 полями, лишенный логики. Мани — часть апи с большим количеством логики, которая предназначена к прямому сохранению.
Чтобы ваш пример с адресом был равносилен примеру использования мани — сделайте свой адрес частью библиотеки для работы с адресами. С (для примера) валидацией полей, проверкой корректности почтовых индексов, правилами нейминга улиц в зависимости от локалей и тд. Тогда я первый предложу использовать ваш andriy.slobodyanyk.javax.address.api.Address вместо трех полей.

Но в текущем варианте — вынос простых трех полей в отдельный класс это излишество. Я не то чтобы религизно против, я не вижу ни одного реального преимущества. Все, что вы перечислили, реализуемо более простыми способами в рамках класса самой сущности.

Мани — часть апи с большим количеством логики

Давайте называть это всё таки классом.

Правильно ли я понимаю, что мы сейчас сравниваем «процедуры + структуры» vs «классы + JPA» и оба предпочитаем второе?

Увы, из-за засилья POJO неочевидно, что в Address конечно же появится логика.

Увы, из-за засилья POJO неочевидно, что в Address конечно же появится логика.

Аааа ну дык, так бы сразу. Тогда это все меняет. Особенно если, допустим, вы этот класс будете реюзать в других сущностях.

Спасибо за статью!

Спасибо за статью. За кадром также остались достаточно важные темы: потенциальные проблемы и bottlenecks Hibernate, почему Hibernate, а не EclipseLink и т.д.

Hibernate, а не EclipseLink

А у вас есть конкретные примеры преимущества EclipseLink над Hibernate? Если да — опишите их в статье, думаю всем будет интересно почитать.
Если нет — то в чем смысл вопроса?

bottlenecks Hibernate

На моей практике «ботлнек хибернейта» при инвестигейте оказывался временной проблемой железа в стойке или самой бд :)

Если Hibernate лучше во всем, почему существуют альтернативные решения в виде EclipseLink, OpenJPA и т.д?

EclipseLink

Так їх спонсують всякі недолугі IBM. Вони ж роблять JavaEE замість спрінгу.

почему существуют альтернативные решения в виде EclipseLink, OpenJPA и т.д?

Потому же, почему существует, например, GlassFish. Какието фанаты решили что оно должно быть, какието благотворители их проспонсировали. Это и ответ.

По той же причине, по какой существуют другие браузеры, кроме встроенного в OS, например.
Каждое решение:
а) считает, что его реализация стандарта технически лучше конкурентов;
б) добавляет свои уникальные фичи сверх стандарта.

Статья на 99% о JPA без привязки к конкретному провайдеру.

проблемы и bottlenecks Hibernate

Які там можуть бути ботлнеки якщо всі проблеми з ормами від повільних запитів у базу?

Задачка на внимательность — что в моём примере является Hibernate specific?

Назва статті.

@Type c несколькими полями (Money в статье) — это Hibernate-specific feature. В JPA только AttributeConverter в одно поле. Отзовитесь, кто ещё работал с EclipseLink?

Оффтопик: Прими восхищение телеграмм-каналом вообще и последним постом в частности!

про мене: t.me/full_of_hatred

Подписаться на комментарии