Initial Entity Values

本指南演示为实体实例设置初始值的几种方式。

在宠物诊所示例中有一些用例,在这些用例中的有的实体默认值应该全局设置,而有的需要针对每个用例设置。

将要构建的内容

本指南对 CUBA 宠物诊所 示例进行了增强,以演示为实体设置初始值的不同方式。 特别是会覆盖到以下用例:

  • Visit 实体的 paid 属性进行全局默认值设置

  • 定期检查(regular checkup )是一个特定类型的就诊活动,应该使用一个特定的描述进行初始化

  • Visitpaid 属性应该在选择了 type 属性后自动填充

  • 自动生成的就诊编号中使用了 Visit 实例的属性

开发环境要求

你的开发环境需要满足以下条件:

下载 并解压本指南的源码,或者使用 git 克隆下来:

示例: CUBA 宠物诊所

这个示例是以 CUBA 宠物诊所项目为基础,而这个项目的基础是众所周知的 Spring 宠物诊所项目。CUBA 宠物诊所应用程序涉及到了宠物诊所的领域模型及与管理一家宠物诊所相关的业务流程。

这个应用程序的领域模型看起来像这样:

领域模型

主要实体是 PetVisit。 一个 Pet 到一家诊所就诊,在诊疗过程(Vist) 会有一名兽医(Vet)负责照顾它。每个宠物都有主人,一个主人可以有多个宠物。一个诊疗过程(Vist)即是一个宠物在主人的帮助下到诊所就诊的活动。

实体字段初始化

简单的属性(BooleanInteger 等) 和枚举可以在实体对应字段的定义时进行初始化。在宠物诊所示例中, Visit 实体有一个 paid 属性,它是布尔类型的, 用于标识一次就诊是否已经付费。这个值应该初始化为 FALSE 。 这个默认值就是直接在字段定义时设置。

Visit.java
public class Visit extends StandardEntity {

    // ...

    @Column(name = "PAID")
    protected Boolean paid = false;

}

另外,可以使用 @PostConstruct 注解给实体创建一个特殊的初始化方法。 在这种情况下,任何全局基础设施接口和托管 Bean 都可以调用。 Visit 实体中就有一个使用 @PostConstruct 进行初始化的示例。

Visit.java
public class Visit extends StandardEntity {

    @Temporal(TemporalType.DATE)
    @NotNull
    @Column(name = "VISIT_DATE", nullable = false)
    protected Date visitDate;

    // ...

    @PostConstruct (1)
    private void initVisitDate() {
        if (visitDate == null) {
            setVisitDate(today());
        }
    }

    private Date today() {
        TimeSource timeSource = AppBeans.get(TimeSource.class); (2)
        return timeSource.currentTimestamp();
    }
}
1 @PostConstruct 注解的方法在所在类的实例创建后执行。 通过 metadata.create(Visit.class) 手动创建实体或通过框架 UI 创建了实例都会调用这个方法。
2 托管 Bean 可以通过 AppBeans.get() 获取。

UI 层初始化

On the UI layer there are two main ways to initialize an entity during screen creation. The first one is internal within the screen controller, that should show the newly created entity. The second one is external, where the entity is defined by the code calling the destination screen.

在 UI 层有两种主要的方式初始化实体。第一种方式是在界面控制器内部,

通过 InitEntityEvent 进行内部初始化

第一个选项是在目标界面控制器内部进行的实体初始化。这种情况下,可以利用 CUBA 平台 UI 事件 。比如 StandardEditor 界面提供了一个用于此目的特殊 UI 事件 :`InitEntityEvent` 。

在宠物诊所示例中,这种内部初始化被用于将常规检查的就诊实例设置为已付费。

RegularCheckup.java
public class RegularCheckup extends StandardEditor<Visit> {

    @Subscribe
    protected void initRegularCheckupVisit(InitEntityEvent<Visit> event) { (1)
        Visit visit = event.getEntity();

        visit.setPaid(false); (2)
    }

}
1 initRegularCheckupVisit 方法订阅了新创建的 Visit 实例的 InitEntityEvent 事件
2 event.getEntity() 返回 Visit 实例,可以在界面初始化期间进行修改

使用这种初始化方式的好处是只需要很少的代码修改就可以达到目标。而缺点是由于初始化是在目标界面内部,不方便响应外部传入的参数。所以,这种方式适用于不需要动态初始化的特定属性。

使用界面构造器进行外部初始化

从 CUBA 7 开始 ,客户端层引入了 ScreenBuilders API 。 它允许使用构造器模式以编程方式创建和打开界面,CUBA 界面构造器模式中可以使用各种选项进行界面配置。

其中一个选项是允许定义一个初始化方法,这个方法会在打开界面前被执行,对于一个编辑界面,可以通过这个方法在界面显示前对实体属性进行修改。

在宠物诊所示例中有一个用于创建特定就诊(visit) 实例的界面,这种就诊是一个“常规检查”,由于这种检查总是有固定的检查项目、检查项目以文本描述的方式的提供。此外,“常规检查” 总是预付费。重复地创建这种就诊记录对于用户来说是一种负担,所以 实现了一个界面,这个界面会为这种情况合理地初始化一些属性值。

VisitBrowse.java
@Inject
private ScreenBuilders screenBuilders;

@Inject
private MessageBundle messageBundle;

@Inject
private GroupTable<Visit> visitsTable;

// ...


@Subscribe("visitsTable.createRegularCheckup")
public void createForPet(Action.ActionPerformedEvent event) {
    screenBuilders.lookup(Pet.class, this)
            .withSelectHandler(pets -> {
                createVisitForPet(pets.iterator().next());
            })
            .withLaunchMode(OpenMode.DIALOG)
            .build()
            .show();
}

private void createVisitForPet(Pet pet) {
    screenBuilders.editor(visitsTable) (1)
        .newEntity() (2)
        .withInitializer(visit -> { (3)
            visit.setPaid(true);
            visit.setDescription(regularCheckupDescriptionContent(pet));
            visit.setPet(pet);
        })
        .withScreenClass(RegularCheckup.class)
        .withLaunchMode(OpenMode.DIALOG)
        .build()
        .show();
}
1 使用 screenBuilders bean 创建一个 EditorBuilder <2> EditorBuilder 被设置为创建模式
2 withInitializer 方法接受一个 Consumer<Visit> 接口,接口实现使用 Visit 实例作为参数,可以在 Consumer<Visit> 接口实现中对 Visit 实例进行调整

使用 ScreenBuilder API 和外部信息,就可以以更加动态地方式创建初始值。如上面的示例所示,在第一步选择了宠物,根据这个选择动态生成了一段描述, 并且这段描述被设置给了新建的 Visit 实体。

The downside of this approach is that it is a little less invasive in terms of source code. 同时这种方式也不能不做任何修改,直接重用默认的操作(创建/编辑)。

如这个示例所示,也可以同时使用两种方式,在不同的场景下使用不同的方式。 宠物实例和就诊描述在外部初始化,支付状态在内部初始化。

相应的 UI 界面看起来是这样:

Step 1: 选择宠物
Step 2: Prefilled visit screen

相关属性的初始化

另一种初始化相关的类型是:基于用户在界面输入的一个字段值来初始化一个属性。这种情况也可以在实体的保存期间处理。

在 Change 事件处理中进行属性初始化

在第一种情况中,实体的一个属性依赖于另一个属性,当依赖的属性发生变化时要计算或初始化这个属性,并且这个属性的变化要能实时显示在界面上。

在宠物诊所示例中,Visit 实体有一个 VisitType 类型属性。 这个类型可以是 VISITREGULAR_CHECKUPSURGERY 。这个类型也标识出就诊是需要预支付还是根据账单支付。 但这个支付方式只是一个建议,还是允许用户根据具体情况来修改支付方式。

要实现这个效果,可以注册一个钩子(hook)方法,这个方法订阅编辑器中实例容器的 ItemPropertyChangeEvent 事件。

VisitEdit.java
public class VisitEdit extends StandardEditor<Visit> {

    @Subscribe(id = "visitDc", target = Target.DATA_CONTAINER)
    protected void onVisitDcItemPropertyChange(
        InstanceContainer.ItemPropertyChangeEvent<Visit> event) { (1)

        if (visitTypeChanged(event)) {
            updateHasToBePayedUpfrontValue(event);
        }
    }

    private boolean visitTypeChanged(
        InstanceContainer.ItemPropertyChangeEvent<Visit> event) {
        return event.getProperty().equals("type");
    }

    private void updateHasToBePayedUpfrontValue(
        InstanceContainer.ItemPropertyChangeEvent<Visit> event) {

        VisitType selectedVisitType = (VisitType) event.getValue(); (2)

        if (selectedVisitType != null) {
            event.getItem().setPaid(selectedVisitType.isToBePayedUpfront()); (3)
        }
    }

}
1 文章实例容器的 ItemPropertyChangeEvent 事件
2 event.getValue() 返回选中的 VisitType 类型的值
3 可以根据 selectedVisitType 调整 "paid" 标志

通过数据上下文的 PreCommitEvent 事件初始化属性

第二种方式是在数据保存期间,如果一个属性只是需要计算,不需要显示在界面上,可以使用这种方式。

在宠物诊所示例中, Visit 实体有一个属性叫作 visitNumber , 它是就诊记录的唯一编号,需要自动生成,与用户输入的输入无关。 visitNumber 应该由就诊年份、就诊类型和以一个6位的数字组成。

这个号码只在就诊记录保存时生成,因此在这里可以使用 UI 控制器中数据上下文的机制。在这方面有有多个选择。 在 CUBA 7 ,数据上下文生命周期事件允许拦截数据保存操作,这样在被编辑实体保存期间就可以执行一些逻辑。

对于宠物诊所示例,这种情形就是上面描述的在就诊实体保存时直接生成 visitNumberVisitNumberGenerator bean 包含生成就诊号码的逻辑。

VisitNumberGenerator.java
@Component(VisitNumberGenerator.NAME)
public class VisitNumberGenerator {

    static final String NAME = "petclinic_VisitNumberGenerator";

    @Inject
    UniqueNumbersService uniqueNumbersService;

    public String generateVisitNumber(Visit entity) {
        int visitType = entity.getType().getCode();
        int visitYear = localDate(entity.getVisitDate()).getYear();

        long nextVisitNumber = uniqueNumbersService.getNextNumber(
            String.format("VISIT_%d_%d", visitYear, visitType)
        ); (1)

        return String.format("%4d%02d%06d", visitYear, visitType, nextVisitNumber);
    }

    // ...
}
1 对于每种 visitYearvisitType 的组合生生成一个唯一号码

在两个 Visit 编辑器中都可以找到对这个 bean 的使用。 DataContext.PreCommitEvent 事件用于注册调用 VisitNumberGenerator 的相关逻辑,只有在没有就诊号码时才生成。

VisitEdit.java
public class VisitEdit extends StandardEditor<Visit> {

    @Inject
    private VisitNumberGenerator visitNumberGenerator;

    // ...

    @Subscribe(target = Target.DATA_CONTEXT)
    protected void onPreCommit(DataContext.PreCommitEvent event) { (1)
        Visit visit = getEditedEntity();
        if (visit.getVisitNumber() == null) {
            visit.setVisitNumber(visitNumberGenerator.generateVisitNumber(visit)); (2)
        }
    }

}
1 注册数据上下文的 PreCommitEvent 事件
2 VisitNumberGenetator 用于生成唯一就诊号码

总结

本指南展示了关于初始化实体值的几种方式。一种方式是在实体上使用 @PostConstruct 注解直接进行属性值的分配。

这种初始化方式在所有场景下都可用,因此这是一种最常用的属性初始化方式。但是有时候初始化属性时需要更多的上下文信息,比如用户在界面上的选择。

因此,可以在 UI 层执行属性的初始化。如前文所述,有多种方式可以做到这点。这种方式允许开发人员设计出更灵活的用户界面。

UI 层的属性初始化方式只在用户使用特定与系统交互时才起作用。经常会有其它的场景,比如 REST API 、批量编辑器功能、数据导入场景等。如果需要满足所有场景的实体属性初始化,应该优先选择 实体字段初始化

此外,演示了两种不同方式的依赖属性初始化,这两种依赖属性初始化方式会被经常用到。可以通过表单字段的 change 事件初始化属性,或者通过数据上下文事件在实体保存前初始化属性。