DataContext

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

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

Чтобы сущность отслеживалась, ее необходимо поместить в DataContext с помощью метода merge(). Если контекст не содержит экземпляра сущности с таким же идентификатором, то контекст создает новый экземпляр и копирует в него состояние переданного. Если контекст уже содержит экземпляр сущности с таким же идентификатором, он копирует в имеющийся экземпляр состояние переданного и возвращает имеющийся экземпляр. Данный механизм позволяет всегда иметь в контексте не более одного экземпляра сущности с конкретным идентификатором.

При помещении сущности в контекст методом merge() весь граф объектов с корнем в данной сущности также помещается в контекст. То есть все связанные сущности, включая коллекции, становятся отслеживаемыми.

Главный принцип использования метода merge() заключается в том, чтобы продолжать работать с возвращенным из метода экземпляром, забывая про переданный. В большинстве случаев возвращенный экземпляр будет другим. Единственное исключение – если в merge() передан экземпляр объекта, ранее возвращенный другим вызовом merge() или find() этого же контекста.

Пример помещения сущности в DataContext:

@ViewComponent
private CollectionContainer<Department> departmentsDc;

@Autowired
private DataManager dataManager;

@ViewComponent
private DataContext dataContext;

private void loadDepartment(Id<Department> departmentId) {
    Department department = dataManager.load(departmentId).one();
    Department trackedDepartment = dataContext.merge(department);
    departmentsDc.getMutableItems().add(trackedDepartment);
}

Для некоторго экрана существует только один экземпляр DataContext. Он создается автоматически, если в XML-дескрипторе экрана есть элемент <data>.

Загрузчики данных, определенные в XML, автоматически помещают загруженные сущности в DataContext, если у них нет атрибута readOnly="true". По умолчанию загрузчики данных в экранах списков сущностей, создаваемых Studio, включают этот атрибут. Поэтому, если вам нужно отслеживать изменения и сохранять измененные сущности в экранах списков, удалите атрибут readOnly="true" у соответствующих загрузчиков.

Если элемент <data> сам имеет атрибут readOnly="true", экран получает пустую реализацию DataContext: merge() возвращает переданный экземпляр без изменений, ничего не отслеживается, а save() ничего не делает, кроме записи сообщения в журнал. Если вы добавите редактируемые UI-компоненты к такому экрану, изменения не сохраняются.

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

Получение DataContext

  1. DataContext экрана можно получить в его контроллере используя инжектирование:

    @ViewComponent
    private DataContext dataContext;
  2. Если имеется ссылка на некоторый экран, то получить его DataContext можно с помощью класса ViewControllerUtils:

    private void sampleMethod(View sampleView) {
        DataContext dataContext = ViewControllerUtils.getViewData(sampleView).getDataContext();
        // ...
    }

Родительский DataContext

Сущности DataContext могут образовывать отношения предок-потомок. Если у экземпляра DataContext есть родительский контекст, он будет сохранять измененные сущности в своего предка вместо того, чтобы сразу отправлять их в хранилище данных. Эта особенность позволяет редактировать агрегаты, когда дочерние сущности должны сохраняться только вместе с родительской. Если атрибут сущности снабжен аннотацией @Composition, фреймворк автоматически установит родительский контекст для экрана деталей этого атрибута, чтобы измененная сущность атрибута сохранялась в контекст сущности-владельца.

Подобное поведение можно легко настроить вручную для любой сущности или экрана.

Если вы программно открываете экран деталей сущности, который должен сохранять изменения в data context текущего экрана, используйте метод withParentDataContext():

private void detailViewWithCurrentDataContextAsParent() {
    DialogWindow<DepartmentDetailView> dialogWindow = dialogWindows.detail(this, Department.class)
            .withViewClass(DepartmentDetailView.class)
            .withParentDataContext(dataContext)
            .build();
    dialogWindow.open();
}

Дополнительные примеры явного использования DataContext приведены в руководстве Data Modeling: Composition.

Удаление и исключение сущностей

  • remove(entity) регистрирует сущность для удаления и останавливает отслеживание. Сущность удаляется из хранилища данных при следующем save(). Контекст также удаляет экземпляр из всех отслеживаемых to-many коллекций.

  • evict(entity) (исключение) просто останавливает отслеживание сущности без регистрации удаления. Используйте его, когда сущность была сохранена или удалена другими способами, и контекст должен забыть о ней.

  • evictModified() исключает все сущности, зарегистрированные как измененные или удаленные; clear() исключает вообще все.

Когда экран деталей сохраняется в родительский DataContext, ничего не записывается в базу данных: модифицированные сущности сливаются в родительский контекст и сохраняются только когда корневой экран сохраняется. Удаленные сущности регистрируются на удаление в родительском контексте.

События и обработчики

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

Чтобы сгенерировать заглушку обработчика в Jmix Studio, выберите элемент данных в XML-коде дескриптора экрана или на панели структуры Jmix UI и используйте вкладку Handlers панели инспектора Jmix UI.

В качестве альтернативы вы можете воспользоваться кнопкой Generate Handler на верхней панели контроллера экрана.

SaveDelegate

По умолчанию DataContext сохраняет измененные и удаленные объекты с помощью метода DataManager.save(SaveContext). Обработчик saveDelegate позволяет настраивать логику сохранения данных, что особенно полезно при работе с DTO сущностями. Например, вы можете сохранить измененные объекты с помощью специального сервиса:

@Autowired
private DepartmentService departmentService;

@Install(target = Target.DATA_CONTEXT)
private Set<Object> saveDelegate(final SaveContext saveContext) {
    return departmentService.saveEntities(
            saveContext.getEntitiesToSave(),
            saveContext.getEntitiesToRemove());
}

Обработчик saveDelegate должен возвращать Set сохраненных экземпляров. Если это невозможно, верните исходные экземпляры из saveContext.getEntitiesToSave() или просто пустой Set. Не возвращайте удаленные экземпляры. Возвращенные экземпляры будут помещены обратно в DataContext, и экран продолжит работу с обновленным состоянием.

См. раздел Работа с сущностями в UI для дополнительной информации о сохранении сущностей с использованием кастомных сервисов.

Экземпляры в saveContext.getEntitiesToSave() являются копиями, созданными DataContext, а не экземплярами, с которыми работает ваш экран. В частности, объект, возвращаемый getEditedEntity() — это не объект, переданный делегату сохранения. Если вашему делегату нужно изменить и сохранить сам редактируемый экземпляр, сначала скопируйте его, используя io.jmix.core.Copier, как описано в разделе Работа с сущностями в UI, в противном случае неудачное сохранение может оставить редактируемый экземпляр с несоответствующей версией оптимистичной блокировки.

ChangeEvent

Это событие отправляется, когда DataContext обнаруживает изменения в отслеживаемой сущности, в контекст помещается новый экземпляр, или при удалении сущности.

@Subscribe(target = Target.DATA_CONTEXT)
public void onChange(final DataContext.ChangeEvent event) {
    log.debug("Changed entity: " + event.getEntity());
}

PreSaveEvent

Это событие отправляется перед сохранением изменений.

В слушателе этого события можно добавлять произвольные экземпляры сущностей в сохраняемые коллекции, возвращаемые методами getModifiedInstances() и getRemovedInstances(), например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave(final DataContext.PreSaveEvent event) {
    event.getModifiedInstances().add(department);
}

Вы также можете предотвратить сохранение, используя метод события preventCommit(), например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave2(DataContext.PreSaveEvent event) {
    if (checkSomeCondition()) {
        event.preventSave();
    }
}

PostSaveEvent

Это событие отправляется после сохранения изменений.

Из слушателя этого события можно получить коллекцию сохраненных сущностей, возвращенных из DataManager или собственного save delegate. Эти сущности уже помещены в DataContext. Например:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPostSave(final DataContext.PostSaveEvent event) {
    log.debug("Saved: " + event.getSavedInstances());
}

Детали реализации

Сущность отслеживается целиком: контекст фиксирует, что сущность изменена, а не какие атрибуты изменились. Установка атрибута обратно на его исходное значение не сбрасывает флаг изменения. Используйте hasChanges(), чтобы проверить, содержит ли контекст измененные или удаленные сущности, и setModified(сущность, false), чтобы вручную сбросить флаг для конкретной сущности.

Когда merge() получает экземпляр, чей идентификатор уже существует в контексте, входящее состояние выигрывает: загруженные атрибуты значения переданного экземпляра копируются в отслеживаемый экземпляр. Такие изменения считаются результатом загрузки более свежего состояния из базы данных, поэтому они не помечают отслеживаемый экземпляр как изменённый.

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