Итоги падведьом ©
Да, мы вроде уже возле финиша.
Люблю Optional, не люблю isPresent(), написал бы через orElse.
С другой стороны, вижу if — пишу тест, а orElse в глаза бросается только JaCoCo.
Map снимает этот вопрос?
Никоим образом.
Извините, поспешил
class Client { ... // /previous code @LazyCollection(LazyCollectionOption.EXTRA) private Map<Client, Friendship> friends; }
В попытке держать метаданные дружбы клиентов и сохранить свою модель вы создали структуру, в которой клиент А содержит мапу из клиентов и объектов, у которых тоже по 2 клиента, причем в каждой паре 1 из клиентов — тот же А? У вас4-уровневая (ой-вей!) структура из клиентов.
Вам не кажется эта структура офигеть каким искусственным, громоздким, излишним, error-prone и вообще неудачным решением
Неа, Map кажется мне простым и понятным решением, не зависящим от JPA и сервисов.
Но спасибо, что подняли эту тему, допишу абзац про *-to-many.
С одной стороны, я понимаю, что любое ТЗ вы можете решить в рамках своего подхода и с помощью сервисов, дробления графа и jpql не проиграть в N+1.
С другой стороны, может правы и те, кто к вашим strict rules дополнительно запрещает *-to-many вообще, потому что сегодня там три записи, а завтра три миллиона, а код уже задеплоен.
Правильно понимаю, что отличие наших подходов в том, что я свободно отношусь к модели и играю DTORRO-шками, а вы достигаете того же вводя правила и платя строками кода?
Насколько втупую? Как в разделе «Обычный подход»? Потом с ними очень неудобно работать, в сервисах начинается код по доставанию данных из разных таблиц вперемешку с бизнес-логикой.
Вы знаете? Поделитесь рецептом, пожалуйста.
Иначе как мне догадаться, что это простецкая схема создалась из таких классов dou.ua/...les/how-to-use-hibernate — раздел «Правильные entities». И ладно аннотации, а там у автора ещё бизнес-методы в них.
Как угадать java-классы для, например, s.dou.ua/...-files/image1_Fh1bISl.png ?
Нет, но извините. И вопрос тот же: как в случае Hibernate угадывают джава классы по таблицам? И насколько хорошо?
Мне кажется, что если делать перевод классы Java -> структура БД -> классы Java, то результат будет не очень хорош, как с любым дизассемблированием. Поэтому интересуюсь, как R -> O делают.
У меня есть небольшой опыт работы с Hibernate (см. статью). Есть мнения, что он тормозит. Поэтому спрашиваю, как его используют, особенно в случае, когда БД уже существует.
А у меня не ООП? Смелое заявление, весьма смелое :)
Убедите в обратном, пожалуйста. У меня возникло ощущение, что вся логика у вас в сервисах, entity — POJO. Как у вас будет выглядеть метод FriendshipService.establishBetween? Тест на него? Почему вы считаете, что изменение класса Client для задачи «добавить друзей» — это
залезли пальцами прямо в модель и увеличили ее сложность
?
Дайте мне информацию о конкретно этой связи.
Извольте
class Client { ... // /previous code private Map<Client, Friendship> friends; }
когда и при каких обстоятельствах мы с вами познакомились.
На ДОУ, в комментариях :-)
В пунктах 1 вы можете выбрать 5000 записей (а вдруг вы блоггер)
Конечно, с отношениями *-к-многим нужно быть аккуратным. Замечание Влада, что @OneToMany следовало бы назвать @OneToFew справедливо.
Map снимает этот вопрос?
Суть моей операции
1) СЕЛЕКТ ФРОМ дружбы ВЕРЕ друг1 = Андрей И друг2 = Дмитрий
2) Если резалт сет не пустой, создать 1 объект
Но делает это сервис. С помощью таблицы. Могут ли Андрей и Дмитрий быть друзьями без них?
Поскольку внешние запросы, скажем веб, каждый в своей сессии, то нет, не шарится.
в вашей архитектуре есть entity-businessDTO-transportDTO. С таким вариантом я могу согласиться
Ок, с этим разобрались. Я всю ветку выше именно это говорил. Спасибо, что помогли четче сформулировать.
мы избегаем бидирекшинал связей, поэтому у нас нет циклов в сущностях.... Причина — мы приняли строгую иерархичность сущностей...имеем структуру зависимостей в сервисах
А то я такого не видел никогда :-)
Обратите внимание, это, имхо, охренительно важно — моя реализация ТЗ не затронула а) ни класс Client б) ни класс ClientService. А что у вас? Вы залезли пальцами прямо в модель и увеличили ее сложность.
Конечно, это охренительно важно. Я топлю за ООП, весь код, который я привёл — это объекты. Не POJO, у них нет сеттеров (Order.setStatus и Order.setExpress — это вполне валидные бизнес-мутаторы). В Order.removeItem есть защита от ухода в отрицительные значения и написан тест на этот метод. Я не ставлю никаких ограничений на иерархию и связи: надо bi-directional (в объектном, не БД смысле) — сейчас в Client добавим Set<Order>
.
Всё это добро свободно работает без JPA. А JPA я воспринимаю именно как Persistance реально существующих объектов. Да, чтобы Money сохранить — надо конвертер написатьвсе уже в jadira сделали. И вообще @Type — это не JPA, а Hibernate-specific. Но я реальный класс сохраняю, а не модель.
Думаю, мы вполне можем с Дмитрием Думанским сработаться: домен у него, утверждалось, есть. Накидаем анноташекНапишем orm.xml и ACID будет вместо файловой системы.
Не возвражаете, если ваши сервисы мы назовём модулями с процедурами, а модель — структурами?
Мои аргументы в пользу ООП.
Сервис у меня выглядит так
class ClientService { ClientDao clientDao; @Transactional void addFriend(Long clientId, Long friendId) { Client client = clientDao.getBy(clientId); Client friend = clientDao.getBy(friendId); client.addFriend(friend); } }
Unit-тест на такой метод я писать не буду: просто перепроверять вызовы — это не black-box тестирование.
А вот на Client.addFriend, наоборот, обязательно AAA-тест напишу: убедиться, что StackOverflowException не поймал, и дружба взаимная получается.
public class ClientTest { @Test public void addFriend() { // arrange Client andriy = new Client("Andriy", "Slobodyanyk"); Client dmitry = new Client("Dmitry", "Bugay"); // act andriy.addFriend(dmitry); // assert assertThat(andriy.getFriends(), hasItem(dmitry)); assertThat(dmitry.getFriends(), hasItem(andriy)); } }
Хибернейт тут роли не играет.
Я переформулирую, расклад такой. Меняется и удаляется вообще всё, как расписал Dmtry Bugay. Поэтому аудит — это незатейливая табличка из трёх столбцов: кто-когда-что сделал (в текстовом виде). Никаких референсов на другие таблицы у неё нет.
Бизнес-логика в таком стиле
<blockquote>public List<Item> getItems() { return items; } ... doSomeFancyStuffWithLotsOfStreamingAndMaybeBitOfReactiveProgrammingAndSneakilyModifyCollectionInASubtleWay(myOrder.getItems());
Лично я предпочитаю бизнес-мутаторы, но Хибернейту, по большому счёту, всё равно, кто и как объект поменял.
Явного сохранения нет, и быть не должно по самому определению персистент объектов.
А теперь делаем аудит. И с EventListener-ами это как раз не проблема. Есть предыдущий state и текущий. Смотрим разницу и пишем в audit table.
явными репозиториями задача решается легко добавлением сравнения
Так а что вообще в CRUD задачах сложного? C хибернейтом просто кода меньше писать, а так всё то же самое.
Оффтопик: Прими восхищение телеграмм-каналом вообще и последним постом в частности!
@Type c несколькими полями (Money в статье) — это Hibernate-specific feature. В JPA только AttributeConverter в одно поле. Отзовитесь, кто ещё работал с EclipseLink?
Сначала хочу поблагодарить вас за проявленное терпение, что не прерываете дискуссию и иллюстрируете её кодом. Благодаря вашим замечания я чётче переформулирую абзац «Entity и RRO». А теперь к делу.
Полностью согласен с pastebin.com/uCAuvESZ. Также согласен с тем, что либо энтити связаны, либо нет и приемлемо их доставать в разных транзакциях.
@Request
JsonModel getBy(id) {
Model model = modelService.getBy(id, FULL);
SomeQty qty = qtyService.get(..., FULL); // 2.1
ModelJson json = mapper.toJson(model, qty); // 2.2
return json;
}
Поговорим о вашем решении с флажком DataIntegrity. Это вы придумали? Я нигде не встречал такого подхода. Его плюс — академическая правильность о разделении слоёв. Недостатки, на которые вы закрываете глаза — это необходимость в интенсивном написании jpql, Hibernate.initialize(), лишние N+1, потенциальные LLE, если в контроллере будет вызов modelService.getBy(id, PARTIAL) и накладывание ограничений на доменную модель, о которых ниже.
Вы это почувствовали, когда я предложил извлечь только Orders+Clients(без Items) на примере из статьи, и стали менять правила, дескать, в жизни всё сложнее, и два графа из трёх не бывает.
По уму, надо было сразу спросить, а как же циклические зависимости, когда вы первый раз упомянули Hibernate.initialize(), и мы бы сэкономили много времени.
Попробую угадать ответ. У вас их нет, потому что домен правильно спроектирован. Ок.
Теперь моё решение. Можете пропустить, и переходить сразу к ТЗ, если
. ваш фасад меня не интересует, он мне не нужен, поскольку мне не нужны ваши маппинги и ваши дто.
1. У меня совершенно классические сервисы, возвращающие энтити, без флажка integrity.
2. Дальше я говорю контроллерам — ребята, раньше вы обращались к сервису за энтити, а теперь новый договор — обращайтесь к фасаду.
3. Есть @Transactional-фасад, содержащий сервис(ы), перемапливающий их и возвращающий Raw Result Object (RRO) — чистые данные для контроллера.
4. Они raw — никакой привязки к DTO, Json, JMS мессадж и JMS транзакции — это просто классы с полями без единой аннотации и зависимости.
5. Контроллер маппит RRO в DTO.
6. Сервисы от контроллера полностью скрыты.
Очевидный недостаток этот решения
не вы решаете как организовать сервисы, чтобы они предоставляли данные для сборки нужного формата (вдумайтесь в это предложение, в нем суть),
... да, на фасад с RRO оказывается определённое влияние.
Недостатки:
1. Контроллер требует от фасада, какие данные ему предоставить. Некоторое логическое нарушение однонаправленности слоёв.
Это осознанный вынужденный компромисс для случаев, когда контроллер не является неизвестной третьей стороной, а разрабатывается тут же в приложении. Впрочем, для неизвестных третих сторон всегда можно создать RRO идентичные по структуре Entity.
Преимущества:
1. Я не накладываю вообще никаких ограничений на доменную модель. Любые ссылки, любые связи, хибернейт всё стерпит, только поменяйте на lazy в many-to-one.
2. Создавая компактные RRO с только необходимыми полями — я выигрываю в N+1.
Теперь специальный синтетический пример для сравнения подходов.
Дополняем ТЗ статьи: у клиента появляются друзья — такие же клиенты.
Тут вы снова можете захотеть изменить условия, в реальной жизни ещё нужны данные о том, когда познакомились, где, но суть не поменяется — это циклическая связь.
Теперь короткий пересказ статьи.
Создаём удобный класс не думая о JPA вообще. Добавляем в Client
class Client { // ... prev code private List<Client> friends = new ArrayList<>(); public void addFriend(Client friend) { friends.add(friend); friend.addFriend(this); }
Ой, ловим StackOverflowException, добавляем проверку и всё же естественнее и логичнее перейти на set и задуматься о equals/hashCode.
Навешиваем аннотации, итого:
class Client { // ... prev code @ManyToMany(cascade = CascadeType.PERSIST) @JoinTable( name = "client_friends", joinColumns = @JoinColumn(name = "client_id"), inverseJoinColumns = @JoinColumn(name = "friend_id") ) private Set<Client> friends = new HashSet<>(); public Set<Client> getFriends() { return Collections.unmodifiableSet(friends); } public void addFriend(Client friend) { if (friends.add(friend)) { friend.addFriend(this); } } }
Как ни крути, мы не может отдавать эту энтити в контроллер.
Либо LLE, либо OOO. Представление влияет даже на сигнатуру метода.
class Controller { ClientFacade clientFacade; int friendsLevel = 2; Json getClient (Long clientId) { ClientRro rro = clientFacade.getBy(clientId, friendsLevel); return mapper.toJson(rro); } } class ClientFacade { ClientService clientService; @Transactional ClientRro getBy(Long clientId, int friendsLevel) { Client client = clientService.getBy(clientId); return clientRro; // custom building of ClientRro by client of friends level } }
не очень понимаю в чем для визуализации Хибернейт выигрывает когда аудит уже сохранен. Он же тоже айдишки будет в _aud хранить, которые ризолвить нужно.
Не факт, что айдишки, может натурально строки, как в log-файле, foreign key все равно не будет. И не все сразу приходят к идее ставить флаг удаления, некоторые физически записи удаляют.
Когда нужно отслеживать изменения, то таки да — лучше это делать явно на уровне домена.
В теории — да. Когда выше речь шла о изменении версии parent-а, я тоже так думал. Но в случае аудита — сразу сдаюсь, не буду я каждый сеттер править.
Хибернейтовский @Audited — штука полезная, но больше для прикрытия формальных требований аудита, чем бизнес требований
Так и есть. Из Envers только идею инструмента — eventlistner — можно взять, тюнинг там примитивный и потому недостаточный. А в тех двух аудитах, которые я написал, бизнес был весьма изобретателен, казалось бы в такой самоочевидной задаче.
немногочисленные изменяемые поля проверяются на изменение еще бизнес-логикой, до персистентности, и трекание их не составляет проблем.
А теперь давайте возьмём мой пример из статьи: Order-Client-Item-Quanity. И представим, что трекать нужно каждый чих.
Но это специфика, засчитываю себе аудит как потенциальный плюс Хибернейта, особенно для более CRUD приложений.
Ага
От меня было предложение назвать фасад внешним сервисом, вы сказали, что суть не измениться, а теперь это всё таки сервис, ок, это не важно.
Я там выше правил код немного, но суть та же.
Итак, у нас есть
1. SmallBO — носитель аннотаций джексона.
2. Маппер из entity в smallBo
3. И строчка
smallBoMapper.toBo(entity);
Объясните про зло другими словами, пожалуйста. Я не понял про
гипотетически позволяет кому-нибудь прибивать гвоздями поля жсона к прямым вызовам базы без разделяющей абстракции.
Давайте дальше двигаться )
Вы не согласны с тем, что полностью загруженное энтити
MyEntity e = service.getBy(id, FULL_INTEGRITY);
Этот вызов — вне транзакции.
является своим собственным представлением.
Поделитесь, пожалуйста, в чём принципиальное отличие? Как вы его используете, кроме сорса для маппера?
Возможно, плохую роль играет моя узкопрофильность? Я никогда не писал em.detach(), чтобы продолжить работать с бизнес-объектом, ловя LLE. Тогда я бы сам пришёл к сервису с парт и фул-интегрити.
Жпа не про стейтфул
Я в том смысле, что stateless mapper-ов много разных есть, а stateful — кроме провайдеров JPA не знаю
Именно так. Кеш
Если написать
...нужно вызывать потом метод по получению Client-а по его ID.