数据上下文

DataContext 是跟踪加载到 UI 层的实体改动的接口。跟踪实体的任何属性修改后都标记成 “dirty”(表示发生变化),然后 DataContext 会在调用 commit() 方法的时候将发生变化的实体发送到中间件进行保存。

DataContext 内,具有唯一标识符的实体总是以单一的对象实例呈现,不管对象关系图中它在哪里被使用或者使用了多少次。

为了能跟踪实体变化,必须使用其 merge() 方法将实体放入 DataContext 中。如果数据上下文不包含同样id的实体,则会创建一个新实例,将传递的实体状态拷贝至新实例,并将新实例返回。如果上下文已经有同样id的实例,则会将传递实例的状态拷贝至已经存在的实例并返回。使用这个机制保证在数据上下文中对于同一个 id 始终只有一个实例。

当合并实体时,实体内包含根节点的整个实体对象关系图都会被合并。也就是说,所有的引用实体(包括集合)都会处于被跟踪状态。

使用 merge() 方法的重要原则就是,使用返回的实例进行继续操作而丢掉传入的那个实例。在很多情况下,返回的对象实例会跟传入的不同。唯一的例外是在给 merge() 方法传递实例时,如果该实例是在同一个数据上下文中调用另一个 merge() 或者 find() 返回的实例,则没有区别。

合并实体到 DataContext 的示例:

@Autowired
private DataContext dataContext;

@Autowired
private DataManager dataManager;

@Autowired
private CollectionContainer<Department> departmentsDc;

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

对于一个特定的界面和它所有的内部 fragment 来说,只存在一个 DataContext 单例。在界面 XML 描述存在 <data> 元素的情况下创建。

<data> 元素可以有 readOnly="true" 属性,此时会使用一个特殊的 “不操作” 的 DataContext 实现,该实现不跟踪实体的改动,因此不会影响性能。默认情况下,Studio 生成的实体浏览界面会有只读的数据上下文,所以如果需要在实体浏览界面跟踪实体改动并且提交改动的实体,需删除 XML 的 readOnly="true" 属性。

如果关联实体没有包含在界面的 fetch plan 中,而是通过 延迟加载 进行获取,那么关联实体不会合并到界面的 DataContext,因此也无法追踪其改动。所以需要确保界面编辑的所有实体必须通过 fetch plan 预加载包含的引用实体。

获取 DataContext

  1. 界面的 DataContext 可以在控制器用注入的方式获取:

    @Autowired
    private DataContext dataContext;
  2. 如果只有界面的引用,则可以通过 UiControllerUtils 类获取其 DataContext

    private void sampleMethod(Screen sampleScreen) {
        DataContext dataContext = UiControllerUtils.getScreenData(sampleScreen).getDataContext();
        // ...
    }
  3. UI 组件可以通过下面的方法获取当前界面的 DataContext

    DataContext dataContext = UiControllerUtils.getScreenData(getFrame().getFrameOwner()).getDataContext();

父 DataContext

DataContext 实例可以形成父子关系。如果一个 DataContext 有父上下文,它会将改动的实体提交给父上下文而不是直接保存至数据存储。通过这个功能支持编辑实体的组合关系,从实体只能跟主实体一起保存到数据库。如果一个实体属性使用 @Composition 注解,框架会自动在此属性的编辑界面设置父上下文,从而该属性的改动会保存到主实体的数据上下文。

可以很容易为任何实体和界面提供与此相同的行为。

如果打开的编辑界面需要提交数据到当前界面的数据上下文,可以使用 builder 的 withParentDataContext() 方法:

@Autowired
private ScreenBuilders screenBuilders;

@Autowired
private DataContext dataContext;

private void editScreenWithCurrentDataContextAsParent() {
    PersonEdit personEdit = screenBuilders.editor(Person.class, this)
            .withScreenClass(PersonEdit.class)
            .withParentDataContext(dataContext)
            .build();
    personEdit.show();
}

如果使用 Screens bean 打开简单界面,需要提供 setter 方法接收父数据上下文:

public class SmplScreen extends Screen {

    @Autowired
    private DataContext dataContext;

    public void setParentDataContext(DataContext parentDataContext) {
        dataContext.setParent(parentDataContext);
    }

}

然后在创建了界面之后使用:

@Autowired
private DataContext dataContext;

@Autowired
private Screens screens;

private void openSmplScreenWithCurrentDataContextAsParent() {
    SmplScreen smplScreen = screens.create(SmplScreen.class);
    smplScreen.setParentDataContext(dataContext);
    smplScreen.show();
}
确保父数据上下文没有使用 readOnly="true" 属性。否则在使用其作为父上下文的时候会抛出异常。

事件和事件处理器

本章节介绍可以在界面控制器处理的 DataContext 生命周期事件。

如需使用 Jmix Studio 生成处理器的桩代码,需要在界面 XML 描述或者 Component Hierarchy 面板选中 data 元素,然后用 Component Inspector 面板的 Handlers 标签页生成。

或者可以使用界面控制器顶部面板的 Generate Handler 按钮。

ChangeEvent

该事件在上下文检测到跟踪实体变化时发送,合并了新实体或者删除了旧实体。

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

PostCommitEvent

该事件在提交改动之后发送。 在事件监听器中,可以获取从 DataManager 或自定义 提交代理 提交之后的实体集合。这些实体已经合并至 DataContext。示例:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPostCommit(DataContext.PostCommitEvent event) {
    log.debug("Committed: " + event.getCommittedInstances());
}

PreCommitEvent

该事件在提交改动之前发送。 在对应的事件监听器中,可以在 getModifiedInstances()getRemovedInstances() 方法返回的实体机和中添加任何实体,示例:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
    event.getModifiedInstances().add(user);
}

还可以用事件的 preventCommit() 方法阻止提交,示例:

@Subscribe(target = Target.DATA_CONTEXT)
public void onPreCommit(DataContext.PreCommitEvent event) {
    if (checkSomeCondition()) {
        event.preventCommit();
    }
}

CommitDelegate

默认情况下,DataContext 使用 DataManager.save(SaveContext) 方法提交更改和删除的实体。commitDelegate 处理器支持自定义保存数据的逻辑,这在使用 DTO 实体 时特别有用。例如,可以使用自定义服务保存更改的实体:

@Autowired
private SampleService service;

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