DataContext
DataContext is an interface for tracking changes in entities loaded to the UI layer. Tracked entities are marked as "dirty" on any modification of their attributes, and DataContext saves dirty entities when its save() method is invoked.
Within DataContext, an entity with the given identifier is represented by a single object instance, no matter where and how many times it is used in graphs of entities contained in this context.
In order to be tracked, an entity must be put into DataContext using its merge() method. If the context does not contain the entity with the same id, the context creates a new instance, copies the state of the passed instance to the new one and returns it. If the context already contains an instance with the same id, it copies the state of the passed instance to the existing one and returns the existing instance. This mechanism allows the context to always have only one instance of an entity with a particular identifier.
When you merge an entity, the whole object graph with the root in this entity will be merged. That is all referenced entities (including collections) will become tracked.
The main rule of using the merge() method is to continue working with the returned instance and discarding the passed one. In most cases, the returned object instance will be different. The only exception is when you pass to merge() an instance that was earlier returned from another invocation of merge() or find() of the same context.
|
An example of merging an entity into 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);
}
A single instance of DataContext exists for a given view. It is created if there is <data> element in the view XML descriptor.
Data loaders defined in XML automatically merge loaded entities into the DataContext if they don’t have the readOnly="true" attribute. By default, data loaders in entity list views scaffolded by Studio include this attribute, so if you need to track changes and save dirty entities in a list view, remove the readOnly="true" XML attribute for respective loaders.
If the <data> element itself has the readOnly="true" attribute, the view receives a no-op implementation of DataContext: merge() returns the passed instance unchanged, nothing is tracked, and save() does nothing except writing a log message. If you add editable components to such a view, changes are silently not saved — remove readOnly="true" from the <data> element first.
If a referenced entity is not included in the fetch plan of the view but loaded by lazy loading, it is not merged into the view’s DataContext and hence not tracked for changes. Make sure all entities edited on the view are loaded eagerly by including references to them in the fetch plan.
|
Obtaining DataContext
-
DataContextof a view can be obtained in its controller using injection:@ViewComponent private DataContext dataContext; -
If you have a reference to a view, you can get its
DataContextusing theViewControllerUtilsclass:private void sampleMethod(View sampleView) { DataContext dataContext = ViewControllerUtils.getViewData(sampleView).getDataContext(); // ... }
Parent DataContext
DataContext instances can form parent-child relationships. If a DataContext instance has parent context, it saves changed entities to the parent instead of saving them to the data store. This feature enables editing aggregates when detail entities are saved only together with the master entity. If an entity attribute is annotated with @Composition, the framework automatically sets parent context in the attribute detail view, so the changed attribute entity will be saved to the data context of the master entity.
You can easily provide the same behavior for any entities and views.
If you open a detail view that should save data to the current view’s data context, use the withParentDataContext() method:
private void detailViewWithCurrentDataContextAsParent() {
DialogWindow<DepartmentDetailView> dialogWindow = dialogWindows.detail(this, Department.class)
.withViewClass(DepartmentDetailView.class)
.withParentDataContext(dataContext)
.build();
dialogWindow.open();
}
For more examples of explicit usage of DataContext, refer to the Data Modeling: Composition guide.
Removing and Evicting Entities
-
remove(entity)registers the entity for deletion and stops tracking it. The entity is deleted from the data store on the nextsave(). The context also removes the instance from all tracked to-many collections. -
evict(entity)just stops tracking the entity without registering a deletion. Use it when an entity was saved or deleted by other means and the context must forget it. -
evictModified()evicts all entities registered as modified or removed;clear()evicts everything.
When a detail view saves to a parent DataContext, nothing is written to the database: modified instances are merged into the parent context and are saved only when the root view saves. Removed instances are registered for removal in the parent the same way.
|
Events and Handlers
This section describes the DataContext lifecycle events that can be handled in view controllers.
|
To generate a handler stub in Jmix Studio, select the element in the view descriptor XML or in the Jmix UI structure panel and use the Handlers tab of the Jmix UI inspector panel. Alternatively, you can use the Generate Handler button in the top panel of the view controller. |
SaveDelegate
By default, DataContext saves changed and removed entities using the DataManager.save(SaveContext) method. The saveDelegate handler allows you to customize the logic of saving data, which is especially useful when working with DTO entities. For example, you can save the changed entities with a custom service:
@Autowired
private DepartmentService departmentService;
@Install(target = Target.DATA_CONTEXT)
private Set<Object> saveDelegate(final SaveContext saveContext) {
return departmentService.saveEntities(
saveContext.getEntitiesToSave(),
saveContext.getEntitiesToRemove());
}
The save delegate should return the set of saved instances. If it’s impossible, return the original instances from saveContext.getEntitiesToSave() or just an empty set. Do not return removed instances. DataContext will merge back the returned instances so the view will continue working with the updated state.
See the Working with Entities in UI section for more information on saving entities using custom services.
The instances in saveContext.getEntitiesToSave() are copies created by DataContext, not the instances your view works with. In particular, the object returned by getEditedEntity() is not the object passed to the save delegate. If your delegate needs to modify and save the edited instance itself, copy it first using io.jmix.core.Copier as described in Working with Entities in UI, otherwise a failed save can leave the edited instance with an inconsistent optimistic-lock version.
|
ChangeEvent
This event is sent when the context detects changes in a tracked entity, a new instance is merged or an entity is removed.
@Subscribe(target = Target.DATA_CONTEXT)
public void onChange(final DataContext.ChangeEvent event) {
log.debug("Changed entity: " + event.getEntity());
}
PreSaveEvent
This event is sent before saving changes.
In this event listener, you can add arbitrary entity instances to the saved collections returned by getModifiedInstances() and getRemovedInstances() methods, for example:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave(final DataContext.PreSaveEvent event) {
event.getModifiedInstances().add(department);
}
You can also prevent the save using the preventSave() method of the event, for example:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPreSave2(DataContext.PreSaveEvent event) {
if (checkSomeCondition()) {
event.preventSave();
}
}
PostSaveEvent
This event is sent after saving changes.
In this event listener, you can get the collection of saved entities returned from DataManager or a custom save delegate. These entities are already merged into the DataContext. For example:
@Subscribe(target = Target.DATA_CONTEXT)
public void onPostSave(final DataContext.PostSaveEvent event) {
log.debug("Saved: " + event.getSavedInstances());
}
Implementation Details
An entity is tracked as a whole: the context records that the entity is modified, not which attributes changed. Setting an attribute back to its original value does not reset the modified flag. Use hasChanges() to check whether the context contains any modified or removed entities, and setModified(entity, false) to reset the flag for a particular entity manually.
When merge() receives an instance whose id already exists in the context, the incoming state wins: loaded attribute values of the passed instance are copied onto the tracked instance. Such changes are considered a result of loading a fresher state from the database, so they do not mark the tracked instance as modified.
| The context cannot distinguish a freshly loaded instance from a stale copy of an entity you have already edited. If you merge an outdated copy of an entity (for example, one selected in a lookup view and loaded with a different fetch plan), its attribute values overwrite unsaved changes on the tracked instance without any warning. Load entities right before merging them, and avoid keeping detached copies of entities that are being edited. |