3.5.3.3. 数据上下文

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

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

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

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

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

合并实体到 DataContext 的示例:

@Inject
private DataContext dataContext;

private void loadCustomer(Id<Customer, UUID> customerId) {
    Customer customer = dataManager.load(customerId).one();
    Customer trackedCustomer = dataContext.merge(customer);
    customersDc.getMutableItems().add(trackedCustomer);
}

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

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

获取 DataContext

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

@Inject
private DataContext dataContext;

如果只有界面的引用,则可以通过 UiControllerUtils 类获取其 DataContext

DataContext dataContext = UiControllerUtils.getScreenData(screenOrFrame).getDataContext();

UI 组件可以通过下面的方法获取当前界面的 DataContext

DataContext dataContext = UiControllerUtils.getScreenData(getFrame().getFrameOwner()).getDataContext();
父数据上下文

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

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

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

@Inject
private ScreenBuilders screenBuilders;
@Inject
private DataContext dataContext;

private void editFooWithCurrentDataContextAsParent() {
    FooEdit fooEdit = screenBuilders.editor(Foo.class, this)
            .withScreenClass(FooEdit.class)
            .withParentDataContext(dataContext)
            .build();
    fooEdit.show();
}

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

public class FooScreen extends Screen {

    @Inject
    private DataContext dataContext;

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

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

@Inject
private Screens screens;
@Inject
private DataContext dataContext;

private void openFooScreenWithCurrentDataContextAsParent() {
    FooScreen fooScreen = screens.create(FooScreen.class);
    fooScreen.setParentDataContext(dataContext);
    fooScreen.show();
}

确保父数据上下文没有使用 readOnly="true" 属性。否则在使用这个上下文作为父上下文的时候会抛出异常。