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экрана можно получить в его контроллере используя инжектирование:@ViewComponent private DataContext dataContext; -
Если имеется ссылка на некоторый экран, то получить его
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() получает экземпляр, чей идентификатор уже существует в контексте, входящее состояние выигрывает: загруженные атрибуты значения переданного экземпляра копируются в отслеживаемый экземпляр. Такие изменения считаются результатом загрузки более свежего состояния из базы данных, поэтому они не помечают отслеживаемый экземпляр как изменённый.
| Контекст не может различить свежезагруженный экземпляр от устаревшей копии сущности, которую вы уже редактировали. Если вы объедините устаревшую копию сущности (например, выбранную в экране поиска и загруженную с другим фетч-планом), ее значения атрибутов перезапишут несохраненные изменения на отслеживаемом экземпляре без какого-либо предупреждения. Загружайте сущности прямо перед их объединением и избегайте хранения копий сущностей, которые редактируются. |