{"id":20645048,"url":"https://github.com/romanow/jpa-example","last_synced_at":"2025-07-29T17:13:29.992Z","repository":{"id":43863040,"uuid":"449456597","full_name":"Romanow/jpa-example","owner":"Romanow","description":"Example of using JPA with spring.jpa.open-in-view=false","archived":false,"fork":false,"pushed_at":"2023-03-16T09:25:25.000Z","size":285,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-16T16:19:12.144Z","etag":null,"topics":["jpa","spring-boot"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Romanow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-01-18T21:41:12.000Z","updated_at":"2024-02-01T13:08:31.000Z","dependencies_parsed_at":"2024-11-16T16:18:58.867Z","dependency_job_id":"b03d358e-1958-4c4d-acac-114fc828af4d","html_url":"https://github.com/Romanow/jpa-example","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fjpa-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fjpa-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fjpa-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Romanow%2Fjpa-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Romanow","download_url":"https://codeload.github.com/Romanow/jpa-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":234339788,"owners_count":18816726,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jpa","spring-boot"],"created_at":"2024-11-16T16:18:33.118Z","updated_at":"2025-01-17T09:24:24.332Z","avatar_url":"https://github.com/Romanow.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# JPA example\n\n[![Build project](https://github.com/Romanow/jpa-example/actions/workflows/build.yaml/badge.svg?branch=master)](https://github.com/Romanow/jpa-example/actions/workflows/build.yaml)\n\n## Использование Hibernate + JPA\n\n##### Настройка JPA spring.jpa.open-in-view=false\n\n\u003e Spring web request interceptor that binds a JPA EntityManager to the thread for the entire processing of the request.\n\u003e Intended for the \"Open EntityManager in View\" pattern, i.e. to allow for lazy loading in web views despite the original\n\u003e transactions already being completed.\n\nКласс `OpenEntityManagerInViewInterceptor` в методе `preHandle` открывает `EntityManager` для текущего запроса, т.е.\nSpring создает обрамляющую транзакцию на _весь_ запрос.\n\nВыключение этого параметра (`spring.jpa.open-in-view=false`) приведет к тому, что инициировать транзакцию для работы со\nсмежными данными нужно будет руками.\n\nЭто правильный подход, т.к. он дает контроль над транзакционной целостью запроса.\n\nЕсли использовать `CrudRepository` (или его наследников), то Spring в runtime в proxy подкладывает\nреализацию `SimpleJpaRepository`, которая помечена аннотацией `@Transasctional(readOnly = true)` на уровне класса, т.е.\nтранзакция создается на каждый запрос.\n\n##### В коде используем явное управление транзакциями через `@Transactional`\n\nИспользование транзакций гарантирует:\n\n* Атомарность (Atomicity) – гарантирует, что никакая транзакция не будет зафиксирована в системе частично. Будут либо\n  выполнены все операции внутри транзакции, либо не выполнено ни одной.\n* Консистентность (Consistency) – транзакция, достигающая своего нормального завершения и, тем самым, фиксирующая свои\n  результаты, сохраняет согласованность базы данных.\n* Изолированность (Isolation) – гарантирует что никакой поток данных не может читать данные из еще не завершенной\n  транзакции.\n* Долговечность (Durability) – если транзакция завершена, то все данные записаны на диск.\n\nЕсли в рамках запроса выполняется модификация нескольких таблиц, то без ипользования общей транзакции в случае ошибки\nоткат изменений не будет выполнен или будет выполнен частично, что приведет к _неконсистентности_ данных.\n\nВ PostgreSQL уровень изоляции по-умолчанию Read Committed, т.е. гарантирует отсутствие Lost Updates и Dirty Reads.\n\nТ.к. операции в бизнес сценарии часто подразумевают изменения в нескольких таблицах, то все эти изменения нужно\nзаворачивать в единую транзакцию, чтобы достичь консистентности данных.\n\nЕсли брать классическое Spring Boot приложение со Spring MVC, то выделяется три главных части:\n\n* web: `@Controller`, `@ControllerAdvice`, `Filter`, и т.п. – уровень представления, здесь находится описание API.\n* service: `@Service`, `@Component` – бизнес логика приложения.\n* dao: `@Entity`, `@Repository`, `CrudRepository`, `JpaRepository` и т.п. – слой доступа к данным.\n\nТранзакции нужно использовать на уровне service, т.к. именно там находится бизнес-логика приложения и именно этот слой\nответственен за корректность (консистентность) работы с данными.\n\nУровень web является представлением и его задача – описание API, а значит бизнес логики (а значит и транзакций) на этом\nуровне быть _не должно_.\n\nУровень dao является слоем доступа к данными, здесь обычно описываются _отдельные_ обращения к БД, а значит оборачивать\nих в транзакцию бессмысленно.\n\nПолучается что использование транзакций должно находится на уровне service, т.к. на этом слое находится бизнес логика\nприложения.\n\nРассмотрим подробнее разбиение бизнес функционала по сервисам. Если в сервисе выделяется больше одной доменной области,\nнапример, User и Wallet, то все классы (web, mappings, models, dao, services), связанные с ними, должны находится в\nотдельном пакете user и wallet соответственно.\n\n```\nsrc/\n  main/\n    java/\n      ru/vtb/\n        user/\n          dao/\n          models/\n          services/\n          web/\n        wallet/\n          dao/\n          models/\n          services/\n          web/\n```\n\n* Для того, чтобы сервис (`@Service`) был изолированный, он должен взаимодействовать только с DAO и репозиториями из\n  своего домена. Т.е. если нам в `WalletService` нужно получить пользователя, то мы должны использовать `UserService`, а\n  не работать напрямую с `UserRepository`. Иначе нарушается Single Responsibility принцип и сильно усложняются unit\n  тесты.\n* Если есть какие-то общие классы, сервисы, то они выносятся в пакет common (например, `@RestControllerAdvice`).\n\nРазбиение по доменным сущностям:\n\n* Доменная область обычно 1 к 1 связана с бизнес-процессом, т.е. у вас в одном пакете есть контроллеры (и сервисы),\n  которые выполняют разную функциональность из разных Use Case (работа с пользователем (создание, блокировка) и работа с\n  кошельком (создание, пополнение, закрытие)), то это обычно различные доменные области.\n* Если у вас есть необходимость в sql / jpa запросе использовать join на таблицы из разных _независимых_ доменных\n  областей, то лучше это делать в java коде, потому что в случае дальнейшего распила сервиса на части, сущности из этого\n  join могут начать относиться к разным сервисам, а значит join придется распиливать. Другими словами, если у нас есть\n  отношение User -\u003e Address, причем Address не может существовать без User, то для этих сущностей можно и нужно\n  использовать join, т.к. они в одной доменной области user. А если у нас есть User и Wallet, то эти сущности уже из\n  разных доменных областей и использование join может усложнить дальнейших рефакторинг.\n* К одной доменной сущности могут относится объекты, которые будут невалидны без основной сущности. Например, User -\u003e\n  Address, адрес будет невалиден без привязки к пользователю, но Address -\u003e Country, Address -\u003e City уже не будут в\n  одном домене, т.к. Country и City могут потребоваться в других процессах.\n\nИ вообще основное правило всего - поддерживать структуру сервисов, мапперов, репозиториев в соответствии с доменной\nмоделью.\n\nЕсли в рамках бизнес операции используются только запросы на чтение, то нужно в транзакции\nуказать `@Transactional(readOnly = true)`.\n\nАннотацию `@Transactional` нужно указывать в реализации и лучше аннотировать ей каждый метод, где это нужно.\nПомечать `@Transactional` декларацию методов в интерфейсе не стоит, т.к. это выдает детали внутренней реализации и, если\nв реализации этой аннотации не будет, то по факту транзакция создастся (т.к. Spring увидит `@Transactional` в\nинтерфейсе), но по коду это будет неочевидно.\n\n##### Использование автогенерации схем данных JPA в прод среде запрещено\n\nАвтогенерация DDL занимает много времени, т.к. Hibernate через метаинформацию вытягивает структуру БД и сравнивает ее с\nописанием в `@Entity`.\n\nПравильным и контролируемым подходом для работы со схемой базы данных являются скрипты миграции. Для Java есть два\nосновных инструмента:\n\n* [Flyway](https://flywaydb.org/documentation/usage/plugins/springboot), интеграция со Spring\n  Boot [Use a Higher-level Database Migration Tool](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto.data-initialization.migration-tool)\n  .\n* [Liquibase](https://liquibase.org/get-started/quickstart), интеграция со Spring\n  Boot [Using Liquibase with Spring Boot](https://docs.liquibase.com/tools-integrations/springboot/springboot.html).\n\nLiquibase более мощный инструмент, например он умеет делать rollback изменений или импорт данных из CSV, но описание\nмиграций в нем реализуется через XML, что приносит некоторые неудобства.\n\nДля production среды нужно _полностью_ выключить генерацию DDL.\n\n```properties\nspring.jpa.generate-ddl=false\nspring.jpa.hibernate.ddl-auto=none\n```\n\nДля тестовых сред возможно использовать уровень `validate`, чтобы гарантировать консистентность схемы БД и\nописания `@Entity`.\n\n```properties\nspring.jpa.generate-ddl=true\nspring.jpa.hibernate.ddl-auto=validate\n```\n\n##### Применять тип загрузки FetchType.LAZY\n\nСуществуют 4 типа связей сущностей в Hibernate:\n\n* `@OneToOne` (EAGER) – связь 1:1, реализуется через Foreign Key, реализовать LAZY без отдельных костылей нельзя.\n* `@OneToMany` (LAZY) – возвратный ключ, указывает на список записей, которые ссылаются через Foreign Key на текущую\n  запись. Делать связь EAGER плохая практика, т.к. на каждый запрос будет подниматься большое количество лишних записей.\n  Если в каком-то случае нужны все записи, то можно использовать `join fetch` или `@EntityGraph`.\n* `@ManyToOne` (EAGER) – прямой ключ на запись, в описании указывается `@JoinColumn`. Если эта связь не нужна во всех\n  запросах, то лучше ее тоже делать LAZY, а поднимать только в случае необходимости.\n* `@ManyToMany` (LAZY) – связь многое-ко-многим, реализуется через смежную таблицу. Делать EAGER нельзя, т.к. это\n  свидетельствует о плохо спроектированной базе данных.\n\nИзменение типа связи с LAZY на EAGER _крайне_ не рекомендуется, это может очень негативно сказаться на\nпроизводительности, т.к. при поднятии одной сущности, будут подниматься еще N дополнительных сущностей.\n\nПри этом, если связь помечена LAZY, а обращение к ней выполняется вне транзакции, то будет\nвыброшен `LazyInitializationException` (подробнее в примерах). Для предотвращения такой ситуации нужно явно использовать\nтранзакции и (или) использовать `join fetch` и `@EntityGraph` в случае, когда эти данные нужны в получаемом результате.\n\n### Пояснения и комментарии\n\n#### Использование MapStruct\n\nОтдавать в ответе сервиса сущность `@Entity` очень плохая практика, т.к. это приводит к некотролируемому поведению\nприложения. Создают специальные сущности, именуемые DTO (Data Transfer Object), которые служат моделями для запросов /\nответов.\n\nЭто в свою очередь приводит к необходимости писать мапперы в/из DTO из/в `@Entity`. Часто для решения этой проблемы\nиспользуют библиотеку [MapStruct](https://mapstruct.org/documentation/stable/reference/html/). Но при сложных объектах\nона может сильно усложнить и запутать код и привести к лишним запросам в базу данных при маппинге.\n\nЗадача маппера просто переложить готовые данные из одного объекта в другой. Маппер – это сервис в рамках многослойной\nархитектуры нашего приложения, а значит он содержит бизнес логику, а следовательно его нужно тестировать.\n\nМапперы следует делать быть максимально простыми, в идеальном случае они должны заниматься только перекладыванием\nплоских полей (`String`, `Integer`, `BigDecimal`) из объекта в объект. Если объект является составным, то для каждой\nдоменной сущности должен быть написан свой маппер. Структурная зависимость мапперов должна повторять структуру доменной\nобласти для простоты понимания, поддержки и соблюдения Single Responsibility Principle.\n\nВесь маппинг строить в виде \"звезды\" _от доменной сущности_, то есть, избегать маппинг DTO1 -\u003e DTO2, – это упрощает\nподдержку быстроменяющихся DTO. Также могут получиться такие зависимости DTO1 -\u003e DTO2 -\u003e `@Entity`, тогда придется\nподдерживать DTO2 даже если он уже не используется, или удалять его с переписыванием маппера в DTO1.\n\nДля сложных составных объектов нужно разделять операции создания и редактирования\n(рассматриваем маппинг DTO -\u003e `@Entity`):\n\n* Для создания можно полностью использовать маппер, а на уровне `@Entity` на поля `@OneToMany`, `@ManyToOne`,\n  `@ManyToMany` проставить `cascade = CascadeType.PERSIST`, чтобы Hibernate по цепочке создал вложенные объекты и\n  привязал их к основному. Т.к. здесь используется Hibernate, эта операция должны выполняться в транзакции.\n* Операцию обновления сложной сущности нужно делать руками, используя MapStruct только для перекладывания плоских полей.\n    * Если требуется обновить сущность `@ManyToOne`, то просто переходим в связную сущность и обновляем в ней\n      необходимые поля, с помощью `cascade = CascadeType.MERGE` Hibernate выполнит обновление связной сущности.\n    * Если выполняется частичное обновление (метод PATCH) и требуется обновить массив записей `@OneToMany`, значит надо\n      по ID из массива получить нужную запись для обновления. Как и в примере выше, с\n      помощью `cascade = CascadeType.MERGE` Hibernate выполнит обновление связной сущности при сохранении.\n    * Если выполняется полное обновление (метод PUT), то среди существующих записей ищутся все записи по ID из запроса:\n      * найденные записи обновляются (с помощью `cascade = CascadeType.MERGE` Hibernate их обновит);\n      * с отсутствующих о записях убирается связь с главной сущностью и с помощью `orphanRemoval = true` Hibernate их\n        удаляет;\n      * новые сущности, которые есть в запросе, просто создаются и помощью `cascade = CascadeType.MERGE` Hibernate их\n        создаст.\n\nВ случае использования `cascade` нужно в явном виде перечислять\nоперации: `{ CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH, CascadeType.DETACH }`. Тип `CascadeType.ALL`\nвключает `CascadeType.REMOVE`, который каскадно удаляет все связанные записи.\n\nДля обновления возникнет ошибка:\n\n\u003e org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientPropertyValueException:\n\u003e object references an unsaved transient instance - save the transient instance before flushing :\n\u003e ru.romanow.jpa.domain.Person.address -\u003e ru.romanow.jpa.domain.Address; nested exception is\n\u003e java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved\n\u003e transient instance - save the transient instance before flushing : ru.romanow.jpa.domain.Person.address -\u003e\n\u003e ru.romanow.jpa.domain.Address\n\nТак же при удалении объекта могут потребоваться удалять все подчиненные сущности, а значит нужно будет\nиспользовать `orphanRemoval = true`.\n\n```java\n\n@Target({ METHOD, FIELD })\n@Retention(RUNTIME)\npublic @interface OneToMany {\n    \n    ...\n\n    /**\n     * (Optional) Whether to apply the remove operation to entities that have\n     * been removed from the relationship and to cascade the remove operation to\n     * those entities.\n     */\n    boolean orphanRemoval() default false;\n}\n```\n\n##### Правила работы с Mapper\n\n* Для связи мапперов использовать `uses`: `@Mapper(uses = { AddressMapper.class }`.\n* Если ваше приложение написано на Spring Boot, то мапперы тоже должны быть под управлением\n  Spring: `@Mapper(componentModel = \"spring\")`.\n* Для создания мапперов\n  использовать `@Mapper(componentModel = \"spring\", injectionStrategy = InjectionStrategy.CONSTRUCTOR)`, что позволит\n  создавать маппер в тестах без создания контекста Spring.\n* Нужно избегать внедрения других сервисов в маппер, это упростит поддержку. Лучше сделать что маппер сможет, а\n  остальное уже доставить в сервисе вызова и туда внедрить зависимости.\n* Не рекомендуется использовать `@BeforeMapping`, так как входная (source) сущность может находиться в Persistence\n  Context и её изменения в рамках метода `@BeforeMapping` могут быть неявно сохранены.\n* Не использовать `expression=\"java()\"` для методов с бизнес логикой – это очень сложно тестировать.\n* Полезно использовать `@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)`, а все неиспользуемые поля в маппинге\n  явно указывать через `ignore = true`. Это позволит не пропустить поля в итоговом объекте.\n\n#### Выполнение внешних вызовов из сервиса, помеченного `@Transcational`\n\n##### REST запрос\n\nЕсли в рамках бизнес операции, реализуемой в методе на уровне сервиса, присутствует вызов ко внешней системе (HTTP\nзапрос, gRPC и т.п.), то заворачивать этот метод в транзакцию не стоит, т.к. транзакция будет ждать завершения всей\nоперации.\n\nС другой стороны это удобно, т.к. в случае негативного ответа 4xx/5xx будет выброшен exception и вся транзакция\nоткатится. Этот подход можно использовать только если есть _маленький_ таймаут (200-300ms) на завершение внешнего\nвызова. В PostgreSQL работа с транзакциями реализуется с помощью версионирования (snapshot), это не блокирует\nпараллельные транзакции, но может привести к rollback в случае если данные были модифицированы в рамках другой ранее\nзавершенной транзакции.\n\nЕсли время работы внешнего вызова не фиксировано или большое, то его нужно делать вне транзакции, т.е. мы разбиваем нашу\nбизнес-операцию на две транзакционные части, а внешний вызов выполняется между этими транзакциями. Таким образом, если\nвызов завершился с ошибкой 4xx/5xx, то мы должны _руками_ откатить изменения в первой части бизнес операции.\n\n##### Отправка данных через очередь\n\nОчередь является инструментом асинхронного взаимодействия. Если в рамках бизнес операции требуется отправить данные в\nочередь, то если эта операция выполняется в рамках транзакции, может произойти ситуация, что получатель\n(consumer) получит заявку до того момента, как на отправителе (producer) завершится транзакция, что может привести к\nнеконсистентным данным между отправителем и получаетелем в момент выполнения операции.\n\nДля решения этой ситуации можно следовать подходу, описанному выше: выносить отправку данных из транзакции, либо в\nзаявку, отправляемую в очередь, класть все данные, чтобы получателю не было необходимости приходить за дополнительной\nинформацией к отправителю. Но здесь стоит помнить, что очередь не предназначена для отправки больших объемов данных:\nсообщение в 5-10Kb – ОК, а вот файл или json размером в 1Mb уже плохо.\n\n## Примеры\n\nПараметр `spring.jpa.open-in-view` маппируется в класс `JpaProperties`.\n\n```java\n\n@ConfigurationProperties(prefix = \"spring.jpa\")\npublic class JpaProperties {\n    ...\n\n    /**\n     * Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the\n     * thread for the entire processing of the request.\n     */\n    private Boolean openInView;\n    \n    ...\n}\n```\n\nЕсли выключаем `spring.jpa.open-in-view=false`, тогда при запросе `GET http://localhost:8080/` получаем\nLazyInitializationException.\n\n```\nServlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: could not initialize proxy [ru.romanow.jpa.domain.Address#1] - no Session] with root cause\n\norg.hibernate.LazyInitializationException: could not initialize proxy [ru.romanow.jpa.domain.Address#1] - no Session\n\tat org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:170) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]\n\tat org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:310) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]\n\tat org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]\n\tat org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.4.32.Final.jar:5.4.32.Final]\n\tat ru.romanow.jpa.domain.Address$HibernateProxy$zoO4cARL.getCity(Unknown Source) ~[classes/:na]\n\tat ru.romanow.jpa.mapper.AddressMapperImpl.toModel(AddressMapperImpl.java:24) ~[classes/:na]\n\tat ru.romanow.jpa.mapper.PersonMapperImpl.toModel(PersonMapperImpl.java:32) ~[classes/:na]\n\tat java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195) ~[na:na]\n\tat java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1654) ~[na:na]\n    ...\n```\n\n### Способы исправления\n\n##### Использование `@Transactional` в сервисном слое\n\nЕсли метод в сервисе пометить аннотацией `@Transactional`, тогда подзапросы будут выполняться в рамках сессии:\n\n```java\n\n@Service\n@RequiredArgsConstructor\npublic class PersonServiceImpl\n        implements PersonService {\n    private final PersonRepository personRepository;\n    private final PersonMapper personMapper;\n\n    @Override\n    @Transactional(readOnly = true)\n    public List\u003cPersonResponse\u003e findAll() {\n        return personRepository.findAll()\n                .stream()\n                .map(personMapper::toModel)\n                .collect(Collectors.toList());\n    }\n}\n```\n\nПри этом сначала будет поднята сущность Person, а поле address будет HibernateProxy, который при первом обращении к\nсущности выполнит дополнительный запрос к базе данных и поднимет Address по ID.\n\n![Hibernate Interceptor](images/hibernate_interceptor.png)\n\nПри LAZY инициализации сущности, по ссылке на объект хранится Hibernate Proxy, который реализован с помощью библиотеки\nByteBuddy. При обращении к методу `person.getAddress()` срабатывает method\ninterceptor `$$_hibernate_interceptor: ByteBuddyInterceptor`, который содержит всю необходимую информацию для выполнения\nзапроса к БД. После первого запроса внутри Hibernate Proxy заполняется поле `target` и уже все последующие запросы к\nсущности делегируются к этому полю.\n\n##### Использование `@Query` и конструкции join fetch\n\nЕсли в запросе указать `join fetch` (вместо просто `join`), то Hibernate в блок `select` включит поля из `join` и\nразмапит результат в связанную сущность.\n\n```java\npublic interface PersonRepository\n        extends JpaRepository\u003cPerson, Integer\u003e {\n\n    @Query(\"select p from Person p join fetch p.address\")\n    List\u003cPerson\u003e findPersonAndAddress();\n}\n\n@Service\n@RequiredArgsConstructor\npublic class PersonServiceImpl\n        implements PersonService {\n    private final PersonRepository personRepository;\n    private final PersonMapper personMapper;\n\n    @Override\n    public List\u003cPersonResponse\u003e findAll() {\n        return personRepository.findPersonAndAddress()\n                .stream()\n                .map(personMapper::toModel)\n                .collect(Collectors.toList());\n    }\n}\n```\n\n##### Использовать EntityGraph для конкретного метода\n\nНачиная с версии JPA 2.1 появилась конструкция `@EntityGraph`, с помощью которой можно переопределять порядок загрузки\nсущностей, описанных в `@Entity`. Т.е. если в `@Entity` описано:\n\n```java\n\n@Entity\n@Table(name = \"person\")\npublic class Person {\n\n    ...\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"address_id\", foreignKey = @ForeignKey(name = \"fk_person_address_id\"))\n    private Address address;\n    \n    ...\n}\n```\n\nа в запросе указано `@EntityGraph(attributePaths = \"address\")`, то в едином запросе будет подняты сущности Person и\nAddress.\n\n```java\npublic interface PersonRepository\n        extends JpaRepository\u003cPerson, Integer\u003e {\n\n    @EntityGraph(attributePaths = \"address\")\n    @Query(\"select p from Person p\")\n    List\u003cPerson\u003e findAllUsingGraph();\n}\n\n@Service\n@RequiredArgsConstructor\npublic class PersonServiceImpl\n        implements PersonService {\n    private final PersonRepository personRepository;\n    private final PersonMapper personMapper;\n\n    @Override\n    public List\u003cPersonResponse\u003e findAll() {\n        return personRepository.findAllUsingGraph()\n                .stream()\n                .map(personMapper::toModel)\n                .collect(Collectors.toList());\n    }\n}\n```\n\n##### Использование `@Column` при поиске по ID поля, помеченного `@ManyToOne`\n\nЕсли для запросов сущностей, помеченных `@ManyToOne` нужно поднять сущность по ID, то можно рядом с `@ManyToOne` описать\nсам ID:\n\n```java\n\n@Entity\n@Table(name = \"person\")\npublic class Person {\n\n    ...\n\n    @Column(name = \"address_id\", updatable = false, insertable = false)\n    private Integer addressId;\n\n    @ManyToOne(fetch = FetchType.LAZY)\n    @JoinColumn(name = \"address_id\", foreignKey = @ForeignKey(name = \"fk_person_address_id\"))\n    private Address address;\n    \n    ...\n}\n```\n\n```jpaql\nselect u.name from User u where u.addressId = :addressId\n```\n\n### Запуск приложения\n\n```shell\n# сборка проекта\n$ ./gradlew clean build\n\n# запуск postgres 13 в docker\n$ docker compose up -d\n\n# запуск приложения\n$ ./gradlew bootRun\n\n# выполняем запрос\n$ curl http://localhost:8080/api/v1/persons -v | jq\n```\n\n### Авторы\n\n* Романов Алексей (tg: @romanowalex)\n* Жегалов Андрей (tg: @zhegalovandrey)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fjpa-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fromanow%2Fjpa-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fromanow%2Fjpa-example/lists"}