CUBA笔记1之Screen与Entity 关系

摘要: 本文根据 CUBA Framework v7.2 官方 Quick Start 文档介绍其 Entity 与 Screen 原理,并进行一些扩展。

  1. CUBA 开发过程简介
  2. 只包含 DataType 属性的Basic Screen
  3. 如何处理 many-to-one 的 Screen
  4. 如何处理 one-to-many 的 Screen
  5. 如何处理 many-to-many 的 Screen
  6. 总结

CUBA 简介

CUBA 是一个 RAD (Rapid Application Development)Framework,之所以叫做快速是因为使用该 Framework 开发传统的软件速度很快。如果使用目前主流的开发方法,主要有两种:一种就是前后端分离式开发,另外一种就是使用 MVC 来开发,但是无论使用哪种方法都无法绕开前端的 HTML+CSS 的开发。

之前在研究 JHipster 的时候,已经有一些 RAD 的影子,JHipster 通过 JHL Domain language 来生成 Angular 或者 React 的模型操作的前端代码。但是我们会发现这种生成代码方法十分笨重,而且前端代码十分 Basic 很难使用,前端的可用性上还是需要大量的前端工作才能最终实现。 比如我们想实现一个复杂的表格或者较为复杂的映射关系就必须自己手动实现。对于纯后端开发者不是很友好(当然如果有独立的前端开发者可以忽略)

究竟有没有一种对于后端友好的全栈开发框架呢?目前来开 CUBA 可以胜任。目前可以总结一下优点:

  1. 完善的 Intellij 插件,提供很多工具用于开发
  2. 数据库开发机制比较完善,比如数据库 SQL 的更新以及数据库更新操作等
  3. 使用 JVM 配合 XML 开发 UI(内核是成熟的 Vaadin
  4. UI 整体感觉很专业,定制性较强,比如很容易创建UI来处理 many-to-one 和 one-to-many 的Entity 的关系
  5. 支持 Kotlin

当然有优点就有缺点,我们还是需要根据我们的需求来选择

  1. UI 在美观度上不容易定制
  2. Frontend 不太容易做出更多的前端特殊功能,比较依赖框架的能力
  3. XML 描述 UI 可能不符合口味
  4. 没有基于 spring boot 而是基于 Spring,可能有些现有的功能集成需要进行一些研究

综上可述, CUBA 适用的场景:

  1. 内部系统,不追求 UI 美观独特而注重功能的实现
  2. 没有独立有经验的前端开发者,而希望快速的完成可用的前端
  3. 功能较为传统

CUBA 开发过程简介

根据官方 7.2 版本的 Quick Start,在使用 CUBA 进行开发的过程主要包括如下几个步骤

  1. 创建项目
  2. 根据业务逻辑创建 Entity
  3. 生成数据库脚本并更新数据库
  4. 根据 Entity 关系和需求创建对应的 Screen 实现对 Entity 的基本 CRUD 操作
  5. 根据业务需求增加其他的功能和 UI,比如增加一个 Button 实现某个操作 (Quick start 不包含)
  6. 运行程序
  7. 打包程序并部署

其中个人认为只有第 4 步可能是最重要的,这一步直接决定了UI 和 Entity 是如何关联的。掌握了这一步就基本掌握了 CUBA 的核心,作为一个后端开发者,纯后端的逻辑不难理解,因为基于 Spring,所以符合一切 Spring 的 IOC 机制。通过掌握模型和视图的关系,就可以理解后端和前端的联通的原理。

Local 属性的 Basic Screen

在创建 Screen 的时候,这里有一个 View 的概念,这个 View 就是 Entity 的 View。如何理解这个 View?其实很简单,我们就可以直接理解为如何展示这个 Entity。比如 _local 这个 view 是内置,表示只显示本地属性,也就是不包含关联属性。

Local attribute
An entity attribute that is not a reference or a collection of references to other entities. Values of all local entity attributes are typically stored in one table (with the exception of certain entity inheritance strategies).

Three views named _local, _minimal and _base are available in the views repository for each entity by default:

_local contains all local entity attributes.

_minimal contains the attributes which are included to the name of the entity instance and specified in the @NamePattern annotation. If the @NamePattern annotation is not specified at the entity, this view does not contain any attributes.

_base includes all local non-system attributes and attributes defined by @NamePattern (effectively _minimal + _local).

所以对于简单的独立的 Entity,我们只需 local view 就足够了。我们可以分析一下创建完 Screen 生成的 XML 文件究竟有哪些信息。

简单的讲这里就有两个XML,一个用于浏览,一个用于编辑

浏览 Browser

这个文件主要由两部分组成:data 和 layout。结构十分清晰,data 表示显示什么数据,而 layout 表示如何显示数据。

注意这里

  1. gameBracketsDl 中的 Dl 是 DataLoader 的含义, 所谓 loader 直观上讲就是一个 SQL 执行器,执行 SQL 获取数据
  2. gameBracketsDc 中的 DcDataCollection DataContainer 的含义, loader 返回了数据后,数据就会按照 Entity 对象存放到内存。对于如何将 SQL 数据转化为对象,是更具 View,这里如果是 collection 的 view 是 _local,就表示只把本地属性加载到内存对象中。

在 layout 中主要是使用 gameBracketsDc 来渲染 UI。比如使用 groupTable 指定 dataContainergameBracketsDc 就可以实现对这个列表进行显示了。

当然这里还有很多细节,以后可以每个控件单独介绍,但是在使用中通过文档和自动提醒可以很容易的理解 XML 标签的字段和属性的含义。

xxx-browse.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd"
caption="msg://browseCaption"
focusComponent="gameBracketsTable"
messagesPack="xx.web.screens.gamebracket">
<data readOnly="true">
<collection id="gameBracketsDc"
class="xx.entity.BoGameBracket"
view="_local">
<loader id="gameBracketsDl">
<query>
<![CDATA[select e from bo_GameBracket e]]>
</query>
</loader>
</collection>
</data>
<dialogMode height="600"
width="800"/>
<layout expand="gameBracketsTable"
spacing="true">
<filter id="filter"
applyTo="gameBracketsTable"
dataLoader="gameBracketsDl">
<properties include=".*"/>
</filter>
<groupTable id="gameBracketsTable"
width="100%"
dataContainer="gameBracketsDc">
<actions>
<action id="create" type="create"/>
<action id="edit" type="edit"/>
<action id="remove" type="remove"/>
</actions>
<columns>
<column id="mathName"/>
<column id="displayName"/>
<column id="enumType"/>
</columns>
<rowsCount/>
<buttonsPanel id="buttonsPanel"
alwaysVisible="true">
<button id="createBtn" action="gameBracketsTable.create"/>
<button id="editBtn" action="gameBracketsTable.edit"/>
<button id="removeBtn" action="gameBracketsTable.remove"/>
</buttonsPanel>
</groupTable>
<hbox id="lookupActions" spacing="true" visible="false">
<button action="lookupSelectAction"/>
<button action="lookupCancelAction"/>
</hbox>
</layout>
</window>

编辑 Edit

基本结构类似,但是数据中不在是 collection 了,而是 instance,因为我们关注的是一个Entity 对象的 instance。同样在 layout 中使用 form 来操作编辑,其 dataContainer 是 data 中的 instance。

xxx-edit.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="msg://editorCaption"
focusComponent="form"
messagesPack="xx.web.screens.gamebracket">
<data>
<instance id="gameBracketDc"
class="xx.entity.BoGameBracket"
view="_local">
<loader/>
</instance>
</data>
<dialogMode height="600"
width="800"/>
<layout expand="editActions" spacing="true">
<form id="form" dataContainer="gameBracketDc">
<column width="250px">
<textField id="mathNameField" property="mathName"/>
<textField id="displayNameField" property="displayName"/>
<lookupField id="enumTypeField" property="enumType"/>
</column>
</form>
<hbox id="editActions" spacing="true">
<button action="windowCommitAndClose"/>
<button action="windowClose"/>
</hbox>
</layout>
</window>

Entity 关联属性的 Screen

有了上面的理解基础,我们就可以扩展 View 来支持 Entity 之间的关联关系。内置的View 不包含关联属性,所以如果我们能创建一个 View 包含了关联属性,那么就可以在 UI 上显示了。

对于关联属性主要有三种

  1. one-to-one
  2. one-to-many (many-to-one)
  3. many-to-many

这里主要介绍第二种,为了方便我们介绍,我们引用文档案例并作出适当抽象总结。

一个 Customer 可以有多个 Order, 一个 Order 只能属于一个 Customer。所以 Customer 和 Order 就是 one-to-many 的关系。反过来 Order 对于 Customer 就是 many-to-one 的关系。那么对于这种关系的对象我们一般需要什么样的 UI 呢?

首先最基础的就是在创建 Order 的时候,可以选择一个 Customer,也就是说创建 Many Entity 的时候需要指定 One Entity。因为指定 One 只需要一个下拉列表就可以实现,所以比较简单 (JHipster 生成的 Frontend 也是这种实现思路)。理论上实现了这个 UI 对于功能上就是已经完整。

如果我们能在 Customer 编辑的时候可以显示和编辑 Order 可能体验更好。也就是在 One Entity 的编辑页面显示 Many Entity,显然这里必须是一个列表来实现,而且还需要支持比如多选的操作。

one-to-many (many-to-one)UI 总结:

  1. many entity 编辑 one entity 下拉列表,相对简单,基础功能
  2. one entity 编辑 many entity 列表,扩展功能,该功能包括显示到编辑

下面我们详细介绍一下两种 UI 在Cuba 的实现

如何处理 many-to-one 的 Screen

按照案例来理解就是在 Order 创建的时候可以选择一个 Customer。通过我们对 View 的理解,我们只需要创建一个 View,这个 View 包含对应的关联属性,同时我们可以选择这个关联的 Entity 的某几个字段用于显示。

对于 View 的命名,我们可以 follow convention:[many]-with-[one],比如 order-with-customer,表示在 Order(many)的页面带有 Customer(one)对象的操作。

有了这个 View,我们在创建 Many 端的 Entity 的时候,只需要选择这个 View 即可。这样 Screen 会自动根据 View 创建新的 DataContainer 用于渲染 UI,下面我们可以看一下生成的 XML,并和前面简单的 Entity 对比。

浏览 Browser

这里主要的区别就是 Data 部分,这里 view 换成我们自定义的 view,但是对于 DataLoader 实际上是一样的,因为我们仍然只需要通过 SQL 获取 Many 的 Entity,但是我们通过 View 可以获取到Many Entity 的关联属性,进而显示其对应的字段信息。

1
2
3
4
5
6
7
8
9
10
11
<data readOnly="true">  
<collection id="boExpVariantsDc"
class="xx.BoExpVariant"
view="boExpVariant-wiht-boExperiment">
<loader id="boExpVariantsDl">
<query>
<![CDATA[select e from bo_BoExpVariant e]]>
</query>
</loader>
</collection>
</data>

编辑 Edit

首先是 Data 部分,同样只有 view 变化

1
2
3
4
5
6
7
<data>  
<instance id="boExpVariantDc"
class="xx.BoExpVariant"
view="boExpVariant-wiht-boExperiment">
<loader/>
</instance>
</data>

在 UI 部分增加了一个新的控件,这个控件就是拥有显示 One entity 的 下拉列表,可以选择一个赋值给 Many entity 的关联属性。

1
2
3
4
5
6
<pickerField id="boExperimentField" property="boExperiment">  
<actions>
<action id="lookup" type="picker_lookup"/>
<action id="clear" type="picker_clear"/>
</actions>
</pickerField>

如何处理 one-to-many 的 Screen

在 many-to-one 的 UI 实现的时候,实际上我们并没有自定义任何 UI,只是通过 View 就生成了所需要的 UI。但是对于 one-to-many 则需要我们自行添加 UI。

这里基本原理就是:

  1. 在 One (Customer) Entity Edit UI 增加一个 Collection 的 Data Component,用于加载 Many(Order) Entity
  2. SQL 控制 Collection 只显示和当前 Customer 关联的 Many Entities
  3. 添加 Table 控制显示对应的 DataContainer
  4. 代码控制在 UI 显示之前,需要将当前编辑的 Entity 信息传入到 DataLoader 用于筛选对象。

注意问题

这里我们注意几个问题:

SQL 语句的属性含义

1
<![CDATA[select e from bo_BoExpVariant e where e.boExperiment = :experiment]]>
  1. 首先 from 后面的表名实际上是 Many Entity 注解的 name, BoExpVariant ,注意不是 Table的名称或者对象名称。这里我们理解为我们这里都是在 Entity 场景下操作,所以应该是 Entity

  1. where 的 key 的属性,这个属性可能是 Many entity 的属性,其使用的名字应该是Entity 对象的属性名称

  1. where 的 value ,这个名字比较特殊,并不是内置的,而是我们在代码中控制的,也就是我们上面的第 4 步

如何理解 Edit 逻辑代码

因为我们手工添加的 Data Collection,而这个 Collection 原本是针对一个 Entity 的列表,但是我们并不希望显示所有的 Entity,而是只和当前关联的 One Entity 相关的 Many Entity。但是这个 One Entity 信息只有当我们点击了一个 One Entity 才能具体确定,所以我们必须动态的想这个 Data Collection 的 Data Loader 传递当前选择的是哪个 One Entity 信息。只有这样这个 UI 才能和数据和逻辑解耦而通用。需要传递的数据就是当前要编辑的对象:

1
2
boExpVariantsDl.setParameter("experiment", editedEntity)  
screenData.loadAll()

操作步骤如下:

  1. 首先把页面的 DataLoader 注入到代码中,直观的说通过这个注入,我们可以在代码中操作这个对象,从而操作为 UI 关联的数据
  2. 订阅一个事件,这个事件是 BeforeShowEvent,就是在显示这个 UI 之前需要完成某些操作
  3. boExpVariantsDl.setParameter("experiment", editedEntity) 动态该 DataLoader 增加一个 parameter,这个 parameter 就是当前编辑的对象
  4. 这个 parameter 用在了 SQL 语句来对 Many Entity 进行筛选

当然这里还有一个问题,这个 SQL JPQL 语句究竟是如何执行? 实际上是一个 JPQL

1
select e from bo_BoExpVariant e where e.boExperiment = :experiment

JPQL supports named parameters, which begin with the colon (:). We could write a function returning a list of authors with the given last name as follows:
ref: https://www.objectdb.com/java/jpa/entity/fields

总结

通过上面的介绍,我们基本掌握了 CUBA 逻辑和 UI 相互通信的原理,核心还是数据。通过实践,CUBA 创建一个基础的后台应用速度很快,而且具有一定的可定制性,而且整个框架比较成熟,在开发的过程中没有遇到太多的未知问题,所以对于某些应用场景推荐使用。