Загрузка ленивых полей

—

от автора

Привет, хабр! Сегодняшняя статья навеяна довольно стандартной ситуацией – существует некий «большой» объект, но для работы приложения далеко не всегда требуется загружать его полностью в память. Для решения такой проблемы существует ленивая загрузка полей. Суть её состоит в том, что загрузка поля объекта откладывается до того момента, как оно [поле] понадобится.

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

Разберем следующий пример: объекты хранятся в локальном хранилище, для работы с которым используем Spring Data.

Существует база данных со следующими таблицами: Организация, Сотрудник, Коды организации (да, коды организации могли бы быть встраиваемой сущностью, но для наглядности выделены в отдельную таблицу). Они [таблицы] представляются следующими сущностями:

Организация
@Data @Accessors(chain = true) @Entity @NoArgsConstructor @Table(name = "org") public class Organization {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @OneToOne(targetEntity = Employee.class, fetch = FetchType.LAZY)     @JoinColumn(name = "head_id")     private Employee head;        @OneToMany(targetEntity = Employee.class, mappedBy = "organization", fetch = FetchType.LAZY)     private Set<Employee> employees = new HashSet();        @OneToOne(targetEntity = OrganizationCodes.class, fetch = FetchType.LAZY)     @JoinColumn(name = "code_id")     private OrganizationCodes codes; }

Сотрудник
@Data @Accessors(chain = true) @Entity @NoArgsConstructor @Table(name = "employee") public class Employee {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "first_name", nullable = false)     private String firstName;      @Column(name = "last_name", nullable = false)     private String lastName;      @ManyToOne(targetEntity = Organization.class, fetch = FetchType.LAZY)     @JoinColumn(name = "org_id")     private Organization organization; }

Коды организации
@Data @Accessors(chain = true) @Entity @NoArgsConstructor @Table(name = "employee") public class OrganizationCodes {      @Id     @GeneratedValue(strategy = GenerationType.IDENTITY)     private Long id;      @Column(name = "inn", nullable = false)     private String inn;          @Column(name = "kpp", nullable = false)     private String kpp;          @Column(name = "ogrn", nullable = false)     private String ogrn; }

Как можно заметить, все поля организации (кроме идентификатора) являются «ленивыми».

Для каждой сущности написаны простые репозитории и сервисные классы.

Репозитории
@Repository public interface OrganizationRepository extends JpaRepository<Organization,Long> { }
@Repository public interface EmployeeRepository extends JpaRepository<Employee, Long> {     Set<Employee> findByOrganization(Organization organization); }
@Repository public interface OrganizationCodesRepository extends JpaRepository<OrganizationCodes, Long> { }

Сервисные классы
@Service public class OrganizationServiceImpl implements OrganizationService {      private final OrganizationRepository repository;      @Autowired     public OrganizationServiceImpl(OrganizationRepository repository){         this.repository = repository;     }      @Override     public Organization getById(long orgId) {         return repository.getById(orgId);     } }

Реализация остальных сервисов не важна в рамках статьи, поэтому приведу только интерфейсы

public interface EmployeeService {     Employee getById(long employeeId);     Set<Employee> getByOrgId(long orgId); }
public interface OrganizationCodesService {     OrganizationCodes getById(long id); }

Итак, если мы загрузим организацию и попробуем что то сделать, например, с полем head

    private String getHeadNameFromOrg(long orgId) {         Employee head = organizationService.getById(orgId).getHead();         return head.getFirstName();     }

то увидим ошибку:org.hibernate.LazyInitializationException: could not initialize proxy [ru.brutforcer.org.service.entity.Organization#1] - no Session. Это потому, что данное поле является «ленивым», и просто так с ним не поработать.

Загрузить ленивое поле в Spring Data (да и вообще в Hibernate) можно следующим образом:

1) Указать аннотацию @Transactional на методе, и работать с полем в пределах одной транзакции:

    @Transactional     public String getHeadNameFromOrg(long orgId) {         Employee head = organizationService.getById(orgId).getHead();         return head.getFirstName();     }

2) Указать необходимость загрузки поля непосредственно в методе репозитория

@Repository public interface OrganizationRepository extends JpaRepository<Organization,Long> {      @Query("SELECT org FROM Organization org join fetch org.head where org.id = :orgId")     Organization getByIdFetchHead(@Param("orgId") long orgId); }

И, добавив соответствующий метод в сервис, использовать уже новый метод

    public String getHeadNameFromOrg(long orgId) {         Employee head = organizationService.getByIdFetchHead(orgId).getHead();         return head.getFirstName();     }

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

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

Интерфейс сервисного класса

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

public interface OrganizationService {      Organization getById(long orgId);      Organization getByIdFetchHead(long orgId);     Organization getByIdFetchCodes(long orgId);     Organization getByIdFetchEmployees(long orgId);          Organization getByIdFetchHeadFetchCodes(long orgId);     Organization getByIdFetchCodesFetchEmployees(long orgId);     Organization getByIdFetchEmployeesFetchHead(long orgId);          Organization getByIdFetchAllProperties(long orgId); }

Если в кратце, количество сочетаний будет 2^n, где n — количество ленивых полей: для 4х полей 16, для 5 полей 32 и т.д.

Рассмотрим другую ситуацию – данные находятся под управлением другого микросервиса, общение с которым происходит посредством HTTP запросов в формате REST. Тут магия Hibernate уже не поможет.

Приведу некоторые уточнения.

  • Имеется возможность запросить каждое из «ленивых полей» отдельно.

  • При загрузке у «ленивых» объектов-полей обязательно заполнено поле id.

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

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

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

Что можно в этом случае сделать? Очевидно, написать метод, который будет кидать запрос и подгружать конкретное поле. Получится примерно то же самое, что и в первом варианте, только немного (совсем чуть-чуть) сложнее.

@Service public class OrganizationServiceImpl implements OrganizationService {      private final OrganizationRepository repository;     private final EmployeeService employeeService;      @Autowired     public OrganizationServiceImpl(OrganizationRepository repository,                                     EmployeeService employeeService) {         this.repository = repository;         this.employeeService = employeeService;     }      @Override     public Organization getByIdFetchHead(long orgId) {         Organization org = repository.getById(orgId);         Employee head = employeeService.getById(org.getHead().getId());         org.setHead(head);         return org;     } }

Теперь загрузка происходит в 2 запроса вместо одного, и тут придется подумать – загружать ли объект жадно, но за один запрос, или только необходимое, но за два. Например, может быть такое, что два мелких запроса будут обрабатываться быстрее одного большого.

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

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

Добавим в основную сущность методы для загрузки ленивых полей (в целом, они копируют поведение обычных геттеров, однако, например, если в проекте используется Project Reactor, они могут возвращать соответствующие Mono или Flux) и копирующий конструктор (т.к. мы говорим о загрузке данных из другого микросервиса, все аннотации, связанные с JPA я убрал):

@Data @Accessors(chain = true) @NoArgsConstructor public class Organization {      private Long id;     private Employee head;     private Set<Employee> employees;     private OrganizationCodes codes;      public Employee loadHead(){         return this.head;     }     public Set<Employee> loadEmployees(){         return this.employees;     }     public OrganizationCodes loadCodes(){         return this.codes;     }      public Organization(Organization org) {         this.id = org.getId();         this.head = org.getHead();         this.codes = org.getCodes();         this.employees = org.getEmployees();     } }

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

@Service public class OrganizationServiceImpl implements OrganizationService {      private final OrganizationRepository repository;     private final EmployeeService employeeService;     private final OrganizationCodesService organizationCodesService;      @Autowired     public OrganizationServiceImpl(OrganizationRepository repository,                                    EmployeeService employeeService,                                    OrganizationCodesService organizationCodesService) {         this.repository = repository;         this.employeeService = employeeService;         this.organizationCodesService = organizationCodesService;     }      @Override     public Organization getById(long orgId) {         Organization org = repository.getById(orgId);         return delegate(org);     }      private Organization delegate(Organization org) {         return new Organization(org){             @Override             public Employee loadHead() {                 if (super.getHead() != null && super.getHead().getFirstName() != null) {                     return super.getHead();                 } else if (super.getHead() != null && super.getHead().getId() != null) {                     Employee head = employeeService.getById(org.getHead().getId());                     super.setHead(head);                     return head;                 } else {                     return null;                 }             }              @Override             public Set<Employee> loadEmployees() {                 if (super.getEmployees() != null)                     return super.getEmployees();                 else {                     Set<Employee> employees = employeeService.getByOrgId(org.getId());                     super.setEmployees(employees);                     return employees;                 }             }              @Override             public OrganizationCodes loadCodes() {                 if (super.getCodes() != null && super.getCodes().getInn() != null) {                     return super.getCodes();                 } else if (super.getCodes() != null && super.getCodes().getId() != null) {                     OrganizationCodes codes = organizationCodesService.getById(super.getCodes().getId());                     super.setCodes(codes);                     return codes;                 } else {                     return null;                 }             }         };     } }

(Примечание: проверка загруженности полей head и codes, помимо обычной проверки на null, содержит проверку еще внутренних полей объектов. Это сделано, потому что по условию у данных объектов обязательно должен быть заполнен id сущности. Соответственно, вероятность того, что эти поля при ленивой загрузке равны null, стремится к нулю. Однако, если уже обязательные поля не загружены, то и объект является лениво загруженным. Так же добавлю, что проверки могут быть иными, а в каких то случаях стоит применить синхронизацию при проверке и загрузке полей таким образом).

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

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


ссылка на оригинал статьи https://habr.com/ru/post/681678/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *