Еволюція Spring бінів

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Всім привіт. Я Сергій Моренець, розробник, викладач, спікер та технічний письменник, хочу поділитися з вами своїм досвідом роботи з такою цікавою технологією, як Spring Framework та розкрити ті теми, які з нею пов’язані. Ми докладно розглядаємо цю технологію на деяких тренінгах, і незайвим буде ще раз докладніше розповісти про її використання.

Витоки написання цієї статті йдуть у далекий 2008 рік, який приніс нам не тільки дефолт та економічну кризу. Він став революційним для багатьох Java-розробників завдяки експансії трьох технологій. Це Spring, Hibernate та Maven. Щасливий випадок звів мене з цими абсолютно незнайомими для мене технологіями, у тому числі і Spring Framework, тоді ще версії 2.5.6, яка повністю перевернула мої уявлення про розробку Java-додатків. У ті роки такі терміни, як DI (Dependency Injection) та IoC (Inversion of Control) тільки входили в наше життя, і про них обов’язково напишу окрему статтю.

А в цій статті я хотів би розповісти про таку цікаву тему, як Spring біни, але звичний tutorial у вигляді Get Started був би не новим і банальним. А ось підготувати історичний екскурс і розповісти про те, що сталося в Spring з бінами за останні 20 років, як змінився спосіб роботи з ними, розвіяти популярні міфи та упередження — це виклик та цікаве нестандартне завдання. Адже це тісно пов’язано з тим, як змінювалися погляди розробників на програмування та пов’язані з цим парадигми. Принаймні такий огляд було б цікаво читати, і статей на подібну тематику я не зустрічав у відкритих джерелах. Всі приклади в статті будуть ґрунтуватися на Spring 6 та Spring Boot 3, які тільки недавно вийшли.

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

Spring біни

Думаю, кожен Java-розробник добре уявляє, що таке бін. Біни (beans) з’явилися ще до виникнення Spring. По-перше, у специфікації Java Beans, доданої в Java 1.2, яка описувала вимоги до Java бін:

  • Публічний дефолтний конструктор.
  • Геттери та сетери для доступу до полів.
  • Клас повинен бути серіалізованим.
  • У такому класі мають бути перевизначені методи equals/hashCode/toString.

Фактично, саме з цього моменту почалася переможна хода гетерів і сеттерів, які спочатку писали вручну, потім автоматично, поки не з’явився Lombok з його анотаціями.

Потім у 1999 році вийшла перша версія специфікації EJB з Java EE, де вже був закріплений такий термін як Enterprise Java Bean. Якщо Java біном міг бути будь-який клас, і часто їх використовували для клієнтської частини (в JSP сторінках), то EJB представляв собою серверний об’єкт з деякою бізнес-логікою, включаючи, наприклад, роботу з даними. Але і Java Bean, і EJB мали щось спільне — керованість, через що і існує синонім до терміну бін — managed object (керований об’єкт). Сам бін створювався не програмістом у коді, як завжди, а деяким контейнером (runtime). У ролі такого контейнера міг бути вебсервер або сервер додатків, який підтримував EJB.

Однак процедура опису та управління EJB була досить складною, як у версії 1.0, так і у версії 2.0, тому розробник Spring Framework Род Джонсон запропонував альтернативу — концепцію легковагих бінів, які були несумісні з EJB і які пізніше стали називати Spring beans. Описати та керувати таким біном було набагато простіше, для цього в принципі навіть не потрібен був і сервер, для цього було достатньо і консольної програми.

Spring Framework є досить складною технологією, але спрощено з нього можна виділити 6 основних пунктів:

  1. Розробники описують конфігурацію бінів (за допомогою XML/Groovy документів або Java коду).
  2. Для сканування/завантаження інформації про бін існує інтерфейс BeanDefinitionReader та його реалізації, які прив’язані до типу конфігурації, наприклад, XmlBeanDefinitionReader.
  3. Вся інформація про бін, його атрибути та властивості інкапсулюється через інтерфейс BeanDefinition та його реалізацію.
  4. Для реєстрації бінів у сховищі (реєстрі) призначений інтерфейс BeanDefinitionRegistry.
  5. Основним типом і відправною точкою для отримання бінів з реєстру є інтерфейс BeanFactory, який надає базовий API для роботи з бінами в режимі read-only, а реалізації BeanFactory якраз і є in-memory сховищем бінів.
  6. Всі перераховані вище інтерфейси є внутрішніми і рідко використовуються безпосередньо. Розробники для керування бінами використовують спеціальний інтерфейс ApplicationContext. Реалізації ApplicationContext пов’язують між собою всі згадані компоненти: завантаження бінів (наприклад, з XML) та їх збереження, зв’язування (на основі DI) бінів та їх властивостей, управління подіями, налаштуваннями оточення (environment), listeners та багато іншого. Тому початок роботи з Spring бінами полягає у створенні Spring контексту та завантаження конфігурації.

За минулі 20 років саме пункт 1 зазнав найбільших змін, і саме про нього ми й говоритимемо у цій статті. При цьому життєвий цикл Spring бін складається з п’яти логічних етапів, кожним з яких управляє Spring Framework:

  1. Створення біна.
  2. Зв’язування з іншими бінами.
  3. Ініціалізація внутрішнього стану.
  4. Використання бізнес-логіки біна у додатку, в тому числі за допомогою AOP (аспект-орієнтованого програмування).
  5. Логічне знищення.

Таким чином, використовуючи Spring та його функціональність, ми делегуємо Spring управління нашими бізнес-об’єктами. Чим складніший додаток та зв’язки всередині нього, тим більше ми виграємо від такого підходу. Ми заощаджуємо час розробки, фокусуючись на пункті 4.

Щоправда, може виникнути питання: якщо Spring біни це настільки зручна фіча, можливо, всі класи в наших проєктах оголосити, як біни? В принципі, це питання схоже на інше: якщо незмінність (immutability) об’єкта це перевага, чому Java не зроблять всі об’єкти незмінними? А все тому, що для деяких об’єктів це настільки ускладнить їх використання (наприклад, через performance), що зведе нанівець усі плюси цього підходу. Також з бінами. Якщо з 5 описаних пунктів для вашого об’єкта жоден не суттєвий, значить він не є кандидатом у Spring біни.

Конфігурація XML

Коли Spring почав розроблятися в 2001 році, найпопулярнішим текстовим форматом для зберігання конфігурації проєктів був XML. Він підтримував ієрархічну структуру документа з вкладеними елементами та атрибутами, а крім того, дозволяв валідувати XML-документ за допомогою XML-схем (XSD). Тому спочатку саме XML-конфігурація була базовою для опису Spring бінів.

Уявімо, що у нас є найпростіший Java-проєкт, де для логування інформації (повідомлень) є інтерфейс Writer:

@FunctionalInterface
public interface Writer {
      
    void write(String text);
}

Є його дефолтна реалізація, яка логує події у консолі:

public class MemoryWriter implements Writer {
       public void init() {
              // State initialization
       }
      
       public void destroy() {
              // Destroy resources
       }
 
       public void write(String text) {
              System.out.println("Saved in memory:" + text);
       }
 
}

І є деякий клас Server, який надає бізнес-функціональність та використовує Writer для логування:

class Server {
 
       private final Writer writer;
      
       public Server(Writer writer) {
              this.writer = writer;
       }
 
       public void start() {
              writer.write("Server started");
       }
      
       public void stop() {
              writer.write("Server stopped");
       }
 }

Тоді XML-документ для опису конфігурації буде виглядати наступним чином:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd">
 
       <bean id="writer" class="org.sample.it.MemoryWriter"
              init-method="init" destroy-method="destroy" />
 
       <bean id="server" class="org.sample.it.Server">
              <constructor-arg ref="writer"></constructor-arg>
       </bean>
 </beans>

Однією з переваг такої конфігурації є те, що вона зрозуміла навіть тим, хто почав вивчати Spring. Тут немає того, що пізніше назвуть «магією Spring», головна магія тут — Java Reflection при створенні об’єктів та виклик методів. Такий фрагмент зрозумілий і людині, і комп’ютеру.

А для завантаження такої конфігурації потрібно використовувати XmlWebApplicationContext або GenericXmlApplicationContext (він з’явиться трохи пізніше, у Spring 3.0):

try (GenericXmlApplicationContext context = new GenericXmlApplicationContext(
       "beans.xml")) {
     Writer writer = context.getBean(Writer.class);

Якщо ви зміните конфігурацію бінів у XML, вам не потрібно перекомпілювати програму, тільки перезавантажити, що є великим плюсом. З іншого боку, така конфігурація містить надмірну інформацію, по-перше, через громіздкість самого XML, по-друге, через те, що потрібно вручну вказувати на зв’язки між бінами, і по-третє, тому що тут не дотримується політика конвенцій, що стала популярною після появи Spring Boot.

І справді, нехай ідентифікатор бина є унікальним, його можна автоматично згенерувати на основі імені класу. Крім того, якщо людина аналізує проєкт, то зазвичай вона починає аналіз з коду, і важко відразу зрозуміти, які класи відносяться до Spring конфігурації, а які ні.

У міру зростання проєкту та додавання технологій починаються нові складності. Коли кількість бінів досягне сотень штук, їх буде незручно зберігати в одному файлі, доведеться розбивати його на дрібніші, через що всю конфігурацію буде важче проглядати і аналізувати. Якщо у вас був повноцінний вебдодаток, що включає Spring Security, Spring MVC, Spring Web Flow та інші технології, сюди ще додавалася і їх XML-конфігурація, що могло перетворити життя розробника на справжнє пекло у разі дослідження помилок.

Більше того, ви можете перейменувати клас/ метод і забути зробити це в XML. Зараз це звучить не так страшно, тому що є Spring плагіни для IDE. Вони ж не дозволять написати некоректний XML, але 15 років тому цього нічого не було, що створювало безліч складнощів. Тому, коли в 2004 році вийшла Java 5 з підтримкою анотацій, на цю фічу в першу чергу звернули увагу не розробники додатків, а розробники бібліотек і фреймворків, таких як Spring або Hibernate.

Annotation-based конфігурація

Ідея Java-конфігурації давно назрівала і з’явилася в Spring 2.5, який вже підтримував Java 5-фітчі. Ми оголошуємо класи як Spring біни не в окремому XML (або Groovy) файлі, а прямо у вихідному коді:

@Component
public class MemoryWriter implements Writer {
       @PostConstruct
       public void init() {
              // State initialization
       }
      
       @PreDestroy
       public void destroy() {
              // Destroy resources
       }
 
       @Override
       public void write(String text) {
              System.out.println( "Saved in memory:" + text);
       }
}

Тоді за допомогою анотацій можна вказати все те, що раніше вказувалося в XML:

  • Ідентифікатор бина (якщо не підходить дефолтний).
  • Ознака primary.
  • Методи init/destroy.
  • Qualifiers.
  • Scope.
  • Профілі.

Ну і, звичайно, можна вказати автоматичне зв’язування (auto-wiring) бінів, яке здійснюється за допомогою анотації @Autowired, а у випадку constructor injection навіть без неї.

Чи можна використовувати інші структури даних як біни, наприклад, перерахування? На жаль, немає. Це відоме обмеження, а ось Java records, можна, починаючи з версії JDK 16:

@Component
public record MemoryWriter() implements Writer {
 
       @Override
       public void write(String text) {
              System.out.println("Saved in memory: " + text);
        }
 }

Все це виглядає дуже привабливо — XML-документи більше не потрібні. Але якщо раніше всі оголошення бінів були статичні, то тепер через їх auto-discovery потрібно вказати базовий пакет, який Spring буде сканувати в пошуках бінів:

              try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext("org.sample")) {
              Writer writer = context.getBean(Writer.class);

Для annotation-based конфігурації потрібно використовувати новий клас-контекст Annota-tionConfigApplicationContext, який включає вже не тільки екземпляр BeanDefinitionRegistry, але і об’єкт типу ClassPathBeanDefinitionScanner, який відповідає за пошук і реєстрацію бінів з classpath. Якщо у разі XML для пошуку бінів використовувався інтерфейс BeanDefinitionReader та його реалізації, то ClassPathBeanDefinitionScanner внутрішньо використовує інтерфейс MetadataReader та його реалізації.

Все це уповільнює створення контексту та старт додатку. Більше того, з’явилися несподівані обмеження, яких раніше не було:

  • Ми не можемо оголосити два біни одного класу.
  • Ми не можемо оголосити бін, якщо у нас немає доступу до його source code.
  • Ми не можемо оголосити бін, якщо у нас бін знаходиться в runtime залежності, а не com-pile (наприклад, JDBC драйвер).

Що далі, то цікавіше. Виявилося, що дуже непросто виключити бін із Java-конфігурації динамічно, не змінюючи класу біну (наприклад, у вас немає доступу до коду класу). І головна відмінність від конфігурації XML — ми на етапі компіляції вказуємо, які класи є бінами і які атрибути вони будуть використовувати. Змінити це в run-time вже неможливо. Причому, якщо ви змінили якийсь бін та його атрибути, потрібно перекомпілювати додаток.

Ще один підводний камінь. Спочатку Spring позиціонував себе як легковажний фреймворк, який не вимагає, щоб ваші біни прив’язувалися до якихось спеціальних інтерфейсів. Що стосується XML так і було. Тепер, якщо ви, наприклад, захочете виділити ваші бізнес-об’єкти в окрему бібліотеку, то вона обов’язково потягне за собою разом з анотаціями та всі Spring залежності. Тобто, ваші бізнес-об’єкти вже не є POJO. А іноді буває необхідно забезпечити можливість використання бізнес-об’єктів і поза Spring.

Обмежень було так багато, що стало зрозуміло, що необхідний новий тип конфігурації, яка поєднує в собі переваги і XML, і анотацій — Java-based конфігурація.

Java-based конфігурація

Цей тип конфігурації з’явився в Spring 3.0 і завоював популярність на довгі роки. У цій конфігурації залишилися анотації, але ми їх розміщуємо не на самі бізнес-класи, а в спеціальному файлі з анотацією @Configuration, який називається Java-конфігурацією:

@Configuration
public class AppConfig {
 
       @Bean
       public Writer writer() {
              return new MemoryWriter();
       }
 
       @Bean
       public Server server() {
              return new Server(writer());    
       }
}

І тепер для завантаження контексту потрібно лише вказати цей клас (або класи, якщо їх кілька):

try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {

Spring створить біни на основі всіх методів, які позначені анотацією @Bean. Такий підхід знімає майже всі обмеження, які існували в попередньому випадку. І головне: наші бізнес-об’єкти знову перетворилися на POJO, які не прив’язані до поточного контейнера (Spring Framework). Є і додаткові плюшки — ми можемо в @Bean-методі виконати зовнішню ініціалізацію нашого біну, чого раніше (з @Component) було неможливо.

Так як конфігурація бінів зберігається в одному (або кількох) Java-класах, то немає необхідності сканувати весь classpath у пошуках бінів. Ви можете передати в Spring контекст тільки базовий клас-конфігурацію для аналізу, а інші підключити (проімпортувати) за допомогою анотації @Import.

Якщо ж у вас legacy-проєкт, і вам потрібно підтримувати інші типи конфігурацій, їх теж легко можна підключити за допомогою анотацій @ComponentScan і @ImportResource. Якщо у вас використовується Spring Boot, то він додає корисні інструкції для завантаження бінів/конфігурацій у вигляді умовного завантаження за деякою умовою, наприклад:

Завантаження тільки у разі наявності класу в classpath:

@Configuration
@ConditionalOnClass(JsonIgnore.class)
public class AppConfig {

Завантаження тільки у випадку, якщо вже є певний бін:

@Configuration
@ConditionalOnBean(name = "writer")
public class AppConfig {

Або навпаки, завантаження лише у випадку, якщо немає бина певного типу:

@Configuration
@ConditionalOnMissingBean(value = Service.class)
public class AppConfig {

Такі анотації — це сильний інструмент в руках розробника і частина автоконфігурації, те, що називають «магією Spring». Але їх невиправдане застосування може призвести до «annotation hell», коли код буде перевантажений анотаціями настільки, що його буде важко читати і аналізувати, особливо коли поєднуються інструкції з різних технологій. Ще одна складність — якщо у вашому проєкті безліч Spring технологій, і всі вони працюють на основі auto-wring, auto-discovery та auto-configuration, то розібратися в тому, як все це разом працює, буває дуже непросто.

Усвідомлення цього факту призвело розробників Spring Framework до думки про те, що непогано б повернутися до витоків і надати програмістам можливість опису конфігурації без анотацій. Вони назвали це функціональною реєстрацією бінів.

Функціональна реєстрація

Такий тип конфігурації був доступний ще в Spring 4.x, але став активно просуватися в п’ятій версії, де Spring розробники оголосили про функціональний підхід як одну з основних фітч. У чому його суть?

Мало хто знає, що ще з ранніх версій Spring ви могли зареєструвати ваш бін без використання XML або анотації, причому вже після створення/завантаження контексту:

try (GenericXmlApplicationContext context = new GenericXmlApplicationContext(
       "beans.xml")) {
       context.getBeanFactory().registerSingleton("writer2", new MemoryWriter());
       Writer writer = context.getBean("writer2", Writer.class);

Власне кажучи, саме так і працює BeanDefinitionRegistry, коли йому потрібно додати бін у сховище BeanFactory. Чому тоді й не користуватися лише цим способом? А все тому, що в метод registerSingleton потрібно передати вже проініціалізований бін. Ніякі init/destroy callbacks та auto-wiring процеси викликатися не будуть.

А для того, щоб повноцінно зареєструвати наш Java клас як Spring бін, потрібно скористатися іншим API:

try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {
       context.registerBean(Server.class);

На жаль, методу registerBean немає в базовому інтерфейсі ApplicationContext, тому вам для функціональної реєстрації необхідний контекст типу GenericApplicationContext або його спадкоємців. На жаль, цей метод повертає void, тому ви не можете з нього отримати створений бін.

У чому користь такого підходу? По-перше, немає ніякої магії на зразок анотацій, ви за допомогою простого зрозумілого API реєструєте ваші біни. Менше використовується Reflection API, а це йде на користь тим, хто використовує GraalVM native image, підтримка яких з’явилася в Spring 5.3. По-друге, такий код набагато компактніший. Якщо знову повернутися до Java-based конфігурації, то ось як ми оголошували там бін Server:

       @Bean
       public Server server() {
              return new Server(writer());    
       }

І як зараз:

       context.registerBean(Server.class);
                  

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

Ще одна перевага — тут не використовується магії, типу bean auto-detection, тобто Spring немає необхідності шукати ваші біни по всьому classpath. Правда, як і раніше, потрібно шукати системні біни типу GenericApplicationContext, але трохи нижче я розповім про те, як цього уникнути.

Що, якщо конструктор біна приймає параметри? Якщо ці параметри беруться з Environment, їх дуже легко отримати і передати. Уявимо, що ми маємо нову реалізацію Writer, яка логує повідомлення до бази даних:

public class DbWriter {
       private final String server;
      
       private final String dbName;
 
       public DbWriter(String server, String dbName) {
              this.server = server;
              this.dbName = dbName;
       }

Якщо ми спробуємо зареєструвати бін звичним способом:

 
              context.registerBean(DbWriter.class);

то отримаємо помилку:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ’java.lang.String’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

Тому потрібно спочатку отримати параметри логування з налаштувань і передати до конструктора:

       String server = context.getEnvironment().getProperty("app.server");
       String dbName = context.getEnvironment().getProperty("app.dbName");
       context.registerBean(DbWriter.class, server, dbName);

У такому способі реєстрації є одна каверза. Ті біни, які ми зараз зареєстрували, не беруть участь у auto-wiring, так ми їх додали вже після завантаження контексту. Потрібен механізм функціональної реєстрації, який відбувається ще до завантаження основного контексту. І такий механізм у Spring Boot називається ApplicationContextInitializer.

Якщо створити реалізацію цього інтерфейсу:

public class AppInitializer implements ApplicationContextInitializer<
GenericApplicationContext>{
 
       @Override
       public void initialize(GenericApplicationContext ctx) {
              ctx.registerBean(Server.class);
       }
}

То в методі initialize можна реєструвати Spring біни. А для того, щоб підключити такий клас до Spring Boot конфігурації, вам пропонують два підходи.:

  1. Для тих, хто віддає перевагу конфігураційним файлам, потрібно створити спеціальний файл spring.factories в папці src/main/resources/META-INF:

org.springframework.context.ApplicationContextInitializer=org.sample.it.AppInitializer

Таким чином, Spring потрібно просто прочитати один текстовий файл у кожному JAR-файлі, і це найшвидший спосіб завантаження бінів. Файл spring.factories був основним до Spring Boot 2.7, в якому було додано новий файл для авто-конфігурації — META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. Правда, і файл spring.factories все ще підтримується, але ця підтримка може припинитися в нових версіях 3.x, тому краще перейменувати цей файл вже зараз, якщо ви використовуєте Spring Boot 2.7+

  1. Для тих, хто є шанувальником конфігурації Java, додати цей клас за допомогою SpringApplicationBuilder:
@SpringBootApplication
public class Application {
 
       public static void main(String[] args) throws Exception {
              new SpringApplicationBuilder(Application.class)
       .initializers(new AppInitializer()).run(args);

І тоді після завантаження Spring контексту ви отримаєте повноцінний бін server. Але в цьому випадку вам потрібно перекомпілювати ваш клас у разі будь-яких змін.

Висновки

У цій статті ми обговорили основні способи оголошення/ реєстрації Spring бінів у Spring/Spring Boot додатках, починаючи від використання конфігурації XML, і закінчуючи функціональною реєстрацією. У кожного способу є свої плюси та мінуси, як і свої шанувальники та недоброзичливці. Головне — чітко розуміти вхідні вимоги та використовувати для їх реалізації відповідний інструмент.

Сподіваюся, що прочитана стаття виявиться корисною і ті знання, які ви отримали, ви використовуватимете у своїх проєктах та напрацюваннях.

👍ПодобаєтьсяСподобалось18
До обраногоВ обраному9
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Сергію, дякую за чудову статтю! Історичний екскурс — це дійсно гарний і цікавий формат, що допоміг мені прибрати деякі білі плями.

Неможливо бути Java розробником і не знати про Java Beans, але є нюанси, пропоную до вашої уваги.

у специфікації Java Beans, доданої в Java 1.2

Відкрив її, щоб перевірити наступний пункт, здається вона навіть з Java 1.1
Я не прискіпуюсь, а скоріше відкрив для себе, що Java Beans були з самого початку, а от Reflection та Annotations зробили трохи пізніше.

У такому класі мають бути перевизначені методи equals/hashCode/toString

Не бачу такого ані у спеці, ані на вікіпедії.
І, мабуть, це логічно: equivalence — це для «data» класів.

Фактично, саме з цього моменту почалася переможна хода гетерів і сеттерів, які спочатку писали вручну, потім автоматично, поки не з’явився Lombok з його анотаціями.

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

У мене страшено болить, що Java Beans застосовуються у бізнес-логіці, вщент ламаючи інкапсуляцію.

Ви мимохідь згадуєте, що

Сам бін створювався не програмістом у коді

а я попросив би написати це великими ЧЕРВОНИМИ літерами.

Java Beans — це для контейнерів та інших фреймворків, а не для бізнес-логіки.

Щоправда, може виникнути питання: якщо Spring біни це настільки зручна фіча, можливо, всі класи в наших проєктах оголосити, як біни?

Геть усі? )
Звісно, Spring Beans — це зазвичай «інфраструктура», домен/модель/entities — ні.

В принципі, це питання схоже на інше: якщо незмінність (immutability) об’єкта це перевага, чому Java не зроблять всі об’єкти незмінними?

Даруйте, мені здається це абсолютно інше питання. Не бачу тут ніякої схожості. Незмінність (immutability) — це однозначний і очевидний плюс, все що можна так зробити — треба робити.
Інша справа, що в типовому додатку мало що незмінне.

Усіляки спрінг-артефакти (сервіси, контролери тощо, створені за допомогою constuctor DI) — іммутабельні, але від цього ні холодно, ні жарко.
Hibernate entities — міняють свій стан (але треба робити це не через сеттери).
RequestDto, ResponseDto — біни, тому не можуть бути іммутабельними по визначенню.

Мені одному здається, що spring, hibernate та більшість enterprise java то є енциклопедичні приклади переускладненого overengineering-у?

Spring це якраз була альтернатива ентерпрайзним контейнерам, а Хайбернейт — альтернатива угрьобіщньому EJB.

spring

Це дуже широке поняття :)

hibernate

В основному заради domain model використовують — це зручно, бо менше шаблонного коду, якщо непотрібний low level access.

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

переускладненого overengineering-у?

Ні

Мені одному здається

Ні, не одному. Спрінг/хіб-ні-нужон то є відоме когнітивне порушення, не парся.

Розкажіть, будь ласка, які альтернативи використовуєте? На яких задачах?

Це гарне питання. Хотів би щоб на нього була гарна відповідь. Але її немає.

Задачі: всякі бекенди, json/grpc/mqtt api, не web.

По ORM ...
З того, що я бачив, найбільше мені сподобався Room. Але хз чи вийде його запустити за межами Андроїда. На звичайній ж Java, 90% проектів ми все ще тягнем на самописній тонкій обгортці з мапером над jdbc з HikariCP (це connection pool). Самописний велик для ORM — то, звісно, причина зараз закидати гнлими помідорами. Але той велик на десь 1500 стрічок їздить вже років 8 і простий як тріска. За цей весь час в нього додавали, хіба, нові типи аля LocalDate, JSONObject, тощо. Пробували Hibernate, пробували JOOQ — занадто тяжко (в сенсі overhead по структурі коду). Була ідея навіть зарелізити його в Open Source, ... але щоб це мало цінність, його варто суттєво доробити.

По Http...
Коли треба опублікувати http api, то використовуєм SparkJava. Коли це API має бути «асинхронним» (всякі long pool-и, тощо), пишем на Netty (трохи складно) або KTor (потрібен Kotlin). Взагалі, для «backend» задач часто зручно працювати з корутинами, kotlin допомагає. Але решту коду тримаєм на Java. Тут, можливо, Project Loom зробить суттєвий вплив на все, але, поки, то в перспективі.

Якщо ж на Java треба/хочеться зробити веб-сайт ... я б радив не шукати фреймфорк на Java і все-таки обрати іншу мову програмування ... ну або на java зробити лише json api.

Самописний велик для ORM — то, звісно, причина зараз закидати гнлими помідорами.

Насправді ні.

JPA/Hibernate — не найкраще рішення, а найпридатніше з точки зору балансу різних факторів. Окрім того, вони накладають немалий такий шар складності на проект, бо як мінімум треба розуміти серіалізацію, транзакції, персистенті стани, а це стабільна проблема більшості розробників.

Якщо ви написали щось саморобне яке оптимально працює саме для вас, і воно читабельне — це є православно і всьо добре.

JPA/Hibernate — не найкраще рішення

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

Без контексту (по дефолту) Hibernate, вважаю, можна порівнювати тільки з іншими JPA реалізаціями — EclipseLink, OpenJPA тощо. А критерій якийсь все одно потрібен — що ми міряємо — розмір джарника, якість документації, набір фіч поза стандартом чи що?

Для всіх інших випадків — а давайте порівняємо ORM — потрібен контекст. Інакше це одразу дурня і флейм, бо контекст у кожного в голові свій, і він хвалить своє болото. От що краще: велосипед чи автомобіль?

Так було з Spring Data JDBC наприклад.

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

По ORM ...

самописній тонкій обгортці з мапером над jdbc

Роблю припущення, що від request-а до БД вам треба зробити злічену множину перетворень?
І головна фіча Хібернейта (зараз скопіюю з інших повідомлень цього топіку)

Після цього апдейт в сервісі виглядає так
@Transactional
class Service {
    void update(SomeChange change) {
         Model model = repository.findById(id).orElseThrow();
         model.updateBy(change);
    }
}

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

вам не потрібна?

Пробували Hibernate, пробували JOOQ — занадто тяжко

І навіть MyBatis вам забагато, бо ви точно знаєте, що робити з реквестом і робите це з мінімальним оверхедом?

По Http...

Коли треба опублікувати http api, то використовуєм SparkJava

Spring(Boot) дозволяє всякі технічні речі як то обробка http реквестів, побудову json http response-ів, пул до бази (той самий HikariCP), і купу іншого, а також збирати все воєдино за допомогою DI — робити декларативно, без написання або з мінімумом коду. Звісно, плата за це — певний оверхед (по розміру fat-jar та часу його запуску), який для багатьох розробників несуттєвий, що і зумовлює його популярність.
Звісно, для кожної ланки можна найти відповідний фреймворк, який буде менший і швидший, але вимагатиме більшої «уваги» і часу(грошей) на розробку. Питання балансу.

Стосовно DI, написав в сусідньому коментарі: використовуєм dagger.

І головна фіча Хібернейта

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

Якщо робиться update до бази, то в коді недвузначним і очевидним чином має бути видно що саме оновлюється і як (які таблиці і які поля). А у вашому випадку ще й, як мінімум, select зайвий робиться.

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

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

Якщо робиться update до бази, то в коді недвузначним і очевидним чином має бути видно

Чому ви так категорично це стверджуєте? )
Переважна більшість моїх проектів — це кривавий enterprise CRUD. Основний challenge — це написання консистентної бізнес-логіки, якої натурально кілометри, і вона постійно змінюється. Для такого типу задач Hibernate — чудовий інструмент, який дозволяє зосередитись саме на логіці, тестувати тільки її та не перейматись написанням DML команд. Однієї транзації зазвичай достатьо, локів майже нема, бо практично всі операції per user. За швидкодією пильнуємо, але зайвий селект погоди не зробить.

Крім того, чому ви вважаєте, не бачачи імплементації метода, що в моєму випадку селект зайвий?

Апдейт моделі model.updateBy(change) це не директивний апдейт в базу. Там може бути яка завгодна логіка, для якої потрібні значення полів, а може і вкладених класів, то селектів буде багато. І без хібернейта логіки буде не километр, а десять, плюс баги та дебаг. А також рано чи пізно з’явиться аудіт. Це неминуче.

Я не вважаю, що хібернейт — це інструмент на всі випадки життя, також працював у проекті, де через великі обсяги даних вся бізнес-логіка була в stored procedures (SP) з усіма мінусами, але й плюсами такого підходу. Java там займалась різними інтеграціями з third-party сервісами, а взаємодія з БД зводилась до здоровених batch-insert-ів у спец таблицю та виклик SP. Все через jdbc, звісно. Не те що хібернейта, селекта в базу не було.

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

що в моєму випадку селект зайвий?

На практиці майже завжди вияляється, що «зайві» селекти які робить хіб — насправді не зайві для підтримки консистентості моделі.

В нас, напевно, дійсно, таки різний досвід.

Самописний фреймворк ... в моєму випадку, це, вважайте, що пряма робота з JDBC, але без boilerplate-а і з мапером. Ті частини, які покриті тестами, тестуються з mock-базою.
Але, оскільки, для моделей використовуються прості data-класи (без parent-a, public поля, тощо), то код, який залежить від них, але не пише/читає базу, можна природнім чином відокремлювати і запускати/тестувати без бази. Не завжди так виходить ... crud так не виходить, але все ж.

model.updateBy(change)

Я вірно розумію, що change — у вас є чимось схожеми на паттен Command, тільки без undo?
Якщо так, тоді стає зрозуміліше, чому вам підійшов hibernate :)

З мого досвіду, пригадую проект, який, для спрощення, скажем, продавав квитки на вистави (там було не лише це, але все ж).
База MySql — не те, щоб багато могла пробачити. І коли кількість транзакцій доходила до однієї в декілька секунд (транзакція — це багатоетапний процес — багато запитів на різні шлюзи/бази/тощо), починалися проблеми — база просто не витягувала.

MySql, схоже лочить записи в таблиці не по рядкам, а по блокам рядків, і тому легко попасти на deadlock (редагуючі різні записи) і потрібно робити retry.

Вирішилось все тим, що в транзакціях залишали лише те, що необхідно має бути атомарним (зазвичай 1-2-3 update/insert-а), частину запитів «закешували», майже всі select-и поза транзакціями, а деякі навіть з хаками аля FORCE INDEX (не те, що це добре, але все ж). Консистентність деяких речей почала базуватися на логіці виконання операції, а не на транзакціях. Наприклад, треба зробити A, B та C. Стараємось зробити A та B ідемпотентними, а C в транзакції — відповідно, якщо C злетить і буде retry, то повтор A та B не призведе до проблеми.

Я намагаюсь уявити подібну роботу на hibernate і розумію, що посивіти можна раніше, ніж розвантажити базу.

Які у вас типові задачі, що hibernate так гарно заходить? Які навантаження?

Їбать трешачина.
Це у вас якісь рукожопи базу утримували. А може базі виділили якісь нереально малі ресурси. Ну це ми вже навіть не торкаємось того питання що мускуль взагалі хєровенька бд.

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

Власне на цьому можна зупинитись, і йти їб@ть адмінів/девопсів, бо 1 транзакція на 2-3 секунди і бд не витягує це апріорі якась феєрична дікуха.

Одна операція на декілька секунд ... під час її обробки відбувалося багато чого і сама ця операція могла секунд 10 щось робити (api, sql, тощо).
Суть не в цьому.

Станом на сьогодні, бази працюють швидше (nvme замість hdd, пам’яті більше), але тоді, коли відбувалась описана історія, ssd лише появлялись в нас на ринку, а про nvme ще ніхто не чув. HDD та 16G RAM на базу і додаток було в сумі. База росла по трохи за рахунок «історії», але як добиралась до десь 5 гіг, старі дані видаляли і ставало легше.

і йти їб@ть адмінів/девопсів

Вони допоможуть, але не на довго. Софт, який пише в базу «графами об’єктів», завжди буде хотіти більше.
Вічно ресурси докидати не вийде (але, часто, вічно і не треба, правда).
Та й не допоможе воно, скоріш всього.

мускуль взагалі хєровенька бд

Але, схоже, друга за популярністю в світі (www.statista.com/...​abase-management-systems). Хоча в моїй «бульбашці» — перша, а за нею mongodb.

Можливо, у ваших задачах, це все сенсу немає і у вас база до 1Gb, де саме оці всі «графові записи» тільки і треба робити і вся складність там. В мене такої задачі на Java не було. Update, зазвичай, одна таблиця (+, можливо, якісь «логи»).

Ви про які транзації ведете мову — на рівні СУБД чи на рівні апплікейшн?
Скільки транзакцій в секунду (і з яким рівнем ізоляції) було на рівні бази, які були розміри таблиці/індексів? Бо кілька транзакцій в сек на рівні бази — це дійсно як якийсь треш виглядає.

Я погано висловився ... на рівні апп. На рівні бази було більше на порядок. Нажаль точного профілю навантаження не згадаю.

Булшіт.
Навіть на старезному залізі, бази данних з народження їх як технології, призначені обробляти суттєво більше ніж вмирати від 1 транзакції раз на 2-3 секунди. І няка бізнес-логіка і якісь там «графи об’єктів» природу бд не зламає.

З ваших пояснень я починаю схилятись до думки, шо ви там шось капітально наплужили, видимуючи якийсь велосипед в стилі «у нас буде своя технологія з блекджеком і шлюхами», вона адово глючила, ви не розібрались в причинах, і вирішили що «ой ой все складно, база не могла обробляти більше 1 транзакції раз на 2-3 секунди».

Наприклад, ви могли написати вручну логіку, яка порядково буде лочить всю таблицю, обмазану форейнкеями, лочить пов’язані рядки і так далі, прийшовшив результаті до фізичного локу всієї бд в 1 транзакції. Тада канешно даааа. Але тут не база винна, а ви.

Мораль такова. Якщо у вас бд здеградувала до того що не могла обробляти більше 1 транзакції раз на кілька секунд — винні ви (не персонально ви, а команда) а не база. Це просто факап, от і всьо.

і у вас база до 1Gb

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

1 транзакції раз на 2-3 секунди

Я вже шкодую, що щось подібне написав :) Ще раз... запитів до бази там було більше. І були ще фонові процеси, тощо. Я не пам’ятаю вже точно профіль навантаження.

Суть в тому, що від стану «все лежить» до «все мурчить» достатньо було акуратно прописати запити без зайвих артефактів (як, наприклад, select в транзакції)

(Жодного foreign key, тригера та подібних речей там не було)

Та й, взагалі ... хоч то й зовсім інша історія, але неакуратний select запросто може вирости від 0.1сек до 10сек, коли кількість даних по трохи росте. Навіть на сучасному залізі. І коли під навантаженням таке відбувається, все лавиноподібно дохне. Десь це вирішується додавання індексу, десь force index-ом, десь кешуванням, тощо. І повна абстракція від бази тут не допомагає.

неакуратний select запросто може вирости від 0.1сек до 10сек

Я то не заперечую.
Я кажу про те, що такий випадок — пряма помилка проектування. І тут не треба видумувать якісь свої фреймворки, щоб танцювать навколо цього запиту який виконується 2-3 тижніхвилини, а докорінно перероблювать схему/логіку роботи, яка до такого призвела. А там глянь і хібернейт після переробки підійде.

Суть в тому, що від стану «все лежить» до «все мурчить» достатньо було акуратно прописати запити без зайвих артефактів (як, наприклад, select в транзакції)

Незрозуміло, як це?
Той селект що, зовсім не потрібен?
Чи він став перед транзакцією і тепер у вас дві транзакції ?

неакуратний select запросто може вирости від 0.1сек до 10сек

Може звісно. Але чому

все лавиноподібно дохне

від цього?

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

Чи він став перед транзакцією і тепер у вас дві транзакції ?

Якщо закешований — то береться з кешу. Але в цілому, так ... «дві транзакції» ... setAutoCommit(true). select+update+select+update в одній транзакції робить більше локів бази ніж все окремо.

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

По різному.
Якщо транзакція (в термінах БД) відкрита, поки робиться ... хай http виклик — це один випадок.
Якщо транзакція з купою SQL запитів, що повільні — інший.
В першому випадку, як ви написали, в пулі не стане з’єднаннь ... якщо сама база витримає, то додаток точно помре.

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

select+update+select+update в одній транзакції робить більше локів бази ніж все окремо.

технічно локів стільки ж, але одномоментно їх більше і тривають довше, тому звичайно верогідність дедлоків вища.

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

А як з бізнес-атомарністю? Консистентністю даних?

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

Звичайно що там де треба, то треба, з цим і не сперичаюсь.

Але і розуміти що там в базах відбувається, всі ці локи, тримати в голові кейси з конкурентним доступом, дедлоками, навіщо for update взагалі і недостатньо просто написати start transaction / commit.
До певного часу можна не звертати увагу звичайно, але коли хоча б тисячі tps на базу, то вже всяке «стріляти» може.

Це таки може бути і на рівні аппки — ми колись при high concurency (насправді ні, просто QA були дуже хитрож... і сценаріїв напридумували) розрулювали mysql дедлоки, відключивши їх автодетект.
Плюс продажа чогось переважно виливається в лок/апдейт якогось ресурса чи каунтера в базі, там собака могла і поритись (особливо коли це явно не видно і треба хібернейт квері подебажити).

Це таки може бути і на рівні аппки

Так, звісно, я про то написав вже. Можна ізі написать селект, який витягне і залочить всю базу. Тоді канешна вафлі.

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

А часто люди не розуміють нащо взагалі існують локи )
З дедлоками через блокування безпосередньо в базі особисто не стикався, слава богу.
Але це добре іллюструє ідею, що SELECT FOR UPDATE придатний тільки для дууже простих сценаріїв, і для криваво-ентерпрайзного блокування краще використовувати окремий шар локів, а не лочити базу.

Я зараз на матюги та особистості, відчуваю, перейду — вибачте заздалегідь, будь ласка, не сприймайте особисто по можливості — це технічна дискусія на ДОУ.

Самописний фреймворк ... в моєму випадку, це, вважайте, що пряма робота з JDBC, але без boilerplate-а і з мапером.

І чим ваш велосипед фреймворк краще MyBatis або Spring JDBC (не плутати з Spring Data JDBC, це якась диверсія по дискредитації JPA)?

MySql, схоже лочить записи в таблиці не по рядкам, а по блокам рядків, і тому легко попасти на deadlock (редагуючі різні записи)

Давайте розберем. Я з MySql серйозно не працював, але ось перше посилання на S/O. Мені звісно пощастило, майже всі операції per user, тому локи не потрібні особливо, але що в вас за задачі такі, що треба select for update by criteria, а не по primary key?!

Може все ж таки у консерваторії щось поправити або базу замінити, якщо вам без цієї можливості ніяк?!

Натомість ви робите

майже всі select-и поза транзакціями

хоча все ж таки розумієте, що атомарність похерена і

Консистентність деяких речей почала базуватися на логіці виконання операції, а не на транзакціях.

Звісно з таким підходом все ляже і

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

транзакції треба ногами retry-ями в БД запихувати.

А винуватий в тому хібернейт, який ви з апломбом називаєте

енциклопедичним прикладом переускладненого overengineering-у

Для довідки: це автоматичний «перекладач» з мови ООП на реляційну БД і назад.
ООП у вас немає

для моделей використовуються прості data-класи (без parent-a, public поля, тощо

тому, звісно, він вам не потрібен. Крім того, це дослівний перекладач, тому дійсно треба розуміти, як він працює, щоб не отримати «нема сечі терпіти ці борошна» ©

Які у вас типові задачі, що hibernate так гарно заходить?

Я вже казав, написання бізнес-логіки, проілюструю.

model.updateAnswer(42);
Я вірно розумію, що change — у вас є чимось схожеми на паттен Command, тільки без undo?

Ні. Вам чомусь здається, що це update model set answer = 42 where id = ?, тільки з «зайвим» селектом, від яких ви настраждались бо чогось дописали FOR UPDATE.

А там через декілька ітерації може назбиратись така задача: оновлювати відповідь тільки якщо вона зростає, на 42 — ставити прапорець, попередні значення зберігати.

@Entity
class Model {
    @Id
    private Long id;

    private Integer answer;

    @ElementCollection
    private List<Integer> prevAnswers = new ArrayList<>();

    private boolean ultimateAnswerFound;

    void updateAnswer(int answer) {
        if ((this.answer != null && answer > this.answer) || this.answer == null) {
            if (this.answer != null) {
               prevAnswers.add(this.answer);
            }
            this.answer = answer;
         
        }
        if (answer == 42) {
            this.ultimateAnswerFound = true;
        }
    }
}

Це бізнес-логіка, яку треба протестувати. Я її не дуже гарно написав, а може і взагалі десь помилився. На кожний кейс треба, звісно, мати окремий тест, але я спішу і тому швидко зліплю все в один

class ModelTest {
    @Test
    void testUpdateAnswerAllCases() {
        Model model = new Model(...);

        model.updateAnswer(1);
        model.updateAnswer(20);
        model.updateAnswer(10);
        model.updateAnswer(42);

        assertThat(model.getAnswer()).isEqualTo(42);
        assertThat(model.getPrevAnswers()).isEqualTo(List.of(1, 20));  // 10 було менше 20, тому випало
        assertThat(model.isUltimateAnswerFound()).isEqualTo(true);
    }
}

Власне — все. Селект (як бачите, він зовсім не зайвий), апдейт, а також інсерт в MODEL_PREV_ANSWERS за мене зробить хібернейт.

Абсолютно ті самі три DML команди ви напишите через JDBC і ця логіка у вас буде в сервісі. А от тести на

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

писати буде ліньки, тому що вони за об’ємом будуть більші за код в три рази.

Які навантаження?

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

За образи та сарказм — вибачте, якщо поясните свою специфіку та обрані рішення — з цікавістю ознайомлюсь.

І чим ваш велосипед краще

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

Порівнювати тут не дуже коректно ... порівнювати можна було б, якщо б те все дописати по людськи ... але поки цього немає :(

select for update by criteria

Ви вірно копаєте, але проблеми бувають не лише в таких запитах. Нажаль.

хоча все ж таки розумієте, що атомарність похерена і

Тут як сказати ... якщо це
INSERT INTO Accounting(debit, credt,amount) ... UPDATE Account SET balance=balance+:amount
то, звісно, це робиться в одній БД транзакції — без варіантів.
Якщо ж при цьому, перед цим робиться 10 запитів до «словникових» таблиць ... то не проблема і зробити їх поза транзакцією.
(Доречі, а у вашому підході, такі UPDATE-и взагалі моживі (аля a=a+1)?)

Інколи, занадто велика «строгість» вимог до БД виливається в занадто багато проблем з підтримкою такої БД.

це автоматичний «перекладач» з мови ООП на реляційну БД

Напевно, десь тут наші погляди і розходяться...
Ви хочете мати гарне ООП і гарну базу. І щоб ORM все зробила.
Я ж кажу, що в загальному, такий ORM занадто важкий та неповороткий. Ще й додає складності, якої і без нього вистачає. І він не може використати всі можливісті, які доступні в цій БД. А інколи, такі можливості дуже потрібні стають.

Тому, коли я думаю про ORM, то бачу перед собою інструмент, що дозволяє працювати з базою легко та типізовано (data/dao-класи), але при цьому робота ведеться саме з базою даних, мовою бази даних і сутностями, які в ній є. А не з віддаленою ООП моделю, що побудована на інших принципах.

Це бізнес-логіка, яку треба протестувати.

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

try (var conn = cpds.getConnection()){
   Model model = SqlQuery.create(conn, "SELECT * FROM Model WHERE id=:id")
     .arg("id",5).query(Model.class);

   model.updateAnswer(42);

   new SqlSimpleUpdate(conn, "Model", "id=:id")
    .arg("id", 5)
    .setFromObject(model, "answer", "ultimateAnswerFound")
    .execute();
}
Це не ідеальний код, але ... зверніть увагу
— замість * я можу вибрати лише ті поля, що портібні.
— Update тут контрольований, в тому сенсі, що явно вказано які поля можуть оновитись, а які точно ні (мінусом є те, що вони завжди попадають в UPDATE запит, але як на мене — не велика біда, якщо там не 20 полів)
— Model — можна так само покрити тестами як і у вас вище (хоча тут це не одне і те ж саме).

Звісно, якщо у вас не одна модель, а граф — то все стає навантаженіше (той update буде в транзакції (але не обов’язково), більше моделей, тощо).

Для таких задач, ваш підхід буде мати більше магії, а мій більше спілкування з базою.
— У вас в фокусі бізнес-логіка, ООП та тести, а база якось робереться. В мене ж запити та транзакції є частиною логіки роботи.
— У вас тестується модель без бази, в моєму випадку, більша частина вимагає щоб БД була.
— Коли у вас проблема з базою, ви ... хз .. колупаєте анотації і йдете до DevOps? Я ж шукаю проблеми, читаю explain-и, переписую запити, доставляю індекси, будую кеші, тощо.
— У вас «save» — і ви не знаєте, що там відбувається (поки весь код до того не прочитаєте). В мене ж відбуваєтся лише те, що явно написано.

Поправте мене, якщо я не правий, але мені здається, що аналіз EXPLAIN-ів у вашому випадку — це щось дуже не природне. Хоча, скоріш всього для різного роду «report»-ів ви, як і всі, пишете великі SQL з купою вкладених JOIN-ів, GROUP-ів та UNION-ів ... там від EXPLAIN-а вас ORM не врятує точно:)

Ви хочете мати гарне ООП і гарну базу. І щоб ORM все зробила.

Так він вже є, хіб називається.

Я ж кажу, що в загальному, такий ORM занадто важкий та неповороткий.

Він не важкий, просто треба його знати.

І він не може використати всі можливісті, які доступні в цій БД.

А і не треба.

А інколи, такі можливості дуже потрібні стають.

Якщо вони вам стають потрібні, це не значить що «орм не справилась», це з вірогідністю 99% значить шо ви накрутили якусь дікуху, вона погано працює, і для цього вам треба низькорівневе бд-шаманство.
Дуже типова ситуація, особливо часто зустрічається у сто або архітекторів, що виросли з дба.

В мене мало якоїсь особистої статистики, але, схоже, що з дба ростуть монструозні пакети на хранимих процедурах :) Ну, або, можливо, намагання зав’язати всі питання консистентності бази на рівень БД.

З того, що я бачив, що одне, що інше, «так собі» працює.

Якщо я вірно зрозумів вас, то у вас вся логіка схована за абстракціями, що дає Hibernate.

Що є інша крайність. Вам тепер треба думати не лише, про артефакти, які створює база, а й про артефакти які додає Hibernate.

Так і живем :)

з дба ростуть монструозні пакети на хранимих процедурах :)

...а потім цей дба відкриває для себе джаву, і...

намагання зав’язати всі питання консистентності бази на рівень БД.

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

особливо часто зустрічається у сто або архітекторів, що виросли з дба.

Ні, Ігор точно не дба, вони select * from ...  фізично не можуть написати.

Я нарешті, здається, зрозумів, що він прямо пише в кожному повідомленні. Він каже:

Як відбувається взаємодія з БД? select, потім логіка, далі update(insert, delete)? Іншого нема? Нема. Ну так і не треба ускладнювати. KISS, коротше.

Оцей ваш Hibernate — від лукавого, чорна магія, ентіті якісь, додаткова складність, коли треба просто рядки з БД завантажити в data класи/record-и.
Запити треба писати руцями, контрольовано, через якийсь простий jdbc-маппер. Що той JPA ще нагенерить — апріорі буде гірше. Переускладнений оверінжинірінг на рівному місці.

ООП? Ну, це хороша концепція, але яке воно має відношення до БД?
На всяк випадок, для інших читачів ось мої спроби пояснити (один, два, три)

Повноцінна ORM має більше фіч, але при цьому має свою ціну, яка, в моїх випадках створювала більше проблем, ніж приносила користі.

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

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

Не проблема. І ще їх закешувати можна. І хібірнейт теж вміє закешувати.
Але ж «все замурчало» не від того, що ви запити до словників за межі головної транзакції винесли?

До речі, чого ви за локи тримаєтесь, якщо атомарності все одно нема?

Ви вірно копаєте, але проблеми бувають не лише в таких запитах. Нажаль.

Ну, ви не пояснюєте, а я не телепат.

(Доречі, а у вашому підході, такі UPDATE-и взагалі моживі (аля a=a+1)?)

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

UPDATE Account SET balance=balance+:amount

Плюс очевидний: це операція на стороні БД, не треба ганяти дані на джаву і назад, особливо відчутно на bulk-операціях.
Мінуси: хай це одна операція, але це бізнес-логіка в SQL (не важливо, query/trigger/stored procedure). Це не заборонено, але означає, что логіка тепер в двох місцях і може розсинхронізуватись.
Другий мінус — це аудит. Це отой наче «зайвий» селект з якого починає хібернейт. Рано чи піздно, але неминуче, бізнес захоче мати таблицю в базі з записам, хто, де, що і на що поміняв. Тоді легко прикручується глобальний хук, який дозволяє трекати ченджи абсолютно по всім ентіті, бо завдяки тому самому «зайвому» селекту є початковий стан. І починаются болісні роздуми, що робити з балк-операціями: чи то переробляти аудит на триггерах, а це шалений monkey work, чи то переробляти балк-апдейт на цикл одиночних апдейтів. На щастя, в моїх проектах балк-операції не типові, хоча я, звісно, розумію що життя різноманітне.

Інколи, занадто велика «строгість» вимог до БД виливається в занадто багато проблем з підтримкою такої БД.

Чи ви про що? Яка строгість, які проблеми? Нічого не зрозумів.

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

Такі пред’яви треба обосновувати ©

Що значить важкий? По використанню CPU/RAM/disk space?
Що значить неповороткий? В якому виді спорту?

І він не може використати всі можливісті, які доступні в цій БД. А інколи, такі можливості дуже потрібні стають.

Чому не може? Хібернейт і ООП підтримує, і RowMapper надає, і ще купу інших речей, а хочете — database specific native sql пишіть.

Тому, коли я думаю про ORM, то бачу перед собою інструмент, що дозволяє працювати з базою легко та типізовано (data/dao-класи)... А не з віддаленою ООП моделю, що побудована на інших принципах.

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

Але це все бла-бла-бла, давайте краще ваш велосипед розберем, хоча це побиття немовляти, нецікаво.

— замість * я можу вибрати лише ті поля, що портібні.

За замовчуванням треба всі поля вибирати, бо невідомо ж, що знадобиться. Але якщо раптом є зміст обмежити, що в хібернейті є Projections. І треба всі поля через кому перераховувати, а не «*» ставити — є нюанс.

Model — можна так само покрити тестами як і у вас вище

Серйозно? У мене в моделі були @ElementCollection prevAnswers. Це окрема таблиця (model 1 - * prevAnswers). Я припускаю, що ви цю аннотацію не знаєте, тому окремо написав

попередні значення зберігати (очевидна неявна вимога — якщо вони змінюються)
а також інсерт в MODEL_PREV_ANSWERS

У вас модель, очевидно, репрезентує тільки поля таблиці.

Ну, ок, дані ми якось оновили, пора залити їх в базу.

new SqlSimpleUpdate(conn, "Model", "id=:id")
    .arg("id", 5)
    .setFromObject(model, "answer", "ultimateAnswerFound")
Update тут контрольований, в тому сенсі, що явно вказано які поля можуть оновитись, а які точно ні (мінусом є те, що вони завжди попадають в UPDATE запит, але як на мене — не велика біда, якщо там не 20 полів)

Це, мабуть, кіллер-фіча вашого велосипеда. Тобто ООП/інкапсуляція — це не контрольовано, а зазирнути в model.updateAnswer() і подивитись, що там може мінятись, а тотім вказати назви-стрінги(???) цих полів — це контрольовано?

Ви робите апдейт буде в любому випадку, навіть якщо нічого не змінилось, а зайвий апдейт — це не «зайвий» селект, сайд-ефекти можуть бути.

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

У вас тестується модель без бази, в моєму випадку, більша частина вимагає щоб БД була.

Так це ваш серйозний мінус. І де тести, до речі? Як вони виглядати будуть? Бо я свої вже написав.

Коли у вас проблема з базою, ви ... хз .. колупаєте анотації і йдете до DevOps? Я ж шукаю проблеми, читаю explain-и, переписую запити, доставляю індекси, будую кеші, тощо.

Чому ви думаєте, що я не можу робити це саме?

У вас «save» — і ви не знаєте, що там відбувається (поки весь код до того не прочитаєте).

Який «save»? Який код? Метод поміняв модель, хібернейт зберіг її в базу. Що мені треба знати і читати?

аналіз EXPLAIN-ів у вашому випадку — це щось дуже не природне.

Це дійсно так. Я працюю з об’єктами, в них є id/primary key, хібернейт генерить select/update/delete from ... where id = :id, що там аналізувати?

Хоча, скоріш всього для різного роду «report»-ів ви, як і всі, пишете великі SQL з купою вкладених JOIN-ів, GROUP-ів та UNION-ів ... там від EXPLAIN-а вас ORM не врятує точно:)

Так — пишу, дійсно — не врятує, і не має рятувати.

Але ж ви не кажете «переважно пишемо репорти, тому, нажаль, хібернейтом не користуємось».

Для таких задач, ваш підхід буде мати більше магії, а мій більше спілкування з базою.

Я не можу вгадати. З одну боку, ви схожі на бувшого базиста, бо тяжієте до ручного написання запитів і

Базу даних потрібно «поважати» ... БД — не проста штука

з іншого — хоч і аналізуєте explain-и, ви точно не DBA/DBD.

Гадаю, дискусію про хібернейт можна завершувати. Я десь в іншому топіку побачив, що ви терабайтні бази на партішени по сотні Гб нарізаєте, і ви так першим коментарем зайшли, що я думав послухати, як Амазон в чорну п’ятницю виживає, а хібернейт пройдений етап.

Я не маю за мету переконати вас їм користуватись, і ви теж, не схоже, щось технічно обґрунтоване напишете. А сперечатись в стилі «товстий і неповороткий — ні, стрункий і спритний» змісту нема.

Spring це перш за все зручний DI. Яким DI користуєтесь ви? Які плюси в порівнянні з Spring?
Як правильно зазначили вище, це лише тулза під задачу, дуже гарна тулза на мою думку.

Для DI використовую Dagger. Величезним плюсом є те, що все працює без reflection-а.

Ну це цікава опція, коли важливий startup time. Але для цього кейсу є Spring Native.
Але якщо startup time не важливий, які ще плюси є в Dagger?

Важливо не лише startup time. Dagger працює як генератор коду, який, хоч і не витвір мистецтва, але можеа читати, коли треба.

IDE його підтягує і працює навігація по коду.

Купа помилок перейдуть з run time в compile time.

Стектрейси, які проходять через DI є відносно нормальними.

Всі зв’язування відбуваються в compile time, що не завжди лише startup.

Ну це можна вважати мінусом, compile time відбувається довше.
Так, теоретично сгенерований код легше дебажити, але це може бути і мінус якщо проект складний.

Воно працює так, але трохи не так. Код генерується, але не так як з SOAP колись :)
Генерація коду відбувається на етапі компіляції, цей процес інтегрується в maven/gradle.
Тобто немає жодного натяку чи шансу його потім правити руками.

Тобто, вам ніколи, толком, туди лізти не треба.

Але при цьому, весь DI чесний та прозорий, без магії аля доступу до private полів, генерування проксі класів, які існують лише в runtime, тощо. Коли вивалюється stacktrace, там все чітко і зрозуміло. Ви можете дебагером пройти крізь якийсь Provider, якщо треба, і в IDE не «зламається мозок»: ви очевидним чином дебагером зайдете в згенерований код, побачите як, в якому порядку і звідки відбувся inject і пройдете до провайдера з проблемою ... як кажуть, «без розривів».

Мені складно знайти випадок, коли це є мінусом. Розмір проекту значення не має.

Якщо ви шукаєте мінуси, знайти їх можна. Є випадки, коли guava (думаю, що spring теж, але важко сказати), можуть за-inject-ити так, як dagger не зможе. Reflection, таки, сила + ці бібліотеки, часом «переписують» код класів в runtime. Dagger цього не робить, але тут вже питання добре це чи погано розраховувати на такі фічі.

генерування проксі класів

...в спрінгу воно зроблене не для DI, a для AOP та підтримки транзакційної поведінки. Що і зробило спрінг основоутворюючим фреймфорком а не просто just another one бібліотекою. Для DI достатньо рефлекшна, що пробігається по коду і збирає граф залежностей.

без магії аля доступу до private полів

@Autowired на поля то є велике зло і важкий спадок. В здравому умі ніхто не використовує field-based DI, все через конструктори, тому зв«язування в цілому теж доволі прозоре.

Код генерується

А от кодогенерація — то набагато чорніша магія, ніж доступ рефлекшном до private поля. I головний її недолік в тому, що без стороннього етапу обробки ваш код не є корректним взагалі.

ви очевидним чином дебагером зайдете в згенерований код, побачите як, в якому порядку і звідки відбувся inject

А нащо вам взагалі це знати?
Концепція DI container в тому то і полягає, що ви декларуєте необхідність залежності компонента А на компонент Б, і перестаєте про це думати.
Жодного разу мені не потрібно було дізнаватись «звідки відбувся інжект».

Коли вивалюється stacktrace, там все чітко і зрозуміло.

А в спрінгу шо не так? Так само він вам пише, що такий-то компонент вимагається, але відсутній в контексті, або потрібен один, а є більше 1 і немає кваліфікатора.

I головний її недолік в тому, що без стороннього етапу обробки ваш код не є корректним взагалі.

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

спрінгу воно зроблене не для DI, a для AOP

Про AOP згоден. Тут він унікальний. В мене лиш є невпевненість в тому, чи така реалізація є «добром» чи «злом». Але це тема для окремої дискусії.

Так само він вам пише, що такий-то компонент вимагається, але відсутній в контексті

Dagger це робить в compile time. Я мав на увазі помилки, коли в трейсі є код, що робить inject.

Жодного разу мені не потрібно було дізнаватись «звідки відбувся інжект».

Не те, щоб це часто треба, але один раз допомогло знайти помилку:)

В цілому, декларативність має плюси та мінуси. Плюс — що не треба про це думати. Мінус — якщо все таки треба, то заглянути під капот є проблемою. Тому й в sql придумали explain. Dagger так само працює декларативно, але при цьому, його логіка в людинозрозумілому і компільованому тексті доступна в один клік.

Dagger працює як генератор коду

FFFFFFFUUUUUUUUUUUUUUUU

Купа помилок перейдуть з run time в compile time.

Можна приклад помилки, будь ласка?

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

Але ось, наприклад, питання до вас і аудиторії.
Написали ми для внутрішної задачи бібліотеку-конвертер одного xml-я в інший, як це заведено в ентерпрайзі. Там для зовнішного використання є тільки один клас new MegaConverter().convert(xml), а логіка розкидана по сотні менших класів, що владені один в оден.

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

    class A {
    }

    @RequiredArgsConstructor
    class B {
        private final A a;
    }

    @RequiredArgsConstructor
    class MegaConverter {
        private final B b;
    }

Є якийсь легковажний DI фрейморк, щоб це підняти одним рядком

     val megaConverter = context.getBean(MegaConverter.class);
?

Дивився на Guice, Pico та інші. Всі хотять якихось додаткових тілорухів по реєстрації A і В спочатку. Ближче всіх підійшов Feather. Але він теж хоче, щоб @Inject на конструкторах був розставлений, а мені ліньки в ста класах ломбоківські конструктори на звичайні розвертати, а автоматом причипити @Inject ломбок теж, здається, не вміє.

Це такий забавний приклад, що самий «автоматичний» DI — раптом в спрингу, хоча я буду радий почути альтернативу.

Можна приклад помилки, будь ласка?

[Dagger/MissingBinding] Test.A cannot be provided without an @Inject constructor or an @Provides-annotated method.
Такого типу, наприклад. В цілому, всі, що пов’язані з DI ... щось не знайдено, десь циклічна залежність, тощо.

Це такий забавний приклад

Якщо я вірно зрозумів, то Dagger в одну стрічку не зможе ... треба буде або Inject-и розставляти, або Module робити для цієї бібліотеки.

Доречі, в Lombok, наче ж можна
projectlombok.org/...​nstructor#onConstructor(

порівнювати DI фреймворки — це вкусовщина, якщо немає якихось сильно специфічних вимог

В цілому, згоден з вами. В моєму випадку вибір на Dagger впав із-за наступних причин:
1. Він достатній для задач, що на нього покладаються.
2. Результат його роботи практично не відрізняється від вручну написаного коду (по performance-у).
3. Кругом працює строга типізація ... немає чогось типу context.get(MyClass.class) ... з dagger-ом аналог буде context.getMyClass() ... відповідно, context. покаже, що там є, а чого немає. (Хоча, насправді, ми стараємось уникати випадків, коли такі контексти треба прокидати всередину класів)

Ну, я на відміну від інших коментаторів, навпаки, кодогенерацію можу зарахувати в плюс, але runtime DI в спрингу — не в топі наших проблем ). Забув девелопер @Bean/@Service/@Component тощо на класі повісити — побачить сам при першому ж запуску, далі нього це не піде.

Доречі, в Lombok, наче ж можна
projectlombok.org/...​nstructor#onConstructor(

Так, точно. Не зробив, бо в lombok.config-у не знайшов. Хоча можна було автозаміною пройтись )

порівнювати DI фреймворки — це вкусовщина, якщо немає якихось сильно специфічних вимог
В цілому, згоден з вами

Тоді це обговорення закриваємо.

2. Результат його роботи практично не відрізняється від вручну написаного коду (по performance-у)

А про який перфоманс йде мова? Спрінговий контекст на моїх середніх проектах підіймається за секунду чи дві, точно не міряв, бо це неважливо. У вас класів мільйон? Чи мікротрейдинг на мілісекундах?

побачить сам при першому ж запуску

Там точно все при запуску вилізе? Циклічна залежність в якомусь per-request контексті?

А про який перфоманс йде мова?

DI буває різний.

Частина — це запуск — як раз ті дві секунди про які ви говорите.
Тут ... хз .. в Андроїді це важливо, в демоні на сервері не дуже.

Частина — це всякі, наприклад, «per-request» контексти. В Dagger-і всі зв’язування відбуваються в коді і компілюються компілятором. У вас ж тут працює reflection.

Частина — це всякі аналоги context.get(ApiClient.class). Хоч я і вважаю, що це антипаттерн, але таке буває потрібно. Тут у вас всередині щось аля HashMap з reflection, в Dagger — просто виклик методу.

Звісно, оце все може виглядати як економія на сірниках. Але цих сірників багато. Якщо чесно, нам не доводилось зіштовхуватись з вузькими місцями саме в цих місцях. Але коли ми обирали DI, це стало суттєвим аргументом.

Дякую за цікаву статтю!

Класний огляд еволюції розвитку технологій конфігурації Spring.

Не вистачає посилань на source для кращого розуміння:
— docs.spring.io/...​ml#beans-factory-metadata
— docs.spring.io/...​html/core.html#beans-java
— docs.spring.io/...​l#beans-annotation-config
— docs.spring.io/...​ing.configuration-classes

на цю тему, використовую анотації + java-based підходи зазвичай

java-based доповнює hexagonal архітектуру добре, коли в domain маємо бізнес логіку, а біни реєструються java-based конфігом в infrastructure, тоді в домені немає залежностей від спрінгу і інших депенденсі

а в інших випадках через анотації @Service, @Repository, @Component, і тд

Цікаво, а чому в доменній моделі мають бути залежності від Spring? Можете навести приклад?
Spring бiни — це насамперед інфраструктурні об’єкти (контролери, послуги, репозиторії).

це терміни з гексагональної архітектури en.wikipedia.org/...​l_architecture_(software

це трендова архітектура зараз, щось типу clean architecture на максималках

там ділиться на домен (бізнес логіка і взагалі вся логіка застосунка)
і інфраструктура (наприклад взаємодія з базою даних, або зовнішні REST виклики)

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

з мого досвіду зарас в тренді для нових мікросервісів

ідея в тому, щоб зробити домен ... тобто там одна джава майже ...
а інфраструктуру зробити як layer, в якому будуть біни, ентіті (для хібернейт) і тд

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

package my.ohuennyi.domain
XxxModel

package my.ohuennyi.infrastructure.layer.dlya.hibernate
XxxEntity

Де XxxModel та XxxEntity це тупо копія одних і тих самих полів блть які існують вдвох тільки бо «так гексагонально», а потім блть весь код пересипаний їбаним мапінгом одного блть на інше і навпаки, а ще такий самий ска третій класс

package my.ohuennyi.dto
XxxDto

де ска ті самі блть поля втретє.
Ссаними тряпками блть треба їбашить за таку «гексанолку» «тренд» «чистий код»

«гексанолку» «тренд» «чистий код»

«з душком»? :)

Якщо хтось упорото пише дікуху, яка відповідає критеріям чистоти формально, але з т.з. простої логіки та KISS є безглуздям, то такі да, це смердючий код. Зазвичай це дуже важко пояснити його автору, бо починається піна з рота і потрясання цитатами улюбленого євангеліста. Працював з такими, це жесть.

потрібно відрізняти тих, хто докопується до імен і форматування, про цих ви напевно і кажете і архітектуру, яка вирішує певні проблеми

А чо ні? «У міня всьо работаєт»

Де XxxModel та XxxEntity це тупо копія одних і тих самих полів блть які існують вдвох тільки бо «так гексагонально»

Я не можу уявити, як таке може працювати, здається пан, що пропонує

ідея в тому, щоб зробити домен ... тобто там одна джава майже ...
а інфраструктуру зробити як layer, в якому будуть біни, ентіті (для хібернейт) і тд

неправильно зрозумів теорію.

А от тут

а ще такий самий ска третій класс
package my.ohuennyi.dto
XxxDto
де ска ті самі блть поля втретє.

не згоден. Entities еволюціонують своїм шляхом, їх (а може і не зовсім їх) json responses — своїм (v1, v2 тощо). Як без мапінгу-то? Ви ж не пропонуєте клієнту entities віддавати?

неправильно зрозумів теорію.

так викладіть свою думку тоді

Я відповідав Dmitry Bugay.

А питання до вас. Якщо model (of domain) і entity окремо, що ви робите після

   model.updateBy(request);

маючи на увазі, що model — це складний клас з підкласами, тобто граф. Як затрекати зміни і відобразити іх в базу? А, головне, навіщо це робити, коли це як раз головна фіча Hibernate?

Моя думка тут dou.ua/...​les/how-to-use-hibernate

це предмет довгої дискусії, коротка відповідь залежить від контексту.

факт існування такої архітектури не означає, що потрібно забути про всі інші підходи.

плюс це приблизно те саме питання, як жити з мікросервісами, де не одна база даних, а декілька?

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

Я не претендую на абсолютну істину, але, як на мене, Hibernate пропонує чудову фічу і вимагає зовсім небагато (@Entity та @Id, можуть бути застосовні «зовні» через xml-конфігурацію, хоча, здається, простіше навісити анотації на клас). Після цього апдейт в сервісі виглядає так

@Transactional
class Service {
    void update(SomeChange change) {
         Model model = repository.findById(id).orElseThrow();
         model.updateBy(change);
    }
}

Все. Вся бізнес-логіка знаходиться в домені, змішування з БД(?) тільки в наявності JPA анотацій.

Тепер те саме питання до вас. Якщо модель та ентіті окремо, як вони синхронізуються? Як виглядає апдейт? В чому переваги такого підходу?
Хочеться почути практичний досвід, а не риторику

це предмет довгої дискусії, коротка відповідь залежить від контексту.

ви намагаєтесь натягнути функціонал хібернейт на архітектуру, а якщо хтось використовує ноСКЛ бази ?

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

updateXXX(XXX xxx);

і імплементувати його в інфраструктурі

інакше кажучи якщо вам хочеться, щоб яблука були зі смаком банану, можливо потрібно їсти банан, а не яблуко

ви намагаєтесь натягнути функціонал хібернейт на архітектуру, а якщо хтось використовує ноСКЛ бази ?

Вірно, це обмеження. Модель дещо «прибита цвяхами» до реляційної БД.

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

в domain маємо бізнес логіку,

.

От я і питаю (втретє), є model, є метод updateBy(...) в ній. Все чудово працює, відповідно змінються якісь атрибути (і атрибути атрибутів, а ще додаються і видаляються елементи в колекціях вкладених класів... чи модель це чисте POJO?)... але як, бляха-муха, відобразити це щастя потім в БД?

ви хочете проапдейтити якесь з атрибутів моделі, нічого не заважає зробити інтерфейс з методом
updateXXX(XXX xxx);
і імплементувати його в інфраструктурі

Не зрозумів. Можете пояснити іншими словами, будь ласка? Для чого мені імплементувати ще якийсь метод, якщо в мене вже є вся бізнес-логіка в домені?

1) якщо казати термінами гексагоналки, то домен з інфраструктурою комунікує через порти(інтерфейси)

тож в домен буде щось типу

interface SomethingRepository {
Field updateSomethingField(Field field);
}

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

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

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

2) тож інакше кажучи для бізнес логіки досить мати інтерфейс, без реалізації, на то вона і бізнес логіка, яка описує процеси бізнесу, а реалізація додається вже «ззовні» через порти (які є джава інтерфейсами)

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

Я не сильний у визначеннях, тож можу помилятися.

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

Це точно бізнес-логіка, а не API? Бо для мене бізнес-логіка це якраз конкретні реалізовані методи, що описують зміни в моделі.

тож в домен буде щось типу

interface SomethingRepository {
Field updateSomethingField(Field field);
}

Домен — це набір репозиторієв? Імплементації методів (бізнес-логіка?), очевидно специфічні для БД (і можуть відрізнятись?)

Ноу, потрібно буде якось на цю тему зробити статтю

Ну, так гарно починали

на цю тему, використовую анотації + java-based підходи зазвичай... коли в domain маємо бізнес логіку...зробити домен незалежним від технологій, тобто там одна джава майже (ну може ще ломбок)

і кількість коментарів в цьому топіку вже на статтю потягне, а прошу приклад коду якогось елементарного апдейту, щоб зрозуміти, де домен, де модель, де бізнес-логіка, де інфраструктура з ентіті і як вони зв’язуються — нема. Сумно

Якщо сумно, то посумуйте, можете ще поплакати

Ви задаєте базові питання, це не цікаво відповідати

А для більш обширних тем окремі топіки потрібно

Плюс ви взагалі не розумієте принципи цієї архітектури, почитайте базу і поговоримо

але як, бляха-муха, відобразити це щастя потім в БД?

Це залежить від того, як ви хочете бачити ці зміни в БД. Дуже широке питання.

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

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

Я до чого. Головна мораль — «чистої» бізнес-моделі, домену, відрізаного від технологічного сховища даних не існує, це іллюзія і обман, яку намагаються продати євангелісти та теоретики, а девелопери з нестійкої психікою сприймають як абсолютно істину.

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

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

Як боженька мовив

Якщо коротко, то все залежить від того, наскільки доменна модель прив"язана до моделі зберігання данних.

У більшості проектів, як не крути, ці поняття абсолютно тотожні.

Можно видумати інший приклад. Скажімо, бізнес-модель складається з більш-менш статичних даних, що зберігаються в реляційній БД, і набору івентів (ну типу Event Sourcing) які зберігаються як історія змін у, наприклад, Кафці. Таким чином, для побудови повністю наповненої цілісної доменної моделі потрібно буде частину даних взяти з бд, частину з кафки.

Це автоматично приводить нас до думки, що у цьому випадку, так, справді існує 3 шари — чиста доменна модель, SQL-класси @Entity та класси-дто, що серіалізуються в/з Кафки.

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

Цю ідею дуже важко донести пуристам і євангелістам, які верещать про генсагоналку та/або розділення шарів без розуміння коли воно доречне, а коли ні.

Згоден. Я б єдине додав, що

для побудови повністю наповненої цілісної доменної моделі потрібно буде частину даних взяти з бд, частину з кафки.

результат об’єднання буде DTO, а не «чиста доменна модель» (не розумію цього терміну, але я в них не сильний, може помиляюсь)

результат об’єднання буде DTO

Це з якого дива?
Давай зроблю іллюстрацію псевдокодом

package com.domain;
class Customer {
    UUID uuid;
    String name;
    String password;
    List<Integer> payments;   
}

package com.storage.relational;
@Entity
class CustomerStaticData {
    @Id
    UUID uuid;
    String name;
    String password;
}

package com.storage.eventsourcing;
class Event {
     LocalDateTime time;
}
class Payment extends Event {
     int sum;
}

package com.storage.relational.impl.springdatajpa;
interface CustomerStaticDataRepository extend CrudRepository<CustomerStaticData, UUID> {
    ... 
}

package com.storage.eventsourcing.impl.kafka;
class PaymentsTopicKafkaRepository {
     List<Payment> getByCustomerUuid(UUID uuid) {
          ... 
     }
}

package com.logic;
class CustomerService {
     Customer getCustomer(UUID uuid) {
          CustomerStaticData data = customerStaticDataRepository .getById(uuid);
           List<Payment> payments = paymentsTopicKafkaRepository.getByCustomerUuid(uuid);

           return new Customer(
                   data.uuid(),
                   data.name(),
                   data.password(), 
                   payments.stream().map(p -> p.sum).collect(toList());
     }

Отут є чиста доменна модель — Customer. Вона є агрегатною збіркою всіх даних, що є залежними від сховищ.
І вона не є дто.

Я зрозумів одразу і мав на увазі, що Customer — навіть якщо і доменна модель, то це anemic model а я такі за не вважаю за моделі, тому й обізвав DTO,

class CustomerService {
     Customer updateCustomer(UUID uuid, Update update) {
         Customer customer = getCustomer(uuid);
         custom.applyAnyMixedChanges(update);
         /// ????
     }     
нажаль, не взлетить, бізнес-логіка апдейту має бути в сервісі.

Це сперечання про терміни, пропоную не продовжувати, я цілком згоден, що домен у випадку двох сховищ — це окремий layer.

не згоден. Entities еволюціонують своїм шляхом, їх (а може і не зовсім їх) json responses — своїм (v1, v2 тощо). Як без мапінгу-то?

Да, да, тут частково погоджуюсь.
Частково — бо є випадки, коли json іде для внутрішнього користування (не назовні) і є не вьюхою, а просто способом серіалізації даних з бази. В такому випадку, ваша модель, вона же @Entity, вона ж і є вашим json. І їй не треба ні класів-дто, ні мапінгу.

Мій поінт був у тому, що можливо такі випадки

коли json іде для внутрішнього користування (не назовні)

мабуть є, але в абсолютній більшості все ж таки json віддається external consumers.

Тому наявність DTOs (v1, v2,...) і мапера — скоріш за все є обов’язковами. Нажаль, у великій кількості проектів, з якими я мав справу, цього правила не дотримувались, що приводило до певних страждань.

Тому я і відреагував на

package my.ohuennyi.dto
XxxDto

Він потрібен

Якщо

json віддається external consumers

То звісно, я погоджуюсь, що

DTOі (v1, v2,...) і мапера — скоріш за все є обов’язковами

Але уявіть іншу картинку. У вас є гейтвей і апі, з якими спілкуються зовнішні користувачі. А під гейтвеєм лежить десяток мікросервісів, кожен з яких прикутий до своєї бази, і займається тільки тим, що зберігає-дістає з бази строчечки, слухаючи кафку і кидає ці строчечки через ту ж кафку своїм сусідам. І у вас там по кафці літають туди-сюди пласкі жсони, які на 100% відповідають змісту строки в якійсь БД. Ви бачите, що в такому випадку, ваші @Entity і є джсонами, і вам не потрібен Dto-класс з маппером ентіті-дто і назад?

Вибачте, не зрозумів різницю.

кидає ці строчечки через ту ж кафку своїм сусідам

Сусіди ж очікують певний формат(назви пропертів), ні?
Що робити, якщо треба щось в entity змінити?

Вибачте, не зрозумів різницю.

Різниця в тому, що жсони не призначені для зовнішнього користувача, тобто немає складних вимог до валідації а-ля-захист-від-дурня, форматування, приховування полів, і т.п.

Що робити, якщо треба щось в entity змінити?

Класс @Entity може знаходиться в шаред лібі (і це буде правильно, якщо не вводити непотрібні дто-класи), і відповідно, це питання зводиться до оновлення залежностей в помніке у всіх сервісах, що використовують ці дані.

Гм.

Класс @Entity може знаходиться в шаред лібі

Я правильно зрозумів, що один сервіс використовує цей клас як справжню ентіті для роботи з БД, а інший — як MessageDto? І, значить, на всі поля є сеттери, але викликати їх замість бізнес-муторів заборонено?

до оновлення залежностей в помніке у всіх сервісах,

А деплоїти їх потім як? Потушити всі consumer services, оновити supplier service, підняти оновленні consumers? Це серйозний даунтайм.

Спробую вгадати. Ніяких таких проблем нема, тому що ентіті еволюціонують строго одним (природнім) чином: до 100500 полів додається ще одне, 100501-е. Тоді так, все майже гладко (а, ні, тре пам’ятати, що supplier перший передеплоюється, інакше consumer-и зловлять null в новому полі.

Не розумію антипатію до підходу «entity-mapper-dto(v1,v2)», використую його завжди. І з крудами з трьох полів, і з external consumer-ами, і с сусідніми сервісами. Мені подобається мати розв’язані руки і починати з

@Entity
class User {
    @Id
    Long id;
    String firstName;
    String lastName;
    Integer age;
}
і абсолютно аналогічного UserDtoV1 (насправді, ні — в них тільки поля збігаються, а конструктори, сеттери/їх відсутність, equals()/hashCode(), JPA/Swagger аннотації — все різне)

Потім: блін, хто age в БД зберіг, він ж змінюєтся! Міняємо на

@Entity
class User {
    @Id
    Long id;
    String firstName;
    String lastName;
    LocalDate birthDate;
    Integer getAge() {...}
}

Потім додали адресу:

@Entity
class User {
    @Id
    Long id;
    String firstName;
    String lastName;
    LocalDate birthDate;
    Integer getAge() {...}

    String addressCountry;
    String addressCity;
    String addressStreet;
}

Потім: стоп, так адресу неправильно додавати, забагато полів стало в класі, міняємо на

@Embeddable
class Address {
    String country;
    String city;
    String street;
}

@Entity
class User {
    @Id
    Long id;
    String firstName;
    String lastName;
    LocalDate birthDate;
    Integer getAge() {...}
    
    @Embedded
    Address address;
}

Потім: ну, тоді, логічно вже так.

@Embeddable
class Address {
    String country;
    String city;
    String street;
}

@Embeddable
class Person {
    String firstName;
    String lastName;
    LocalDate birthDate;
    Integer getAge() {...}
}

@Entity
class User {
    @Id
    Long id;
    
    @Embedded
    Person person;

    @Embedded
    Address address;
}

UserDtoV1,V2,V3 живуть паралельно своїм життям.

Оп! Тепер мапінг з UserDtoV1 поламався, поправляємо.

Може проблема

потім блть весь код пересипаний їбаним мапінгом одного блть на інше і навпаки,

в тому, що недолугі архітектори мапінг руками пишуть? Так з MapStruct-ом це натурально 1 (один) рядок mapper.toDtoV1(entity), 0 (нуль) рядків коду для тривіальних конфігурацій, і, саме головне, compile check, тобто гарантовано нічого не відвалиться від будь-яких змін в ентіті.

Мені воно однозначно того варте, але звісно, справа хазяйська.

А! На дтошки ще з Swagger/OpenApi аннотації навішані. Не в ентіті ж їх тримати.

P.S.

Dto-класс з маппером ентіті-дто і назад

«Назад» немає. З реквеста ентіті мапінгом не побудувати/не змінити. Мапінг тільки одностороній entity -> dto

Я правильно зрозумів, що один сервіс використовує цей клас як справжню ентіті для роботи з БД, а інший — як MessageDto?

Так, правильно )

І, значить, на всі поля є сеттери, але викликати їх замість бізнес-муторів заборонено?

І що? Вам прийшов меседж з Кафки. Так вже повелось, що серіалізація джексона і вопще POJO вимагає наявності сеттерів. Є там сеттери, і що? Ви тут же біжете робити сет нул в дані що отримали? А навіть якщо то що? Ви отримали дані? Отримали. Що ви з ними зробите — то абсолютно фіолетово тому сервісу який вам їх прислав. Припустимо, вам прислали об«єкт, над класом якого висить ентіті. І що сталось? Значення полів якесь інше стало? Не змінилось рівно н і ч о г о. Але у всій вашій логіці:
— мінус класс-дто,
— мінус класс-мапер (похер, самописний він чи мапстрактівській інтерфейс),
— мінус різниця в типізаціях аргументів
В сумі дає великий мінус складності. Варте воно того? Варте.

А деплоїти їх потім як? Потушити всі consumer services, оновити supplier service, підняти оновленні consumers? Це серйозний даунтайм.

А ви вмієте змінювати класи на їх же новіші версії в рантаймі без шатдауна жвм? Це у вас якісь рантайм-змінювані модулі OSGi? Чи кастомний класс-релоадінг вручну? Нє? Ну так тоді даунтайм буде в будь-якому випадку. Так, для оновлення системи її доведеться тушити, хочаб частково, ось так несподіванка )
Серьозний даунтайм, ну хз, такоє. Кілька секунд поки один под згасне і інший народиться. І не переконуйте мене що у вас все працює без даунтайму.

Потім: блін, хто age в БД зберіг, він ж змінюєтся! Міняємо на
...
LocalDate birthDate;
Integer getAge() {...}

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

Не розумію антипатію до підходу «entity-mapper-dto(v1,v2)»
...
UserDtoV1,V2,V3 живуть паралельно своїм життям.

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

А! На дтошки ще з Swagger/OpenApi аннотації навішані.

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

Є там сеттери, і що?

Я сильно проти сеттерів в @Entity. Так, сеттери потрібні для MessageDto, але, якщо це шаред-клас, то вони є й у «справжній» ентіті. На мою думку, це погано. Розписати чому?

А ви вмієте змінювати класи на їх же новіші версії в рантаймі без шатдауна жвм?

Та ні, звісно, все примитівно. Сервіси між собою переважно спілкуються синхронно, по http. Тому спочатку на supplier-i створюється енд-поінт V2, а потім consumer-и на нього плавно переходять. По суті, різниця між internal та external consumer-ами лише в тому, що deprecated endpoint для перших можна потім видали, а для других — назавжди.

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

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

Дизайн entity — треба виправляти. Для того, що мати змогу це робити і не зачіпати кастомерів при цьому, їм потрібно віддавти не ентіті, а responseDto.

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

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

Я ніяк не можу зрозуміти в чому тут такий жах. Так, є деяка кількість UserDtoV123456 і маппінг однієї(!) актульної версії UserEntity на кожну з них. Завдяки MapStruct-у це пара рядків без тестів.

А все через шо? «ой ми не можем змінити поле в класі, бо його читають ще 2 сервіси»?

В якому класі? В entity можна змінити, що завгодно. В responseDto, яке колись викатили external consumer-ам — ні.

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

Тобто ви усіма силами хейтите підхід «entity-mapper-dto», але визнаєте його правильність для зовнішньої комунікації?
У мене все життя 99% API — external, 1% — internal. У вас прямо навпаки, всі сервіси під контролем, своя рука владика, і можна робити зміни не заморочуюсь версіоністю?

А, і ще поправка на майбутнє.

Коли в JDK завезуть Project Valhalla, розмова про DTO стане мати зовсім інший контекст.

Ссаними тряпками блть треба їбашить за таку «гексанолку» «тренд» «чистий код»

вау, можете це зробити і задокументувати, скиньте сюди в тред потім і чекайте на візит служб

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

а взагалі запропонуйте альтертативу

KISS

ок.
але це ще не означає, що оту гексагоналку треба усюди пихати

ні, для простеньких сервісів це тупо трата часу і ресурсів, ніхто і не казав інакше

KISS хороша тим, що озброївшись ним, можна озалуплювать різних «архітекторів» які ще пісяють в штані ледь побачивши NPE, але вже навчились з причмокуванням вимовлять «гексагоналка».
Вони думають що все має бути гексагональним, даже нєбо, даже аллах, а виходить сране гівно, які на 70% складається з маперів одних і тих же полів, яке без сліз не прочитаєш.

Альтернатива — не використовувати підходи, якщо вони не потрібні. Якщо у тебе JSON круд з трьома полями — нєхер вбудовувать туди гексагоналку.

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

ви чомусь побачили фразу «ЗАВЖДИ використовуйте гексагональну архітектуру», яку тут ніхто і не казав, дон кіхот стайл

Так, тонко нарізана гексагонка у одному невеличкому сервici з 5ма копiями ДТО — ще те збочення

Догадуюсь, що проєкт використовує Spring як контейнер бінів, то @Service, @Repository, якщо додати в classpath дозволяє розробникам використовувати також інші залежності @PostConstructor і це зменшує контроль на рівні domain, application, тобто framework просочиться в domain, а це додаткова складність.

Тому беруть hexagonal архітекту — де розділено: domain -> application -> framework в результаті є обмеження, що domain сlasspath нічого не знає про Spring + потенціально можна заміняти кожен рівень в залежності від вимог.

P.S. Гарно писано про hexagonal архітекту — fideloper.com/hexagonal-architecture

О, іще один :)

Тому беруть hexagonal архітекту — де розділено: domain -> application -> framework

модель і ентіті хібернейту будеш в різних класах писать навіть якщо вони на 100% збігаються по змісту? :)

модель і ентіті хібернейту будеш в різних класах писать навіть якщо вони на 100% збігаються по змісту? :)

model == enity === domain

model = POJO
enity = POJO + hiber annotations

Щастить, але ніколи не зустрічав model і hiber в різних класах.

А чого POJO-то?

В смислі?
А @Entity може бути не-POJO? Наприклад, не мати сеттерів?

Я в своїй статті як міг намагався показати, що в @Entity може бути майже що завгодно, навіть небо, навіть аллах. Навіть third-party клас без анотацій, сеттерів і пустого конструктора. Здається, тоді порозумілись.

Я неправильно зрозумів тоді попередній пост.
@Entity не моде не бути pojo але може мати в классі і інший, не-pojo, і не-@Entity функціонал. Так, звичайно!

Так, тут треба в термінах не заплутатись, я (і, здається, не тільки я) чогось все життя вважав, що POJO == Java Bean == emtpy constructor + getters+ setters, а виявляється — ні.

Тому явно напишу. @Entity може не мати сеттерів. Більше того, я вважаю сеттери в @Entity великим злом і борюсь як можу.

а виявляється — ні

Ну, з цього НЕ випливає пряме твердження що хіб може працювати без геттерів-сеттерів.
Колись, в мохнаті часи, вимога щодо геттерів/сеттерів, була. Можливо канешна мені пам"ять зраджує.
Вибач, зараз не буде рити носом доку, щоб з"ясувати, так це чи ні.
Як мінімум, я пам"ятаю, що хіб перехоплює виклики setXxx(xxx) для визначення dirty-стану персистентної сутності, підключеної до поточної сесії. Якщо ми не говоримо про immutable-only сутності, я не уявляю як хіб буде коретно працювать з dirty state без сеттерів.

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

Більше того, я вважаю сеттери в @Entity великим злом

І як тоді змінити ентіті, якщо у неї немає сеттера?

Як мінімум, я пам«ятаю, що хіб перехоплює виклики setXxx(xxx) для визначення dirty-стану персистентної сутності, підключеної до поточної сесії.

Це не основний механізм.

я не уявляю як хіб буде коретно працювать з dirty state без сеттерів.

По дефолту, при завантаженні ентіті (той самий псевдо-зайвий селект) хібернейт запам’ятовує snapshot ентіті і робить dirty-check відносно нього. Так, з bytecode enhancement-ом і сеттери будуть проімпрувлені, але давайте зараз в ці дебрі не лізти.

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

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

І як тоді змінити ентіті, якщо у неї немає сеттера?

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

У мене неймовірно підгорає, що куди не плюнь, попадеш в убер-гранд-сіньйор девелопера, який розкаже (ні, бо не опускається до примітивних питань) про гексональну архітектуру, ООП як частковий випадок концепції обміну повідомленнями, гальмівний хібернейт з 20-річним шлейфом помилок та інші мультипарадигми. А потім дивишся код, а там «а кто ето сделал?» ©

// @Transactional нема, працює і так
class Service {
    Entity create(...) {
        Entity entity = new Entity();
        entity.setFoo(..);
        entity.setBar(...);
       // потім ще 100500 сеттерів

       // а в кінці
       em.persist(entity);
       entity = em.merge(entity);
       em.flush();
       em.clear();

       return entity;
    }

    void update(...) {
        Entity entity = repository.findById(id).orElseThrow();
        if (....) {
            entity.setFoo(...);
            entity.setBar(...);
            // ще 100500 сеттерів
        } else {
            entity.setFoo(...);
            // хз, чи так треба, але скоріш .setBar(...) тупо забули
            // ще 100500 сеттерів
        }

        // і в кінці обов'язково
        repository.save(entity);
    }
}

Тестів, звісно, нема, бо дійсно тяжко з таким підходом їх писати.

В мене від такого кров з очей іде, але воно... працює. Так — криво, так — з багами, але ми не в 1937-ому софт для АЕС пишемо, ніхто не помре, за баги не розстріляють, а навпаки — заплатять за виправлення.

А звідки таке пішло? Правильно, з концепції Java Bean, яка є суто технічною, для ліб, а не для людей, але слабкі духом девелопери потягнули її скрізь, в тому числі в бізнес-логіку, бо це як процідурки з структурами на Паскалі в школі, тільки ООП, бо є сеттер. А тепер взагалі останній сором втрачають і на серйозних щах обговорюють тролінг про інкапсуляцію

Кінець відступу, вибачте, наболіло.

Тепер по суті питання.

І як тоді змінити ентіті, якщо у неї немає сеттера?

Ентіті/модель, вважаю, перше за все, має бути інкапсульованим ООП класом, і мати

  • Непустий конструктор, який ініціалізує її в консистентний стан
  • Звичані методи, що переводять її в інші консистентні стани. Бізнес-логіка. Для солідності назвем їх бізнес-мутаторами
Все.

А, ні. Ще хібернейт має мати змогу її створювати при завантаженні з БД, тому за часів динозаврів вона мала бути Java Bean-ом (ось для чого були потрібні сеттери, а не для dirty-check), а за сучасних часів, щоб не спокушати зайвий раз:

  • Пустий конструктор, який краще приховати protected-ом, хібернейту і такого достатньо
  • @Id над field-ом, а не над геттером як ознаку ініціювати через reflection
  • Сеттери не потрібні
Ентіті/модель, вважаю, перше за все, має бути інкапсульованим ООП класом, і мати

Отут я повністю погоджуюсь.

@Id над field-ом, а не над геттером як ознаку ініціювати через reflection

@Id / @Column над гет/сет бачив тільки в старезній доці хіба років 7 назад.
Ніхто так не робить, навіть самі упороті.

Тепер за хфілософію, я теж свого роду філософлюблю поп***іть за концепції.

Entity entity = new Entity();
entity.setFoo(..);
entity.setBar(...);
// потім ще 100500 сеттерів

Перепитаю.
Я правильно розумію, що основний хейт у тебе викликає наявність викликів .setXxx() сукупність яких формує логіку, замість:

@Entity
@Getters // only getters, no setters
class Model {
     private id;
     // all other private fields 

     static class Update {
            id; // identify that update belongs to particular instance
     }

     static class UpdateLogicA extends Model.Update {
           // fields for logic A
     }

     static class UpdateLogicB extends Model.Update {
          // fields for logic B
     }

     public void make(Update update) {
           // apply updates depending on update subclass
     }
}

@Service
class ModelService {
     
      @Transactional
      void doA(Model.UpdateLogicA updateA) {
            Model m = em.find(Model.class, updateA.id);
            m.make(updateA);
      }
}
Таким ти бачиш ідеальний світ?
Я бачу приблизно так.
Таким ти бачиш ідеальний світ?
Я бачу приблизно так.

Приблизно так. Не впізнав брата Колю Дмитра.

А то після

І як тоді змінити ентіті, якщо у неї немає сеттера?

можна різне нехороше подумати.

Я правильно розумію, що основний хейт у тебе викликає наявність викликів .setXxx() сукупність яких формує логіку

Правильно. Тому що приходиш на проект, береш таску трохи бізнес-логіку поправити... а там километровий сервіс, в якому парканадцять ентіті по 100500 сеттерів кожна. І ти три дні тупо це читаєш і намагаєшся зрозуміти: що, куди і для чого. А поруч сидить аксакал, який природньо тримає всі ці зв’язки в голові, бо на проекті з самого початку, для нього це справа на 5 хвилин: поправити тут 7-ий сеттер, і там 34-ий в дереві if-ів. І розказуй тепер комусь про clean code, ООП і прочую фігню, особливо менеджеру, який порівнює твою продуктивність та його.

Тепер про концепції. Коли я писав model.updateBy(change) — це про те, що мутатор має бути один, а не купка сеттерів, тут ми зійшлись.

А кількість аргументів — це окреме питання. Між ними має бути low cohesion, а якщо їх багато — то ще й тупо незручно писати.

static class UpdateLogicA extends Model.Update {
           // fields for logic A
}

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

static class UpdateLogicA extends Model.Update {
      // fields for logic A
}
ти це зараз з голови придумав

Так, але на практиці я приблизно так і пишу, коли це доречно.

Йому ж констуктор з усіма аргументами потрібен

Саме так. Більш того, це скоріш за все immutable класс, тому у нього і сеттерів теж немає, тільки конструктор з всіма необхідними аргументами.

але звідки такий гарний об’єкт візьметься?

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

---- Service A ---- 
import com.domain.shared.Model;
....
Model.SomeUpdate update = new Model.SomeUpdate(....args....);
kafka.send(update);

---- Service B ----
import com.domain.shared.Model;
....
Kafka.onMessage(update -> {
      modelService.process(update); // @Transactional inside
}
... без SomeUpdateDto.
маппінг, який ти не любиш.

Я не не люблю маппінг, я не люблю концептуально збитковий, непотрібний маппінг.

Коротше, метод з декількома аргументами — це норм, не треба переускладнювати

З цим я погоджуюсь. Тут прикладі прості, тому аргумент один, який інкапсулює дані про апдейт. Взагалі то, саме в цьому прикладі, якщо строго, то він і має бути 1. Бо якщо у нас є, наприклад,

class Model {
     ....
     void make(Model.Update update, Object wtf) {
           // apply update
     }
}

тоді постає концептуальне питання — якщо Object wtf потрібен для апдейту, то чому він не є частиною цілісної моделі

static class UpdateWithWtf extends Model.Update {
     .... // fields
     private Object wtf;
}
?

Я про інакше.
От прийшов тобі з UI реквестдля employeeId перевести його в departmentId на посаду positionId. В сервісі буде:

@Transactional
class EmployeeService {
    void makeTransfer(employeeId, transitionDetails) {
        val employee = empoloyeeRepo.findById(employeeId);
        val department = departmentRepo.findById(transitionDetails.getDepartmentId());
        val position = positionRepo.findById(transitionDetails .getPositionId());

        employee.transfer(department, position);
    }
}

Два ортогональні аргументи — норм.

Від того, що написати

employee.transfer(new Transfer(department, position));

профіту нема.

Якщо десять і одного типу — мабуть є. Це дискусія рівня як взагалі в метод аргументи передавати. Ну, в інших різних мовах є імпрувменти, а в джаві — або всі типізовані та поіменовані, або Object[] )

В твоєму коді вище Model.Update — це одночасно і реквест, якому пощастило і не треба enrich-ити, і сам model update.

Ще теоретично невірно, що чи то кафка-фасад (а Service B — це фасад, що містить modelService) знає про model update, чи то модель знає про реквест фасада, але я і сам так (модель знає про requestDto контролера — о, боги!) роблю, бо не бачу сенсу два рази перемаплювати чисто заради академічної правильності.

емм...
«консистентний стан» — це що таке? консистентний з точки зору бізнес-логіки?
і от в ентіті, скажімо, 20 філдів і для кожного є якась логіка валідації. ви пропонуєте викликати конструктор з 20 параметрами, і всередині конструктора їх провалідувати, прямо чи через якийсь метод валідації? не те щоб я був проти валідації, чи не юзав ентіті без деяких сеттерів, але виглядає дещо занадто.
ящо мені потрібно провалідувати ентіті — я провалідую її явно там де це потрібно. тому що варіант «ми створимо валідну ентіті і вона буде завжди у валідному стані» в реальному житті часто не працює, і ти завантажуєш з дев.бази щось що теж треба провалідуваати.
P.S.
про ті ж тести. будемо писати тести на ті кейси коли ентіті «невалідна»? тоді, схоже, створювати її будемо з JSON через Jackson, бо просто створити об’єкт і, умовно, засетати якесь поле в null, не вийде бо у нас сеттерів немає.

«консистентний стан» — це що таке? консистентний з точки зору бізнес-логіки?

Так

от в ентіті, скажімо, 20 філдів

Це багато і тому незручно. @Embeddable в поміч.

є якась логіка валідації

Ну, взагалі, якщо ми про rest, то валідувати треба RequestDto, яке має бути окремим класом, і цього зазвичай достаньо. З іншого боку, якщо ентіті може бути створена будь де і ким завгодно, і ми за bullet proof консистетність, то так — всі аргументи конструктора/методів мають бути провалідовані.

тому що варіант «ми створимо валідну ентіті і вона буде завжди у валідному стані» в реальному житті часто не працює

ООП інкапсуляція і особливо констрейни на базі закликані щоб еніті завжди була бізнес-валідною.

ти завантажуєш з дев.бази щось що теж треба провалідуваати.

Це сум, звісно, якщо в БД некоректні дані і треба думати, що з цим робити. Ну так, якось валідувати після завантаження.

будемо писати тести на ті кейси коли ентіті «невалідна»?

Продовження цього суму.

тоді, схоже, створювати її будемо з JSON через Jackson

Через рефлекшн. Але це все якісь нездорові практики.

Це багато і тому незручно. @Embeddable в поміч.

Отут не погоджуюсь. Якщо в моделі 20, 30, 40 полів — то хай так і буде, бо так і потрібно.
@Embeddable це зайвий шар абстракції, який тільки додає складності у прочитанні моделі.
Мене особисто завжди бісила лапша з

@Embedded 
A a;
@Embeddable
static class A

@Embedded 
B b;
@Embeddable
static class B

@Embedded 
С с;
@Embeddable
static class С

тим більше, що вони насправді не приносять цінності — це лише додаткова когнітивна складність. А ще й поплутають неймінг @Embeddable-классів з @Entity-классами, завжди я бачив шось типу

@Entity
class User

@Embeddable
static class UserAddress

@Embeddable
static class UserShippingAddress

@Entity
class Address

class ShippingAddress extends Address
сук@ бл@ть!
Я таке бачив не так щоб один раз.
Забороною embeddable весь цей цирк закінчується.

Не погоджусь з не погоджуюсь )

Якщо в моделі 20, 30, 40 полів — то хай так і буде, бо так і потрібно.

Це звідки таке взялось? У класі має бути невелика кількість ортогональних полів. Інакше їм банально незручно користуватись — ініціювати через конструктор. Тут же сеттери заведуться і бізнес-логіку через них робити почнуть.

Оці 20, 30, 40 полів — вони всі одночасно в бізнес-логіці задіяні? Really?

Чи ці 20, 30, 40 полів — це 1:1 таблицю прочитати? Ну так я з цим і борюсь.

тим більше, що вони насправді не приносять цінності

Цінність в: 1) менша кількість полів — це само по собі зручно і добре; 2) бонусом strict typing і а-ля mixin-и, тобто, якщо є

@Embeddable
static class UserAddress

то туди можна методи додавати.
Мені взагалі здається, що кожний примітив, з яким потім щось роблять, а не просто зберігають, має бути в @Embeddableобгорнутий.

Чесно кажучи не зрозумів, що ти

class ShippingAddress extends Address

хотів сказати.

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

Тобто якщо треба

class Auditable {
    Instant createdAt;
    User createdBy;
    Instant modifiedAt;
    User modifiedBy;
}

то в мене потрібні ентіті будуть не

class Entity extends Auditable

а

class Entity {
    @Id
    Long id;

    @Emdedded;
    Auditable auditable;

   ...
}
Забороною embeddable весь цей цирк закінчується.

У мене навпаки: про embeddable ніхто не знає і тому high cohesion примітиви по методах тягаються окремо, замість бути згрупованими в один клас, як ми тут обговорюємо.

Так бачу.

У класі має бути невелика кількість ортогональних полів.

А це хто і де сказав? Десь у джаві написано що має бути якась кількість полів?
Ні.
Отже, не видумуй. В классі має буть стільки полів, скільки потрібно. Якщо їх потрібно, скажім 36, значить їх потрібно не 2, не 10, не 20, а 36.

Інакше їм банально незручно користуватись 

Ну, програмувати в принципі незручно, код якийсь писать, кавички оці.

Оці 20, 30, 40 полів — вони всі одночасно в бізнес-логіці задіяні? Really?

Емм ну да, звісно. Бізнес логіка інколи буває складною, так.

то туди можна методи додавати.

Рівно ті ж методи можна додавати і в основний клас.

Мені взагалі здається, що кожний примітив, з яким потім щось роблять, а не просто зберігають, має бути в @Embeddableобгорнутий.

Це вже схоже на якийсь нездоровий фанатизм і ідею-фікс.

1) менша кількість полів — це само по собі зручно і добре;

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

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

У класі має бути невелика кількість ортогональних полів.
А це хто і де сказав? Десь у джаві написано що має бути якась кількість полів?

Це не в джаві написано. Особливість людського розуму, якому з тим класом працювати. Людині тяжко оперувати більше ніж 7 сутностями.

Забороною embeddable весь цей цирк закінчується.

Оце і є

нездоровий фанатизм і ідею-фікс.

Вдруге питаю, чим тебі embeddable насолив, тут ж навіть нема про що сперечатись.

Він же для

Якщо це відбувається через декомпозицію бізнес логіки в різні сутності — так.

Поясню кодом.
У тебе embeddable на законодавчому рівні заборонений,
і

пласкі жсони, які на 100% відповідають змісту строки в якійсь БД

тому ентіті виглядає строго так:

@Entity
class User {
    @Id
    Long id;
    
    String firstName;
    String lastName;
    LocalDate birthDate;

    String addressCountry;
    String addressCity;
    String addressStreet;
}

Навіть технічні натяки про однакові префікси/суфікси, розділення полів на блоки — ти ігноруєш.

Далі ти цей json відправляєш з rest-а. Ну а що, фронтендери в питаннях API зазвичай права голоса не мають, якщо собі і перемаплять, тобі не скажуть.

Далі треба зробити getFullName() і ти його робиш в основному класі.

Рівно ті ж методи можна додавати і в основний клас.

firstName + " " + lastName призводить до "John null", бо вони опціональні, треба написати складніше і перевірити тестом.

Ми про сеттери і непустий конструктор вже, здається, з’ясували, тому тест буде виглядати так

class UserTest {
  @Test
  void testFullNameIfBothAreAbsent() {
    assertThat(new User(null, null, null, null, null, null).getFullName()).isEqualTo("[no name]");
  }
}

Натяк, що решта полів в User#getFullName() не задіяні, і, відповідно, їх треба null-ами забивати, ти знову ігноруєш.

У цьому місці я знову зусумнівався, ти точно без сеттерів пишеш?

Ну, більше аргументів нема — працює і так. Але мені незручно. І не треба підйобувати, що

Ну, програмувати в принципі незручно, код якийсь писать, кавички оці.

Завдання 2.

Додаємо до юзера ІПН.

Буде нове поле... якого типу? KISS? Long? А чого ні?

private Long taxId;

І тут вилазять два нюанса.

Перший — концептуальний. Цей Long taxId можна десь випадково з Long id сплутати (я пам’ятаю, що тебе uuid, але Long id — теж норм), а typedef в джаві нема.

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

Другий — треба ІПН декодувати: getBirthDate(), getGender(), isValid().

Куди ти ці методи покладеш? В UserEntity? В TaxIdUtils?
Мені подобається ООП, пому в мене буде так

user.getTaxId().getBirthDate();
В цьому ти не переконав

Ну, не переконав, то не переконав.

в емеддабле це частина ідеального світу, і ти не бачиш мінусів.

Розкажи про мінуси.

Ще раз відповім.

будемо писати тести на ті кейси коли ентіті «невалідна»? ... просто створити об’єкт і, умовно, засетати якесь поле в null, не вийде бо у нас сеттерів немає.

Це ми точно про entity? Може все ж таки про валідацію @RequestBody? Так там DTO з сеттерами, через які і клієнт, і тест може засетити будь-що для перевірки валідації.

і ти завантажуєш з дев.бази щось що теж треба провалідуваати.

Чи все ж таки про ентіті? І процес у нас: прийшов реквест, завантажили ентіті, провалідували, і, якщо не так, кинули ексепшн юзеру «Вибачайте, далі кіна не буде, виявилось в БД дані corrupted». Це сумно якось звучить.

Я про валідацію «консистентності» і «з одного консистентного стану в інший». І про те, що не варто на це закладатися. Умовно, у нас є метод який приймає ентіті в одному консистентному стані і переводить в інший консистентний. Писати тести на кейс коли в нього прийде «неконсистентне» ентіті будемо? Чи не будемо — «такого відбутися не може бо у нас все завжди консистентне»?
Я ж мову веду про етап розробки, у нас бізнес-вимоги і модель по ходу розробки міняється ітераційно. Як і дані в базі. Що не виключає випадків, коли це може відбутись пізніше.

завантажили ентіті, провалідували, і, якщо не так, кинули ексепшн юзеру «Вибачайте, далі кіна не буде, виявилось в БД дані corrupted». Це сумно якось звучить.

ну є кілька варіантів:
1) у нас є якийсь флоу на випадок коли в метод/сервіс і т.д. приходять невалідні дані
2) у нас цього флоу немає, але є який загальний флоу на «something go wrong»
3) «такого бути не може, дані завжди будуть ок».
Переважно все зводиться до комбінації 1) і 2).
P.S.
Я все це до того, що відсутність сеттерів не 100% гарантія що дані будуть консистентні. А раз так, то не варто наявністю сеттерів надто перейматися. Хоча в випадках коли ми сторимо частково ненормалізовані дані — ну, скажімо, крім повного e-mail
ще й окремо домен (ото треба нам швидкий пошук по домену), відсутність сеттера на домен цілком резонна.

Третій раз питати чи відрізняєте модель від реквеста не буду, і так бачу.

Добре, я вам зараз у коментарі на ДОУ швиденько OOP/JPA розкажу, як я бачу, а тут детальніше. А ну як вийде спасти хоч одну душу? )

Задача. Зробити CRUD для рядків, для яких також зберігати кількість букв «А», ну, щоб не діставати з БД і перераховувати кожен раз.

Модель, вона ж ентіті.

@Entity
class Model {
  @Id
  private Long id;
  private String str;
  private int countOfA;

  public Model(String str) {
    this.str = str;
    this.countOfA = ...; // якось порахував
  }
}

Порахунок — це логіка, про StringUtils чи щось подібне я не здогадався, тому пишу тест

class ModelTest {
  @Test
  void testModel {
    assertThat(new Model("Andriy).getCountOfA()).isEqualTo(1);
  }
}

Зараз це immutable entity, а нам потрібно міняти значення.
Додаємо

void update(String str) {
  this.str = str;
  this.countOfA = ...; // якось порахував
}

Додаю тест на метод, можна і конструктор порефакторити, бо там те саме, можна тести взагалі спочатку написати, привіт ТДД.

Це — ООП/інкапсуляція.
Консистентність данних — це те, що в countOfA чи то при створенні, чи то при змінах знаходиться відповідне значення.

Тепер робимо рест контролер

@RestController
@RequestMaping("/models")
class ModelController {
  @PutMapping("{id}")
  void update(
    @PathParameter
    Long id,
 
    @RequestBody
    @NotBlank
    @Size(max = 1024) 
    String updateStr
  ) {
    modelService.update(id, updateStr);
  }
}

updateStr — це реквест, над ним валідація.

Далі сервіс

@Transactional
@Service
class ModelService {
  void update(Long id, String updateStr) {
    Model model = repo.findById(id).orElseThrow();
    model.update(updateStr);
   // без repo.save(), бляха-муха!!
  }
}

Все. Якщо не суто rest crud, а модель/ентіті ще десь використовуєтся, є сенс підвищити «строгість». Насправді, перевірку аргументів треба робити завжди і спочатку, але ліньки.

void update(String str) {
  if (str == null || str.length() > 1024) {
    throw new IllegalArgumentException(...);
  }

  this.str = str;
  this.countOfA = ...; // якось порахував
}

Це — перевірка аргументів. Валідація, мабуть, на таке некоректно говорити.

Кінець.

Тепер ваші питання

Умовно, у нас є метод який приймає ентіті в одному консистентному стані і переводить в інший консистентний.

Це процедурний підхід. modelService#update() «не приймає і не переводить», він передає реквест в модель.

у нас є якийсь флоу на випадок коли в метод/сервіс і т.д. приходять невалідні дані

Валідація реквеста та/або перевірка параметрів. Ентіті нікуди за межі транзакції не виходить.

«такого бути не може, дані завжди будуть ок»

Ну а як воно може бути не ок? Інкапсуляція-с. Хтось через рефлекшн ентіті поламає? Чи напряму апдейтом в базі?

У такому випадку є сенс розглянути варіант бізнес-логіки в базі (triggers, stored procedures)

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

Нова вимога: тепер треба не кількість букв «А», а натомість кількість букв «Б».
Якщо дозволяється даунтайм: зупиняти прийом нових даних, мігрувати дані в базі, міняти модель та її поведінку/методи.

Ще питання?

Статтю я читав колись. Цей сферичний кінь в вакуумі не налазить на глобус, якщо треба orm натягнути на існуючу модель бази, при цьому залишивши частину sp i sequence-s як є. Точніше, налазить, звичайно, бо інакше нахрена ж нам платять гроші. ))
А бази було взагалі-то дві («практично ідентичні по DDL» Oracle і MS SQL) і вилазили проблеми з маппінгом і ти матюгаєшся як же database agnostic approach, а ще проблеми з батчінгом, точніше неможливістю заставити хібернейт все ж закомітити транзакцію при ексепшн на вставці одного з рекордів (бо є кілька тредів плюс кілька інстансів і можливі дублікати при вставці). Ну і ще різні дрібниці, але неважливі то й не запамятались.
Але нічого кращого немає, не подобається — допиши сам, так що хаваєм що дають. ))

Це процедурний підхід. modelService#update() «не приймає і не переводить», він передає реквест в модель.

а як я мав здогадатись, що параметри метода ви називаєте «реквестом»? ок, якщо вести мову про сервіси на такому рівні і припустити що між сервісами чи в сервіси вищого рівня передається дто, а у нього сеттери є, то проблема з тестуванням умовно знімається.

Ну а як воно може бути не ок? Інкапсуляція-с. Хтось через рефлекшн ентіті поламає? Чи напряму апдейтом в базі?

Так у нас може куча сервісів в базу писати/модель апдейтити.
Які гарантії що хтось не накосячить? Винесена окремо логіка валідації трохи допомагає, але не у 100%. Напевно підхід «database per service» якось цю проблему нівелює, але я особисто ні разу з таким підходом в житті не стикався, тому хз.

Якщо дозволяється даунтайм: зупиняти прийом нових даних, мігрувати дані в базі, міняти модель та її поведінку/методи.

Ну я якби вів мову про дев-базу, так що звичайно дозволяється. Але вже на стейжі, скажімо, у нас таке б не проконало (дані заливаються кілька днів, потім ще пара сервісів їх апдейтять). Тому ми фактично тримаємо частину «неповних/невалідних» даних в базі. В результаті є частина «false-positive» багів, які ми ловимо, але, в той же час, це дозволяє виловлювати інші, які б ніколи не виплили якби ми мали тільки коректні дані в базі. Свого роду antifragile. Ну доки якийсь QA не залетить, але у нас їх немає ))
Але це так, лірика, до дискусії не зовсім стосується, бо зараз на проекті юзаємо нереляційки (documentdb + opensearch), що дозволяє вільніше поводитись з структурою даних, позбавляє від деяких потенційних проблем і ... створює нові ))

якщо треба orm натягнути на існуючу модель бази

Мені тяжко зрозуміти, що заважає, там два слова для перекладу з ООП в БД і назад — @ManyToOne і @Embeddable/@Embedded. Ну ще @MappedSuperClass та @Inheritance (але я їми майже не користуюсь, надаю перевагу композиції). Тут ось намагаюсь переконати.

Це процедурний підхід. modelService#update() «не приймає і не переводить», він передає реквест в модель.
а як я мав здогадатись, що параметри метода ви називаєте «реквестом»?

Реквестом updateStr був в контролері, в сервісі це вже параметри метода.

З контексту було неочевидно, що у вас request, model/entity, response — це три класи, здавалося, що один на все, вибачте, якщо це не так.

ок, якщо вести мову про сервіси на такому рівні і припустити що між сервісами чи в сервіси вищого рівня передається дто,

Мені бачиться, що має бути строго так.

Напевно підхід «database per service» якось цю проблему нівелює, але я особисто ні разу з таким підходом в житті не стикався, тому хз.

Так, звісно має бути «database per service». Різали моноліт на мікросервіси і не дорізали? Я без засудження питаю, плавали, змаємо.

Тому ми фактично тримаємо частину «неповних/невалідних» даних в базі.

Ну, це суворо, звісно. Тоді, мабуть, варіант як я писав вище

І процес у нас: прийшов реквест, завантажили ентіті, провалідували/пофіксили, і, якщо не вдалось, кинули ексепшн юзеру «Вибачайте, далі кіна не буде, виявилось в БД дані corrupted». Це сумно якось звучить.

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

Так, звісно має бути «database per service». Різали моноліт на мікросервіси і не дорізали? Я без засудження питаю, плавали, змаємо.

Та ні, скоріше такий варіант навіть не обговорювався (неочевидні переваги проти очевидних проблем).
Перефразуючи Подеревянського, «Одна думка, шо так можна зробити — це вже х...я, Мирон Опанасович.
Мирон Опанасович: Звичайно, х...я, Зоя Жорівна, ще й яка х...я. Це ідеалістичний онанізм.»

(бо є кілька тредів плюс кілька інстансів і можливі дублікати при вставці).

А це вже ваш особистий пройоб в архітектурі і керуванні даними. Не треба перекладати «хіб не може» те, що ви мали продумати і зробити самостійно.

Так можна все «зробити самостійно», нахрена тоді хібернейт.
Проблема практично стандартна — «BULK INSERT/UPDATE ignore errors» і має рішення. От тільки хібернейт не дає можливості цей випадок нормально обробити. Наскільки памятаю (я це тоді дебажив), всередині у нього є окремий ексепшн на випадок помилки по pk/fk, але якогось окремого хендлера/флоу практично немає, це на якомусь етапі хериться, експшн генералізується і все зводиться просто до роллбеку.
P.S.
це не "

пройоб в архітектурі і керуванні даними

, це спроба покращити перформанс коли у тебе по-факту ETL і ти розпаралелюєш всі етапи, причому source (SWIFT Alliance server) на це не дуже розрахований (ти можеш читати дані умовно з кількох портів і з якимось оффсетом, але наступні дані можуть посилатися на попередні, а порт сервера взагалі може «підвиснути»).
Так от, перформанс то суттєво виріс, але з’явились проблеми які треба було в процесі розрулювати. Чи можна було цю конкретну проблему вирішити по іншому — так, звісно, але це потребувало синхронізації даних між потоками/інстансами, що було ніфіга не тривіально порівнянні з ретрай у випадку дублікатів, які траплялись у 5-10% батчів.

Чи можна було цю конкретну проблему вирішити по іншому — так, звісно, але це потребувало синхронізації даних між потоками/інстансами, що було ніфіга не тривіально порівнянні з ретрай у випадку дублікатів, які траплялись у 5-10% батчів.

О, люблю це!

«Можна було вирішити проблему по суті, але ми впиляли костиль, бо для рішення проблеми треба думати і робити а ретрай все полікував без зусиль. Ну да, воно валиться але ж ретрай»

А потім з’являються статті «спочатку ми все зробили правильно, і продовжували теж правильно але РАПТОМ розробники порозбігались бо з’ясувалось шо вийшла ацтекська піраміда з костилів і з тотемом бага зверху, як уникнути наших помилок» :)

Я в своїй статті як міг намагався показати, що в @Entity може бути майже що завгодно

Прекрасний аргумент

@Entity може бути майже що завгодно

На жаль з таким підходом clear code не напишеш

Ultimately the application domain model is the central character in an ORM. They make up the classes you wish to map. Hibernate works best if these classes follow the Plain Old Java Object (POJO) / JavaBean programming model.
© docs.jboss.org/...​r_Guide.html#domain-model

Hibernate works best if these classes follow the Plain Old Java Object (POJO) / JavaBean programming model. Документація — сила :)

Прекрасний аргумент
... На жаль з таким підходом clear code не напишеш

Не зрозумів вашу контр-аргументацію. Ось моя стаття, як я бачу clear code. Як пишите ви?

Dmitry Bugay свого часу вшанував її великою кількістю коментарів, тому маю надію, що мені не потрібно вставляти лінк на неї в кожне повідомлення, це нескромно.

Документація — сила :)

Так, особливо якщо дочитати абзац до кінця.

However, none of these rules are hard requirements. Indeed, Hibernate assumes very little about the nature of your persistent objects. You can express a domain model in other ways (using trees of java.util.Map instances, for example).

що можно, вважаю, перекласти на українську як

в @Entity може бути майже що завгодно
На жаль з таким підходом clear code не напишеш

Напишеш, ще й як. Просто не треба пхати в ентіті-класс те, що контекстно не відноситься до домену цієї сутності.

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

Андрій каже

Андрій каже, що в ентіті має бути тільки бізнес-логіка.

утільні методи для роботи з цим інстансом

Це що, наприклад?

статичні методи для обробки коллекцій інстансів цього ж классу.

А це що і навіщо? Я такого ніде не казав, особливо про статичні методи.

Підписатись на коментарі