Извлечение данных

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

Сущности модели данных часто имеют ссылки на другие сущности, например, Order имеет связанный Customer в атрибуте Order.customer. Jmix позволяет вам обращаться к связанным сущностям путем навигации по ссылочным атрибутам в коде Java с помощью геттеров, например order.getCustomer().getName(), и в data-aware компонентах UI с помощью точечной нотации, например order.customer.name.

В целом, существует две стратегии загрузки связанных сущностей:

  • Жадная загрузка (eager fetching) означает, что связанная сущность загружается из базы данных вместе с корневой сущностью.

  • Ленивая загрузка (lazy loading) означает, что связанная сущность автоматически загружается из базы данных при обращении к ссылочному свойству.

Ленивая загрузка

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

Например:

String getCustomerName(Id<Order> orderId) {
    Order order = dataManager.load(orderId).one();
    return order.getCustomer().getName(); (1)
}

List<String> getProductNames(Id<Order> orderId) {
    Order order = dataManager.load(orderId).one();
    return order.getLines().stream() (2)
            .map(orderLine -> orderLine.getProduct().getName()) (3)
            .collect(Collectors.toList());
}
1 Загрузка сущности Customer.
2 Загрузка коллекции сущностей OrderLine.
3 Загрузка сущности Product.

Ленивая загрузка очень удобна, но зачастую не обеспечивает наилучшую производительность. Это особенно верно при работе с коллекциями сущностей. Посмотрите на метод getProductNames() в примере выше: он загружает коллекцию строк заказа, а затем для каждой строки отправляется в базу данных за соответствующим продуктом. Это приводит к N+1 запросам, где N - количество строк заказа в коллекции.

Другим примером проблемы N+1 является ленивая загрузка на экране браузера сущностей. Например, если вы загружаете список заказов и для каждого заказа показываете связанного клиента, вам необходимо определить столбец таблицы пользовательского интерфейса, привязанный к атрибуту Order.customer. Затем при ленивой загрузке вы получите 1 запрос базы данных для заказов, затем N запросов для клиентов, где N - размер страницы таблицы заказов.

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

Фетч-планы

Фетч-план (fetch plan) определяет, какой граф объектов должен быть жадно загружен из базы данных в конкретной операции. Его можно использовать в DataManager и компонентах данных UI для оптимизации производительности, а также в REST API для определения формы возвращаемых данных без создания отдельного набора DTO.

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

Пример использования фетч-плана

Давайте рассмотрим несколько вариантов использования фетч-плана с сущностью Order и связанных с ней сущностями, которые составляют следующую модель данных:

fetching diagram 1
  1. Предположим, нам нужно отобразить список заказов на экране просмотра, а таблица UI должна содержать столбцы number, date, amount и customer.name. Тогда оптимальной была бы загрузка следующего графа объектов:

    fetching diagram 2

    Чтобы жадно загрузить этот граф, определим в экране следующий фетч-план:

    <collection id="ordersDc"
                class="dataaccess.ex1.entity.Order">
        <fetchPlan>
            <property name="number"/>
            <property name="date"/>
            <property name="amount"/>
            <property name="customer">
                <property name="name"/>
            </property>
        </fetchPlan>

    В результате фреймворк выполняет один SQL-запрос с соединением таблиц заказов и клиентов, и связанные клиенты жадно загружаются вместе с заказами. Это устраняет проблему N+1, которая возникла бы, если клиенты загружались бы лениво.

    Кроме того, поскольку фетч-план определяет отдельные локальные атрибуты, результирующий набор SQL включает только эти атрибуты и опускает поле customer.email. Это поле не извлекается из базы данных и не использует память сервера в экземпляре сущности. Это хорошо для производительности, но с такими частично загруженными экземплярами сущностей необходимо обращаться осторожно.

    При создании экрана для сущности с помощью Studio мастер создания экрана по умолчанию предлагает фетч-план _base для корневой сущности и выбранных связанных сущностей, поэтому определение фетч-плана будет выглядеть несколько иначе:

    <fetchPlan extends="_base">
        <property name="customer" fetchPlan="_base"/>
    </fetchPlan>

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

  2. Теперь рассмотрим случай редактирования заказа. Экран редактора сущности должен позволять пользователю изменять атрибуты заказа, выбирать клиента, а также создавать и редактировать строки заказа, выбирать продукты для них и пересчитывать общую сумму заказа. В этом случае нам нужны почти все сущности, но некоторые атрибуты связанных сущностей можно опустить:

    fetching diagram 3

    Если вы используете мастер создания экранов Studio и выбираете связанные сущности с фетч-планом по умолчанию _base, в экране создается следующий фетч-план:

    <instance id="orderDc"
              class="dataaccess.ex1.entity.Order">
        <fetchPlan extends="_base">
            <property name="customer" fetchPlan="_base"/>
            <property name="lines" fetchPlan="_base">
                <property name="product" fetchPlan="_base"/>
            </property>
        </fetchPlan>
    Вы можете выбрать для сущностей отдельные локальные атрибуты вместо фетч-плана _base, но мы не рекомендуем использовать его для экрана редактора. Подробнее о частично загруженных объектах см. ниже.

Частичные сущности

Если вы используете фетч-план, который не включает некоторые локальные атрибуты сущности, сущность будет загружена частично. Атрибуты, отсутствующие в фетч-плане, будут пустыми в экземпляре сущности. Если вы обращаетесь к такому атрибуту, вызывая его геттер/сеттер или используя его в компоненте UI, фреймворк выбрасывает исключение, подобное такому:

java.lang.IllegalStateException: Cannot get unfetched attribute [foo] from
    detached object com.company.entity.Bar-7f9e689a-fe04-8b5f-35db-5fa51a9a9d71 [detached].

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

Если вы загружаете одну корневую сущность или небольшую коллекцию, лучше положиться на ленивую загрузку или использовать фетч-план, основанный на _base, чтобы избежать проблемы N+1. В противном случае, с одной стороны, выигрыш в производительности от частичных сущностей будет незначительным; с другой стороны, вы можете столкнуться с проблемами, если случайно обратитесь к неизвлеченным атрибутам.

Идентификатор сущности и атрибуты Version загружаются всегда, независимо от фетч-плана.

Встроенные фетч-планы

Jmix предоставляет три встроенных фетч-плана для каждой сущности:

  • Фетч-план _local включает в себя все локальные атрибуты (непосредственные атрибуты, которые не являются ссылками).

  • Фетч-план _instance_name включает в себя все атрибуты, формирующие имя экземпляра. Это могут быть локальные атрибуты и ссылки. Если для сущности не указано имя экземпляра, этот фетч-план будет пуст.

  • Фетч-план _base включает в себя все атрибуты фетч-планов _local и _instance_name. Он отличается от _local только в том случае, если фетч-план _instance_name включает ссылочные атрибуты.

Используйте фетч-план _base, пока не столкнетесь с проблемой с производительностью при загрузке больших списков "широких" сущностей. Это избавит вас от проблем с неизвлеченными атрибутами, описанными в разделе Частичные сущности.

Создание фетч-планов

Вы можете определить собственные фетч-планы следующими способами:

  1. Как встроенные фетч-планы в дескрипторах экранов UI. Это показано на примере выше.

  2. Программно в Java.

    Вы можете создать фетч-план с помощью фабрики FetchPlans и использовать его в операции DataManager следующим образом:

    @Autowired
    private FetchPlans fetchPlans;
    
    private List<Order> loadOrders() {
        FetchPlan fetchPlan = fetchPlans.builder(Order.class)
                .addFetchPlan(FetchPlan.BASE)
                .add("customer")
                .build();
    
        return dataManager.load(Order.class).all().fetchPlan(fetchPlan).list();
    }

    Также вы можете использовать билдер фетч-планов прямо в fluent интерфейсе загрузки DataManager:

    List<Order> orders = dataManager.load(Order.class)
            .all()
            .fetchPlan(fpb -> fpb.addFetchPlan(FetchPlan.BASE).add("customer"))
            .list();
  3. В общем репозитории фетч-планов.

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

    Во-первых, создайте fetch-plans.xml файл в каталоге resources/<base package> и задайте свойство jmix.core.fetch-plans-config. Можно сделать это в Studio, выбрав New → Advanced → Fetch Plan Configuration File в окне инструментов Jmix.

    После создания файла в меню New → Advanced окна инструментов Jmix появится пункт Fetch Plan. Он позволяет определить фетч-план с помощью дизайнера.

    Ниже приведен пример фетч-плана, определенного в fetch-plans.xml.

    <fetchPlan class="dataaccess.ex1.entity.Order"
               name="full"
               extends="_base">
        <property name="customer" fetchPlan="_instance_name"/>
        <property name="lines">
            <property name="product" fetchPlan="_instance_name"/>
            <property name="quantity"/>
        </property>
    </fetchPlan>

    Этот фетч-план может использоваться по имени в операции DataManager:

    Order order = dataManager.load(Order.class).id(orderId).fetchPlan("full").one();

    Другой вариант – извлечь экземпляр фетч-плана из репозитория:

    @Autowired
    private FetchPlanRepository fetchPlanRepository;
    
    private Order loadOrder(UUID orderId) {
        FetchPlan fetchPlan = fetchPlanRepository.getFetchPlan(Order.class, "full");
        return dataManager.load(Order.class).id(orderId).fetchPlan(fetchPlan).one();
    }