- @PrimaryKeyJoinColumn
-
在
JOINED
继承策略的情况下用于为实体指定外键列,该外键是父类实体主键的引用。参数:
-
name
– 实体的外键列的名称 -
referencedColumnName
– 父类实体的主键列的名称
例如:
@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
-
序言
本手册提供关于 CUBA 框架的参考信息,并且介绍了使用此平台开发商业应用程序的最重要的内容。
如果要使用此平台,需要掌握以下技术知识:
-
Java SE
-
关系型数据库(SQL, DDL)
另外,掌握以下技术和框架对更深层次的了解此平台有很大帮助:
-
Gradle 构建系统
-
Spring Framework - Spring 框架
-
Java Persistence API - JPA
-
Vaadin web 应用程序框架
-
HTML / CSS / JavaScript
-
Java Servlets
如果有任何关于改进本手册的建议,请提交 issue 到 GitHub。如果发现拼写或者单词错误,缺陷或者不一致的地方,请直接 fork 这个仓库并提交修改。谢谢!
1. 安装
- 系统要求
-
-
64-bit 操作系统: Windows,Linux 或者 macOS 。
-
内存 – 使用 CUBA Studio 开发,8GB。
-
硬盘空间 – 10 GB
-
- Java SE Development Kit (JDK)
-
-
安装 JDK 8 然后在终端运行下面命令检查 Java 版本:
java -version
这个命令会输出 Java 版本信息,比如:
1.8.0_202
。CUBA 7.2 支持 Java 8,9,10 和 11。如果不需要支持使用 CUBA 老版本创建的项目(包括迁移到 CUBA 7.2 的项目),那么可以直接安装 Java 11。
注意:不支持 OpenJ9 JVM。
-
设置
JAVA_HOME
环境变量为 JDK 的根目录路径,比如:C:\Java\jdk8u202-b08
。-
在 Windows 系统,可以通过 Computer → Properties → Advanced System Settings → Advanced → Environment variables 来设置。这个变量的值需要加到 系统变量(System variables) 列表中。
-
在 macOS 系统,建议在
/Library/Java/JavaVirtualMachines
安装 JDK,比如/Library/Java/JavaVirtualMachines/jdk8u202-b08
,然后在~/.bash_profile
用下面的命令设置JAVA_HOME
:export JAVA_HOME="$(/usr/libexec/java_home -v 1.8)"
-
-
如果是通过代理服务器上网,在运行开发工具和 Gradle 的时候需要给 JVM 设置一些关于代理的系统参数。这些参数在 这里 有解释(可以查查 HTTP 和 HTTPS 协议的相关参数)。建议在系统级别通过
JAVA_OPTS
环境变量设置这些参数。
-
- 开发工具
-
下面这些工具有助于使用 CUBA 框架开发:
-
CUBA Studio - 构建在 IntelliJ 平台之上并且针对 CUBA 项目量身定做的集成开发环境。可以安装为系统的独立运行程序,或者作为 IntelliJ IDEA 的插件安装(社区版或者终极版都行)。更多信息参考 CUBA Studio 使用手册。
-
CUBA CLI - 一个命令行工具,能帮助创建项目和项目元素的基本脚手架代码:实体,界面,服务,等等。使用这个工具的话,可以用任何 Java IDE 开发 CUBA 应用程序。更多信息参考 CUBA CLI GitHub。
如果不熟悉 Java,推荐使用 CUBA Studio,因为这是最高级最直观的工具。
-
- 数据库
-
最基本的场景中,内置的 HyperSQL (http://hsqldb.org) 就可以用来做数据库服务。对于研究平台能力和做应用程序原型来说足够了。如果需要构建生产环境应用程序,还是推荐安装平台支持的全功能 DBMS,比如 PostgreSQL。
- 网页浏览器
-
基于平台开发的应用的 Web 界面支持所有流行的浏览器,包括 Google Chrome 、Mozilla Firefox 、 Safari 、 Opera 15+ 、 Internet Explorer 11 、 Microsoft Edge。
2. 快速开始
如想进一步学习 CUBA,CUBA 平台的 学习 网页有更多的培训示例。 下一个开发中必要的步骤就是如何在您的应用程序中实现业务逻辑。参考以下指南: 如需设计更为复杂的数据模型,参考以下指南: 通过下面的指南,您可以了解到在 CUBA 应用程序中如何读写数据: 其他关于如何处理事件,本地化语言消息,提供用户访问权限以及如何测试应用程序,可以访问 指南 页面。 本手册中的大多数示例代码都是基于 Sales Application 的数据模型。 |
更多信息 |
网课指导详细介绍了概念和技术 |
|
对平台中各个要素做了重点指导 |
|
在线应用程序演示,展示平台功能 |
3. 框架详细介绍
本章节包含关于平台架构、通用组件以及工作机制的详细介绍。
3.1. 架构
本节从不同的角度介绍 CUBA 应用程序的体系结构,将按层(tier)、块(block)、模块(module)和组件(component)来介绍。
3.1.1. 应用程序层和块
框架支持使用多种客户端、中间层、数据库层构建多层应用程序。后续将主要介绍中间层和客户端层,因此,后面看到的“所有层”仅指这两个层。
应用程序的每个层都可以创建一个或多个应用程序 块(block)。每一个块都是独立可执行程序,可与应用程序中的其它块(block)交互。通常,一个块就是在 JVM 上运行的一个 web 应用程序。
- Middleware - 中间件
-
中间件层包含应用程序的核心业务逻辑,并提供对数据库的访问功能。由运行在 Java servlet 容器上的单独 Web 应用程序表示。参阅中间件组件。
- Web Client - Web 客户端
-
是客户端层的主要块(block)。包含主要为内部用户(即管理系统用户)设计的界面。由运行在 Java servlet 容器上的单独 Web 应用程序表示。用户界面基于 Vaadin 框架实现。参阅通用用户界面(GUI)。
- Web Portal - Web 门户
-
客户端层的附加块(block)。可以包含外部用户的界面以及作为与移动设备和第三方应用程序集成的入口点。由运行在 Java servlet 容器上的单独 Web 应用程序表示。用户界面基于 Spring MVC 框架实现。参阅Portal 组件。
- Frontend UI - 前端 UI
-
为外部用户设计的可选客户端。与 Web Portal 不同,这个是纯客户端侧应用程序(例如,一个执行在浏览器中的 JavaScript 应用程序)。与运行在 Web Client 或 Web Portal 内的中间件通过 REST API 通信。可以使用 React 或其他库、框架开发。参阅 前端用户界面。
Middleware 是任何应用程序都需要的 block。用户界面可以由一个或多个 block 实现,例如 Web Client 和 Web Portal。
所有基于 Java 的客户端 block 都通过 HTTP 协议统一与中间层交互,从而可以任意部署中间层,即使在防火墙内也可以。在最简单的情况下,当中间层和 Web 客户端部署在同一服务器上时,它们之间的本地交互可以绕过网络栈以获得更好的性能。
3.1.2. 应用程序模块
模块是 CUBA 应用程序的最小组成部分。是应用程序项目中的单个模块,并具有相应的包含可执行代码的 JAR 文件。
标准模块:
-
global – 包含所有层共享的实体类、服务接口和其它类。用于所有应用程序 block。
-
core – 实现中间层的服务和所有其它组件。
-
gui – 包含通用用户界面的组件。用于 Web Client block。
-
web – 基于 Vaadin 的通用用户界面实现。
-
portal – 可选模块 - 基于 Spring MVC 的 Web 门户的实现。
-
front – 可选模块 - 使用 JavaScript 实现的前端用户界面。
3.1.3. 应用程序组件
框架支持将应用程序功能分成不同的组件。每个 应用程序组件(又名 扩展插件 )都可以有自己的数据模型、业务逻辑以及用户界面。应用程序将组件作为类库来使用并且包含了组件的所有功能。
应用程序组件的概念可以使得基础框架保持在一个比较小的范围,同时通过组件来交付一些可选功能,比如报表功能、全文检索功能、图表功能、WebDAV 以及其它功能。另外,应用程序开发人员也可以用这个机制来对大项目进行解耦,拆分成一组功能模块,从而每个模块可以有单独的开发计划和不同的发布周期。当然,应用程序组件是可重用的,并且可以在基础框架之上提供针对特定领域的抽象层。
技术上来说,核心框架也是一个名为 cuba 的应用程序组件。唯一不同的是,这个组件对于任何应用程序来说都是必不可少的。所有其它的组件都依赖 cuba,也可以互相依赖。
下面的图展示了应用中使用的标准组件之间的依赖关系。实线表示强制依赖,虚线表示可选依赖。
下面的图展示了标准组件和自定义应用程序组件之间可能的依赖结构。
任何 CUBA 应用程序都可以很容易的变成一个组件,从而为其它应用程序提供一些功能。要作为组件使用,应用程序项目应包含 app-component.xml 文件以及在 global 模块 JAR 的 manifest 中配置特殊的条目。CUBA Studio 可以为当前项目自动生成 XML 文件和 manifest 条目。
请参阅应用程序组件示例部分中使用自定义应用程序组件的分步介绍。
3.1.4. 应用程序结构
上面列出的架构原则在组装完成的应用程序结构能直接反映出来。假设我们有个简单的应用程序,包含两个 block - Middleware 和 Web Client ;并依赖了两个应用程序组件的功能 - cuba 和 reports。
该图展示了 Tomcat 服务的几个目录的内容,其中包含已部署的应用程序。
Middleware block 由 app-core
WEB 应用程序表示,Web Client block 由 app
Web 应用程序表示。Web 应用程序包含 WEB-INF/lib
目录下的 JAR 文件。每个 JAR(工件)都是一个应用程序模块或组件构建的结果。
比如,中间层 Web 应用程序 app-core
包含哪些 JAR 文件是由以下情况决定的:Middleware block 包含 global 和 core 模块,同时应用程序依赖了 cuba 和 reports 组件。
3.2. 通用组件
本章介绍平台组件,这些组件对应用程序的所有层都是通用的。
3.2.1. 数据模型
数据模型实体分为两类:
-
持久化实体 - 使用 ORM 将此类实体的实例存储在数据库中。
-
非持久化实体 – 实例仅存在于内存中,或通过不同的机制存储在某处。
可以参考我们关于如何进行数据模型设计的指南。 数据建模: 多对多关系 演示了不同情况下怎么使用多对多关联。 数据建模:组合 举了不同的例子,演示如何使用实体间的组合关系。 |
实体通过其属性描述。属性对应到实体代码的字段以及字段的访问方法(get / set)。如果省略 setter,则该属性变为只读。
持久化实体可能包含未存储在数据库中的属性。对于非持久化属性,Java 字段不是必须的,可以只创建访问方法。
实体类应满足以下要求:
以下类型可用于实体属性:
-
java.lang.String
-
java.lang.Boolean
-
java.lang.Integer
-
java.lang.Long
-
java.lang.Double
-
java.math.BigDecimal
-
java.time.LocalDate
-
java.time.LocalTime
-
java.time.LocalDateTime
-
java.time.OffsetTime
-
java.time.OffsetDateTime
-
java.util.Date
-
java.sql.Date
-
java.sql.Time
-
java.util.UUID
-
byte[]
-
enum
-
Entity
基础实体类(见下文)重写 equals()
和 hashCode()
方法,这样可以通过比较它们的标识符来检查实体实例是否相同。即,如果它们的标识符相等,则认为他们是同一个实例。
3.2.1.1. 基础实体类
本节将详细介绍基础实体类和接口。
-
Instance
– 定义了使用应用程序领域对象的基本方法:-
获取对象元类的引用;
-
生成实例名称;
-
根据名称读写属性值;
-
添加监听器用于接收有关属性更改的通知。
-
-
Entity
– 继承自Instance
,添加了实体标识符;同时Entity
没有定义标识符的类型,该类型由其继承者决定。 -
AbstractInstance
– 实现了使用属性更改监听器的逻辑。 -
BaseGenericIdEntity
– 持久化和非持久化实体的基础类。实现了Entity
接口但没有指定实体标识符(即主键)的类型。 -
EmbeddableEntity
- 可嵌入的持久化实体的基础类。
下面是关于在项目实体中继承基础实体的一些建议。非持久化实体应该继承与持久化实体相同的基类。框架是根据实体所在注册文件:persistence.xml 或 metadata.xml 来确定实体是否是持久化实体。
- StandardEntity
-
继承自
StandardEntity
的实体带有一组标准功能:UUID 类型的主键、包含创建人和修改人及创建时间和修改时间、还支持乐观锁和软删除机制。
- BaseUuidEntity
-
继承自
BaseUuidEntity
的实体带有 UUID 类型主键,但不具备StandardEntity
的所有功能。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。
- BaseLongIdEntity
-
继承自
BaseLongIdEntity
或BaseIntegerIdEntity
的实体具有Long
或Integer
类型的主键。可以在具体实体类中有选择地实现一些接口,Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以提供一些优化,并可以确保实例在分布式环境中的唯一标识。
- BaseStringIdEntity
-
继承自
BaseStringIdEntity
的实体具有String
类型的主键。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以提供一些优化,并可以确保实例在分布式环境中的唯一标识。具体实体类必须有一个使用@Id
JPA 注解的字符串字段用来作为实体的主键。
- BaseIdentityIdEntity
-
继承自
BaseIdentityIdEntity
的实体,会映射到具有 IDENTITY 主键的表。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以实现一些优化,并可以确保实例在分布式环境中的唯一标识。实体的id
属性(即getId()
/setId()
)是IdProxy
类型,用来替换真实标识符,真实标识符会在插入数据时由数据库生成。
- BaseIntIdentityIdEntity
-
继承
BaseIntIdentityIdEntity
的实体,会映射到Integer
类型的 IDENTITY 为主键的表(区别于BaseIdentityIdEntity
中的Long
)。在其它方面,BaseIntIdentityIdEntity
与BaseIdentityIdEntity
类似。
- BaseGenericIdEntity
-
除了上面情况之外,如果需要将实体映射到具有复合主键的表,则直接继承
BaseGenericIdEntity
。在这种情况下,具体实体类必须有一个嵌入类型的字段代表复合主键,并使用@EmbeddedId
JPA 注解。
3.2.1.2. 实体注解
本节介绍平台支持的实体类和属性的所有注解。
在本手册中,如果注解的标识是一个简单的类名,那么指的是 com.haulmont.*
包中的框架类之一。
3.2.1.2.1. 类注解
- @Embeddable
-
定义一个与所属实体存储在同一表中的嵌入实体。
应使用 @MetaClass 注解来指定实体名称。
- @EnableRestore
-
表示软删除的实体实例是否可以通过 Administration > Data Recovery 菜单打开的
core$Entity.restore
界面进行恢复。
- @Entity
-
声明一个类是一个数据模型实体。
参数:
-
name
– 实体的名称,必须带有前缀,以_
符号分隔前缀。建议使用项目的简称作为前缀来形成单独的命名空间。
例如:
@Entity(name = "sales_Customer")
-
- @Extends
-
表示该实体是一个对基础实体的扩展,在应用程序中应该使用它来代替其基础实体。参阅 功能扩展。
- @DiscriminatorColumn
-
在
SINGLE_TABLE
和JOINED
继承策略的情况下,用于定义负责区分实体类型的数据库列。参数:
-
name
– 鉴别器列名。 -
discriminatorType
– 鉴别器列的类型。
例如:
@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
-
- @DiscriminatorValue
-
定义此实体的鉴别器列值。
例如:
@DiscriminatorValue("0")
- @IdSequence
-
如果实体是
BaseLongIdEntity
或BaseIntegerIdEntity
的子类,则应明确定义用于生成标识符的数据库序列名称。如果实体没有此注解,则框架将自动生成名称并创建一个序列。参数:
-
name
– 序列名称。 -
cached
- 可选参数,定义序列应该以 cuba.numberIdCacheSize 递增,并将未使用的 ID 值缓存在内存中。默认为 False。
默认情况下,序列都在主 数据存储 创建。但是如果 cuba.useEntityDataStoreForIdSequence 应用程序属性设置为
true
,序列则会创建在实体所在的数据存储中。 -
- @Inheritance
-
定义实体类的继承策略。此注解在实体继承层次的根类上指定。
参数:
-
strategy
– 继承策略,默认为SINGLE_TABLE
。
-
- @Listeners
- @MappedSuperclass
-
表示该类用作其它实体类的父类,其属性必须用作后代实体的一部分。这种类不关联任何特定的数据库表。
数据建模:实体继承 向导演示了如何定义实体继承关系。
- @MetaClass
-
用于声明非持久化或嵌入实体(也就是不能用
@javax.persistence.Entity
注解)参数:
-
name
– 实体名称,必须以一个前缀开头,以_
符号分隔前缀。建议使用项目的简称作为前缀来形成单独的命名空间。
例如:
@MetaClass(name = "sales_Customer")
-
- @NamePattern
-
定义如何创建表示单一实体的字符串。可以认为是应用程序级别的
toString()
方法。在 UI 中到处都用得上,比如在类似TextField
或者LookupField
的单一字段中需要展示一个实体。也可以通过编程的方式使用MetadataTools.getInstanceName()
方法获取实例名称。注解值应该是
{0}|{1}
格式的字符串,其中:例如:
@NamePattern("%s|name")
@NamePattern("%s - %s|name,date")
@NamePattern("#getCaption|amount,customer") ... public String getCaption(){ String prefix = ""; if (amount > 5000) { prefix = "Grade 1 "; } else { prefix = "Grade 2 "; } return prefix + customer.name; }
- @PostConstruct
-
可以为方法指定此注解。在使用 Metadata.create() 或者类似的
DataManager.create()
和DataContext.create()
方法创建实体实例之后将立即调用带此注解的方法。参考 初始化实体值 指南,了解如何在实体类中使用
@PostConstruct
注解直接定义初始值。使用此注解的方法可以使用在
global
模块可用的 Spring bean 作为参数。示例:@PostConstruct public void postConstruct(Metadata metadata, SomeBean someBean) { // ... }
- @PublishEntityChangedEvents
-
表示实体在数据库改动时,框架会发送 EntityChangedEvent 事件。
- @Table
-
定义实体的数据库表。
参数:
-
name
– 表名
例如:
@Table(name = "SALES_CUSTOMER")
-
- @TrackEditScreenHistory
-
表示系统将会保存编辑界面的打开历史记录,并能够在
sec$ScreenHistory.browse
展示。界面可以使用下面的 web-menu.xml 元素添加到主菜单:
<item id="sec$ScreenHistory.browse" insertAfter="settings"/>
3.2.1.2.2. 属性注解
应该为相应的字段设置属性注解,此情况除外:如果需要声明只读且非持久属性 foo
,则只创建 getFoo()
方法并使用 @MetaProperty
注解就可以了。
- @CaseConversion
-
表明使用此注解的实体属性绑定的文本输入框控件会自动转换大小写。
参数:
-
type
- 转换类型:UPPER
(默认值)、LOWER
。
示例:
@CaseConversion(type = ConversionType.UPPER) @Column(name = "COUNTRY_CODE") protected String countryCode;
-
- @Column
-
定义用于存储属性值的 DB 列。
参数:
-
name
– 列名。 -
length
– (可选参数,默认为255
) - 列字段的长度。还用于生成元数据,可以限制绑定到此属性的可视化组件中输入文本的最大长度。添加@Lob
注解可以移除对属性长度的限制。 -
nullable
– (可选参数,默认为true
) - 确定属性是否可以包含null
值。当nullable = false
时,JPA 会确保该字段在保存时有值。此外,带有该属性的可视化组件会要求用户必须输入值。
-
- @Composition
-
表示一种组合的关系,是比关联更紧密的一种关系。本质上,这意味着相关实体仅作为已有实体的一部分存在,也就是与已有实体一起创建和删除。
数据建模:组合 举了不同的例子,演示如何使用实体间的组合关系。
例如,订单中的条目列表(
Order
类包含Item
实例的集合,Item
实例不能独立存在):@OneToMany(mappedBy = "order") @Composition protected List<Item> items;
另一个例子是一对一的关系:
@Composition @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "DETAILS_ID") protected CustomerDetails details;
- @CurrencyValue
-
表示带有该注解的字段包含货币值。如果用了该注解,Studio 在实体编辑界面的 表单 中会为该属性生成 CurrencyField。
参数:
-
currency
– 货币名称:USD,GBP,EUR,$,或其他货币符号。 -
labelPosition
- 货币标签的位置:RIGHT
(默认),或LEFT
。
例如:
@CurrencyValue(currency = "$", labelPosition = CurrencyLabelPosition.LEFT) @Column(name = "PRICE") protected BigDecimal price;
-
- @Embedded
-
定义可嵌入类型的引用属性。被引用实体应该具有
@Embeddable
注解。例如:
@Embedded protected Address address;
- @EmbeddedParameters
-
默认情况下,如果嵌入实体的所有属性在数据库中为空,则 ORM 不会创建嵌入实体的实例。当实例始终为非空时,可以使用
@EmbeddedParameters
注解指定不同的行为,例如:@Embedded @EmbeddedParameters(nullAllowed = false) protected Address address;
- @Id
-
表示该属性是实体主键。通常,此注解在基类的字段上设置,例如 BaseUuidEntity。仅在继承
BaseStringIdEntity
基类(即用于创建主键是字符串类型的实体)的情况下,才需要对特定实体类使用此注解。
- @IgnoreUserTimeZone
-
使框架忽略时间戳类型属性的用户时区(如果当前会话设置了时区),时间戳属性使用
@javax.persistence.Temporal.TIMESTAMP
注解。
- @JoinColumn
-
定义确定实体之间关系的 DB 列。存在此注解则表示该关联关系的拥有方(owning side)。
参数:
-
name
– 列名
例如:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
-
- @JoinTable
-
定义
@ManyToMany
关系拥有方的关联表。参数:
-
name
– 关联表表名 -
joinColumns
– 关联表中的@JoinColumn
元素对应于关联关系拥有方的主键(包含@JoinTable
注解的那个) -
inverseJoinColumns
– 关联表中的@JoinColumn
元素对应于关系非拥有方(non-owning side)的主键。
关系拥有方的
Group
类的customers
属性示例:@ManyToMany @JoinTable(name = "SALES_CUSTOMER_GROUP_LINK", joinColumns = @JoinColumn(name = "GROUP_ID"), inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID")) protected Set<Customer> customers;
同一关系非拥有方的
Customer
类的groups
属性的示例:@ManyToMany(mappedBy = "customers") protected Set<Group> groups;
-
- @Lob
-
表示该属性没有任何长度限制。此注解与
@Column
注解一起使用。如果设置了@Lob
,则忽略@Column
中的默认或明确定义的长度。示例:
@Column(name = "DESCRIPTION") @Lob private String description;
- @Lookup
-
定义引用属性的查找类型设置。
参数:
@Lookup(type = LookupType.DROPDOWN, actions = {"open"}) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
- @ManyToMany
-
定义具有多对多关系类型的集合属性。
数据建模: 多对多关系 演示了不同情况下怎么使用多对多关联。
多对多关系可以有一个拥有方和一个反向的非拥有方。拥有方应使用
@JoinTable
注解,非拥有方则使用mappedBy
参数。参数:
不推荐使用
cascade
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用实体监听器。
- @ManyToOne
-
定义具有多对一关系类型的引用属性。
参数:
例如,几个
Order
实例引用相同的Customer
实例。在这种情况下,Order.customer
属性应该具有以下注解:@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
不推荐使用
cascade
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用 实体监听器。
- @MetaProperty
-
表明元数据应包含带有此注解的属性。可以为字段设置此注解,但是如果没有字段,也可以为 getter 方法设置此注解。
已经带有
javax.persistence
包中的以下注解的字段不需要这个注解:@Column
、@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
、@Embedded
。这些字段自动包含在元数据中。因此,@MetaProperty
主要用于定义实体的非持久化属性。参数(可选):
字段示例:
@Transient @MetaProperty protected String token;
方法示例:
@MetaProperty(related = "firstName,lastName") public String getFullName() { return firstName + " " + lastName; }
- @NumberFormat
-
指定
Number
类型(BigDecimal
、Integer
、Long
或Double
)属性的格式。在所有的 UI 展示中,将按照注解参数提供的格式对属性值进行格式化和解析:-
pattern
- DecimalFormat 所描述的格式模板. -
decimalSeparator
- 用作小数位分隔符的字符(可选)。 -
groupingSeparator
- 用作千位分隔符的字符(可选)。
如果未指定
decimalSeparator
或groupingSeparator
,框架会使用当前用户的本地化格式字符串或服务器操作系统的本地化格式字符串。例如:
@Column(name = "PRECISE_NUMBER", precision = 19, scale = 4) @NumberFormat(pattern = "0.0000") protected BigDecimal preciseNumber; @Column(name = "WEIRD_NUMBER", precision = 19, scale = 4) @NumberFormat(pattern = "#,##0.0000", decimalSeparator = "_", groupingSeparator = "`") protected BigDecimal weirdNumber; @Column(name = "SIMPLE_NUMBER") @NumberFormat(pattern = "#") protected Integer simpleNumber; @Column(name = "PERCENT_NUMBER", precision = 19, scale = 4) @NumberFormat(pattern = "#%") protected BigDecimal percentNumber;
-
- @OnDelete
-
在实体软删除的情况下,确定关联实体的处理策略。参阅软删除。
例如:
@OneToMany(mappedBy = "group") @OnDelete(DeletePolicy.CASCADE) private Set<Constraint> constraints;
- @OnDeleteInverse
-
从关系的反向软删除实体的情况下,确定属性关联实体的处理策略。参阅 软删除。
例如:
@ManyToOne @JoinColumn(name = "DRIVER_ID") @OnDeleteInverse(DeletePolicy.DENY) private Driver driver;
- @OneToMany
-
定义一对多关系类型的集合属性。
参数:
例如,几个
Item
实例使用@ManyToOne
注解的字段Item.order
引用相同的Order
实例。在这种情况下,Order
类可以包含Item
实例的集合:@OneToMany(mappedBy = "order") protected Set<Item> items;
不推荐使用 JPA
cascade
和orphanRemoval
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用 实体监听器。orphanRemoval
注解属性不遵循 软删除机制。
- @OneToOne
-
使用一对一关系类型定义引用属性。
参数:
Driver
类中关系的拥有方示例:@OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CALLSIGN_ID") protected DriverCallsign callsign;
DriverCallsign
类中关系的非拥有方示例:@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign") protected Driver driver;
- @OrderBy
-
定义从数据库检索关联时集合属性中元素的顺序。需要对有序的 Java 集合(例如
List
或LinkedHashSet
)指定此注解,这样获得可预测的元素序列。参数:
-
value
– 确定排序的字符串格式:
orderby_list::= orderby_item [,orderby_item]* orderby_item::= property_or_field_name [ASC | DESC]
例如:
@OneToMany(mappedBy = "user") @OrderBy("createTs") protected List<UserRole> userRoles;
-
- @Temporal
-
指定
java.util.Date
类型属性的存储值类型:日期、时间或日期加时间。参数:
-
value
– 存储值的类型:DATE
、TIME
、TIMESTAMP
例如:
@Column(name = "START_DATE") @Temporal(TemporalType.DATE) protected Date startDate;
-
- @Transient
-
表示该字段不存储在数据库中,即属性是非持久化的。
字段的类型如果是 JPA 支持的(请参阅 Basic 注解类型),那么在默认情况下是持久化的,这就是为什么对于这些类型的非持久化属性必须要加上
@Transient
注解。如果元数据中需要包含
@Transient
属性,应该给属性加上 @MetaProperty 注解。
- @Version
-
表明注解的字段是用于支持乐观锁的版本字段。
当实体类实现
Versioned
接口时,需要这样的字段,StandardEntity
基类已经包含这样的字段。例如:
@Version @Column(name = "VERSION") private Integer version;
3.2.1.3. 枚举属性
JPA 对 enum
属性的标准用法是使用数据库的整型字段,保存从 ordinal()
方法获得的值。在生产环境下对系统进行扩展时,这种方法可能会导致以下问题:
-
如果数据库中枚举的值不等于任何
ordinal
值,则无法加载实体实例。 -
不能在现有的值之间添加新的枚举值,但是这在需要按枚举值排序时很重要。
CUBA 中解决这些问题的方式是将存储在数据库中的值与枚举的 ordinal
值分离。要到这一点,实体的字段应该用存储在数据库中的字段类型声明(Integer
或 String
型),而实体的访问方法(getter / setter)则使用实际的枚举类型来定义。
例如:
@Entity(name = "sales_Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {
@Column(name = "GRADE")
protected Integer grade;
public CustomerGrade getGrade() {
return grade == null ? null : CustomerGrade.fromId(grade);
}
public void setGrade(CustomerGrade grade) {
this.grade = grade == null ? null : grade.getId();
}
...
}
在这种情况下,枚举类可以如下所示:
public enum CustomerGrade implements EnumClass<Integer> {
PREMIUM(10),
HIGH(20),
MEDIUM(30);
private Integer id;
CustomerGrade(Integer id) {
this.id = id;
}
@Override
public Integer getId() {
return id;
}
public static CustomerGrade fromId(Integer id) {
for (CustomerGrade grade : CustomerGrade.values()) {
if (grade.getId().equals(id))
return grade;
}
return null;
}
}
为了将枚举属性正常地反映在元数据中,枚举类必须实现 EnumClass
接口。
如示例所示,grade
属性对应于存储在数据库中的 Integer
类型值,该值由 CustomerGrade
枚举的 id
字段指定,即 10
、 20
或 30
。同时,应用程序代码和元数据框架通过访问方法(getter/setter)使用 CustomerGrade
枚举,这些方法中执行类型的转换。
如果数据库中的值没有对应的枚举值,这时 getGrade()
方法将只返回 null
。如果要添加一个新枚举值,例如 HIGHER
在 HIGH
和 PREMIUM
之间,只需添加 id = 15
的新枚举值就可以了,这样可以确保按 Customer.grade
字段正确排序。
Integer
字段类型可以提供有序的常量列表,并允许在 JPQL 和 SQL 查询中排序( >
、 <
、 >=
、 ⇐
、 order by
),同时也基本没有存储空间和性能方面的问题。但另一方面,在查询结果中,Integer
值不是“自描述”的,这使得在对数据库的原始数据或序列化后的数据进行调试时变地复杂了。就这点而言,使用 String
类型更方便。
可以在 CUBA Studio 中使用 Data Model > New > Enumeration 菜单创建枚举。要将枚举用作实体属性,请在属性编辑器的 Attribute type 字段中选择 ENUM
,然后在 Type 字段中选择枚举类。枚举值可以与在应用程序界面中显示的本地化名称相关联。
3.2.1.4. 软删除
CUBA 框架支持软删除模式,记录并不从数据库中物理删除,而是打上特定的标记,此时常规的方式将不能访问这些记录。后续可以使用某种定时任务将这些记录从数据库中完全删除或恢复。
软删除机制对于应用程序开发人员来说是透明的,唯一的要求是实体类实现 SoftDelete
接口。平台将自动调整数据操作。
软删除模式具有以下优点:
-
显著降低因不正确的用户操作而导致数据丢失的风险。
-
可以将某些记录标记为不可访问,即使系统中还存在对这些记录的引用。
使用 Orders-Customers 数据模型作为示例,假设某个客户已经下了多个订单,但我们需要禁止用户访问此客户实例。对于传统的硬删除来说,这是不可能的,因为删除客户实例需要删除所有相关订单或在订单中将所有对客户的引用设置为 null(意味着数据丢失)。软删除后,虽然客户实例无法进行搜索和修改,但是用户依然可以在订单编辑中看到客户的名称,因为在获取相关实体时有意忽略了删除属性。
上述标准行为可以使用关联实体处理策略进行修改。
已删除的实体实例可以在应用程序的 Administration 菜单中的 Restore Deleted Entities 界面上手动恢复。此功能仅适用于对所有实体具有所有操作权限的应用程序管理员,并且应该谨慎使用,因此建议拒绝让普通用户访问此界面。
软删除的负面影响是会增加数据库大小,以及可能需要额外的数据清理程序。
3.2.1.4.1. 软删除的使用
要支持软删除,实体类应该实现 SoftDelete
接口,相应的数据库表应该包含以下列:
-
DELETE_TS
– 删除记录的时间。 -
DELETED_BY
– 删除记录的用户登录名。
实现 SoftDelete
接口的实例的默认行为是,查询或按 id 搜索不返回软删除的实体。如果需要,可以使用以下方法动态关闭此特性:
-
当前EntityManager实例调用
setSoftDeletion(false)
。 -
通过DataManager请求数据时,
LoadContext
对象调用setSoftDeletion(false)
方法。 -
在数据加载器级别 - 调用
DataLoader.setSoftDeletion(false)
方法或在界面的 XML 描述中设置loader
元素的softDeletion="false"
属性。
在软删除模式下,平台在按标识符加载实例时、使用JPQL 查询加载实例时以及在实体的集合属性中都会自动过滤掉被删除的实例。但是,在单一值(*ToOne)关联实体的时候却会加载相关联的实体,不管这个关联的实体是否已经被删除。
3.2.1.4.2. 关联实体处理策略
对于软删除实体,平台提供了一种在删除时管理关联实体的机制,很大程度上类似于数据库外键的 ON DELETE 规则。此机制在中间层有效,并且需要在实体属性上使用 @OnDelete,@OnDeleteInverse 注解。
使用 @OnDelete
注解的实体被删除时会处理此注解,而不是此注解指向的实体(这是与数据库级别级联删除的主要区别)。
@OnDeleteInverse
注解指向的实体被删除时处理此注解(类似于数据库中外键级别的级联删除)。当被删除的对象没有可以在删除之前检查的属性时,此注解很有用。典型情况下,被检查的对象具有要删除的对象引用,并且此属性应该使用 @OnDeleteInverse
注解。
注解值可以是:
-
DeletePolicy.DENY
– 如果带注解的属性不是null
或不是空集合,则禁止删除实体。 -
DeletePolicy.CASCADE
– 级联删除带注解的属性。 -
DeletePolicy.UNLINK
– 与注解属性断开链接。仅在关联关系拥有方(在实体类中具有@JoinColumn
注解的实体)断开链接是合理的。
例如:
-
禁止删除被引用的实体:如果尝试删除被至少一个
Order
引用的Customer
实例,则将抛出DeletePolicyException
。Order.java
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") @OnDeleteInverse(DeletePolicy.DENY) protected Customer customer;
Customer.java
@OneToMany(mappedBy = "customer") protected List<Order> orders;
异常窗口中的消息可以在主消息包中进行本地化。使用以下键值:
-
deletePolicy.caption
- 通知标题。 -
deletePolicy.references.message
- 通知消息。 -
deletePolicy.caption.sales_Customer
- 具体实体的通知标题。 -
deletePolicy.references.message.sales_Customer
- 具体实体的通知消息。
-
-
关联集合元素的级联删除:删除
Role
实例也会导致所有Permission
实例被删除。Role.java
@OneToMany(mappedBy = "role") @OnDelete(DeletePolicy.CASCADE) protected Set<Permission> permissions;
Permission.java
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ROLE_ID") protected Role role;
-
断开与关联集合元素的连接:删除
Role
实例会导致对集合中包含的所有Permission
实例的Role
属性设置为空引用。Role.java
@OneToMany(mappedBy = "role") protected Set<Permission> permissions;
Permission.java
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "ROLE_ID") @OnDeleteInverse(DeletePolicy.UNLINK) protected Role role;
实现说明:
-
当保存实现了
SoftDelete
接口的实体到数据库时,会在中间件上处理关联实体策略。 -
将
@OnDeleteInverse
与CASCADE
和UNLINK
策略一起使用时要注意。在此过程中,将从数据库中提取关联对象的所有实例,进行修改然后保存。例如,如果
@OnDeleteInverse(CASCADE)
策略设置在Customer
–Job
关联内的Job.customer
属性上,代表一个 customer 有多个 job,删除 Customer 实例时将获取和修改所有 Job。这可能会导致应用程序服务器或数据库超负荷。另一方面,使用
@OnDeleteInverse(DENY)
是安全的,因为它只涉及统计关联对象的数量。如果数量大于0
,则抛出异常。这使得@OnDeleteInverse(DENY)
适用于Job.customer
属性。
3.2.1.4.3. 数据库级别的唯一约束
为了在软删除模式中对特定值应用唯一约束,也就是说在数据库中可以存在至少一个具有该值的未删除记录和任意数量的具有相同值的已删除记录。
此逻辑可以针对每种数据库服务类型以特定方式实现:
-
如果数据库服务支持部分索引(例如 PostgreSQL),则可以按如下方式实现唯一约束:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
-
如果数据库服务不支持部分索引(例如 Microsoft SQL Server 2005),则 DELETE_TS 字段可以包含在唯一索引中:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)
3.2.2. 元数据框架
元数据框架用于在 CUBA 应用程序中高效处理数据模型。此框架:
-
提供 API 以获取有关实体、实体属性和实体之间关系的信息,还能用来遍历对象关系图;
-
作为 Java 反射 API 更专用和更方便的替代品;
-
控制允许的数据类型以及实体之间的关系;
-
允许实现通用的数据操作机制;
3.2.2.1. 元数据接口
以下是基本的元数据接口:
- Session
-
元数据框架的入口点。允许按名称或相应的 Java 类获取
MetaClass
实例。注意方法的不同:getClass()
方法可以返回null
而getClassNN()
(NotNull)方法不能。可以使用元数据基础接口获得
Session
对象。示例:
@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
- MetaModel
-
很少使用的接口,用于对元类进行分组。
元类按照 metadata.xml 文件中指定的 Java 项目包的根名称进行分组。
- MetaClass
-
实体类元数据接口。
MetaClass
始终与它所代表的 Java 类相关联。基本方法:
-
getName()
– 实体名称,根据惯例,在_
符号之前名称的第一部分是项目命名空间代码,例如sales_Customer
。 -
getProperties()
– 元属性列表(MetaProperty
)。 -
getProperty()
、getPropertyNN()
– 按名称返回元属性。如果没有与提供的名称对应的属性,则第一个方法返回null
,第二个方法抛出异常。示例:
MetaClass userClass = session.getClassNN(User.class); MetaProperty groupProperty = userClass.getPropertyNN("group");
-
getPropertyPath()
– 允许通过引用进行属性遍历。此方法接收属性路径参数 - 以点分隔的属性名字符串。方法返回MetaPropertyPath
对象,可通过调用此对象的getMetaProperty()
方法访问所需的(路径中的最后一个)属性。示例:
MetaClass userClass = session.getClassNN(User.class); MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty(); assert groupNameProp.getDomain().getName().equals("sec$Group");
-
getJavaClass()
– 对应于这个MetaClass
的实体类。 -
getAnnotations()
– 元注解集合。
-
- MetaProperty
-
实体属性元数据接口。
基本方法:
-
getName()
– 属性名,对应于实体属性名。 -
getDomain()
– 拥有这个属性的元类。
-
-
getType()
- 属性类型:-
简单类型:
DATATYPE
-
枚举:
ENUM
-
两种引用类型:
-
ASSOCIATION - 关联
− 简单引用另一个实体。例如,订单 - 客户的关系就是一种关联关系。 -
COMPOSITION - 组合
− 引用一个实体,这个实体在脱离拥有它的实体时会失去实际意义。COMPOSITION
被认为是一种比ASSOCIATION
更紧密的关系。例如,Order 和它的 Items 之间的关系是一个COMPOSITION
,因为如果没有 Item 所属的 Order,Item 就不能存在。引用属性的
ASSOCIATION
或COMPOSITION
类型影响实体编辑模式:在第一种情况下,相关实体独立地持久化到数据库,在第二种情况下,相关实体仅与父实体一起持久化。有关详细信息,请参阅组合结构。
-
-
-
getRange()
–Range
接口提供属性类型的详细描述。 -
isMandatory()
– 表示必须属性。例如,可视化组件以此来提示用户必须输入值。 -
isReadOnly()
– 表示只读属性。 -
getInverse()
– 对于引用类型的属性,返回关联另一侧的元属性(如果存在)。 -
getAnnotatedElement()
– 对应于实体属性的字段(java.lang.reflect.Field
)或方法(java.lang.reflect.Method
)。 -
getJavaType()
– 实体属性的 Java 类。可以是相应字段的类型,也可以是相应方法的返回值的类型。 -
getDeclaringClass()
– 包含这个属性的 Java 类。
- Range
-
详细描述实体属性类型的接口。
基本方法:
-
isDatatype()
– 如果是简单类型属性则返回true
。 -
asDatatype()
– 对于简单类型属性返回对应的Datatype实例。 -
isEnum()
– 如果是枚举类型返回true
。 -
asEnumeration()
– 对于枚举类型属性返回Enumeration实例。 -
isClass()
– 如果是ASSOCIATION
或COMPOSITION
类型属性,则返回true
。 -
asClass()
– 对于引用属性返回关联实体的元类。 -
isOrdered()
– 如果属性由有序集合(例如List
)表示,则返回true
。 -
getCardinality()
– 引用属性的关系类型:ONE_TO_ONE
、MANY_TO_ONE
、ONE_TO_MANY
、MANY_TO_MANY
。
-
3.2.2.2. 元数据构建
元数据结构生成的主要来源是带注解的实体类。
实体类满足以下情况将出现在元数据中:
-
带
@Entity
、@Embeddable
、@MappedSuperclass
注解并且位于 metadata.xml 中指定的根包中的持久化实体类。 -
带
@MetaClass
注解且位于metadata.xml
中指定的根包中的非持久化实体类。
同一根包中的所有实体都放在同一个 MetaModel
实例中,该实例被赋予该包的名称。同一个 MetaModel
中的实体可以包含任意彼此的引用。可以按 cuba.metadataConfig 属性中的 metadata.xml
文件的声明顺序创建来自不同元模型的实体之间的引用。
实体属性满足以下条件时将出现在元数据中:
-
使用
@Column
、@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
、@Embedded
注解的类字段。 -
使用
@MetaProperty
注解的类字段或字段访问方法(getter)。
Metaclass 和 metaproperty 参数是基于上面列出的注解参数以及字段类型和类方法确定的。但是,如果属性没有写设置方法(setter),那么该属性将是只读的。
3.2.2.3. 数据类型接口
Datatype
接口定义了值和字符串互转的方法(格式化和解析)。每个实体属性,如果不是引用属性的话,就会有一个对应的 Datatype
,平台用 Datatype
来格式化和解析属性值。
数据类型注册在 DatatypeRegistry
bean 中,这个 bean 根据项目以及项目依赖的应用程序组件中的 metadata.xml 文件对 Datatype
的实现类进行加载和初始化。
实体属性的数据类型可以通过相应的元属性使用 getRange().asDatatype()
方法获取。
也可以使用注册的数据类型来格式化或者解析支持的数据类型的任意属性值。如需这么做,可以从 DatatypeRegistry
通过调用 get(Class)
或者 getNN(Class)
方法得到数据类型实例,传入需要转换的 Java 类型即可。
数据类型通过下列规则跟实体属性关联:
-
大多数情况下,一个实体属性都关联一个注册的
Datatype
实例,用来处理属性的 Java 类型。下面的例子中,
amount
属性关联的是BigDecimalDatatype
@Column(name = "AMOUNT") private BigDecimal amount;
因为
com/haulmont/cuba/metadata.xml
有下面的记录:<datatype id="decimal" class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype" default="true" format="0.####" decimalSeparator="." groupingSeparator=""/>
-
可以通过 @MetaProperty 注解的
datatype
属性来显式指定一个数据类型。下面的例子中,实体的
issueYear
属性将会关联year
数据类型。@MetaProperty(datatype = "year") @Column(name = "ISSUE_YEAR") private Integer issueYear;
如果项目的
metadata.xml
文件有下面的记录:<datatype id="year" class="com.company.sample.YearDatatype"/>
所以可以看到,
@MetaProperty
的datatype
属性包含标识符,这个标识符用来在metadata.xml
文件中注册数据类型的实现类。
datatype
接口的基本方法:
-
format()
– 将传入的值格式化成字符串。 -
parse()
– 将字符串转换成相应类型的值。 -
getJavaClass()
– 返回这个数据类型对应的 Java 类型。这个方法有一个默认实现,如果类带了@JavaClass
注解,这个实现会返回此注解的值。
Datatype
定义了用来做格式化和解析的两组方法:一组会考虑应用所在的 locale,另一组不会考虑。考虑了 locale 的转换用在用户界面的各个地方,不考虑 locale 的方法用在系统机制中,比如,在 REST API 的序列化时。
使用忽略 locale 的解析需要在代码中写死或者在 metadata.xml
文件注册数据类型时显式指定。
参考下一节了解怎么使用依赖 locale 的解析。
3.2.2.3.1. 数据类型格式化字符串
依赖 locale 的解析格式通过应用程序或者应用程序组件的主语言消息包来提供。遵循标准的 Java SE 类逻辑,比如,DecimalFormat
(参阅 https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html)或 SimpleDateFormat
(参阅 https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html)。
格式需要使用下列键值提供:
-
numberDecimalSeparator
– 数值类型的小数分隔符。# 逗号作为小数分隔符 numberDecimalSeparator=,
-
numberGroupingSeparator
– 数值类型的千分位符。# 空格作为千分位符 numberGroupingSeparator = \u0020
-
integerFormat
–Integer
和Long
类型的格式。# 禁止在整数使用千分位符 integerFormat = #0
-
doubleFormat
–Double
类型的格式。注意,用来做小数点和千分位符使用它们自己的键值定义,如上所述。# 四舍五入至小数点后三位 doubleFormat=#,##0.###
-
decimalFormat
–BigDecimal
类型的格式。注意,用来做小数点和千分位符使用它们自己的键值定义,如上所述。# 小数点后总是显示两位数。比如,显示货币时 decimalFormat = #,##0.00
-
dateTimeFormat
–java.util.Date
类型的格式。# 俄罗斯的日期时间显示方法 dateTimeFormat = dd.MM.yyyy HH:mm
-
dateFormat
–java.sql.Date
类型的格式。# 美国日期时间显示 dateFormat = MM/dd/yyyy
-
timeFormat
–java.sql.Time
类型的格式。# hours:minutes 时间格式 timeFormat=HH:mm
-
offsetDateTimeFormat
–java.time.OffsetDateTime
类型的格式。# 用与 GMT 时区偏移的方式显示日期时间 offsetDateTimeFormat = dd/MM/yyyy HH:mm Z
-
offsetTimeFormat
–java.time.OffsetTime
类型的格式。# hours:minutes 用与 GMT 时区偏移的方式显示 offsetTimeFormat=HH:mm Z
-
trueString
–Boolean.TRUE
类型对应的显示字符串。# boolean 值的显示方法 trueString = yes
-
falseString
–Boolean.FALSE
类型对应的显示字符串。# boolean 值的显示方法 falseString = no
通过 Studio 可以设置针对应用程序使用的语言的格式化字符串。编辑 Project Properties,点击 Available locales 字段的编辑按钮,然后勾选 Show data format strings。 |
locale 相应的格式化字符串可以通过 FormatStringsRegistry
bean 获得。
3.2.2.3.2. 自定义数据类型示例
假设应用程序中实体的某些属性用来存储日期的年份,用整数数字表示。用户能查看和编辑年份,并且如果用户只是输入两个数字,应用程序会将其转换为 2000 至 2100 之间的一个年份,否则,将整个输入的数字作为年份。
首先,在 global 模块创建下面的类:
package com.company.sample.entity;
import com.google.common.base.Strings;
import com.haulmont.chile.core.annotations.JavaClass;
import com.haulmont.chile.core.datatypes.Datatype;
import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;
@JavaClass(Integer.class)
public class YearDatatype implements Datatype<Integer> {
private static final String PATTERN = "##00";
@Override
public String format(@Nullable Object value) {
if (value == null)
return "";
DecimalFormat format = new DecimalFormat(PATTERN);
return format.format(value);
}
@Override
public String format(@Nullable Object value, Locale locale) {
return format(value);
}
@Nullable
@Override
public Integer parse(@Nullable String value) throws ParseException {
if (Strings.isNullOrEmpty(value))
return null;
DecimalFormat format = new DecimalFormat(PATTERN);
int year = format.parse(value).intValue();
if (year > 2100 || year < 0)
throw new ParseException("Invalid year", 0);
if (year < 100)
year += 2000;
return year;
}
@Nullable
@Override
public Integer parse(@Nullable String value, Locale locale) throws ParseException {
return parse(value);
}
}
然后在项目的 metadata.xml 文件中添加 datatypes
元素:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<datatypes>
<datatype id="year" class="com.company.sample.entity.YearDatatype"/>
</datatypes>
<!-- ... -->
</metadata>
在 datatype
元素中,还可以指定 sqlType
属性来包含一个数据库的 SQL 类型,这个 SQL 类型适合在数据库保存这个新类型。SQL 类型会在 CUBA Studio 生成数据库脚本的时候使用。Studio 可以为下面这些 Java 类型自动确定 SQL 类型:
-
java.lang.Boolean
-
java.lang.Integer
-
java.lang.Long
-
java.math.BigDecimal
-
java.lang.Double
-
java.lang.String
-
java.util.Date
-
java.util.UUID
-
byte[]
在上面的例子中,年份应该绑定 Integer
类型(这个类型通过 @JavaClass
注解的 Integer.class
值指定),所以 sqlType
可以省去。
最终,为实体属性设置新的数据类型(编程的方式或者通过 Studio 的帮助):
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
3.2.2.3.3. 用户界面中数据格式化示例
这里以如何在 Orders 表格中显示 Order.date
属性为例。
order-browse.xml
<table id="ordersTable">
<columns>
<column id="date"/>
<!--...-->
Order
类的 date
属性是用 "date" 类型来定义的:
@Column(name = "DATE_", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;
如果当前用户使用俄语 locale 登录,会从主语言消息包获取下面的字符串:
dateFormat=dd.MM.yyyy
结果是 "2012-08-06" 日期会被转换为 "06.08.2012" 字符串显示在表格的单元格中。
3.2.2.3.4. 代码中的日期和数字格式化示例
如果需要按照用户当前的 locale 格式化或者解析 BigDecimal
、 Integer
、 Long
、 Double
、 Boolean
或者 Date
类型,可以使用 DatatypeFormatter
bean,示例:
@Inject
private DatatypeFormatter formatter;
void sample() {
String dateStr = formatter.formatDate(dateField.getValue());
// ...
}
下面是直接使用 Datatype
的例子。
-
日期格式化示例:
@Inject protected UserSessionSource userSessionSource; @Inject protected DatatypeRegistry datatypes; void sample() { Date date; // ... String dateStr = datatypes.getNN(Date.class).format(date, userSessionSource.getLocale()); // ... }
-
在 Web 客户端格式化数字,显示最多 5 位小数示例:
com/sample/sales/web/messages_ru.propertiescoordinateFormat = #,##0.00000
@Inject protected Messages messages; @Inject protected UserSessionSource userSessionSource; @Inject protected FormatStringsRegistry formatStringsRegistry; void sample() { String coordinateFormat = messages.getMainMessage("coordinateFormat"); FormatStrings formatStrings = formatStringsRegistry.getFormatStrings(userSessionSource.getLocale()); NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols()); String formattedValue = format.format(value); // ... }
3.2.2.4. 元注解
实体元注解是一组提供有关实体的附加信息的键值对(key-value)。 使用元类的 getAnnotations()
方法访问元注解。 元注解的来源是:
-
@OnDelete
、@OnDeleteInverse
、@Extends
注解。通过这些注解将创建描述实体之间关系的特殊元注解。 -
可扩展的元注解用
@MetaAnnotation
标明。这些注解会转换成元注解,key 是注解对应的 Java 类的全名称,value 是一组注解属性的映射。比如,@TrackEditScreenHistory
注解的 value 就是一个单一的映射:value → true
。平台提供几个这种类型的注解:@NamePattern
,@SystemLevel
,@EnableRestore
,@TrackEditScreenHistory
。在应用程序或者应用程序组件中,可以创建自定义的注解类然后使用@MetaAnnotation
对自定义注解类进行标注。 -
可选:实体元注解也可以在metadata.xml文件中定义。如果 XML 中的元注解与 Java 实体类注解创建的元注解具有相同的名称,那么它将覆盖后者。
下面的示例演示了如何通过
metadata.xml
中的元注解覆盖 Java 类中的注解:<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd"> <!-- ... --> <annotations> <entity class="com.company.customers.entity.Customer"> <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory"> <attribute name="value" value="true" datatype="boolean"/> </annotation> <property name="name"> <annotation name="length" value="200"/> </property> <property name="customerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.Lookup"> <attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/> <attribute name="actions" datatype="string"> <value>lookup</value> <value>open</value> </attribute> </annotation> </property> </entity> <entity class="com.company.customers.entity.CustomerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore"> <attribute name="value" value="false" datatype="boolean"/> </annotation> </entity> </annotations> </metadata>
3.2.3. 视图
从数据库中检索实体时,我们经常面临一个问题:如何确保将关联实体加载到所需的深度?
例如,需要在 Order 浏览界面中显示日期、金额以及客户名称,这意味着需要获取相关的 Customer 实例。还有,在 Order 编辑界面,需要获取 Item 集合,此外每个 Item 应包含一个相关的 Product 实例以显示其名称。
延迟加载在大多数情况下都无济于事,因为数据处理通常不是在加载实体的事务中执行,而是在像 UI 这样的客户端层执行。同样,使用实体注解来应用贪婪加载也是不可行的,因为这样做会导致总是查询对象关系图中所有的关联实体,这可能是非常大的数据。
另一个类似的问题是需要限制加载对象图中的本地属性的集合:例如,某个实体可以有 50 个属性,包括 BLOB,但是只需要在界面上显示 10 个属性。此时,对于不需要的 40 个额外属性,为什么要从数据库加载然后将它们序列化并传输到客户端?
视图 机制通过从数据库中检索并向客户端传输有深度和属性限制的实体关系图来解决这些问题。特定 UI 界面或数据处理操作所需的对象图通过 视图 来描述。
视图处理过程按以下方式执行:
无论视图中的属性如何定义,始终会加载以下属性:
|
尝试获取或设置未加载属性的值(未包含在视图中)会引发异常。您可以使用 |
参考下一章节了解如何定义视图。
下面解释一些视图机制的内部原理。
视图由 View
类的实例确定,其中:
-
entityClass
– 定义视图的实体类。换句话说,它是加载的实体树的“根”。 -
name
– 视图名称。可以是“null”或实体的所有视图中的唯一名称。 -
properties
– 对应需要加载的实体属性的ViewProperty
实例的集合。 -
includeSystemProperties
– 如果设置,则视图将包含系统属性(由持久化实体的基础接口定义,如BaseEntity
和Updatable
)。
-
loadPartialEntities
- 指定视图是否影响本地(立即加载)属性的加载。如果为 false,则仅影响引用属性,并且会始终加载本地属性,而无论它们是否存在于视图中。此属性在某种程度上由平台数据加载机制控制,请参阅有关在 DataManager 和 EntityManager 中加载部分实体的章节。
ViewProperty
类具有以下属性:
-
name
– 实体属性名。 -
view
– 对于引用属性,指定将用于加载关联实体的视图。 -
fetch
- 对于引用属性,指定如何从数据库中获取关联实体。其值对应于FetchMode
枚举类型:-
AUTO
- 平台将根据关系类型选择最佳模式。 -
UNDEFINED
- 将根据 JPA 规则执行提取,这实际上意味着通过单独的 select 进行加载。 -
JOIN
- 通过关联引用的表来在同一个 select 查询中获取。 -
BATCH
- 相关对象的查询将通过分批的方式进行优化。参阅:这里。
如果未指定
fetch
属性,则应用AUTO
模式。如果引用是一个可缓存的实体,则无论视图中指定的什么值,都将使用UNDEFINED
。 -
3.2.3.1. 创建视图
视图可以通过下列方式创建:
-
编程方式 – 通过创建
View
实例。这种方式适合在业务逻辑中创建并视图。视图实例,包括内嵌的视图,可以通过构造器创建:
View view = new View(Order.class) .addProperty("date") .addProperty("amount") .addProperty("customer", new View(Customer.class) .addProperty("name") );
也可以使用
ViewBuilder
:View view = ViewBuilder.of(Order.class) .addAll("date", "amount", "customer.name") .build();
ViewBuilder
还能用于 DataManager 的流式操作接口:// explicit view builder dataManager.load(Order.class) .view(viewBuilder -> viewBuilder.addAll("date", "amount", "customer.name")) .list(); // implicit view builder dataManager.load(Order.class) .viewProperties("date", "amount", "customer.name") .list();
-
界面中声明式创建 – 通过在界面的 XML 中定义内嵌的视图,参考 声明式创建数据组件 中的例子。推荐在 CUBA 7.2+ 中使用这种方式在 Generic UI 界面中加载数据。
-
共享仓库中声明式创建 – 通过在项目的 views.xml 文件中定义视图。应用程序启动时会部署
views.xml
文件,创建的View
实例都缓存在ViewRepository
中。此外,通过调用ViewRepository
并提供实体类和视图名称,可以在应用程序代码的任何部分中获取所需的视图。
下面是 XML 视图声明的示例,能加载 Order
实体的所有本地属性、关联的 Customer
以及 Items
集合:
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
<property name="items" view="item-in-order"/>
</view>
- 使用共享的视图仓库
-
ViewRepository
是一个 Spring bean,所有应用程序 block 都可以访问这个 bean。可以使用注入或通过AppBeans
静态方法得到ViewRepository
的引用。然后用ViewRepository.getView()
方法从视图库中获取视图实例。默认情况下,每个实体的视图库中都有三个名为
_local
、_minimal
和_base
的视图:-
_local
包含所有本地实体属性。 -
_minimal
包含实体实例名称中包含的属性,这些属性通过@NamePattern注解指定。如果未在实体中指定@NamePattern
注解,则此视图不包含任何属性。 -
_base
包括所有本地非系统属性和由@NamePattern
定义的属性(实际上是_minimal
+_local
)。
CUBA Studio 会在项目内自动创建并维护单一的 views.xml 文件。但是,也可以按照下面的方法创建多个视图描述文件:
-
global
模块必须包含带有所有视图描述文件的views.xml
文件,这些视图文件必须是全局可访问的(即能被所有的层访问)。该文件需要在所有层的 cuba.viewsConfig 应用程序属性中注册,比如,core
模块的app.properties
,web
模块的web-app.properties
等。Studio 默认会做这些事。 -
如果有大量的共享视图,可以将它们放在多个文件内,比如在标准的
views.xml
文件以及附加的foo-views.xml
、bar-views.xml
文件。然后在cuba.viewsConfig
应用程序属性中注册所有这些文件:cuba.viewsConfig = +com/company/sales/views.xml com/company/sales/foo-views.xml com/company/sales/bar-views.xml
-
如果有某些视图只在一个应用程序层使用,则可以单独在该层的类似文件中定义,比如
web-views.xml
,然后在该层的cuba.viewsConfig
属性中注册该文件。
如果视图库已经包含某个实体的特定名称的视图,则不会为此实体部署其它同名视图。如果需要使用新的视图替换视图库中的现有视图并保证其部署,需要为新视图指定
overwrite="true"
属性。建议为共享视图提供描述性名称。例如,不仅仅是 "browse",而是 "customerBrowse"。这样可以简化 XML 描述中视图的搜索。
-
3.2.4. Spring Beans
Spring Beans 是一些类,它们实例的创建和依赖关系的管理都由 Spring 框架容器处理。Beans 设计用于实现应用程序业务逻辑。
默认情况下,Spring Bean 是 单例,即每个应用程序 block 中只存在此类的一个实例。如果单例 bean 在字段中包含可变数据(换句话说,具有状态),对这些数据的访问必须同步。 |
3.2.4.1. 创建 Spring bean
参考 在 CUBA 中创建业务逻辑 指南,了解如何使用 Spring bean 创建业务逻辑。 |
如需创建 Spring bean,只需要在 Java 类添加 @org.springframework.stereotype.Component
注解。示例:
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;
@Component(OrderWorker.NAME)
public class OrderWorker {
public static final String NAME = "sales_OrderWorker";
public void calculateTotals(Order order) {
}
}
建议为 bean 分配一个唯一的名称,按照 {project_name}_{class_name}
格式,并且用常量 NAME
定义。
|
Bean 的类定义需要放在 spring.xml 文件的 context:component-scan
元素指定的扫描目录树下。对于上面例子中的 bean,spring.xml
需要包含元素:
<context:component-scan base-package="com.sample.sales"/>
也就是说,扫描此应用程序 block 中带注解的 bean 会从包的 com.sample.sales
目录开始。
Spring bean 可以在任何层创建。
3.2.4.2. 使用 Spring bean
托管给 Spring 的 bean 可以通过注入或者通过 AppBeans
类获取。作为使用 bean 的示例,看看 OrderService
bean 的实现,它将具体的执行代理给了 OrderWorker
bean 来做:
package com.sample.sales.core;
import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Inject
protected Persistence persistence;
@Inject
protected OrderWorker orderWorker;
@Transactional
@Override
public BigDecimal calculateTotals(Order order) {
Order entity = persistence.getEntityManager().merge(order);
return orderWorker.calculateTotals(entity);
}
}
3.2.5. JMX Beans
有时,有必要让系统管理员能够在运行时查看和更改某些Spring bean 的状态。在这种情况下,建议创建一个 JMX bean - 一个具有 JMX 接口的程序组件。JMX bean 通常是一个包装器,代理了对托管 bean 的调用,但实际上还是由托管 bean 维护状态:比如缓存、配置数据或统计信息。
从图中可以看出,JMX bean 由接口和实现类组成。实现类应该是一个Spring bean,即应该具有 @Component
注解和唯一名称。JMX bean 的接口以一种特殊的方式在 spring.xml 中注册,以便在当前 JVM 中创建 JMX 接口。
使用 Spring AOP 通过 MBeanInterceptor
拦截器类拦截对所有 JMX bean 接口方法的调用,该类在当前线程中设置正确的 ClassLoader
并对未处理异常进行日志记录。
JMX bean 接口名称必须符合以下格式: |
JMX 接口可以由外部工具使用,例如 jconsole 或 jvisualvm。此外,平台的 Web 客户端提供了 JMX console 功能,可以通过这个功能查看托管 Bean 状态、调用 JMX bean 方法。
3.2.5.1. 创建 JMX Bean
以下示例展示如何创建 JMX Bean。
-
JMX bean 接口:
package com.sample.sales.core; import org.springframework.jmx.export.annotation.*; import com.haulmont.cuba.core.sys.jmx.JmxBean; @JmxBean(module = "sales", alias = "OrdersMBean") @ManagedResource(description = "Performs operations on Orders") public interface OrdersMBean { @ManagedOperation(description = "Recalculates an order amount") @ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")}) String calculateTotals(String orderId); }
-
接口和其方法上可定义注解,通过这些注解来对 JMX Bean 和它提供的操作进行描述。这些描述会显示在使用 JMX 接口的所有工具中,这样可以帮助系统管理员了解接口。
-
可选的
@JmxBean
注解用来自动注册类实例至 JMX 服务,按照module
和alias
属性来注册。可以使用这个注解来注册 JMX bean,代替在 spring.xml 中配置。 -
可选的
@JmxRunAsync
注解用来标识出一个需要长时间执行的操作。当使用内置的 JMX console 运行此操作时,平台将会显示一个带有无限进度条和 Cancel 按钮的对话框。用户可以中断操作并继续使用应用程序。该注解还可以包含timeout
参数,该参数用来设置操作的最长执行时间(以毫秒为单位),例如:@JmxRunAsync(timeout = 30000) String calculateTotals();
如果执行超时,对话框将关闭并显示错误消息。
请注意,如果在用户界面上取消操作,或者观察到操作超时,但操作实际上继续在后台运行,也就是说这些操作不能实际终止,只是将控制权交给用户。
-
由于 JMX 工具只支持有限的数据类型,因此最好使用
String
作为方法的参数和结果类型,必要时,需要在方法内进行类型转换。除了String
外,还支持以下参数类型:boolean
、double
、float
、int
、long
、Boolean
、Integer
。
-
-
JMX bean 类:
package com.sample.sales.core; import com.haulmont.cuba.core.*; import com.haulmont.cuba.core.app.*; import com.sample.sales.entity.Order; import org.apache.commons.lang.exception.ExceptionUtils; import org.springframework.stereotype.Component; import javax.inject.Inject; import java.util.UUID; @Component("sales_OrdersMBean") public class Orders implements OrdersMBean { @Inject protected OrderWorker orderWorker; @Inject protected Persistence persistence; @Authenticated @Override public String calculateTotals(String orderId) { try { try (Transaction tx = persistence.createTransaction()) { Order entity = persistence.getEntityManager().find(Order.class, UUID.fromString(orderId)); orderWorker.calculateTotals(entity); tx.commit(); }; return "Done"; } catch (Throwable e) { return ExceptionUtils.getStackTrace(e); } } }
@Component
注解将类定义为具有sales_OrdersMBean
名称的 Spring bean,该名称直接在注解中指定而不是在常量中,因为不需要从 Java 代码访问 JMX bean。下面概述一下
calculateTotals()
方法的实现。 -
在
spring.xml
中注册 JMX bean:<bean id="sales_MBeanExporter" lazy-init="false" class="com.haulmont.cuba.core.sys.jmx.MBeanExporter"> <property name="beans"> <map> <entry key="${cuba.webContextName}.sales:type=Orders" value-ref="sales_OrdersMBean"/> </map> </property> </bean>
项目的所有 JMX beans 都是在
beans
属性的map/entry
元素中的一个MBeanExporter
实例中声明的。键是 JMX ObjectName,值是在@Component
注解中指定的 bean 名称。ObjectName 以 web 应用程序的名称开始,因为可以将输出相同 JMX 接口的多个 web 应用程序部署到一个应用程序服务实例中(部署到 JVM 中)。
3.2.5.2. 平台内置 JMX Beans
本节介绍平台中可用的一些 JMX beans。
3.2.5.2.1. CachingFacadeMBean
JMX 对象名: app-core.cuba:type=CachingFacade
和 app.cuba:type=CachingFacade
3.2.5.2.2. ConfigStorageMBean
该接口具有独立的一组操作,用于处理存储在文件 (*AppProperties
) 中和存储在数据库 (*DbProperties
) 中的属性。这些操作只显示在对应的存储中明确设置了值的属性。也就是说如果配置接口定义了属性和其默认值,但未在数据库(或文件)中设置该值,则这些方法将不显示该属性及其当前值。
请注意,对存储在文件中的属性值的修改不是持久化的,并且只有在重启应用程序模块后才能生效。
与上述操作不同,getConfigValue()
操作返回的值与在应用程序代码中调用配置接口的相应方法返回的值完全相同。
JMX 对象名:
-
app-core.cuba:type=ConfigStorage
-
app.cuba:type=ConfigStorage
-
app-portal.cuba:type=ConfigStorage
3.2.5.2.4. PersistenceManagerMBean
PersistenceManagerMBean 提供以下功能:
-
管理实体统计信息机制。
-
使用
findUpdateDatabaseScripts()
方法查看新的数据库更新脚本。使用updateDatabase()
方法触发数据库更新。 -
使用
jpqlLoadList()
、jpqlExecuteUpdate()
方法可以在中间层上下文中执行任意的 JPQL 查询
JMX 对象名: app-core.cuba:type=PersistenceManager
3.2.5.2.5. ScriptingManagerMBean
ScriptingManagerMBean 是 Scripting 基础接口的 JMX 外观设计模式实现。
JMX 对象名: app-core.cuba:type=ScriptingManager
JMX 属性:
JMX 操作:
-
runGroovyScript()
– 在中间层上下文中执行 Groovy 脚本并返回结果。会将以下变量传递给脚本:-
persistence
- Persistence 类型的参数。 -
metadata
- Metadata 类型的参数。 -
configuration
- Configuration 类型的参数。 -
dataManager
- DataManager 类型的参数。结果类型应该是可显示 JMX 界面上的字符串类型。除此之外,此方法与 Scripting.runGroovyScript() 完全一样。
用于创建一组测试用户的示例脚本,如下所示:
import com.haulmont.cuba.core.* import com.haulmont.cuba.core.global.* import com.haulmont.cuba.security.entity.* PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class) Transaction tx = persistence.createTransaction() try { EntityManager em = persistence.getEntityManager() Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93')) for (i in (1..250)) { User user = new User() user.setGroup(group) user.setLogin("user_${i.toString().padLeft(3, '0')}") user.setName(user.login) user.setPassword(passwordEncryption.getPasswordHash(user.id, '1')); em.persist(user) } tx.commit() } finally { tx.end() }
-
3.2.5.2.6. ServerInfoMBean
ServerInfoMBean 提供了有关此中间层模块的通用信息:内部版本号、 构建日期 和 服务器 ID.
JMX 对象名: app-core.cuba:type=ServerInfo
3.2.6. 基础设施接口
基础设施接口提供对平台的常用功能的访问。它们中的大多数位于 global 模块中,并且可以在中间层和客户端层使用。但是,其中一些(例如 Persistence )只能用于中间层代码。
此外,与任何其它 bean 一样,基础接口可以通过 AppBeans
类的静态方法获取,然后可以在非托管组件(POJO、helper 类等)中使用。
3.2.6.1. Configuration
该接口有助于获取配置接口的引用。
例如:
// field injection
@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection
protected GlobalConfig globalConfig;
@Inject
public void setConfiguration(Configuration configuration) {
this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location
String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
3.2.6.2. DataManager
DataManager
接口在中间层和客户端层提供 CRUD 功能,是一种通用工具,用于从数据库加载实体关系图并保存已更改的游离实体实例。
参考 在 CUBA 中进行数据处理 指南,了解在 CUBA 中如何使用 DataManager API 处理不同的数据访问。 |
有关 DataManager 与 EntityManager 之间差异的信息,请参阅 DataManager 与 EntityManager。 |
实际上,DataManager
只是委托给一个数据存储实现,并在需要时处理跨数据库引用。当使用标准 RdbmsStore
处理存储在关系型数据库中的实体时,下面描述的大多数实现细节都有效。对于另一种类型的数据存储,除接口方法名称之外的所有内容都可能不同。为简单起见,DataManager 在没有另外说明时指的是 基于 RdbmsStore 的 DataManager。
下面列出了 DataManager
的方法:
-
load(Class)
- 加载指定类的实体。此方法是流式 API 的入口点:@Inject private DataManager dataManager; private Book loadBookById(UUID bookId) { return dataManager.load(Book.class).id(bookId).view("book.edit").one(); } private List<BookPublication> loadBookPublications(UUID bookId) { return dataManager.load(BookPublication.class) .query("select p from library_BookPublication p where p.book.id = :bookId") .parameter("bookId", bookId) .view("bookPublication.full") .list(); }
-
loadValues(String query)
- 通过查询纯数值加载键值对。此方法是流式 API 的入口点:List<KeyValueEntity> list = dataManager.loadValues( "select o.customer, sum(o.amount) from demo_Order o " + "where o.date >= :date group by o.customer") .store("legacy_db") (1) .properties("customer", "sum") (2) .parameter("date", orderDate) .list();
1 - 指定实体所在的数据存储。 如果实体位于主数据存储,那么可以忽略这个方法。 2 - 指定返回的 KeyValueEntity
实体中的属性名称。 属性的顺序必须与查询结果集的列对应。 -
loadValue(String query, Class valueType)
- 通过查询纯数值加载单个值。此方法是流式 API 的入口点:BigDecimal sum = dataManager.loadValue( "select sum(o.amount) from demo_Order o " + "where o.date >= :date group by o.customer", BigDecimal.class) .store("legacy_db") (1) .parameter("date", orderDate) .one();
1 - 指定实体所在的数据存储。 如果实体位于主数据存储,那么可以忽略这个方法。 -
load(LoadContext)
,loadList(LoadContext)
– 根据传递给它的LoadContext
对象的参数加载实体。LoadContext
必须包含 JPQL 查询语句或实体标识符。如果两者都定义的话,则使用查询语句而忽略实体标识符。例如:
@Inject private DataManager dataManager; private Book loadBookById(UUID bookId) { LoadContext<Book> loadContext = LoadContext.create(Book.class) .setId(bookId).setView("book.edit"); return dataManager.load(loadContext); } private List<BookPublication> loadBookPublications(UUID bookId) { LoadContext<BookPublication> loadContext = LoadContext.create(BookPublication.class) .setQuery(LoadContext.createQuery("select p from library_BookPublication p where p.book.id = :bookId") .setParameter("bookId", bookId)) .setView("bookPublication.full"); return dataManager.loadList(loadContext); }
-
loadValues(ValueLoadContext)
- 加载键值对列表。该方法接受ValueLoadContext
,定义纯数值的查询语句和键值列表。返回包含KeyValueEntity
实例的列表。例如:ValueLoadContext context = ValueLoadContext.create() .setQuery(ValueLoadContext.createQuery( "select o.customer, sum(o.amount) from demo_Order o " + "where o.date >= :date group by o.customer") .setParameter("date", orderDate)) .addProperty("customer") .addProperty("sum"); List<KeyValueEntity> list = dataManager.loadValues(context);
-
getCount(LoadContext)
- 返回传递给方法的查询语句的记录数。可能的情况下,RdbmsStore
中的标准实现使用与原始查询相同的条件执行select count()
查询,以获得最佳性能。 -
commit(CommitContext)
– 将CommitContext
中传递的一组实体保存到数据库中。必须分别指定用于更新和删除的实体的集合。该方法返回 EntityManager.merge() 返回的实体实例集合,实际上这些就是刚刚在 DB 中更新的新实例。后续的操作需要使用这些返回的实例,以防止数据丢失或造成乐观锁。通过使用
CommitContext.getViews()
获得的视图映射为每个保存的实例设置视图,这样可以确保返回实体中包含需要的属性。DataManager
可以为保存的实体进行 bean 验证。保存实体集合的示例:
@Inject private DataManager dataManager; private void saveBookInstances(List<BookInstance> toSave, List<BookInstance> toDelete) { CommitContext commitContext = new CommitContext(toSave, toDelete); dataManager.commit(commitContext); } private Set<Entity> saveAndReturnBookInstances(List<BookInstance> toSave, View view) { CommitContext commitContext = new CommitContext(); for (BookInstance bookInstance : toSave) { commitContext.addInstanceToCommit(bookInstance, view); } return dataManager.commit(commitContext); }
-
reload(Entity, View)
- 使用视图从数据库重新加载指定实例的便捷方法。委托给load()
方法执行。 -
remove(Entity)
- 从数据库中删除指定的实例。委托给commit()
方法执行。 -
create(Class)
- 在内存中创建给定实体的实例。这是一个便捷的方法,委托给了Metadata.create()
。 -
getReference(Class, Object)
- 返回一个实体实例,该实例可以用作对数据库中存在的对象的引用。例如,如果要创建
User
,则必须设置用户所属的Group
。如果知道 group ID,可以从数据库加载然后设置给用户。此方法可以避免不必要的数据库多次访问:user.setGroup(dataManager.getReference(Group.class, groupId)); dataManager.commit(user);
引用也可用于通过 id 删除现有对象:
dataManager.remove(dataManager.getReference(Customer.class, customerId));
- 查询
-
当系统使用关系型数据库时,用JPQL查询语句加载数据。参阅 JPQL 函数、不区分大小写的子串搜索 和 JPQL 中的宏 章节了解 CUBA 中的 JPQL 和 JPA 标准之间的差异。另外需要注意,
DataManager
只能执行 "select" 查询。流式接口的
query()
方法可以接收完整的或者省略的查询语句。如果使用省略的查询语句,需要符合下面的规则:-
可以省略
"select <alias>"
子句。 -
如果
"from"
子句包含单一实体,而且又不想用实体别名,则可以省略"from <entity> <alias> where"
子句。此时,框架会使用e
作为该实体的别名。 -
可以使用位置标记查询条件并同时将调价值作为额外的参数传递给
query()
方法。
示例:
// named parameter dataManager.load(Customer.class) .query("e.name like :name") .parameter("name", value) .list(); // positional parameter dataManager.load(Customer.class) .query("e.name like ?1", value) .list(); // case-insensitive positional parameter dataManager.load(Customer.class) .query("e.name like ?1 or e.email like ?1", "(?i)%joe%") .list(); // multiple positional parameters dataManager.load(Order.class) .query("e.date between ?1 and ?2", date1, date2) .list(); // omitting "select" and using a positional parameter dataManager.load(Order.class) .query("from sales_Order o, sales_OrderLine l " + "where l.order = o and l.product.name = ?1", productName) .list();
需要注意的是,位置标记条件参数只在流式接口支持。
LoadContext.Query
还是只支持命名条件参数。 -
- 事务
-
DataManager 总是启动一个新的事务并在操作完成时提交事务,从而返回游离状态的实体。在中间层,如果需要实现复杂的事务行为,可以使用 TransactionalDataManager。
- 部分实体
-
部分(Partial) 实体是一个实体实例,这个实例的属性可以是已加载的本地属性的一个子集。默认情况下,DataManager 根据视图加载部分实体(事实上,
RdbmsStore
只是将视图的 loadPartialEntities 属性设置为 true 并将其传递给 EntityManager )。在下面这些情况下,DataManager 会加载所有本地属性,视图仅用来获取引用:
3.2.6.2.1. DataManager 与 EntityManager
DataManager和EntityManager都可以用于实体的 CRUD 操作。这两个接口之间存在以下差异:
DataManager | EntityManager |
---|---|
DataManager 在中间层和客户端层都可用。 |
EntityManager 仅在中间层可用。 |
DataManager 是一个单例 bean。它可以通过 |
应该通过Persistence接口获取 EntityManager 的引用。 |
DataManager 定义了一些使用游离实体的高级方法: |
EntityManager 与标准的 |
DataManager 在保存实体是可以进行 bean 验证。 |
EntityManager 不进行 bean 验证。 |
实际上,DataManager 委托给DataStore实现,因此下面列出的 DataManager 功能仅适用于处理关系型数据库中的实体最常见的情况:
DataManager | EntityManager |
---|---|
DataManager 始终在内部启动新的事务。在中间层,如果需要实现复杂的事务行为,可以使用 TransactionalDataManager。 |
在使用 EntityManager 之前,必须先打开一个事务。 |
EntityManager 加载所有本地属性。如果指定了视图,则仅影响引用属性。详情请参阅这里。 |
|
DataManager 仅执行 JPQL 查询。此外,它有单独的加载实体的方法: |
EntityManager 可以运行任何 JPQL 或原生(SQL)查询。 |
在客户端层调用时,DataManager 会检查安全限制。 |
EntityManager 不会应用安全限制。 |
在客户端层处理数据时,只有一个选择 - DataManager
。在中间件层,当需要在事务内部实现某些原子逻辑或者 EntityManager
接口更适合该任务时,请使用 TransactionalDataManager
。通常来说,在中间件上两者都可以使用。
3.2.6.2.2. TransactionalDataManager
TransactionalDataManager
是一个中间层的 bean,跟 DataManager
接口类似,但是可以使用一个已经存在的事务。有如下功能:
-
如果有活跃的事务,则会使用它,否则跟
DataManager
一样会创建并提交一个新事务。 -
接收并返回游离态的实体。开发者需要使用合适的视图加载实体并使用
save()
方法显式的保存修改的实例至数据库。 -
使用行级安全机制,使用跟
DataManager
一样的方式处理动态属性和跨数据存储引用。
下面是在 service 的方法中使用 TransactionalDataManager
的简单示例:
@Inject
private TransactionalDataManager txDataManager;
@Transactional
public void transfer(Id<Account, UUID> acc1Id, Id<Account, UUID> acc2Id, Long amount) {
Account acc1 = txDataManager.load(acc1Id).one();
Account acc2 = txDataManager.load(acc2Id).one();
acc1.setBalance(acc1.getBalance() - amount);
acc2.setBalance(acc2.getBalance() + amount);
txDataManager.save(acc1);
txDataManager.save(acc2);
}
可以在框架测试代码中找到更复杂的例子: DataManagerTransactionalUsageTest.java
如果需要在当前事务中处理 EntityChangedEvent 事件的话, |
3.2.6.2.3. DataManager 安全机制
DataManager 的 load()
、loadList()
、loadValues()
和 getCount()
方法会检查用户是否对要加载的实体有 READ 权限。此外,从数据库加载实体也受访问组约束的限制。
commit()
方法检查新建实体需要的 CREATE 权限、更新实体需要的 UPDATE 权限和删除实体需要的 DELETE 权限。
默认情况下,在客户端调用时,DataManager
会检查实体操作(READ / CREATE / UPDATE / DELETE)的权限,但是在从中间件代码调用时忽略这些检查。默认情况不强制检查实体属性权限。
如果要在中间件代码中使用 DataManager
时检查实体操作权限,请通过 DataManager.secure()
方法获取包装类并调用其方法。或者,可以设置 cuba.dataManagerChecksSecurityOnMiddleware 应用程序属性以打开整个应用程序的安全检查。
只有在将 cuba.entityAttributePermissionChecking 应用程序属性设置为 true 时,才会在中间件上强制执行属性权限。如果中间件为理论上可以被攻击的远程客户端提供服务,比如桌面客户端(这是可能的)。在这种情况下,还要将 cuba.keyForSecurityTokenEncryption 应用程序属性设置为唯一值。如果应用程序仅使用 Web 或 Portal 客户端,则可以安全地保留这些属性的默认值。
请注意,无论上述条件如何始终都会实施访问组约束 (行级别安全性)。
参阅 数据访问检查 章节全面了解框架中如何使用安全权限和约束。
3.2.6.2.4. 去重查询
如果一个界面包含带分页的表格,以及用于加载数据的 JPQL 查询,由于应用了通用过滤器或访问组约束,JPQL 查询在运行时会被修改。在 JPQL 查询中省略 distinct
运算符时会发生以下情况:
-
如果在数据库级别进行数据集合的组合,则加载的数据集将包含重复行。
-
在客户端级别,由于数据被添加到 map 中(
java.util.Map
),重复项在数据源中会消失。 -
对于分页表格,界面可能显示的行数少于请求的行数,而总行数超出请求的数量。
因此,我们建议在 JPQL 查询中包含 distinct
,以确保从数据库返回的数据集中不存在重复项。但是,如果返回的记录数很大(超过 10000),则在使用 distinct
执行 SQL 查询时,某些数据库服务器(特别是 PostgreSQL)会出现性能问题。
为了解决这个问题,平台包含一种在 SQL 级别上没有 distinct
的情况下正常运行的机制。此机制由 cuba.inMemoryDistinct 应用程序属性启用。启用后,会执行以下操作:
-
JPQL 查询仍然应包含
select distinct
。 -
在将数据发送到 ORM 之前,
DataManager
会从 JPQL 查询中删除distinct
。 -
数据页由
DataManager
加载后,会删除重复项在 DB 运行其它查询,用来检索返回给客户端需要的行数。
3.2.6.2.5. 级联查询(Sequential Queries)
DataManager
可以从之前的请求的结果中再次选择数据。此功能被通用过滤器用在连续使用过滤器的场景。
该机制的工作原理如下:
-
如果提供一个定义了
prevQueries
和queryKey
属性的LoadContext
,DataManager
将执行先前的查询并将检索到的实体的标识符保存在SYS_QUERY_RESULT
表中(对应于sys$QueryResult
实体),保存时会根据用户会话和查询会话的queryKey
分隔记录集。 -
修改当前查询以便与前一个查询的结果组合,这样能得到按照 AND 组合两个查询条件的结果。
-
之后重复该过程。在这种情况下,逐渐减少的之前的结果集会被从
SYS_QUERY_RESULT
表中删除并重填。
已经终止的用户会话在 SYS_QUERY_RESULT
表中留下的历史查询结果会被定期清理。这是由 QueryResultsManagerAPI
bean 的 deleteForInactiveSessions()
方法完成的,该方法由 cuba-spring.xml
中定义的 Spring 调度程序调用。 默认情况下,每 10 分钟执行一次,但可以使用 core 模块的 cuba.deleteOldQueryResultsInterval
应用程序属性按需要设置不同的时间间隔(以毫秒为单位)。
3.2.6.3. EntityStates
获取由 ORM 管理的持久化实体信息的接口。与 Persistence 和 PersistenceTools bean 不同,此接口可用于所有层。
EntityStates
接口具有以下方法:
-
isNew()
– 确定传递的实例是否是新创建的,即是否在 New 状态。如果此实例实际上处于 Managed 状态但在当前事务中刚被持久化,或者不是持久化实体,也会返回true
。 -
isManaged()
- 确定传递的实例是否被托管,比如是否添加到持久化上下文。 -
isDetached()
– 确定传递的实例是否处于游离状态。如果此实例不是持久化实体,也返回true
。 -
isLoaded()
- 确定是否从数据库加载了属性。属性已加载:如果属性包含在视图中,或者如果是本地属性并且未向加载机制(EntityManager 或 DataManager)提供视图。此方法只能检查实体的直接属性。 -
checkLoaded()
- 与isLoaded()
一样,但如果传递给方法的属性中至少有一个未加载,则抛出IllegalArgumentException
。 -
isLoadedWithView()
- 接收实例和视图作为参数,如果实际加载了视图所需的所有属性,则返回 true。 -
checkLoadedWithView()
- 与isLoadedWithView()
一样,只不过是抛出IllegalArgumentException
而不返回 false。 -
makeDetached()
- 接收新创建的实体实例并将其转换为游离状态。游离的对象可以传递给DataManager.commit()
或EntityManager.merge()
以将其状态保存在数据库中。请参阅 API 文档中的详细信息。 -
makePatch()
- 接受新创建的实体实例并使其成为 补丁对象(patch object)。该补丁对象可以传递给DataManager.commit()
或EntityManager.merge()
以将其状态保存在数据库中。与游离对象不同,补丁对象只保存非空属性。请参阅 API 文档中的详细信息。
3.2.6.4. Events
参考 用应用程序事件解耦业务逻辑 向导,了解应用程序事件的不同示例。 |
Events
bean 封装了应用程序范围内的事件发布功能。应用程序事件可用于在松耦合的组件之间交换信息。Events
bean 是 Spring Framework 的 ApplicationEventPublisher
的简单外观设计模式实现。
public interface Events {
String NAME = "cuba_Events";
void publish(ApplicationEvent event);
}
此接口只有接收一个事件对象作为参数的 publish()
方法。Events.publish()
会通知所有应用程序内注册的匹配的监听器。可以使用 PayloadApplicationEvent
将任何对象发布为事件。
另请参阅 Spring 框架入门
- bean 中的事件处理
-
首先,创建一个新的事件类,继承
ApplicationEvent
类。事件类可以包含任何其它数据。例如:package com.company.sales.core; import com.haulmont.cuba.security.entity.User; import org.springframework.context.ApplicationEvent; public class DemoEvent extends ApplicationEvent { private User user; public DemoEvent(Object source, User user) { super(source); this.user = user; } public User getUser() { return user; } }
Bean 对象可以使用
Events
bean 发布事件:package com.company.sales.core; import com.haulmont.cuba.core.global.Events; import com.haulmont.cuba.core.global.UserSessionSource; import com.haulmont.cuba.security.global.UserSession; import org.springframework.stereotype.Component; import javax.inject.Inject; @Component public class DemoBean { @Inject private Events events; @Inject private UserSessionSource userSessionSource; public void demo() { UserSession userSession = userSessionSource.getUserSession(); events.publish(new DemoEvent(this, userSession.getUser())); } }
默认情况下,所有事件都是同步处理的。
处理事件有两种方法:
-
实现
ApplicationListener
接口。 -
方法使用
@EventListener
注解。
在第一种情况下,必须创建一个实现
ApplicationListener
接口的 bean,以需要事件类型作为泛型参数:@Component public class DemoEventListener implements ApplicationListener<DemoEvent> { @Inject private Logger log; @Override public void onApplicationEvent(DemoEvent event) { log.debug("Demo event is published"); } }
第二种方法可用于隐藏实现接口的细节并在单个 bean 中监听多个事件:
@Component public class MultipleEventListener { @Order(10) @EventListener protected void handleDemoEvent(DemoEvent event) { // handle event } @Order(1010) @EventListener protected void handleUserLoginEvent(UserLoggedInEvent event) { // handle event } }
BeanInitializationException: Failed to process @EventListener annotation on bean. Need to invoke method declared on target class, but not found in any interface(s) of the exposed proxy type. Either pull the method up to an interface or switch to CGLIB proxies by enforcing proxy-target-class mode in your configuration.
如果确实需要在带有接口的 bean 中监听事件,可以通过实现
ApplicationListener
接口来代替。可以使用 Spring 框架的
Ordered
接口和@Order
注解来对事件处理方法排序。框架中所有 bean 和事件处理方法使用的是 100 到 1000 之间的order
值,因此可以在框架的 order 值之前或之后添加自定义的处理顺序。如果要在平台 bean 之前添加 bean 或事件处理程序,可使用小于 100 的值。另请参阅登录事件.
-
- UI 界面中的事件处理
-
通常,
Events
将事件的发布委托给ApplicationContext
执行。在 Web 层,可以为事件类使用特殊的接口 -UiEvent
。这是一个事件的标记接口,实现这个接口的事件被发送到当前 UI 实例中的 UI 界面(当前 Web 浏览器标签页)。请注意,
UiEvent
实例不会发送到 Spring bean。示例事件类:
package com.company.sales.web; import com.haulmont.cuba.gui.events.UiEvent; import com.haulmont.cuba.security.entity.User; import org.springframework.context.ApplicationEvent; public class UserRemovedEvent extends ApplicationEvent implements UiEvent { private User user; public UserRemovedEvent(Object source, User user) { super(source); this.user = user; } public User getUser() { return user; } }
可以在窗口的控制器中使用
Events
bean 触发事件,使用方式和在 bean 中一样:@Inject Events events; // ... UserRemovedEvent event = new UserRemovedEvent(this, removedUser); events.publish(event);
为了处理事件,必须在 UI 界面中使用注解
@EventListener
定义方法(不支持ApplicationListener
接口):@Order(15) @EventListener protected void onUserRemove(UserRemovedEvent event) { notifications.create() .withCaption("User is removed " + event.getUser()) .show(); }
可以使用
@Order
注解为事件监听器设定执行顺序。如果一个事件是
UiEvent
并使用来自 UI 线程的Events
bean 触发,那么带有@EventListener
方法的打开的窗口或 frame 将接收到该事件。事件处理是同步的。只有用户打开的当前 Web 浏览器标签页中 UI 界面才会收到该事件。
3.2.6.5. Messages
Messages
接口提供了获取本地消息字符串的方法。
主要方法如下:
-
getMessage()
– 根据消息键名、包名和需要的语言环境返回本地化的消息。该方法有几个重载,如果未在方法参数中指定语言环境,则使用当前用户语言设置。例如:
@Inject protected Messages messages; ... String message1 = messages.getMessage(getClass(), "someMessage"); String message2 = messages.getMessage("com.abc.sales.web.customer", "someMessage"); String message3 = messages.getMessage(RoleType.STANDARD);
-
formatMessage()
– 通过消息键名、包名和需要的语言环境检索本地化消息,然后使用获取到的消息格式化输入的参数。格式化消息根据String.format()
方法的规则定义。该方法有几个重载,如果未在方法参数中指定语言环境,则使用当前用户的语言设置。例如:
String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
-
getMainMessage()
– 从应用程序 block 的主消息包返回本地化消息。例如:
protected Messages messages = AppBeans.get(Messages.class); ... messages.getMainMessage("actions.Ok");
-
getMainMessagePack()
– 返回应用程序 block 的主消息包的名称。例如:
String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
-
getTools()
– 返回MessageTools
接口实例(见下文)。
3.2.6.5.1. MessageTools
MessageTools
接口是一个Spring bean,包含用于处理本地化消息的其它方法。可以使用 Messages.getTools()
方法访问 MessageTools
接口,或像任何其它任何 bean 一样,通过注入的方式或 AppBeans
类来访问。
MessageTools
的方法:
-
loadString()
– 返回一个本地化的消息,由msg://{messagePack}/{key}
格式的消息引用指定消息引用字符串的结构:
-
msg://
– 必须的前缀。 -
{messagePack}
– 消息包的可选名称。如果未指定,则假定包名称作为单独的参数传递给loadString()
。 -
{key}
– 包中消息的键名。
消息引用字符串的示例:
msg://someMessage msg://com.abc.sales.web.customer/someMessage
-
-
getEntityCaption()
– 返回实体的本地化名称。 -
getPropertyCaption()
– 返回实体属性的本地化名称。 -
hasPropertyCaption()
– 检查实体属性是否被赋予了本地化名称。 -
getDefaultLocale()
– 返回应用程序默认语言环境,这是 cuba.availableLocales 应用程序属性中列出的第一个语言环境。 -
useLocaleLanguageOnly()
– 如果应用程序支持的所有语言环境(在cuba.availableLocales
属性中定义)都只指定了语言参数,没有指定 country 和 variant,则返回true
。平台机制使用此方法,当语言环境信息是从外部源(例如操作系统或 HTTP 请求)接收到的,此机制需要确定一个最合适的支持语言。 -
trimLocale()
– 如果useLocaleLanguageOnly()
方法返回true
,则从传递的语言环境信息中清除除语言参数外的所有信息(country 和 variant)。
在应用程序中可以通过 覆盖 MessageTools
bean 来扩展它的方法集。下面是使用扩展接口的示例:
MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
3.2.6.6. Metadata
Metadata
接口提供对元数据会话和视图仓库的访问功能。
接口方法:
-
getSession()
– 返回元数据会话实例。 -
getViewRepository()
– 返回视图仓库实例。 -
getExtendedEntities()
– 返回ExtendedEntities
类实例,用于处理扩展实体。在扩展实体章节查看更多信息。 -
create()
– 创建一个实体实例,此方法考虑了可能的实体扩展。对于持久化的
BaseLongIdEntity
和BaseIntegerIdEntity
子类,在创建后立即分配标识符。新标识符是通过自动创建的数据库序列获取的。默认情况下,序列在主数据存储中创建。但是,如果 cuba.useEntityDataStoreForIdSequence 应用程序属性设置为 true,则会在实体所属的数据存储中创建序列。 -
getTools()
– 返回MetadataTools
接口实例(见下文)。
3.2.6.6.1. MetadataTools
MetadataTools
是一个Spring bean,包含了处理元数据的其它方法。可以通过使用 Metadata.getTools()
方法访问 MetadataTools
接口,也可以像其它 bean 一样,通过注入或 AppBeans
类来获取。
MetadataTools
接口的方法:
-
getAllPersistentMetaClasses()
– 返回持久化实体元类的集合。 -
getAllEmbeddableMetaClasses()
– 返回可嵌入实体元类的集合。 -
getAllEnums()
– 返回用作实体属性类型的枚举类的集合。 -
format()
– 根据给定元属性(meta-property) 的数据类型格式化传递的值。 -
isSystem()
– 检查元属性是否是系统级属性,即在基础实体接口中定义的。 -
isPersistent()
– 检查元属性是否是持久化属性,即属性有对应的数据库字段。 -
isTransient()
– 检查元属性或任意属性是否为非持久化的。 -
isEmbedded()
– 检查元属性是否为嵌入对象。 -
isAnnotationPresent()
– 检查在一个类或其父类中是否存在指定的注解。 -
getNamePatternProperties()
– 返回实例名称中包含的属性的元属性集合,实例名由Instance.getInstanceName()
方法返回。请参阅 @NamePattern。
可以通过在应用程序中 覆盖 MetadataTools
bean 来扩展其方法集。使用扩展接口的示例:
MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
3.2.6.7. Resources
Resources
接口根据以下规则管理资源的加载:
-
如果提供的位置是 URL,则通过该 URL 下载资源;
-
如果提供的位置以
classpath:
前缀开头,则从类路径(classpath)加载资源; -
如果该位置不是 URL 并且它不以
classpath:
开头,那么:-
使用提供的位置作为相对路径,在应用程序的 配置文件目录 中搜索该文件。如果找到该文件,则从中下载资源;
-
如果在前面的步骤中找不到资源,则从类路径(classpath)下载。
-
实际上,很少使用显式的 URL 标识或 classpath:
前缀,因此通常可以通过配置文件目录或类路径下载资源。配置文件目录中的资源会覆盖类路径下相同名称的资源,即配置文件目录下的资源的优先级更高。
Resources
接口的方法:
-
getResourceAsStream()
– 返回指定资源的InputStream
,如果找不到资源则返回null
。应该在使用后关闭流,例如:@Inject protected Resources resources; ... InputStream stream = null; try { stream = resources.getResourceAsStream(resourceLocation); ... } finally { IOUtils.closeQuietly(stream); }
也可以使用 "try with resources" 的编码方式:
try (InputStream stream = resources.getResourceAsStream(resourceLocation)) { ... }
-
getResourceAsString()
– 则以字符串形式返回所指定的资源内容,如果没有找到资源,返回null
。
3.2.6.8. Scripting
Scripting
接口用于动态地编译和加载 Java 和 Groovy 类(在运行时),以及执行 Groovy 脚本和表达式。
Scripting
接口的方法:
-
evaluateGroovy()
– 执行 Groovy 表达式并返回执行结果。cuba.groovyEvaluatorImport 应用程序属性用于定义插入到每个被执行表达式中的导入类的公共集合。默认情况下,所有标准应用程序 block 都导入 PersistenceHelper 类。
已编译的表达式会被缓存,这大大加快了重复执行的速度。
例如:
@Inject protected Scripting scripting; ... Integer intResult = scripting.evaluateGroovy("2 + 2", new Binding()); Binding binding = new Binding(); binding.setVariable("instance", new User()); Boolean boolResult = scripting.evaluateGroovy("return PersistenceHelper.isNew(instance)", binding);
-
runGroovyScript()
– 执行 Groovy 脚本并返回执行结果。该脚本应位于应用程序 配置文件夹或类路径(classpath)中(当前的
Scripting
实现仅支持 JAR 文件中的类路径资源)。配置文件夹中的脚本将覆盖类路径中相同名称的脚本。脚本的路径是使用分隔符
/
构造的,路径的开头不需要分隔符。例如:
@Inject protected Scripting scripting; ... Binding binding = new Binding(); binding.setVariable("itemId", itemId); BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
-
loadClass()
– 使用以下步骤加载 Java 或 Groovy 类:-
如果类已加载,则返回该类。
-
在配置文件夹中搜索 Groovy 源代码(
*.groovy
文件)。如果找到 Groovy 文件,它将被编译并返回 class 文件。 -
在配置文件夹中搜索 Java 源代码(
*.java
文件)。如果找到它,它将被编译并返回 class 文件。 -
在类路径中搜索已编译的 class 文件。如果找到,它将被加载并返回。
-
如果找不到任何内容,将返回
null
。
可以在运行时修改配置文件夹中的包含 Java 和 Groovy 源代码的文件。在下一次调用
loadClass()
时,将重新编译相应的类,并返回新的类,这个机制有以下限制:-
源代码的类型不能从 Groovy 更改为 Java;
-
Groovy 源代码一旦编译,删除这个源代码文件不会导类重新加载,而是仍然返回之前编译好的类。
例如:
@Inject protected Scripting scripting; ... Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
-
-
getClassLoader()
– 返回一个类加载器(ClassLoader)
,它能够按照上述loadClass()
方法的规则加载类。
可以使用CachingFacadeMBean JMX bean 在运行时清理编译的类的缓存。
另请参阅 ScriptingManagerMBean。
3.2.6.9. Security
此接口用于检查用户对系统中不同对象的访问权限。大多数接口方法委托给当前 UserSession 对象的相应方法,但在此之前,会搜索实体的原始 meta-class,这对于具有扩展的项目很重要。 除了复制 UserSession
功能的方法之外,该接口还包含 isEntityAttrReadPermitted()
和 isEntityAttrUpdatePermitted()
方法,这两个方法用于检查指定属性路径的可用性,在检查的过程中会检查包含在这个路径中的所有实体和属性的可用性。
建议应用程序代码中使用 Security
接口,而不是直接调用 UserSession.isXYXPermitted()
方法。
在用户认证中查看更多信息。
3.2.6.10. TimeSource
TimeSource
接口用于获取当前时间。不建议在应用程序代码中使用 new Date()
或类似的方法来获取当时时间。
例如:
@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
3.2.7. 应用程序上下文(AppContext)
AppContext
是一个系统类,它在其静态字段中存储对每个应用程序block的某些公共组件的引用,包括:
-
Spring 框架的
ApplicationContext
。 -
从
app.properties
文件加载的应用程序属性集合。 -
ThreadLocal
变量,存储SecurityContext实例。 -
应用程序生命周期监听器的集合(
AppContext.Listener
)。
启动应用程序时,使用加载器类初始化 AppContext
,对于每个应用程序block:
-
中间件加载器 –
AppContextLoader
-
Web 客户端加载器 –
WebAppContextLoader
-
Web 门户加载器 –
PortalAppContextLoader
AppContext
可以在应用程序代码中用于以下任务:
-
将
SecurityContext
传递给新的执行线程,请参阅用户认证。 -
注册监听器,在完全初始化之后或应用程序终止之前触发,例如:
AppContext.addListener(new AppContext.Listener() { @Override public void applicationStarted() { System.out.println("Application is ready"); } @Override public void applicationStopped() { System.out.println("Application is closing"); } });
请注意,在应用程序启动和关闭时执行代码的推荐方法是使用应用程序生命周期事件。
3.2.8. 应用程序生命周期事件
CUBA 应用程序中有以下几种类型的生命周期事件:
- AppContextInitializedEvent
-
在AppContext初始化后立即触发。 此时:
-
所有bean都已完全初始化,并且执行了
@PostConstruct
方法。 -
可通过静态方法
AppBeans.get()
获取 bean。 -
AppContext.isStarted()
方法返回false
。 -
AppContext.isReady()
方法返回false
。
-
- AppContextStartedEvent
-
在
AppContextInitializedEvent
之后并且运行完所有AppContext.Listener.applicationStarted()
之后触发。此时:-
AppContext.isStarted()
方法返回true
。 -
AppContext.isReady()
方法返回false
。 -
在中间件上,如果启用了 cuba.automaticDatabaseUpdate应用程序属性,所有数据库更新脚本已成功执行。
-
- AppContextStoppedEvent
-
在应用程序关闭之前并且运行完所有
AppContext.Listener.applicationStopped()
之后触发。此时:-
所有 bean 都可以运行并且可以通过
AppBeans.get()
方法获得。 -
AppContext.isStarted()
方法返回false
。 -
AppContext.isReady()
方法返回false
。
-
通过指定 @Order
注解可以影响监听器调用的顺序。Events.HIGHEST_PLATFORM_PRECEDENCE
和 Events.LOWEST_PLATFORM_PRECEDENCE
常量指定平台中定义的监听器的使用范围。
例如:
package com.company.demo.core;
import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.sys.events.*;
import org.slf4j.Logger;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
@Component
public class MyAppLifecycleBean {
@Inject
private Logger log;
// event type is defined by annotation parameter
@EventListener(AppContextInitializedEvent.class)
// run after all platform listeners
@Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)
protected void appInitialized() {
log.info("Initialized");
}
// event type is defined by method parameter
@EventListener
protected void appStarted(AppContextStartedEvent event) {
log.info("Started");
}
@EventListener
protected void appStopped(AppContextStoppedEvent event) {
log.info("Stopped");
}
}
- ServletContextInitializedEvent
-
在 servlet 和应用程序上下文初始化之后触发。此时:
-
可以通过静态方法
AppBeans.get()
获取 bean。 -
此事件包含应用程序和 servlet 上下文,因此可以注册自定义 servlet、过滤器和监听器,请参阅Servlet 和过滤器的注册。
-
- ServletContextDestroyedEvent
-
在 Servlet 和应用程序即将关闭时触发,在这个阶段可以手动释放资源。
例如:
@Component public class MyInitializerBean { @Inject private Logger log; @EventListener public void foo(ServletContextInitializedEvent e) { log.info("Application and servlet context is initialized"); } @EventListener public void bar(ServletContextDestroyedEvent e) { log.info("Application is about to shut down, all contexts are now destroyed"); } }
3.2.9. 应用程序属性
应用程序属性表示不同类型的命名值,它们决定着应用程序配置和功能的各个方面。平台广泛使用应用程序属性,还可以使用它们来配置应用程序的某些特性。
平台应用程序属性可按预期目的进行分类,如下所示:
-
配置参数 – 指定配置文件集合和一些用户界面参数,即确定应用程序功能。通常在开发时为应用程序项目定义配置参数的值。
-
部署参数 – 描述连接应用程序block的各种 URL、DBMS 类型、安全设置等。部署参数的值通常取决于安装应用程序实例的环境。
-
运行时参数 – 审计设置、电子邮件发送参数等。可以在应用程序运行时根据需要更改这些属性的值而不必重新启动应用程序。
- 设置应用程序属性
-
-
Java 系统属性(最高优先级)
-
操作系统环境变量
-
属性文件
-
数据库(最低优先级)
比如,在属性文件中定义的值会覆盖在数据库中定义的值。
对于操作系统环境变量,框架会先尝试寻找严格匹配的属性名称,如果没找到,则会尝试找名称全大写、点被下划线替代的属性名称。举个例子,环境变量
MYAPP_SOMEPROPERTY
可以赋值给myapp.someProperty
应用程序属性。如果想禁用这个功能,还是使用严格匹配的话,可以设置cuba.disableUppercaseEnvironmentProperties
应用程序属性为 true。某些属性不支持设置在数据库中,原因是:在应用程序代码还无法访问数据库时就需要使用这些属性值。这些属性是上面提到的配置和部署参数。因此,只能在属性文件中或通过 Java 系统属性和操作系统环境变量定义它们。运行时参数始终设置在数据库中(可能被文件或系统属性中的值覆盖)。
通常情况下,应用程序属性用于一个或多个应用程序block。例如cuba.persistenceConfig仅用于中间件,cuba.web.appWindowMode用于 Web 客户端,而cuba.springContextConfig用于所有的 block。这意味着如果你需要为一个属性设置值,就应该在所有使用这个属性的block中进行设置。存储在数据库中的属性可自动用于所有 block,因此可以仅在一个位置设置它们的值(在数据库表中),而不用关心哪个 block 将会使用它们。而且,还有一个标准 UI 界面来管理此类属性:请参考 Administration > Application Properties 。存储在文件中的属性应分别在各 block 的相应文件中设置。
当需要为平台属性设置值时,请在文档中找到相关属性。如果文档声明该属性存储在数据库中,请使用 Administration > Application Properties 界面设置其值。否则,找出哪些 block 使用该属性并在这些 block 的
app.properties
文件中定义它。例如,如果文档声明该属性在所有 block 中使用,并且应用程序由中间件和 Web 客户端组成,则应在 core 模块的app.properties
文件和 web 模块的web-app.properties
文件中定义该属性。部署参数也可以在外部的local.app.properties
文件设置。有关详细信息,请参阅在文件中存储属性。 -
- 应用程序组件属性
-
应用程序组件可以通过在app-component.xml文件定义属性来对外暴露这些属性。 如果使用该组件的应用程序没有为这些属性指定值,则从该组件获取这些属性值。如果应用程序使用的多个组件定义了相同的属性,则应用程序中的实际值将从组件依赖关系层次结构从最近的组件中获得。如果多个组件位于依赖层次中的同一级,则该属性值会变得不确定。
- 累加 Properties
-
有时,需要从项目中使用的所有应用程序组件中获取属性值的组合。特别是应用程序的行为依赖于多个组件的同一属性的情况,平台需要读取多个组件的同一属性来配置应用程序。
通过在属性值的开头添加加号来使这些属性成为_累加_属性。此符号表示在运行时从应用程序组件组装属性值。例如,cuba.persistenceConfig是一个累加属性。在项目中,指定了一个定义项目数据模型的
persistence.xml
文件。但是由于实际的属性值还包括应用程序所依赖的应用程序组件的persistence.xml
文件,因此应用程序的完整数据模型还要包括组件中定义的实体。如果属性省略了
+
,则只能从当前项目获取其值。如果不想从组件继承某些配置(例如,在定义菜单结构时),它会很有用。在运行时获得的累加属性值是由 空格 连接的元素组成。
- 以编程的方式访问应用程序属性
-
可以使用以下机制访问代码中的应用程序属性:
-
Configuration 接口。如果将应用程序属性定义为配置接口的带注解方法,则应用程序代码将可对属性进行类型化访问。配置接口允许定义和访问所有存储类型的属性:数据库、文件和系统属性。
-
AppContext类的
getProperty()
方法。如果在文件或 Java 系统属性中设置属性,则可以使用此方法读属性值。该方法有以下弊端:-
不支持存储在数据库中的属性。
-
与调用接口方法不同,必须提供 String 类型的属性名称。
-
不能获取特定类型的属性值,只能获取 String 类型的属性值。
-
-
3.2.9.1. 在文件中存储属性
确定配置和部署参数的属性在特定的属性文件中指定,这些文件以 *app.properties
模式命名。每个应用程序block都包含一组此类文件,定义在web.xml的 appPropertiesConfig
参数中。
例如,中间件 block 的属性文件在 core
模块的 web/WEB-INF/web.xml
文件中指定,如下所示:
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
"file:${app.home}/local.app.properties"
</param-value>
</context-param>
classpath:
前缀表示在 Java 类路径中查找相应的文件,而 file:
前缀表示它应该从文件系统加载。没有此前缀的路径表示相对于 Web 应用程序根目录的路径。可以使用 Java 系统属性:在此示例中,app.home
指向应用程序主目录。
声明文件的顺序很重要,因为在每个后续文件中指定的值将覆盖前面文件中指定的相同名称的属性值。如果其中有任何指定的文件不存在,则会忽略该文件。
上面集合中的最后一个文件是 local.app.properties
。它可用于在部署特定环境时设置或者覆盖应用程序属性。
创建
|
3.2.9.2. 在数据库中存储属性
表示运行时参数的应用程序属性存储在 SYS_CONFIG
表中。
这些属性具有以下特征:
-
由于属性值存储在数据库中,因此无论哪个应用程序 block 使用,都只要在一个地方定义。
-
可以通过以下方式在运行时更改和保存属性值:
-
使用 Administration > Application Properties 界面。
-
使用 ConfigStorageMBean JMX bean。
-
如果配置接口有 setter 方法,则可以在应用程序代码中设置属性值。
-
-
有几种方式重写属性值:在特定应用程序 block 的
*app.properties
文件、Java 系统属性或者操作系统环境变量中定义同名属性。
需要注意的是,客户端通过向中间件发送请求来访问存储在数据库中的属性,要比从本地 *app.properties
文件中检索属性效率低。为了减少请求数,客户端在配置接口实例生命周期内对属性进行了缓存。因此,如果需要从某个 UI 界面多次访问配置接口的属性,建议在界面初始化时获取配置接口的引用,并将其保存到界面控制器的字段中以便后续访问。
3.2.9.3. 配置接口
配置接口机制允许使用 Java 接口方法处理应用程序属性,从而带来以下好处:
-
类型化访问 - 在应用程序代码中可以使用实际数据类型(String 、 Boolean 、 Integer 等)。
-
应用程序代码使用接口方法而不是字符串类型的属性标识符,这些接口方法可由编译器检查,并且在集成开发环境中使用代码自动完成。
读取中间件 block 中的事务超时属性值的示例:
@Inject
private ServerConfig serverConfig;
public void doSomething() {
int timeout = serverConfig.getDefaultQueryTimeoutSec();
...
}
如果无法注入,可以通过Configuration基础接口获取配置接口引用:
int timeout = AppBeans.get(Configuration.class)
.getConfig(ServerConfig.class)
.getDefaultQueryTimeoutSec();
配置接口不是常规的 Spring bean。它们只能通过显式接口注入或通过 |
3.2.9.3.1. 使用配置接口
要在应用程序中创建配置接口,请执行以下操作:
-
创建一个继承自
com.haulmont.cuba.core.config.Config
的接口(不要与实体类com.haulmont.cuba.core.entity.Config
混淆)。 -
添加
@Source
注解以指定应属性值的存储位置:-
SourceType.SYSTEM
– 将使用System.getProperty()
方法从给定 JVM 的系统属性中获取值。 -
SourceType.APP
– 将从*app.properties
文件中获取值。 -
SourceType.DATABASE
– 将从数据库中获取值。
-
-
创建属性访问方法(getters / setters)。如果不打算通过代码更改属性值,那么就不要创建 setter 方法。getter 方法的返回类型即是属性的类型。可能的属性类型描述在这里。
-
添加
@Property
注解,定义 getter 方法对应的属性名称。 -
如果某个特定属性的来源与接口上定义的来源不同,可以为该属性单独设置
@Source
注解。 -
如果
@Source
值是SourceType.DATABASE
,则可以在平台提供的 Administration > Application Properties 界面上编辑该属性。可以使用@Secret
注解以掩码的方式在界面上显示属性值(将使用PasswordField而不是常规的文本字段)。
配置接口必须定义在应用程序的根包内(或者根包的内部包内)。 |
例如:
@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {
@Property("sales.companyName")
String getCompanyName();
@Property("sales.ftpPassword")
@Secret
String getFtpPassword();
}
不要创建任何实现类,因为当注入配置接口或通过Configuration获取配置接口时,平台将自动创建所需的代理类。
3.2.9.3.2. 属性类型
平台支持以下开箱即用的属性类型:
-
String
, 原始类型及其封装类型(boolean
、Boolean
、int
、Integer
等) -
enum
,属性值作为枚举的值名称存储在文件或数据库中。如果枚举实现了
EnumClass
接口并且具有用于通过标识符获取值的静态方法fromId()
,则可以使用@EnumStore
注解指定存储枚举标识符而不是具体值。例如:@Property("myapp.defaultCustomerGrade") @DefaultInteger(10) @EnumStore(EnumStoreMode.ID) CustomerGrade getDefaultCustomerGrade(); @EnumStore(EnumStoreMode.ID) void setDefaultCustomerGrade(CustomerGrade grade);
-
持久化实体类。访问实体类型的属性时,将从数据库加载由属性值定义的实例。
要支持任意类型,请使用 TypeStringify
和 TypeFactory
类将值转换为字符串或从字符串转换值,并使用 @Stringify
和 @Factory
注解为属性指定这些类。
我们以 UUID
类型为例来了解这个过程。
-
创建类
com.haulmont.cuba.core.config.type.UuidTypeFactory
继承于com.haulmont.cuba.core.config.type.TypeFactory
类 并实现下面的方法:public Object build(String string) { if (string == null) { return null; } return UUID.fromString(string); }
-
在这种情况下没有必要创建
TypeStringify
,因为有toString()
方法。 -
在配置接口中注解属性:
@Factory(factory = UuidTypeFactory.class) UUID getUuidProp(); void setUuidProp(UUID value);
平台为以下类型提供了 TypeFactory
和 Stringify
实现:
-
UUID
–UuidTypeFactory
, 如上所述。TypeStringify
冗余了,可以直接使用UUID
的toString()
方法。 -
java.util.Date
–DateFactory
和DateStringify
。日期值必须以yyyy-MM-dd HH:mm:ss.SSS
格式指定,例如:cuba.test.dateProp = 2013-12-12 00:00:00.000
在配置接口中定义一个
date
属性的示例:@Property("cuba.test.dateProp") @Factory(factory = DateFactory.class) @Stringify(stringify = DateStringify.class) Date getDateProp(); void setDateProp(Date date);
-
List<Integer>
(整数列表) –IntegerListTypeFactory
和IntegerListStringify
。必须以数字的形式指定属性值,用空格分隔,例如:cuba.test.integerListProp = 1 2 3
在配置接口中定义一个
List<Integer>
属性的示例:@Property("cuba.test.integerListProp") @Factory(factory = IntegerListTypeFactory.class) @Stringify(stringify = IntegerListStringify.class) List<Integer> getIntegerListProp(); void setIntegerListProp(List<Integer> list);
-
List<String>
(字符串列表) –StringListTypeFactory
和StringListStringify
。必须将属性值指定为由 "|" 分隔的字符串列表,例如:cuba.test.stringListProp = aaa|bbb|ccc
在配置接口中定义一个
List<String>
属性的示例:@Property("cuba.test.stringListProp") @Factory(factory = StringListTypeFactory.class) @Stringify(stringify = StringListStringify.class) List<String> getStringListProp(); void setStringListProp(List<String> list);
3.2.9.3.3. 默认值
可以为配置接口定义的属性指定默认值。如果未在存储位置(数据库或 *app.properties
文件)中设置该属性,则将返回这里定义的默认值而不是 null
。
可以使用 @Default
注解将默认值指定为字符串,或使用 com.haulmont.cuba.core.config.defaults
包中的其它注解将默认值指定为其它特定类型:
@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();
@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();
@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();
@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();
@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();
@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();
实体的默认值是 {entity_name}-{id}-{optional_view_name}
格式的字符串,例如:
@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();
@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();
3.2.10. 消息本地化
基于 CUBA 框架的应用程序支持消息本地化,这意味着所有用户界面元素都可以以用户选择的语言显示。
参考 CUBA 应用程序中的本地化 指南,了解 CUBA 应用程序中本地化消息是如何定义并使用的。 |
语言选择选项由 cuba.localeSelectVisible 和 cuba.availableLocales 应用程序属性共同确定。
本节介绍本地化机制和本地化消息创建规则。
3.2.10.1. 语言消息包
消息包是一组位于单个 Java 包中名称为 messages{_XX}.properties
格式的属性文件。XX
后缀表示在此文件中消息的语言,对应 Locale.getLanguage()
中的语言代码。也能够使用其它 Locale
属性,例如,country
。在这种情况下,消息包文件名看起来像 messages{_XX_YY}.properties
。如果包中的某个消息文件没有语言后缀 - 则是默认消息文件。消息的包名称对应于包含消息文件的 Java 包的名称。
我们看看以下示例:
/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties
/com/abc/sales/gui/customer/messages_en_US.properties
这个包由 4 个文件组成 - 一个用于俄语,一个用于法语,一个用于美式英语(带有美国国家代码),以及一个默认文件。包名是 com.abc.sales.gui.customer
。
消息文件包含键/值对,其中键是应用程序代码中引用消息的标识符,值是对应语言的消息本身。配对规则与 java.util.Properties
属性文件类似,同时具有以下特点:
-
文件编码 – 仅限
UTF-8
。 -
可以使用
@include
键引入其它消息包。可以使用以逗号分隔的列表包含多个包。在这种情况下,如果在当前包和引入包中都找到某个消息键,则使用来自当前包的消息。引入包的示例:@include=com.haulmont.cuba.web, com.abc.sales.web someMessage=Some Message ...
根据以下规则使用 Messages 接口方法来从包中检索消息:
-
首先在应用的配置目录中执行搜索。
-
在消息包名称指定的目录中搜索
messages_XX.properties
文件,其中XX
是所需语言的代码。 -
如果没有这样的文件,则在同一目录中搜索默认的
messages.properties
文件。 -
如果找到所需的语言文件或默认文件,则将其与所有
@include
文件一起加载,并在其中搜索消息键名。 -
如果没有找到文件或者文件中没有正确的消息键,则将目录更改为父目录并重复搜索过程。搜索将继续,直到到达配置目录的根目录。
-
-
如果在配置目录中没有找到该消息,则根据相同的算法在类路径中执行搜索。
-
如果找到该消息,则将其缓存并返回。如果没有,则消息不存在的事实也被缓存并返回搜索时传递的键。因此,复杂搜索过程仅执行一次,后续将从应用程序模块的本地高速缓存中加载结果。
建议按如下方式组织消息包:
|
3.2.10.2. 主语言消息包
每个标准的应用程序 block 都应该有它自己的 主(main) 消息包。对于客户端层的 block,主消息包包含主菜单条目和常用的 ui 元素名称(例如,ok 和 cancel 按钮的名称)。主程序包还决定所有应用程序模块(包括中间层)的数据类型转换格式。
cuba.mainMessagePack 应用程序属性用于指定主消息包。其属性值可以是单个包或由空格分隔的包名列表。例如:
cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web
在这种情况下,列表第二个包的消息将覆盖第一个包中的消息。因此,可以在应用程序项目覆盖应用程序组件中定义的消息。
通过在项目的主消息包中指定新消息也可以覆盖 CUBA 基础项目中已存在的消息。
com.haulmont.cuba.gui.backgroundwork/backgroundworkprogress.timeoutmessage = overridden error message
违反数据库唯一约束的消息需要包含与数据库约束名称全大写相同的键值:
IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
3.2.10.3. 实体和属性名称本地化
要在 UI 中显示实体和属性的本地化名称,请在包含实体的 Java 包中创建特殊的消息包。在消息文件中使用以下格式:
-
实体名称键 – 简单类名(不带包名)。
-
属性名称键 – 简单类名,后面跟上以点分隔的属性名。
com.abc.sales.entity.Customer
实体的默认英文本地化示例 – /com/abc/sales/entity/messages.properties
文件:
Customer=Customer
Customer.name=Name
Customer.email=Email
Order=Order
Order.customer=Customer
Order.date=Date
Order.amount=Amount
此类消息包通常由框架隐式地使用,例如,被 Table 和 FieldGroup 可视化组件使用。除此之外,还可以使用以下方法来获取实体和属性的名称:
-
编程方式 – 通过 MessageTools
getEntityCaption()
、getPropertyCaption()
方法; -
在界面 XML 描述中 – 根据 MessageTools.loadString() 规则引用消息:
msg://{entity_package}/{key}
,例如:caption="msg://com.abc.sales.entity/Customer.name"
3.2.10.4. 枚举名称本地化
要本地化枚举的名称(names)和值(values),将具有以下键的消息添加到枚举类所在 Java 包的本地化消息包中:
-
枚举名称键 – 简单的类名(不带包名);
-
值健 – 简单类名,后面跟上以点分隔的值名称。
例如,对于枚举
package com.abc.sales;
public enum CustomerGrade {
PREMIUM,
HIGH,
STANDARD
}
默认的英文本地化文件 /com/abc/sales/messages.properties
应包含以下行:
CustomerGrade=Customer Grade
CustomerGrade.PREMIUM=Premium
CustomerGrade.HIGH=High
CustomerGrade.STANDARD=Standard
本地化的枚举值可被不同的可视化组件自动利用,例如 LookupField。也可以通过编程方式获取本地化的枚举值:使用 Messages 接口的 getMessage()
方法并简单地将 enum
实例传递给它。
3.2.11. 用户认证
本节从开发人员的角度描述了一些访问控制方面的内容。有关配置用户数据访问限制的完整信息,请参阅安全子系统。
3.2.11.1. 用户会话
用户会话信息是 CUBA 应用程序访问控制机制的主要元素。它由 UserSession
对象表示,该对象与当前已通过身份验证的用户关联,并包含了用户的权限信息。在任何应用程序 block 中都可以使用 UserSessionSource 基础接口获取 UserSession
对象。
在使用用户名和密码对用户进行身份验证后,会执行 AuthenticationManager.login()
方法,此方法执行时会在中间件上创建 UserSession
对象。然后将对象缓存在中间件 block 中并返回到客户端层。在集群中运行时,会话对象将复制到所有集群成员。客户端层也会在接收会话对象之后将其存储下来,并以某种方式将其与活动用户相关联(例如,将其存储在 HTTP 会话中)。后续,此用户的所有中间件方法调用都会带上会话标识符( UUID
类型)。此过程不需要在应用程序代码中提供任何特殊处理,会话标识符会自动传递,与调用的方法的签名无关(即会话信息不通过方法参数传递)。中间件在处理客户端调用时,首先通过获得的标识符从缓存中检索会话,然后会话被与请求(http request)的执行线程关联。调用 AuthenticationManager.logout()
方法时或 cuba.userSessionExpirationTimeoutSec 应用程序属性定义的超时时间到期时,会从缓存中删除会话对象。
这样,在用户登录系统时创建的会话标识符用于在每次中间件调用期间进行用户验证。
UserSession
对象还包含当前用户_验证相关_的方法 - 验证对系统对象的访问权限: isScreenPermitted()
、isEntityOpPermitted()
、isEntityAttrPermitted()
和 isSpecificPermitted()
。但是,建议使用 Security 基础接口以编程的方式进行权限验证。
UserSession
对象可以包含任意可序列化类型的命名属性。属性由 setAttribute()
方法设置,并由 getAttribute()
方法获取。后者也能够像属性一样返回以下会话参数:
-
userId
– 当前注册的或代替的用户的 ID; -
userLogin
– 当前注册的或代替的用户登录名的小写形式。
会话属性与其它用户会话数据一样在中间件集群中被复制分发。
3.2.11.2. 登录
CUBA 框架提供内置的可扩展身份验证机制。这些机制包括不同的身份验证方案,例如登录/密码、记住账号、信任和匿名登录。
参考 匿名访问 & 社交登录 向导学习如何为应用程序中某些界面设置公共访问权限,并实现用 Google、Facebook 或 GitHub 账号的自定义登录。 |
本节主要介绍中间层的身份验证机制。有关 Web 客户端身份验证机制的详细信息,请参阅 Web 登录。
平台在中间件包含以下身份验证机制:
-
由
AuthenticationManagerBean
实现的AuthenticationManager
。 -
AuthenticationProvider
实现。 -
由
AuthenticationServiceBean
实现的AuthenticationService
。 -
UserSessionLog
- 参阅用户会话日志。
此外,它还使用以下附加组件:
-
由
TrustedClientServiceBean
实现的TrustedClientService
- 为受信任客户端提供匿名会话或系统会话。 -
AnonymousSessionHolder
- 为受信任的客户端创建并保存匿名会话实例。 -
UserCredentialsChecker
- 检查用户凭据是否可以使用,比如可用于防止暴力破解。 -
UserAccessChecker
- 检查用户是否可以通过给定的上下文访问系统,例如,控制用户是否可以通过 REST 访问系统、控制指定的 IP 地址是否可以访问系统。
身份验证的主要接口是 AuthenticationManager
,它包含四个方法:
public interface AuthenticationManager {
AuthenticationDetails authenticate(Credentials credentials) throws LoginException;
AuthenticationDetails login(Credentials credentials) throws LoginException;
UserSession substituteUser(User substitutedUser);
void logout();
}
有两个方法具有相似的功能: authenticate()
和 login()
。两个方法都检查提供的凭据是否有效且对应于有效用户,然后返回 AuthenticationDetails
对象。它们之间的主要区别在于 login
方法还激活了用户会话,这样它随后就可以用于调用服务方法。
Credentials
表示用于身份验证子系统的一组凭据。平台有 AuthenticationManager
支持的以下几种类型的凭据:
适用于所有层:
-
LoginPasswordCredentials
-
RememberMeCredentials
-
TrustedClientCredentials
仅适用于中间层:
-
SystemUserCredentials
-
AnonymousUserCredentials
AuthenticationManager
的 login / authenticate 方法返回 AuthenticationDetails
实例,其中包含 UserSession 对象。此对象可用于检查其它权限、读取 User 属性和会话属性。平台只有一个内置的 AuthenticationDetails
接口实现 - SimpleAuthenticationDetails,它只存储用户会话对象,但是应用程序可以提供自己的带有附加信息的 AuthenticationDetails
实现。
AuthenticationManager 的 authenticate() 方法中执行以下三种操作之一:
-
如果可以验证输入的是一个有效用户,则返回
AuthenticationDetails
。 -
如果无法通过传递的凭据对象对用户进行身份验证,则抛出
LoginException
。 -
如果不支持传递的凭据对象,则抛出
UnsupportedCredentialsException
。
AuthenticationManager
的默认实现是 AuthenticationManagerBean
,它将身份验证委托给 AuthenticationProvider
实例链。 AuthenticationProvider
是一个可以处理特定 Credentials
实现的身份验证模块,它还有一个特殊的方法 supports()
,允许调用者查询它是否支持给定的 Credentials
类型。
标准的用户登录过程:
-
用户输入用户名和密码。
-
应用程序客户端使用用户名和密码作为参数调用
Connection.login()
方法。 -
Connection
创建Credentials
对象并调用AuthenticationService
的login()
方法。 -
AuthenticationService
将验证操作委托给AuthenticationManager
bean ,AuthenticationManager
bean 使用了AuthenticationProvider
对象链 。LoginPasswordAuthenticationProvider
可以使用LoginPasswordCredentials
对象。它通过输入的登录名加载User
对象,使用用户标识符作为盐值再次散列获得的密码哈希值,并将获得的哈希值与存储在 DB 中的密码哈希值进行比较。如果不匹配,则抛出LoginException
。 -
如果身份验证成功,则将用户的所有访问参数(角色列表、权限、约束和会话属性)加载到创建的 UserSession 实例中。
-
如果启用了用户会话日志,则会把包含用户会话信息的记录保存到数据库中。
另外请参阅 Web 登录过程。
密码散列算法由 EncryptionModule
类型 bean 实现,并在 cuba.passwordEncryptionModule 应用程序属性中指定。默认情况下使用 BCrypt。
- 内置验证提供程序
-
平台包含以下
AuthenticationProvider
接口的实现:-
LoginPasswordAuthenticationProvider
-
RememberMeAuthenticationProvider
-
TrustedClientAuthenticationProvider
-
SystemAuthenticationProvider
-
AnonymousAuthenticationProvider
所有实现都从数据库加载用户,使用
UserSessionManager
验证传递的凭据对象并创建非活动的用户会话。随后调用AuthenticationManager.login()
后,该会话实例将变为活动状态。LoginPasswordAuthenticationProvider
、RememberMeAuthenticationProvider
和TrustedClientAuthenticationProvider
使用额外检查插件:实现了UserAccessChecker
接口的 bean。如果有一个UserAccessChecker
实例抛出LoginException
,则认为验证失败并抛出LoginException
。此外,
LoginPasswordAuthenticationProvider
和RememberMeAuthenticationProvider
使用 UserCredentialsChecker beans 检查凭据实例。UserCredentialsChecker 接口只有一个内置实现 - BruteForceUserCredentialsChecker,用于检查用户是否使用暴力破解攻击来找出有效凭据。 -
- 异常类型
-
AuthenticationManager
和AuthenticationProvider
的authenticate()
方法和login()
方法会抛出 LoginException 或其子类异常。需要确认 此外,如果传递的凭据对象没有可用的AuthenticationProvider
bean,则抛出 UnsupportedCredentialsException。请参阅以下异常类:
-
UnsupportedCredentialsException
-
LoginException
-
AccountLockedException
-
UserIpRestrictedException
-
RestApiAccessDeniedException
-
- 事件
-
AuthenticationManager
的标准实现 -AuthenticationManagerBean
在登录或验证过程中触发以下应用程序 事件 :-
BeforeAuthenticationEvent
/AfterAuthenticationEvent
-
BeforeLoginEvent
/AfterLoginEvent
-
AuthenticationSuccessEvent
/AuthenticationFailureEvent
-
UserLoggedInEvent
/UserLoggedOutEvent
-
UserSubstitutedEvent
中间层的 Spring bean 可以使用 Spring
@EventListener
注解来处理这些事件:@Component public class LoginEventListener { @Inject private Logger log; @EventListener protected void onUserLoggedIn(UserLoggedInEvent event) { User user = event.getUserSession().getUser(); log.info("Logged in user {}", user.getLogin()); } }
上面提到的所有事件的事件处理器(不包括
AfterLoginEvent
、UserSubstitutedEvent
和UserLoggedInEvent
)都可以抛出LoginException
来中断身份验证/登录过程。例如,可以为应用程序实现一个维护模式开关,如果维护模式处于激活状态,它将阻止登录。
@Component public class MaintenanceModeValve { private volatile boolean maintenance = true; public boolean isMaintenance() { return maintenance; } public void setMaintenance(boolean maintenance) { this.maintenance = maintenance; } @EventListener protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException { if (maintenance && event.getCredentials() instanceof AbstractClientCredentials) { throw new LoginException("Sorry, system is unavailable"); } } }
-
- 扩展点
-
可以使用以下类型的扩展点来扩展身份验证机制:
-
AuthenticationService
- 替换现有的AuthenticationServiceBean
。 -
AuthenticationManager
- 替换现有的AuthenticationManagerBean
。 -
AuthenticationProvider
实现类 - 实现额外的或替换现有的AuthenticationProvider
。 -
Events - 实现事件处理.
可以使用 Spring Framework 机制替换现有 bean,例如通过在 core 模块的 Spring XML 配置中注册新 bean。
<bean id="cuba_LoginPasswordAuthenticationProvider" class="com.company.authext.core.CustomLoginPasswordAuthenticationProvider"/>
public class CustomLoginPasswordAuthenticationProvider extends LoginPasswordAuthenticationProvider { @Inject public CustomLoginPasswordAuthenticationProvider(Persistence persistence, Messages messages) { super(persistence, messages); } @Override public AuthenticationDetails authenticate(Credentials credentials) throws LoginException { LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) credentials; // for instance, add new check before login if ("demo".equals(loginPassword.getLogin())) { throw new LoginException("Demo account is disabled"); } return super.authenticate(credentials); } }
事件处理器可以使用
@Order
注解来排序。所有平台 bean 和事件处理器都使用 100 到 1000 之间的order
值,因此可以在平台代码之前或之后添加自定义处理。如果要在平台 bean 之前添加 bean 或事件处理器 - 请使用小于 100 的值。事件处理器排序:
@Component public class DemoEventListener { @Inject private Logger log; @Order(10) @EventListener protected void onUserLoggedIn(UserLoggedInEvent event) { log.info("Demo"); } }
AuthenticationProvider 可以使用 Ordered 接口并实现
getOrder()
方法。@Component public class DemoAuthenticationProvider extends AbstractAuthenticationProvider implements AuthenticationProvider, Ordered { @Inject private UserSessionManager userSessionManager; @Inject public DemoAuthenticationProvider(Persistence persistence, Messages messages) { super(persistence, messages); } @Nullable @Override public AuthenticationDetails authenticate(Credentials credentials) throws LoginException { // ... } @Override public boolean supports(Class<?> credentialsClass) { return LoginPasswordCredentials.class.isAssignableFrom(credentialsClass); } @Override public int getOrder() { return 10; } }
-
- 额外功能
-
-
平台具有防止暴力破解密码的机制。通过中间件上的 cuba.bruteForceProtection.enabled 应用程序属性启用保护。如果启用了保护,则在多次登录尝试失败的情况下,用户登录名和 IP 地址的组合将被限制一段时间。 用户登录名和 IP 地址组合的最大登录尝试次数由 cuba.bruteForceProtection.maxLoginAttemptsNumber 应用程序属性定义(默认值为 5)。锁定间隔时间以秒为单位由 cuba.bruteForceProtection.blockIntervalSec 应用程序属性定义(默认值为 60)。
-
用户密码(实际上是密码哈希值)可能不存储在数据库中,而是通过外部手段验证,例如,通过 LDAP 集成的方式。在这种情况下,身份验证实际上是由客户端 block 执行的,而中间件通过使用带有
TrustedClientCredentials
的AuthenticationService.login()
方法创建基于用户登录名而没有密码的会话来“信任”客户端。此方法需要满足以下条件:-
客户端 block 必须传递所谓的受信任密码,该密码在中间件和客户端 block 的 cuba.trustedClientPassword 应用程序属性中指定。
-
客户端 block 的 IP 地址必须位于 cuba.trustedClientPermittedIpList 应用程序属性中指定的列表中。
-
-
对于计划的自动处理程序和使用 JMX 接口连接中间件 bean 也需要登录系统。事实上,这些操作被视为管理操作,只要数据库中没有更改实体,就不需要身份验证。当实体持久化到数据库时,该过程需要正在执行更改的用户登录,以保证登录的用户对存储的更改负责。
要求自动处理程序或 JMX 调用登录系统的另一个好处是,如果给执行线程设置了用户会话,服务器日志输出就可以显示出日志对应的用户信息。这对日志分析很有帮助,可以很方便地搜索特定处理程序产生的日志。
中间件中处理程序对系统的访问是使用
AuthenticationManager.login()
和SystemUserCredentials
完成的,SystemUserCredentials
包含将要执行处理程序的用户登录信息(无密码)。最终,会在相应的中间件 block 中创建并缓存 UserSession 对象,但这个对象不会在集群中分发。
可在 系统身份验证 中查看有关中间件处理身份验证的更多信息。
-
3.2.11.3. 安全上下文
SecurityContext
类实例存储关于当前执行线程的用户会话信息。它在以下情况下被创建并传递给 AppContext.setSecurityContext()
方法:
-
对于 Web 客户端 block 和 Web 门户 block - 在开始处理用户浏览器的每个 HTTP 请求时。
-
对于中间件 block - 在开始处理来自客户端层和 CUBA 计划任务 的每个请求时。
在前两种情况下,当请求执行完成时,将从执行线程中删除 SecurityContext
。
如果通过应用程序代码创建一个新的执行线程,需要将当前的 SecurityContext
实例传递给它,示例如下:
final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
public void run() {
AppContext.setSecurityContext(securityContext);
// business logic here
}
});
使用 SecurityContextAwareRunnable
或 SecurityContextAwareCallable
封装类可以完成同样的操作,例如:
executor.submit(new SecurityContextAwareRunnable<>(() -> {
// business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
// business logic here
return some_string;
}));
3.2.12. 异常处理
本节介绍在 CUBA 应用程序中处理异常的各方面知识。
3.2.12.1. 异常类
创建自己的异常类时应该遵循以下规则:
平台包含一个特殊的非受检异常类 SilentException
。它可用于中断执行而不向用户显示任何消息或将其写入到日志。SilentException
声明在全局模块中,因此在中间层和客户端模块都能够访问到。
3.2.12.2. 传递中间件异常
如果从中间件抛出的异常是由于处理客户端请求引发的,则中止执行并且将异常对象返回给客户端。该对象通常包括底层异常链。此链可能包含客户端层不能够访问的类(例如,JDBC 驱动异常)。因此,平台不是发送此异常链到客户端,而是将其描述存储到专门创建的 RemoteException
对象中,将 RemoteException
对象发送到客户端。
导致异常的信息存储为 RemoteException.Cause
对象列表。每个 Cause
对象都包含一个异常类名及其消息。此外,如果异常类是 "客户端支持" 的,则 Cause
也会存储异常对象。这样可以在 Exception 对象的字段中传递信息给客户端。
如果异常类的对象应该作为 Java 对象传递给客户端层,则异常类应使用 @SupportedByClient
注解。例如:
@SupportedByClient
public class WorkflowException extends RuntimeException {
...
这样,当在中间件上抛出没有使用 @SupportedByClient
注解的异常时,客户端调用代码将接收到包含字符串形式的原始异常信息的 RemoteException
。 如果源异常带有 @SupportedByClient
注解,则调用者能直接接收到异常实例。这使得在客户端能够以传统方式(使用 try/catch 块)处理应用程序代码中间层服务声明的异常。
请注意,如果需要将客户端支持的异常作为对象传递给客户端。则它的 getCause()
链上不应该包含任何不支持的异常。因此,如果在中间层创建异常实例并且希望将其传递给客户端, 请确保 cause 参数包含的异常类型能被客户端识别。
ServiceInterceptor
类是一个服务拦截器,可以在异常对象传递给客户层之前将其打包。此外,它还执行异常日志记录。 默认情况下会记录有关异常的所有信息,包含完整的 stacktrace 日志。如果不需要,可添加 @Logging
注解到异常类并指定日志级别:
-
FULL
– 完整信息,包括 stacktrace(默认)。 -
BRIEF
– 仅包含异常类名称和消息。 -
NONE
– 没有输出。
例如:
@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...
3.2.12.3. 处理客户端层的异常
在客户端层抛出或从中间层传递的未处理的异常,将被传递给 Web 客户端 block 的特殊处理机制。
一个异常处理类是实现了 UiExceptionHandler
接口的Spring bean,在其 handle()
方法中执行处理异常的逻辑并返回 true
,或者如果这个处理器无法处理传递的异常,则立即返回 false
。这个行为能够为处理器创建一个 "责任链"。
建议这个处理器继承 AbstractUiExceptionHandler
基类,该基类能够解析异常链(包括打包在 RemoteException
中的异常链)并且可以处理特定的异常类型。此处理器支持的异常类型是通过将字符串数组从处理器的构造器传递给基类构造器来定义的。数组中的每个字符串应包含一个要处理的异常的完整类名。
假设有如下异常类:
package com.company.demo.web;
public class ZeroBalanceException extends RuntimeException {
public ZeroBalanceException() {
super("Insufficient funds in your account");
}
}
那么处理此异常的处理器必须有如下构造器:
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
...
如果在客户端无法访问异常类,请使用字符串指定其名称:
@Component("sample_ForeignKeyViolationExceptionHandler")
public class ForeignKeyViolationExceptionHandler extends AbstractUiExceptionHandler {
public ForeignKeyViolationExceptionHandler() {
super("java.sql.SQLIntegrityConstraintViolationException");
}
...
在使用 AbstractUiExceptionHandler
作为基类的情况下,业务逻辑在 doHandle()
方法中,如下所示:
package com.company.demo.web;
import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
}
如果异常类的名称不足以确定是否可以将此处理器应用于此异常,则应该定义 canHandle()
方法。此方法也接收异常的文本信息。如果处理器能够应用于此异常,则方法必须返回 true
。例如:
package com.company.demo.web.exceptions;
import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
@Override
protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
return StringUtils.containsIgnoreCase(message, "Insufficient funds in your account");
}
}
通过 doHandle()
方法的 UiContext
参数可以获取到 Dialogs
接口,此接口提供了一个用来展示异常的特殊对话框,对话框中包含了一个可折叠的区域,这里能展示异常所有堆栈信息。这个对话框用在默认的异常处理器中,但是也可以为自己定义的异常使用,示例:
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
if (throwable != null) {
context.getDialogs().createExceptionDialog()
.withThrowable(throwable)
.withCaption("Error")
.withMessage(message)
.show();
} else {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
}
3.2.12.3.1. 处理违反唯一约束异常
CUBA 框架中,可以为 exception handler 展示的违反数据库约束的异常消息进行自定义。
自定义的消息需要在 web
模块的主消息包内定义,消息键值与数据库中唯一索引的名称全大写一致。示例:
IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
这样,界面展示的通知如下:
然后添加
IDX_DEMO_PRODUCT_UNIQ_NAME = A product with this name already exists
则会看到这样的消息:
检测数据库违反约束错误是通过 UniqueConstraintViolationHandler
类实现,根据数据库类型不同使用正则表达式做匹配。如果默认的表达式未能识别您数据库的异常,可以通过 cuba.uniqueConstraintViolationPattern 应用程序属性调整。
当然,也可以完全替换标准的 handler,将您自己的 handler 优先级调高,比如,@Order(HIGHEST_PLATFORM_PRECEDENCE - 10)
。
3.2.13. Bean 验证
Bean 验证是一种可选机制,可在通用 UI 和 REST API 中提供中间件上数据的统一验证。它基于 JSR 380 - Bean Validation 2.0 及其参考实现: Hibernate Validator。
3.2.13.1. 定义约束
可以使用 javax.validation.constraints
包中的注解或者自定义注解来定义约束。可以在一个实体或 POJO 类声明、字段或 getter 方法以及中间件服务方法上设置注解。
在实体字段上使用标准验证注解的示例:
@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo_Customer")
public class Customer extends StandardEntity {
@Size(min = 3) // length of value must be longer then 3 characters
@Column(name = "NAME", nullable = false)
protected String name;
@Min(1) // minimum value
@Max(5) // maximum value
@Column(name = "GRADE", nullable = false)
protected Integer grade;
@Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
@Column(name = "EMAIL")
protected String email;
//...
}
使用自定义类级别注解的示例(见下文):
@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo_Task")
public class Task extends StandardEntity {
//...
}
验证服务方法的参数和返回值的示例
public interface TaskService {
String NAME = "demo_TaskService";
@Validated // indicates that the method should be validated
@NotNull
String completeTask(@Size(min = 5) String comment, @Valid @NotNull Task task);
}
如果需要方法参数的级联验证,可以使用 @Valid
注解,在上面的例子中,还将验证声明在 Task
对象上的约束。
- 约束组
-
约束组允许根据应用程序逻辑仅应用所有已定义约束的子集。例如,可能想强制用户输入实体属性的值,但是同时又能够通过某种内部机制设置此属性为空,为此,应该在约束注解上指定
groups
属性。然后,只有将相同的组传递给验证机制时,约束才会生效。平台将以下约束组传递给验证机制:
-
RestApiChecks
- 在 REST API 中验证时。 -
ServiceParametersChecks
- 验证服务参数时。 -
ServiceResultChecks
- 验证服务返回值时。 -
UiComponentChecks
- 验证单个 UI 字段时。 -
UiCrossFieldChecks
- 在实体编辑器提交时进行类级别约束验证时。 -
javax.validation.groups.Default
- 除了 UI 编辑器上的提交操作之外,都会传递这个组。
-
- 验证消息
-
约束可包含要显示给用户的消息。
消息可以直接在验证注解上设置,例如:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid format") @Column(name = "EMAIL") protected String email;
也可以将消息放在本地化消息包中并且使用以下格式在注解中指定消息:
{msg://message_pack/message_key}
或简单的{msg://message_key}
(仅用于实体中)。例如:@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}") @Column(name = "EMAIL") protected String email;
或者,如果为实体定义约束并且消息在实体消息包中:
@Pattern(regexp = "\\S+@\\S+", message = "{msg://Customer.email.validationMsg}") @Column(name = "EMAIL") protected String email;
消息可以包含参数和表达式。参数包含在
{}
中,可使用的参数包括本地化消息或注解参数,例如{min}
、{max}
、{value}
。表达式包含在${}
中并且可以包含验证值变量(validatedValue
) 、注解参数(如value
或min
) 和 JSR-341 (EL 3.0)表达式。例如:@Pattern(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}") @Column(name = "EMAIL") protected String email;
本地化消息值也可以包含参数和表达式。
- 自定义约束
-
可以使用编程或声明式验证来创建自己的特定领域约束。
要以编程方式的验证器创建约束,请执行以下操作:
-
在项目的 global 模块中创建注解。使用
@Constraint
进行标注。这个注解必须包含message
、groups
和payload
属性:@Target({ ElementType.TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = TaskFeasibilityValidator.class) public @interface CheckTaskFeasibility { String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
-
在项目的 global 模块中创建验证器类:
public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> { @Override public void initialize(CheckTaskFeasibility constraintAnnotation) { } @Override public boolean isValid(Task value, ConstraintValidatorContext context) { Date now = AppBeans.get(TimeSource.class).currentTimestamp(); return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90); } }
-
使用注解:
@CheckTaskFeasibility(groups = UiCrossFieldChecks.class) @Table(name = "DEMO_TASK") @Entity(name = "demo_Task") public class Task extends StandardEntity { @Future @Temporal(TemporalType.DATE) @Column(name = "DUE_DATE") protected Date dueDate; @Min(0) @Max(100) @Column(name = "PROGRESS", nullable = false) protected Integer progress; //... }
还可以使用现有的约束的组合来创建自定义约束,例如:
@NotNull @Size(min = 2, max = 14) @Pattern(regexp = "\\d+") @Target({METHOD, FIELD, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = {}) public @interface ValidProductCode { String message() default "{msg://om.company.demo.entity/ValidProductCode.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
当使用复合约束时,生成的“约束违反”集合将包含每个约束的“约束违反”。如果想返回单个“约束违反”,请使用
@ReportAsSingleViolation
注解这个复合注解类。 -
- CUBA 定义的验证注解
-
除了使用
javax.validation.constraints
包中的标准注解之外,可以使用在 CUBA 框架中定义的以下注解:-
@RequiredView
- 可以添加到服务方法定义中,以确保实体实例加载了视图中指定的所有属性。如果注解标记到方法上,则检查返回值。如果注解标记到参数上,则检查参数。如果返回值或者参数是集合,则检查集合中的所有元素。例如:
public interface MyService { String NAME = "sample_MyService"; @Validated void processFoo(@RequiredView("foo-view") Foo foo); @Validated void processFooList(@RequiredView("foo-view") List<Foo> fooList); @Validated @RequiredView("bar-view") Bar loadBar(@RequiredView("foo-view") Foo foo); }
-
3.2.13.2. 运行时验证
- 在 UI 中验证
-
连接到数据源的通用 UI 组件获取
BeanValidator
实例来检查字段的值。验证器是从可视化组件实现的Component.Validatable.validate()
方法调用的。如果验证不通过,会抛出CompositeValidationException
异常,这个异常实例中包含了一组违规信息的集合。可以移除标准的验证器,也可以使用不同的约束组初始化标准验证器:
@UiController("sample_NewScreen") @UiDescriptor("new-screen.xml") public class NewScreen extends Screen { @Inject private TextField<String> field1; @Inject private TextField<String> field2; @Subscribe protected void onInit(InitEvent event) { field1.getValidators().stream() .filter(BeanPropertyValidator.class::isInstance) .forEach(field1::removeValidator); (1) field2.getValidators().stream() .filter(BeanPropertyValidator.class::isInstance) .forEach(validator -> { ((BeanPropertyValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class}); (2) }); } }
1 从 UI 组件中完全删除 bean 验证。 2 这里,验证器将仅检查显式设置了 UiComponentChecks 组的约束,因为没有传递默认组。 默认情况下,
BeanValidator
具有Default
和UiComponentChecks
分组。如果实体属性带有
@NotNull
注解且没有定义约束组,则在元数据中这个属性会被标记为强制的(mandatory),并且通过数据源使用此属性的 UI 组件将具有required = true
属性。DateField和DatePicker组件使用
@Past
、@PastOrPresent
、@Future
、@FutureOrPresent
注解自动设置其rangeStart
和rangeEnd
属性,不过这里忽略了时间部分。如果约束包含
UiCrossFieldChecks
组并且所有属性级别的检查都通过了,编辑界面将在提交时做类级别约束的验证。可以在控制器使用setCrossFieldValidate()
关闭此验证:public class EventEdit extends StandardEditor<Event> { @Subscribe public void onInit(InitEvent event) { setCrossFieldValidate(false); } }
- DataManager中的验证
-
DataManager 可以对保存的实体实例进行验证。以下参数会影响验证:
-
cuba.dataManagerBeanValidation 应用程序属性设置设置是否进行验证的全局默认值。
-
也可以覆盖上面的全局默认值,在 UI 界面保存数据的时候,可以在使用
DataManager.commit()
或者DataContext.PreCommitEvent
里为CommitContext
设置CommitContext.ValidationMode
-
也可以提供一个 验证组 的列表给
CommitContext
或者DataContext.PreCommitEvent
,这样可以只应用定义的约束的一部分。
-
- 中间件服务验证
-
如果服务接口中的方法带有
@Validated
注解,则中间件服务会对方法的参数和返回结果执行验证。例如:public interface TaskService { String NAME = "demo_TaskService"; @Validated @NotNull String completeTask(@Size(min = 5) String comment, @NotNull Task task); }
@Validated
注解可以指定约束组以使验证应用到某组约束上,如果没有指定任何组,默认使用以下约束组:-
Default
和ServiceParametersChecks
- 进行方法参数验证时 -
Default
和ServiceResultChecks
- 进行方法返回值验证时
在验证错误时会抛出
MethodParametersValidationException
和MethodResultValidationException
异常。如果要在服务中以编程的方式执行某些自定义验证,请使用
CustomValidationException
来通知客户端有关验证的错误信息,这样可以与标准 bean 验证错误信息保持相同的格式。此异常也可以跟 REST API 客户端有特定的关联。 -
- 在 REST API 中验证
-
对于创建和更新操作, 通常 REST API 会自动执行 bean 验证。验证错误会以如下方式返回给客户端:
-
MethodResultValidationException
和ValidationException
导致500 Server error
HTTP 状态 -
MethodParametersValidationException
、ConstraintViolationException
和CustomValidationException
导致400 Bad request
HTTP 状态 -
格式为
Content-Type: application/json
的响应体将包含一个对象列表,每个对象都包含属性message
、messageTemplate
、path
和invalidValue
属,例如:[ { "message": "Invalid email: aaa", "messageTemplate": "{msg://com.company.demo.entity/Customer.email.validationMsg}", "path": "email", "invalidValue": "aaa" } ]
-
path
- 表示被验证对象的无效属性在对象关系图中的路径。 -
messageTemplate
- 消息模板字符串,这个模板字符串是在message
注解属性中定义。 -
message
- 包含验证消息的实际值 。 -
invalidValue
- 属性值类型是String
、Date
、Number
、Enum
、UUID
中的其中之一时才返回。
-
-
- 以编程的方式进行验证
-
可以使用
BeanValidation
基础设施接口以编程的方式执行验证。该接口可在中间件和客户端层使用。它用于获取执行验证的javax.validation.Validator
实现。验证的结果是一组ConstraintViolation
对象。例如:@Inject private BeanValidation beanValidation; public void save(Foo foo) { Validator validator = beanValidation.getValidator(); Set<ConstraintViolation<Foo>> violations = validator.validate(foo); // ... }
参阅我们的 blog Java中的数据验证 了解更多内容。 文章中的示例程序可以在 GitHub 找到。 |
3.2.14. 实体属性访问控制
安全子系统 允许根据用户权限设置对实体属性的访问。也就是说,框架可以根据分配给当前用户的角色自动将属性设置为只读或隐藏。但有时可能还想根据实体或其关联实体的当前状态动态更改对属性的访问。
该机制的工作原理如下:
-
当 DataManager 加载一个实体时,它会找到实现
SetupAttributeAccessHandler
接口的所有 Spring bean,并传递SetupAttributeAccessEvent
对象作为参数调用它们的setupAccess()
方法。此对象包含处于托管状态的已加载实例,以及三个用于存储属性名称的集合:只读属性集合、隐藏属性集合和必须属性集合(这些集合最初为空)。 -
SetupAttributeAccessHandler
接口的实现类分析实体的状态并适当地填充事件对象中的属性名称集合。这些类实际上是用于定义给定实例的属性访问规则的容器。 -
该机制将由规则定义的属性名称保存在实体实例本身(在关联的
SecurityState
对象中)。 -
在客户端层,通用 UI 和 REST API 使用
SecurityState
对象来控制对实体属性的访问。
要为特定实体类型创建规则,请执行以下操作:
-
在项目的 core 模块中创建Spring Bean 并实现
SetupAttributeAccessHandler
接口。使用被处理实体的类型对接口进行参数化。bean 范围(scope)必须是默认的单例(singleton)。必须实现接口方法:-
supports(Class)
如果处理器设计为处理给定的实体类,则返回 true。 -
setupAccess(SetupAttributeAccessEvent)
在这个方法中通过操作属性集合来设置访问权限。应该使用事件对象的addHidden()
、addReadOnly()
和addRequired()
方法填充只读、隐藏和必须属性的集合。可通过getEntity()
方法获得实体实例,实体实例处于托管状态,因此可以安全地访问其属性和其关联实体的属性。
-
例如,假设 Order
实体具有 customer
和 amount
属性,可以根据客户(customer)创建以下规则来限制对 amount
属性的访问:
package com.company.sample.core;
import com.company.sample.entity.Order;
import com.haulmont.cuba.core.app.SetupAttributeAccessHandler;
import com.haulmont.cuba.core.app.events.SetupAttributeAccessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component("sample_OrderAttributeAccessHandler")
public class OrderAttributeAccessHandler implements SetupAttributeAccessHandler<Order> {
@Override
public boolean supports(Class clazz) {
return Order.class.isAssignableFrom(clazz);
}
@Override
public void setupAccess(SetupAttributeAccessEvent<Order> event) {
Order order = event.getEntity();
if (order.getCustomer() != null) {
if ("PLATINUM".equals(order.getCustomer().getGrade().getCode())) {
event.addHidden("amount");
} else if ("GOLD".equals(order.getCustomer().getGrade().getCode())) {
event.addReadOnly("amount");
}
}
}
}
- 通用 UI 中的属性访问控制
-
在发送BeforeShowEvent和AfterShowEvent事件之间,框架会自动在界面应用属性访问限制。如果不想在特定界面中使用,可以在界面控制器类添加
@DisableAttributeAccessControl
注解。可能希望在界面打开时重新计算并应用限制,以响应用户操作。您可以使用
AttributeAccessSupport
bean 来完成它,传递当前界面和状态已更改的实体。例如:@UiController("sales_Order.edit") @UiDescriptor("order-edit.xml") @EditedEntityContainer("orderDc") @LoadDataBeforeShow public class OrderEdit extends StandardEditor<Order> { @Inject private AttributeAccessSupport attributeAccessSupport; @Subscribe(id = "orderDc", target = Target.DATA_CONTAINER) protected void onOrderDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<Order> event) { if ("customer".equals(event.getProperty())) { attributeAccessSupport.applyAttributeAccess(this, true, getEditedEntity()); } } }
applyAttributeAccess()
方法的第二个参数是一个布尔值,它指定在应用新限制之前是否将组件访问权限重置为默认值。如果参数值为 true,则程序中对组件状态的更改(如果有)将丢失。在界面打开时自动调用该方法时,此参数的值为 false。但是在响应 UI 事件时调用该方法时,将其设置为 true,否则对组件的限制将被累加而不是替换。属性访问限制仅适用于绑定到单个实体属性的组件,如 TextField 或 LookupField。Table 和实现
ListComponent
接口的其它组件不受影响。因此,如果要编写可以隐藏多个实体实例的一个属性的规则,建议直接不要在表格中显示此属性。
3.3. 数据库
本节介绍如何配置数据库连接以使用特定的 DBMS。同时介绍了一种数据库迁移的机制,该机制可以创建数据库,并在应用程序开发和上线运行后的整个周期中使其保持最新。
数据库相关的组件属于 Middleware block;应用程序的其它 block 无法直接访问数据库。
3.3.1. 连接至数据库
CUBA 应用程序通过 JDBC DataSource-数据源
获取数据库的连接。一个数据源可以在应用程序中配置,也可以通过 JNDI 获取。获取数据源的方法通过应用程序属性 cuba.dataSourceProvider
来指定:可以设置为 application
或 jndi
。
使用 CUBA Studio 可以很方便的配置主数据库连接和附加数据存储,参阅 文档 。下面的信息对于排查问题很有帮助,也可以用来定义那些 Studio 中没有的参数,比如连接池配置。
- 在应用程序中配置数据源
-
当在应用程序中配置数据源时,框架会使用 HikariCP 创建一个连接池。连接参数和连接池配置都通过
core
模块app.properties
中的应用程序属性来配置。如果不需要特别指定由应用程序服务提供的连接池,推荐直接使用这种配置。下列应用程序属性定义数据库类型和连接参数:
-
cuba.dbmsType
- 定义 DBMS 类型。 -
cuba.dataSourceProvider
-application
值表示数据源必须使用应用程序属性来配置。 -
cuba.dataSource.username
- 数据库的用户名称。 -
cuba.dataSource.password
- 数据库的用户密码。 -
cuba.dataSource.dbName
- 数据库的名称。 -
cuba.dataSource.host
- 数据库的地址。 -
cuba.dataSource.port
- 可选参数,设定数据库端口,如果使用了非标准端口的话。 -
cuba.dataSource.jdbcUrl
- 可选参数,设置 JDBC URL 的全路径,如果需要一些额外的连接参数。注意,对于数据迁移任务,所有以上的单独属性还是需要配置的。
如需配置连接池参数,使用
cuba.dataSource.
前缀指定 HikariCP 的属性,比如cuba.dataSource.maximumPoolSize
或cuba.dataSource.connectionTimeout
。参考 HikariCP 文档 了解所有支持的参数及其默认值。如果应用程序使用了附加数据存储,需要同样为每个数据存储定义一组参数。数据存储的名称添加到每个属性名称的第二部分:
示例:
# main data store connection parameters cuba.dbmsType = hsql cuba.dataSourceProvider = application cuba.dataSource.username = sa cuba.dataSource.password = cuba.dataSource.dbName = demo cuba.dataSource.host = localhost cuba.dataSource.port = 9111 cuba.dataSource.maximumPoolSize = 20 # names of additional data stores cuba.additionalStores = clients,orders # 'clients' data store connection parameters cuba.dbmsType_clients = postgres cuba.dataSourceProvider_clients = application cuba.dataSource_clients.username = postgres cuba.dataSource_clients.password = postgres cuba.dataSource_clients.dbName = clients_db cuba.dataSource_clients.host = localhost # 'orders' data store connection parameters cuba.dbmsType_orders = mssql cuba.dataSourceProvider_orders = application cuba.dataSource_orders.jdbcUrl = jdbc:sqlserver://localhost;databaseName=orders_db;currentSchema=my_schema cuba.dataSource_orders.username = sa cuba.dataSource_orders.password = myPass123 cuba.dataSource_orders.dbName = orders_db cuba.dataSource_orders.host = localhost
另外,对于每个附加数据存储,
core
模块的spring.xml
文件必须包含一个CubaDataSourceFactoryBean
bean 的定义,该定义需要使用合适的storeName
参数,示例:<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean"> <property name="storeName" value="clients"/> </bean> <bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean"> <property name="storeName" value="orders"/> </bean>
如果在应用程序中配置数据源,数据库迁移的 Gradle 任务可能没有参数,因为这些参数会通过应用程序属性获取。这是在应用程序中配置数据源的另外一个好处。示例:
task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) { } task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) { }
-
- 从 JNDI 获取数据源
-
如果需要通过 JNDI 使用由应用程序服务提供的数据源,需要在
core
模块的app.properties
文件定义以下应用程序属性:-
cuba.dbmsType
- 定义 DBMS 类型。 -
cuba.dataSourceProvider
-jndi
值表示数据源必须通过 JNDI 获取。
数据源的 JNDI 名称通过 cuba.dataSourceJndiName 应用程序属性指定,默认为
java:comp/env/jdbc/CubaDS
。对于附加数据存储,定义同样的属性,但是需要添加数据存储名称。示例:
# main data store connection parameters cuba.dbmsType = hsql cuba.dataSourceProvider = jndi # names of additional data stores cuba.additionalStores = clients,orders # 'clients' data store connection parameters cuba.dbmsType_clients = postgres cuba.dataSourceProvider_clients = jndi cuba.dataSourceJndiName_clients = jdbc/ClientsDS # 'orders' data store connection parameters cuba.dbmsType_orders = mssql cuba.dataSourceProvider_orders = jndi cuba.dataSourceJndiName_orders = jdbc/OrdersDS
另外,对于每个附加数据存储,
core
模块的spring.xml
文件必须包含一个CubaDataSourceFactoryBean
bean 的定义,该定义需要使用合适的storeName
参数和jndiNameAppProperty
参数,示例:<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean"> <property name="storeName" value="clients"/> <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_clients"/> </bean> <bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean"> <property name="storeName" value="orders"/> <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_orders"/> </bean>
通过 JNDI 提供的数据源需要根据应用程序服务做特殊的配置。在 Tomcat 中,配置在 context.xml 文件。CUBA Studio 会将连接参数写入
modules/core/web/META-INF/context.xml
并在开发应用程序时通过标准部署过程使用该文件。如果数据源配置在
context.xml
,数据库迁移的 Gradle 任务必须有定义数据库连接的参数,示例:task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) { dbms = 'hsql' host = 'localhost:9111' dbName = 'demo' dbUser = 'sa' dbPassword = '' } task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) { dbms = 'hsql' host = 'localhost:9111' dbName = 'demo' dbUser = 'sa' dbPassword = '' }
-
3.3.1.1. 连接到非默认数据库schema
PostgreSQL 和 Microsoft SQL Server 支持连接到指定的数据库架构。默认情况下,PostgreSQL 会连接到 public
,SQL Server 会连接到 dbo
。
- PostgreSQL
-
如果使用的 Studio,在 Data Store Properties 窗口的 Connection params 字段添加
currentSchema
连接参数。Studio 会根据数据源的配置方法自动更新项目文件。否则,需要按照下面的介绍手动配置连接参数。如果在应用程序中配置数据源,添加 URL 属性全路径,示例:
cuba.dataSource.jdbcUrl = jdbc:postgresql://localhost/demo?currentSchema=my_schema
如果是从 JNDI 获取数据源,需要在数据源定义的连接 URL 添加参数
currentSchema
(Tomcat 中是在context.xml
文件),还需要为 createDb 和 updateDb Gradle 任务添加connectionParams
属性。示例:task createDb(dependsOn: assembleDbScripts, type: CubaDbCreation) { dbms = 'postgres' host = 'localhost' dbName = 'demo' connectionParams = '?currentSchema=my_schema' dbUser = 'postgres' dbPassword = 'postgres' }
现在可以更新或者重新创建数据库,所有的表格会在指定的 schema 中创建。
- Microsoft SQL Server
-
此时,只提供一个连接属性就不够了,需要将 schema 和用户做关联。下面是创建一个新数据库并使用非默认 schema 的例子。
-
创建一个 login:
create login JohnDoe with password='saPass1'
-
创建一个新数据库:
create database my_db
-
用
sa
连接至新数据库,创建一个 schema,然后创建用户并赋予权限:create schema my_schema create user JohnDoe for login JohnDoe with default_schema = my_schema exec sp_addrolemember 'db_owner', 'JohnDoe'
如果是使用 Studio,在 Data Store Properties 窗口的 Connection params 字段添加
currentSchema
连接参数。Studio 会根据数据源的配置方法自动更新项目文件。否则,需要按照下面的介绍手动配置连接参数。如果在应用程序中配置数据源,添加 URL 属性全路径,示例:
cuba.dataSource.jdbcUrl = jdbc:sqlserver://localhost;databaseName=demo;currentSchema=my_schema
如果是从 JNDI 获取数据源,需要在数据源定义的连接 URL 添加参数
currentSchema
(Tomcat 中是在context.xml
文件),还需要为 createDb 和 updateDb Gradle 任务添加connectionParams
属性。task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) { dbms = 'mssql' dbmsVersion = '2012' host = 'localhost' dbName = 'demo' connectionParams = ';currentSchema=my_schema' dbUser = 'JohnDoe' dbPassword = 'saPass1' }
请注意,由于 SQL Server 的特性 - 非默认 schema 需要与用户关联,所以无法从 Studio 或在命令行中执行
createDb
来重新创建 SQL Server 数据库。但是,如果在 Studio 中运行 Update database 或在命令行中运行updateDb
,则现有数据库中指定 schema 下所有必须的表都会被创建。 -
3.3.2. DBMS 类型
应用程序中使用的 DBMS 的类型由cuba.dbmsType和cuba.dbmsVersion(可选)应用程序属性定义。这些属性会影响各种依赖于数据库类型的平台机制。
应用程序通过 javax.sql.DataSource
连接数据库,javax.sql.DataSource
是通过cuba.dataSourceJndiNamecuba.dataSourceJndiName中指定的名称(默认情况下是 java:comp/env/jdbc/CubaDS
)从 JNDI 中获取的。标准部署方式的数据源配置在 core 模块的 context.xml 文件中定义。数据源应使用适用于所选 DBMS 的 JDBC 驱动程序。
平台支持以下“开箱即用”的 DBMS 类型:
cuba.dbmsType | cuba.dbmsVersion | JDBC driver | |
---|---|---|---|
HSQLDB |
hsql |
org.hsqldb.jdbc.JDBCDriver |
|
PostgreSQL 8.4+ |
postgres |
org.postgresql.Driver |
|
Microsoft SQL Server 2005 |
mssql |
2005 |
net.sourceforge.jtds.jdbc.Driver |
Microsoft SQL Server 2008 |
mssql |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
|
Microsoft SQL Server 2012+ |
mssql |
2012 |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
Oracle Database 11g+ |
oracle |
oracle.jdbc.OracleDriver |
|
MySQL 5.6+ |
mysql |
com.mysql.jdbc.Driver |
|
MariaDB 5.5+ |
mysql |
org.mariadb.jdbc.Driver |
下表描述了 Java 中的实体属性与不同 DBMS 中的表列之间推荐的数据类型映射关系。生成创建和更新数据库的脚本时,CUBA Studio 会自动使用这些类型。使用这些类型,可以保证所有平台机制正常运行。
Java | HSQL | PostgreSQL | MS SQL Server | Oracle | MySQL | MariaDB |
---|---|---|---|---|---|---|
UUID |
varchar(36) |
uuid |
uniqueidentifier |
varchar2(32) |
varchar(32) |
varchar(32) |
Date |
timestamp |
timestamp |
datetime |
timestamp |
datetime(3) |
datetime(3) |
java.sql.Date |
timestamp |
date |
datetime |
date |
date |
date |
java.sql.Time |
timestamp |
time |
datetime |
timestamp |
time(3) |
time(3) |
BigDecimal |
decimal(p, s) |
decimal(p, s) |
decimal(p, s) |
number(p, s) |
decimal(p, s) |
decimal(p, s) |
Double |
double precision |
double precision |
double precision |
float |
double precision |
double precision |
Long |
bigint |
bigint |
bigint |
number(19) |
bigint |
bigint |
Integer |
integer |
integer |
integer |
integer |
integer |
integer |
Boolean |
boolean |
boolean |
tinyint |
char(1) |
boolean |
boolean |
String (limited) |
varchar(n) |
varchar(n) |
varchar(n) |
varchar2(n) |
varchar(n) |
varchar(n) |
String (unlimited) |
longvarchar |
text |
varchar(max) |
clob |
longtext |
longtext |
byte[] |
longvarbinary |
bytea |
image |
blob |
longblob |
longblob |
通常,在数据库和 Java 代码之间转换数据的整个工作由ORM 层使用合适 JDBC 驱动程序来完成。这意味着使用EntityManager方法和JPQL 查询处理数据时不需要手动转换数据;对于开发人员来说,在编写与数据库交互的代码时应避免使用表格左栏没有列出的 Java 类型。
当通过 EntityManager.createNativeQuery() 或 QueryRunner 使用本地 SQL 时,Java 代码中的某些类型将与上面提到的类型不同,具体取决于所使用的 DBMS。特别是对于 UUID
类型的属性 - 只有 PostgreSQL 驱动程序使用此类型返回相应列的值; 其它数据库服务都返回 String
。要对不同的数据库类型抽象出通用应用程序代码,建议使用DbTypeConverter接口转换参数类型和查询结果。
3.3.2.1. 对其它 DBMS 的支持
在应用程序项目中,可以使用ORM框架(EclipseLink)支持的任何 DBMS。请按照以下步骤操作:
-
在 cuba.dbmsType 属性中以任意形式代码的指定数据库的类型。代码必须与平台中使用的代码不同:
hsql
、postgres
、mssql
、oracle
。 -
实现
DbmsFeatures
、SequenceSupport
、DbTypeConverter
接口,实现类用以下格式命名:<Type>DbmsFeatures
、<Type>SequenceSupport
和<Type>DbTypeConverter
,其中Type
是 DBMS 类型代码。实现类的包必须与接口的包相同。 -
如果在应用程序中配置了数据源,按照 连接至数据库 章节的介绍和要求的格式配置连接 URL 的全路径。
-
在以 DBMS 类型代码命名的目录中创建数据库初始化和更新脚本。初始化脚本必须创建平台实体所需的所有数据库对象(可以从现有的
10-cuba
等目录中复制并修改这些脚本,使其适用于新的 DBMS 类型)。 -
要通过 Gradle 任务创建和更新数据库,需要在
build.gradle
中为这些任务指定额外的参数:task createDb(dependsOn: assemble, type: CubaDbCreation) { dbms = 'my' // DBMS code driver = 'net.my.jdbc.Driver' // JDBC driver class dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL masterUrl = 'jdbc:my:myserver://192.168.47.45/master' // URL of a master DB to connect to for creating the application DB dropDbSql = 'drop database mydb;' // Drop database statement createDbSql = 'create database mydb;' // Create database statement timeStampType = 'datetime' // Date and time datatype - needed for SYS_DB_CHANGELOG table creation dbUser = 'sa' dbPassword = 'saPass1' } task updateDb(dependsOn: assemble, type: CubaDbUpdate) { dbms = 'my' // DBMS code driver = 'net.my.jdbc.Driver' // JDBC driver class dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL dbUser = 'sa' dbPassword = 'saPass1' }
也可以在 CUBA Studio 中添加自定义数据库的支持。实现了集成之后,开发者可以在 Studio 中使用标准的对话框修改数据存储的设置。还有,最重要的是,Studio 能为平台、扩展插件以及项目中定义的实体自动生成数据库迁移脚本。集成自定义数据库的介绍在 Studio 用户向导 的相应章节。
在 CUBA 平台和 Studio 集成自定义数据库(Firebird)的示例,可以参考: https://github.com/cuba-labs/firebird-sample
3.3.2.2. DBMS 版本
除了 cuba.dbmsType 应用程序属性外,还有一个可选的cuba.dbmsVersion属性。数据库版本属性影响对 DbmsFeatures
、SequenceSupport
、 DbTypeConverter
的接口实现的选择,同时也影响对数据库初始化和更新脚本的搜索。
这些集成接口实现类的名称结构如下:<Type><Version><Name>
。这里 Type
是 cuba.dbmsType
属性的值(大写),Version
是 cuba.dbmsVersion
的值,Name
是接口名称。类的包必须与接口的包一致。如果这个名称的类不可用,则会尝试查找名称中不带版本的类:<Type><Name>
。如果这样的类也不存在,则会抛出异常。
例如,在平台中定义了 com.haulmont.cuba.core.sys.persistence.Mssql2012SequenceSupport
类。如果在项目中指定了以下属性,则此类会生效:
cuba.dbmsType = mssql
cuba.dbmsVersion = 2012
对于数据库初始和更新脚本的搜索,<type>-<version>
目录优先于 type
目录。这意味着 <type>-<version>
目录中的脚本会代替 <type>
目录中具有相同名称的脚本。<type>-<version>
目录还可以包含一些具有唯一名称的脚本;它们也会被添加到公共的脚本集中以供执行。脚本按照路径排序,从 <type>
或 <type>-<version>
目录的第一个子目录开始,即不管脚本在哪个的目录(是否有版本)。
例如,对于 Microsoft SQL Server 2012 之前和之后版本的数据库,其初始化脚本应如下所示:
modules/core/db/init/
mssql/
10.create-db.sql
20.create-db.sql
30.create-db.sql
mssql-2012/
10.create-db.sql
3.3.3. 创建和更新数据库的脚本
CUBA 应用程序项目总是包含两组脚本:
-
用于创建数据库的脚本,该脚本用于从零开始创建数据库。它们包含一组 DDL 和 DML 操作,这些操作会创建一个空数据库结构,该结构与应用程序数据模型的当前状态完全一致。这些脚本还可以使用必要的初始化数据填充数据库。
-
用于更新数据库的脚本,用于将数据库结构从任意一个旧的结构更新为与当前数据模型对应的结构。
更改数据模型时,必须通过 Create 和 Update 脚本使数据库结构同步相应的更改。例如,在将 address
属性添加到 Customer
实体时,有必要:
-
更改建表脚本:
create table SALES_CUSTOMER ( ID varchar(36) not null, CREATE_TS timestamp, CREATED_BY varchar(50), NAME varchar(100), ADDRESS varchar(200), -- added column primary key (ID) )
-
添加修改同一个表的更新脚本:
alter table SALES_CUSTOMER add ADDRESS varchar(200)
请注意,Studio 更新脚本生成器不会跟踪属性 Column definition 和自定义数据类型的
sqlType
的更改。因此,如果更改了它们,请手动创建相应的更新脚本。
主数据存储的创建脚本位于 core 模块的 /db/init
目录下。附加数据存储(如果有)的创建脚本位于 /db/init_<datastore_name>
目录。对于应用程序支持的每种类型的 DBMS,都会创建一组单独的脚本,这些脚本位于cuba.dbmsType应用程序属性中指定的子目录中,例如 /db/init/postgres
。创建脚本的名称应该符合以下格式:{optional_prefix}create-db.sql
。
主数据存储的更新脚本位于 core 模块的 /db/update
目录中。附加数据存储(如果有)的更新脚本位于 /db/update_<datastore_name>
目录。对于应用程序支持的每种类型的 DBMS,都会创建一组单独的脚本,这些脚本位于cuba.dbmsType应用程序属性中指定的子目录中,例如 /db/update/postgres
。
更新脚本有两种类型:使用 *.sql
扩展名或 *.groovy
扩展名。更新数据库的主要方法是使用 SQL 脚本。Groovy 脚本仅由数据库脚本服务器执行机制执行,因此它们主要用于生产环境,在数据迁移或导入无法使用纯 SQL 实现时。如果要跳过 Groovy 更新脚本,可以在命令行中运行以下命令:
delete from sys_db_changelog where script_name like '%groovy' and create_ts > (now() - interval '1 hour')
更新脚本应具有特定格式的名称,更新机制会按脚本名称的字母顺序排序以形成正确的执行顺序(一般情况下,脚本名称根据创建时间来命名)。因此,在手动创建此类脚本时,建议使用以下格式指定更新脚本的名称: {yymmdd}-{description}.sql
,其中 yy
表示年份,mm
是表示月份,dd
表示日期,description
是脚本的简短描述。例如,121003-addCodeToCategoryAttribute.sql
。Studio 自动生成脚本时也遵循此格式。
要使用 updateDb
任务(gradle task)执行 groovy 更新脚本,groovy 脚本的扩展名应该是 .upgrade.groovy
, 并且遵循相同的命名规则。注意在此时,该脚本中不允许 Post Update 操作,相同的 ds
(访问数据源)和 log
(访问日志记录)变量需要用来做数据绑定。可以通过在 build.gradle
的 updateDb
任务中设置 executeGroovy = false
来禁用 groovy 脚本的执行。
可以将更新脚本分组到子目录中,但是,带有子目录的脚本路径不应该违背按时间排序的顺序。例如,可以使用年份或年份和月份创建子目录。
在已部署的应用程序中,用于创建和更新数据库的脚本位于特定的数据库脚本目录中,该目录由cuba.dbDir应用程序属性设置。
3.3.3.1. SQL 脚本的结构
数据库迁移的 SQL 脚本是一组由 “^” 字符分隔的 DDL 和 DML 命令组成的文本文件。这里使用了 “^” 字符,以便 “;” 分隔符可以用来分隔复杂的命令;例如,在创建函数或触发器时。脚本执行机制使用 “^” 分隔符将输入文件拆分为单独的命令,并在独立的事务中执行每个命令。这意味着如果需要的话可以将几条单独的语句(例如,insert
)组合在一起,用分号分隔,确保它们在同一个事务中执行。
“^” 分隔符可以通过使用两个 “^” 来转义。例如,如果要将 |
SQL 格式的更新脚本示例:
create table LIBRARY_COUNTRY (
ID varchar(36) not null,
CREATE_TS time,
CREATED_BY varchar(50),
NAME varchar(100) not null,
primary key (ID)
)^
alter table LIBRARY_TOWN add column COUNTRY_ID varchar(36) ^
alter table LIBRARY_TOWN add constraint FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID) references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN (COUNTRY_ID)^
3.3.3.2. Groovy 脚本的结构
Groovy 更新脚本的结构如下:
-
主要(main) 部分,包含要在应用程序上下文启动之前执行的代码。在这一部分,可以使用任何 Java、Groovy 和 Middleware 应用程序块(block)中的类。但要注意,这时尚未实例化 bean、基础设施接口和其它应用程序对象,所以无法使用它们。
这部分主要用于更新数据库 schema,通常使用普通的 SQL 脚本。
-
PostUpdate 部分 - 一组闭包(Groovy 中的概念),将在应用程序上下文启动后和更新过程完成后执行。在这些闭包中,可以使用任何中间件对象。
在脚本的这一部分中,可以比较方便地执行数据导入,因为可以使用Persistence接口和数据模型对象。
执行机制将以下变量传递给 Groovy 脚本:
-
ds
– 用于应用程序数据库的javax.sql.DataSource
实例; -
log
–org.apache.commons.logging.Log
实例,用于在服务端日志中输出信息; -
postUpdate
– 包含add(Closure closure)
方法的对象,用于添加上述 PostUpdate 闭包。
Groovy 脚本仅由执行数据库脚本的服务端机制执行。 |
Groovy 更新脚本的示例:
import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql
log.info('Executing actions in update phase')
Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION varchar(100); """
// Add post update action
postUpdate.add({
log.info('Executing post update action using fully functioning server')
def p = AppBeans.get(Persistence.class)
def tr = p.createTransaction()
try {
def em = p.getEntityManager()
Colour c = new Colour()
c.name = 'yellow'
c.description = 'a description'
em.persist(c)
tr.commit()
} finally {
tr.end()
}
})
3.3.3.3. Gradle 任务执行数据库脚本
应用程序开发人员通常使用此机制来更新自己的数据库实例。脚本的执行本质上是通过运行定义在build.gradle中特定的 Gradle 任务。这个操作可以通过命令行或 Studio 界面完成。
要运行脚本创建数据库,需使用 createDb 任务。在 Studio 中,它对应于主菜单中的 CUBA > Create Database 命令。启动此任务时,会执行以下操作:
-
当前项目的应用程序组件的脚本和 core 模块的
db/**/*.sql
脚本被构建到modules/core/build/db
目录中。应用程序组件的脚本集位于有数字前缀的子目录中。前缀用于根据组件之间的依赖关系提供脚本执行的字母排序。 -
如果数据库存在,它会被完全清除。一个新的空数据库会被创建。
-
modules/core/build/db/init/**/*create-db.sql
子目录中的所有创建脚本按字母顺序依次执行,并且它们的名称以及相对于 db 目录的路径会注册在 SYS_DB_CHANGELOG 表中。 -
类似地,所有当前可用的
modules/core/build/db/update/**/*.sql
更新脚本会注册在 SYS_DB_CHANGELOG 表中。这对于后续进行数据库增量更新是必须的。
要运行脚本更新数据库,需使用 updateDb 任务。在 Studio 中,它对应于主菜单中的 CUBA > Update Database 命令。启动此任务时,会执行以下操作:
-
脚本的构建方式与上述
createDb
命令的构建方式相同。 -
执行机制检查是否已运行应用程序组件的所有创建脚本(通过检查
SYS_DB_CHANGELOG
表)。如果没有,则执行应用程序组件创建脚本并在SYS_DB_CHANGELOG
表中注册。 -
在
modules/core/build/db/update/**
目录中搜索未在SYS_DB_CHANGELOG
表中注册的更新脚本,即以前没有执行过的更新脚本。 -
对上一步中找到的所有脚本按字母顺序依次执行,同时脚本的名称以及相对于
db
目录的路径都会注册到SYS_DB_CHANGELOG
表中。
3.3.3.4. 在 web Server 中执行数据库脚本
Web Server 执行数据库脚本的机制用于更新数据库,这个操作在应用程序服务启动、中间件块(block)初始化期间激活。显然,应用程序应该已经构建并部署在 Web Server 上,即在生产环境或开发人员的 Tomcat 实例中。
根据下面描述的条件,该机制执行创建或更新脚本,也就是它可以从头开始初始化 DB 并对其进行更新。但是,与上一节中描述的 Gradle createDb
任务不同,数据库必须存在才能初始化 - 在 Web Server 中不会自动创建 DB,而只是执行脚本。
Web Server 中执行脚本的机制如下:
-
应用程序从数据库脚本目录读取脚本,该目录由cuba.dbDir应用程序属性定义,该属性的默认设置为
WEB-INF/db
。 -
如果数据库没有
SEC_USER
表,则被视为空数据库,会使用创建脚本运行完整初始化过程。执行初始化脚本后,这些脚本的名称会存储在SYS_DB_CHANGELOG
表中。所有可用的更新脚本的名称也存储在同一个表中,但是并没有执行过这些更新脚本。 -
如果数据库有
SEC_USER
表,但没有SYS_DB_CHANGELOG
表(当在现有生产环境数据库上首次启动这里描述的机制时就会出现这种情况),这种情况下不执行任何脚本,而是创建SYS_DB_CHANGELOG
表,并存储所有当前可用的创建和更新脚本的名称。 -
如果数据库同时具有
SEC_USER
和SYS_DB_CHANGELOG
表,则执行先前未将名称存储在SYS_DB_CHANGELOG
表中的更新脚本,然后将这些脚本名称存储到SYS_DB_CHANGELOG
表中。脚本执行的顺序由两个因素决定:应用程序组件的优先级(参阅数据库脚本目录:10-cuba
、20-bpm
,…)和按字母顺序排序的脚本文件名称(参考update
目录的子目录)。在执行更新脚本之前,先检查是否已运行应用程序组件的所有创建脚本(通过检查
SYS_DB_CHANGELOG
表)。如果某个应用程序组件使用的数据库未初始化,则会执行其创建脚本。
通过cuba.automaticDatabaseUpdate应用程序属性启用在服务器启动时执行脚本的机制。
在运行中的应用程序中,可以使用 update
作为参数调用 app-core.cuba:type=PersistenceManager
JMX bean 的 updateDatabase()
方法来启动脚本执行机制。显然,这种方式只能更新现有的 DB,因为无法登录只有空 DB 的系统来运行 JMX bean 方法。请注意,如果部分在中间件启动或用户登录期间初始化数据模型与数据架构不匹配的话,会发生不可恢复的错误。这就是一般都在服务器启动时数据模型初始化前执行数据库自动更新的原因。
JMX app-core.cuba:type=PersistenceManager
bean 还有一个与 DB 更新机制相关的方法: findUpdateDatabaseScripts()
。它返回目录中可用的但未在 DB 中注册(尚未执行)新的更新脚本列表。
有关使用服务器数据库更新机制的建议,请参阅在生产环境中创建和更新数据库。
3.4. 中间件组件
下图显示了 CUBA 应用程序中间件的主要组件。
服务(Service)是Spring beans,用于形成应用边界并为客户端层提供接口。服务自身可以包含业务逻辑,也可以将业务逻辑的实现委托给托管 bean。
Persistence是一个基础设施接口,用于访问数据存储功能:ORM和事务管理。
3.4.1. 服务
服务构成了应用程序的一层,在这一层定义了客户端层可用的一组中间层操作。换句话说,服务是中间层业务逻辑的入口点。在服务中,可以管理事务、检测用户权限、使用数据库或将操作委托给中间层的其它Spring Bean去执行。
下图展示了服务层组件的类关系:
服务接口位于 global 模块中,所以在中间层和客户端层都是可用的。在运行时,在客户端层会为服务接口创建代理。代理使用 Spring HTTP Invoker 机制提供服务 bean 方法的调用。
服务实现 bean 位于 core 模块,仅在中间层可用。
使用 Spring AOP 的任何服务方法都会自动调用 ServiceInterceptor
。它检测当前线程中用户会话的可用性,并且会在从客户端层调用服务时执行转换和记录异常。
3.4.1.1. 创建服务
参考 在 CUBA 中创建业务逻辑 指南,了解如何将业务逻辑做成中间件服务。 |
服务接口的名称应以 Service
结尾,实现类的名称应以 ServiceBean
结尾。
CUBA Studio 能帮助轻松创建服务接口的脚手架代码和存根类。Studio 还会自动在 spring.xml
中注册新服务。要创建服务,请使用 CUBA 项目树的 Middleware 节点中的 New>Service 任务。
如果要手动创建服务,请按照以下步骤操作。
-
在 global 模块中创建服务接口(因为服务接口必须在所有层中可用),并在其中指定服务名称。建议使用以下格式指定名称:
{project_name}_{interface_name}
。例如:package com.sample.sales.core; import com.sample.sales.entity.Order; public interface OrderService { String NAME = "sales_OrderService"; void calculateTotals(Order order); }
-
在 core 模块中创建服务类,并使用接口中指定的名称向其添加
@org.springframework.stereotype.Service
注解:package com.sample.sales.core; import com.sample.sales.entity.Order; import org.springframework.stereotype.Service; @Service(OrderService.NAME) public class OrderServiceBean implements OrderService { @Override public void calculateTotals(Order order) { } }
作为Spring bean 的服务类应放在包的树结构中,其根目录需要在spring.xml文件的 context:component-scan
元素中指定。这时,spring.xml
文件会包含以下元素:
<context:component-scan base-package="com.sample.sales"/>
这意味着将从 com.sample.sales
包开始搜索此应用程序 block 中带注解的 bean。
如果不同的服务或其它中间件组件需要调用相同的业务逻辑,则应将其提取并封装在适当的Spring bean中。例如:
// service interface
public interface SalesService {
String NAME = "sample_SalesService";
BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private SalesCalculator salesCalculator;
@Transactional
@Override
public BigDecimal calculateSales(UUID customerId) {
return salesCalculator.calculateSales(customerId);
}
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {
@Inject
private Persistence persistence;
public BigDecimal calculateSales(UUID customerId) {
Query query = persistence.getEntityManager().createQuery(
"select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
return (BigDecimal) query.getFirstResult();
}
}
3.4.1.2. 使用服务
为了调用服务,应该在应用程序的客户端 block 中创建相应的代理对象。各个 block 中都有一个特殊的工厂可以创建服务代理,Web 客户端:WebRemoteProxyBeanCreator
、Web 门户:PortalRemoteProxyBeanCreator
。
代理对象工厂在相应客户端 block 的spring.xml中配置,并包含服务名称和接口。
例如,要从 sales 应用程序中的 Web 客户端调用 sales_OrderService
服务,请将以下代码添加到 web 模块的 web-spring.xml
文件中:
<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
<property name="serverSelector" ref="cuba_ServerSelector"/>
<property name="remoteServices">
<map>
<entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
</map>
</property>
</bean>
所有导入的服务都应该在 remoteServices
属性中使用 map/entry
元素声明。
CUBA Studio 自动在项目的所有客户端 block 中注册服务。 |
从应用程序代码的角度来看,客户端级别的服务代理对象是标准的 Spring bean,可以通过注入或通过 AppBeans
类获得。例如:
@Inject
private OrderService orderService;
public void calculateTotals() {
orderService.calculateTotals(order);
}
或者
public void calculateTotals() {
AppBeans.get(OrderService.class).calculateTotals(order);
}
3.4.1.3. 数据服务
DataService
使用外观设计模式提供了从客户端层调用DataManager中间件实现的功能。建议不要在应用程序代码中使用 DataService
接口,应该直接在中间层和客户端层使用 DataManager
。
3.4.2. 数据存储
在 CUBA 应用程序中处理数据的常用方法是操作实体 - 可通过具有数据感知功能的可视化组件进行声明式处理,也可通过 DataManager 或 EntityManager 进行编程式处理。实体映射到数据存储中的数据,数据存储通常是关系型数据库。应用程序可以连接到多个数据存储,因此其数据模型将包含映射到位于不同数据库中的数据的实体。
实体只能属于单个数据存储,但是可以在单个 UI 界面上显示来自不同数据存储的实体,DataManager
可以确保在保存时将实体分派到适当的数据存储中去。根据实体类型,DataManager
选择一个已注册的数据存储,这个数据存储实现了 DataStore
接口,然后委托其加载和保存实体。当以编程的方式控制事务并通过 EntityManager
使用实体时,必须明确指定要使用的数据存储。有关详细信息,请参阅 Persistence 接口方法和 @Transactional 注解参数。
平台提供了 DataStore
接口的单一实现,名称为 RdbmsStore
,这个实现的目的是通过 ORM 层来使用关系型数据库。可以在自己的项目中实现 DataStore
接口以进行数据整合,例如,可以与非关系型数据库或具有 REST 接口的外部系统进行数据整合。
在任何 CUBA 应用程序中,必定存在一个主数据存储,它包含系统实体和安全实体,用户登录也是在主数据存储。在本手册中提及数据库时,如果没有明确说明,则指的是主数据存储。主数据存储必须是通过 JDBC 数据源连接的关系型数据库。附加数据存储可以是任何 DataStore
接口的实现。
使用 CUBA Studio 可以配置附加数据存储,参考 文档 。会自动创建所有需要的应用程序属性和 JDBC 数据源,并能维护额外添加的 |
如果没有使用 Studio 的话,下面的信息可以帮助你排查问题。
附加数据存储的名称通过 cuba.additionalStores 应用程序属性指定。如果附加存储是 RdbmsStore
,还为其定义了下面这些属性:
-
cuba.dbmsType_<store_name>
- 数据存储 DBMS 类型。 -
cuba.persistenceConfig_<store_name>
- 数据存储对应的persistence.xml
文件位置。 -
cuba.dataSource…
- 连接至数据库描述的连接参数。
如果项目中实现了 DataStore
接口,实现类的名称必须使用 cuba.storeImpl_<store_name>
属性定义。
比如,如果使用了两个附加存储 db1
(PostgreSQL 数据库)和 mem1
(一个自定义的内存存储,通过某些项目中的 bean 实现),core
模块的 app.properties
文件必须定义以下属性:
cuba.additionalStores = db1, mem1
# RdbmsStore for Postgres database with data source obtained from JNDI
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.dataSourceJndiName_db1 = jdbc/db1
# Custom store
cuba.storeImpl_mem1 = sample_InMemoryStore
还应在所有应用程序 block 使用的属性文件(web-app.properties
、 portal-app.properties
等)中指定 cuba.additionalStores
和 cuba.persistenceConfig_db1
属性。
- 来自不同数据存储的实体之间的引用
-
如果正确定义了 DataManager,则可以自动维护来自不同数据存储的实体之间的 TO-ONE 引用。比如,在主数据存储中有
Order
实体,在附加数据存储中有Customer
实体,并且希望在Order
中引用Customer
。可以这样做:-
在
Order
实体中,定义一个存储Customer
实体 ID 的属性。该属性应使用@SystemLevel
注解,以将其从用户可用的各种列表中排除,例如 Filter 中的可选属性:@SystemLevel @Column(name = "CUSTOMER_ID") private Long customerId;
-
在
Order
实体中,定义对Customer
的非持久引用,并将 "related" 指定为customerId
:@Transient @MetaProperty(related = "customerId") private Customer customer;
-
在适当的视图中包含非持久化的
customer
属性。
之后,当使用包含
customer
属性的视图加载Order
时,DataManager
会自动从附加数据存储加载关联的Customer
。集合的加载针对性能进行了优化:在加载 Order 列表之后,从附加数据存储加载引用的 Customer 是分批完成的。每批加载的记录数由 cuba.crossDataStoreReferenceLoadingBatchSize 应用程序属性定义。当提交引用了
Customer
的Order
实体图时,DataManager
会通过相应的DataStore
进行数据保存,然后将 Customer 的 ID 保存在 Order 的customerId
属性中。Filter 组件也支持跨数据存储引用。
在 Studio 中,如果使用了跨数据存储的实体引用属性,Studio 会自动维护这种引用关系。
-
3.4.3. 持久化接口
Persistence
接口是ORM层数据存储功能的入口。
该接口有以下方法:
-
createTransaction()
、getTransaction()
– 获取管理事务的接口。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储名称,则使用主数据存储。 -
callInTransaction()
、runInTransaction()
- 在新的事务中执行指定操作,操作可以有返回值,也可以没有。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储,则使用主数据存储。 -
isInTransaction()
– 检查当前是否有活动的事务。 -
getEntityManager()
– 返回绑定到当前事务的EntityManager实例。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储,则使用主数据存储。 -
isSoftDeletion()
– 检查是否启用了软删除模式。 -
setSoftDeletion()
– 启用或禁用软删除模式。设置此属性会影响所有新创建的EntityManager
实例。默认启用软删除。 -
getDbTypeConverter()
– 返回主数据存储或其它数据存储的DbTypeConverter实例。 -
getDataSource()
– 返回主数据存储或附加数据存储的javax.sql.DataSource
实例。对于通过
getDataSource().getConnection()
方法获得的所有javax.sql.Connection
对象,在使用连接后,应在finally
中调用close()
方法。否则,连接不会被重新放回连接池。随着时间的推移连接池将溢出,应用程序将无法执行数据库查询。 -
getTools()
– 返回PersistenceTools
接口的实例(见下文)。
3.4.3.1. PersistenceTools
Spring bean,包含了与数据存储功能相关的辅助方法。可以通过调用 Persistence.getTools()
方法获得,或者像任何其它 bean 一样,通过注入或 AppBeans
类来获得。
PersistenceTools
bean 有以下方法:
-
getDirtyFields()
– 返回自最后一次从数据库加载实例以来已更改的实体属性的名称集合,对于新实例,返回空集合。 -
isLoaded()
– 检查是否从数据库加载了指定的实例属性。如果在加载实例时指定的视图中不存在该属性,则可能没有加载该属性。此方法仅适用于托管状态的实例.
-
getReferenceId()
– 返回关联实体的 ID 而不需要从数据库加载关联实体的数据。假设在持久化上下文中加载了一个
Order
实体并且需要获得这个Order
关联的Customer
实例的 ID 值。如果调用order.getCustomer().getId()
方法,将执行数据库查询来加载Customer
实例,但此时这个数据库查询是没必要的,因为 Customer ID 的值作为外键也存在于Order
表中。而执行persistence.getTools().getReferenceId(order, "customer")
则不会向数据库发送任何其它查询。
此方法仅适用于托管状态的实例。
在应用程序中,可以通过重写 PersistenceTools
bea 来扩展默认的辅助方法的集合。使用扩展接口的示例如下所示:
MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
3.4.3.2. DbTypeConverter
该接口包含了在数据模型属性值和 JDBC 查询的参数/结果之间的进行转换的方法。通过Persistence.getDbTypeConverter()方法可以获得此接口的实例对象。
DbTypeConverter
接口有以下方法:
-
getJavaObject()
– 将 JDBC 查询的结果转换成为适合分配给实体属性的类型。 -
getSqlObject()
– 将实体属性的值转换为适合分配给 JDBC 查询参数的类型。 -
getSqlType()
– 返回与传递的实体属性类型相应的java.sql.Types
常量
3.4.4. ORM 层
对象关系映射(ORM)是一种将关系型数据库表映射到编程语言对象的技术。CUBA 使用基于 EclipseLink 框架的 ORM 实现。
ORM 提供一些明显的优点:
-
通过操控 Java 对象来操控关系型 DBMS。
-
通过消除枯燥的 SQL 查询语句的编写来简化编程。
-
通过一个命令来加载和保存整个对象图来简化编程。
-
允许将应用程序轻松地移植到不同的 DBMS。
-
允许使用简洁的对象查询语言 – JPQL。
同时,ORM 也存在一些缺点。首先,直接使用 ORM 的开发者需要对它有很深的了解,知道它是如何工作的。此外,由于使用了 ORM,使得 SQL 的直接优化和使用 DBMS 的特性变得困难。
如果对数据库访问出现任何性能问题,首先应检查的是实际执行的 SQL 语句。可以使用 |
3.4.4.1. EntityManager
EntityManager
- 用于处理持久化实体的主要 ORM 接口。
有关 EntityManager 和 DataManager 之间差异的信息,请参阅 DataManager 与 EntityManager。 |
可以通过调用 Persistence 接口的 getEntityManager()
方法获取 EntityManager
的引用。获取到的 EntityManager
实例绑定到当前事务,在一个事务中对 getEntityManager()
方法的所有调用都将返回同一个 EntityManager
实例。在事务结束后便不能再使用此事务的 EntityManager
实例。
EntityManager
的实例包含 持久化上下文 ,持久化上下文存储了从数据库加载的或新创建的一组实体实例。持久化上下文是处于事务中的数据缓存。EntityManager
会自动将持久化上下文中所做的所有更改在事务提交时或者调用 EntityManager.flush()
方法时更新到数据库。
CUBA 应用程序中使用的 EntityManager
接口主要复制标准 javax.persistence.EntityManager 接口。下面是它的主要方法:
-
persist()
– 将实体的新实例添加到持久化上下文中。提交事务时,会使用INSERT
SQL 语句在 DB 中创建相应的记录。 -
merge()
– 通过以下方式将游离实例的状态复制到持久化上下文:从 DB 中加载相同标识符的实例,并将传递的游离实例的状态复制到这个加载的实例,然后返回加载的托管实例。之后,应该使用返回的托管实例。在事务提交时,会使用UPDATE
SQL 语句将该实体的状态存储到 DB 中。 -
remove()
– 从数据库中删除对象,或者,如果启用了软删除模式,则只设置deleteTs
和deletedBy
属性。如果传递的实例处于游离状态,则首先执行
merge()
方法。 -
find()
– 通过标识符加载实体实例。当向数据库发送请求时,系统会将传递的视图作为参数传递给该方法。因此,持久化上下文将包含加载了所有视图属性的对象图。如果没有传递视图,则默认使用
_local
视图。 -
createQuery()
– 创建一个Query
或TypedQuery
对象来执行JPQL 查询。 -
createNativeQuery()
– 创建一个Query
对象来执行SQL 查询。 -
reload()
– 使用提供的视图重新加载实体实例。 -
isSoftDeletion()
– 检查EntityManager
是否处于软删除模式。 -
setSoftDeletion()
– 为EntityManager
设置软删除模式。 -
getConnection()
– 返回当前事务对应的连接。这种连接不需要主动关闭,它会在事务完成时自动关闭。 -
getDelegate()
– 返回 ORM 实现提供的javax.persistence.EntityManager
。
在服务中使用 EntityManager
的示例:
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private Persistence persistence;
@Override
public BigDecimal calculateSales(UUID customerId) {
BigDecimal result;
// start transaction
try (Transaction tx = persistence.createTransaction()) {
// get EntityManager for the current transaction
EntityManager em = persistence.getEntityManager();
// create and execute Query
Query query = em.createQuery(
"select sum(o.amount) from sample_Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
result = (BigDecimal) query.getFirstResult();
// commit transaction
tx.commit();
}
return result != null ? result : BigDecimal.ZERO;
}
}
- 部分实体
-
默认情况下,在 EntityManager 中,视图仅影响引用属性,本地属性会被全部加载。
如果将视图的loadPartialEntities属性设置为 true,则可以强制 EntityManager 加载 部分(partial) 实体(像DataManager所做的一样)。但是,如果加载的实体是缓存的(cached),则忽略此视图属性,仍将加载实体的所有本地属性。
3.4.4.2. 实体状态
- New(新建状态)
-
刚在内存中创建的实例:
Car car = new Car()
。可以将新实例传递给
EntityManager.persist()
以存储到数据库,在这种情况下,会将其状态更改为 Managed。 - Managed(托管状态)
-
从数据库加载的实例,或传递给
EntityManager.persist()
的新实例。这个实例属于EntityManager
实例,即包含在其持久化上下文中。当提交
EntityManager
所属的事务时,托管实例的任何更改都将保存到数据库中。 - Detached(游离状态)
-
从数据库加载并与其持久化上下文分离的实例(事务结束或实体实例通过序列化产生)。
只有通
EntityManager.merge()
方法将此实例变成托管状态时,应用到游离实例的更改才能保存到数据库中。
3.4.4.3. 延迟加载
延迟加载(按需加载,也称懒加载)启用关联实体的延迟加载,即在第一次访问其属性时加载它们。
延迟加载会比贪婪加载生成更多的数据库查询,但是这些查询会在时间维度上推迟。
-
例如,在延迟加载实体 A 的 N 个实例的列表的情况下,每个实例包含到实体 B 的实例的链接,将需要对 DB 进行 N+1 个请求,1 个请求用来加载 N 个 A,N 个请求用来为每个 A 加载 B。
-
在大多数情况下,最小化对数据库的请求数会使响应时间和数据库负担减少。平台使用视图的机制来实现这一目标。使用视图允许 ORM 仅为上述关联表的情况执行一次数据库请求。
延迟加载仅适用于托管状态的实例,即在加载实例的事务中。
3.4.4.4. 执行 JPQL 查询
本章节介绍 Query
接口,该接口用于在 ORM 级别执行 JPQL 查询。可以通过调用 createQuery()
方法从当前的 EntityManager
实例获得其引用。如果查询语句用于加载实体,建议使用结果类型作为参数调用 createQuery()
。这将创建一个 TypedQuery
实例。
Query
的方法主要对应于标准 JPA javax.persistence.Query 接口的方法。我们了解下之间的差异。
-
setView()
、addView()
– 定义用于加载数据的视图。 -
getDelegate()
– 返回由 ORM 实现提供的javax.persistence.Query
实例。
如果为查询指定了视图 ,则默认情况下查询使用 FlushModeType.AUTO
模式,这会影响当前持久化上下文包含已更改实体实例的情况:这些实例将在执行查询之前保存到数据库中。换句话说,ORM 首先同步持久化上下文和数据库中的实体状态,之后才执行查询。这样可以保证查询结果包含所有相关实例,即使它们尚未明确地保存到数据库中。这样做的缺点是会有一个隐式刷新(flush),也就是为所有当前已更改的实体实例执行 SQL 更新语句,这可能会影响性能。
如果在没有视图的情况下执行查询,则默认情况下查询使用 FlushModeType.COMMIT
模式,这意味着查询不会导致刷新(flush),并且查询结果将不会体现出当前持久化上下文数据。
在大多数情况下,忽略当前的持久化上下文是可以接受的,并且是首选行为,因为它不会导致额外的 SQL 更新。但是在使用视图时存在以下问题:如果持久化上下文中存在已更改的实体实例,并且使用视图以 FlushModeType.COMMIT
模式执行查询去加载相同的实例,则更改将丢失。这就是在运行带有视图的查询时默认使用 FlushModeType.AUTO
的原因。
还可以使用 Query
接口的 setFlushMode()
方法显式设置刷新(flush)模式,这样将覆盖上述默认设置。
- 对软删除的实体使用 DELETE FROM 语句
-
如果开启了软删除模式,然后对已经软删除的实体执行
DELETE FROM
的 JPQL 语句的话会抛出异常。这种语句在翻译成 SQL 时,会删除所有没有标记删除的数据。这种不好理解的行为默认是关闭的,可以通过 cuba.enableDeleteStatementInSoftDeleteMode 应用程序属性开启。
- 查询提示
-
使用
Query.setHint()
方法可以为生成的 SQL 语句添加 hints - 提示。提示通常用来设置查询语句应如何使用索引或其它数据库特性。框架定义了下面这些常量,可以用来传递给该方法作为提示名称:-
QueryHints.SQL_HINT
- 提示的值添加在生成的 SQL 语句后面。这里需要提供全部的提示字符串,也包括注释分隔符。 -
QueryHints.MSSQL_RECOMPILE_HINT
- 为 MS SQL Server 数据库添加OPTION(RECOMPILE)
SQL 提示。提示的值会被忽略。
当使用DataManager的时候,查询提示可以使用
LoadContext.setHint()
方法添加。 -
3.4.4.4.1. JPQL 函数
下表描述了 CUBA 框架支持和不支持的 JPQL 函数。
函数 | 支持 | 查询 |
---|---|---|
聚合函数 |
支持 |
|
不支持: 带标量表达式的聚合函数(EclipseLink 特性) |
|
|
ALL、 ANY、 SOME |
支持 |
|
算术函数 (INDEX 、 SIZE 、 ABS 、 SQRT 、 MOD) |
支持 |
|
CASE 表达式 |
支持 |
|
不支持: UPDATE 查询中的 CASE |
|
|
日期 函数(CURRENT_DATE、CURRENT_TIME、CURRENT_TIMESTAMP) |
支持 |
|
EclipseLink 函数 (CAST、 REGEXP、 EXTRACT) |
支持 |
|
不支持: GROUP BY 子句中的 CAST |
|
|
实体类型表达式 |
支持: 实体类型作为参数 |
|
不支持: 直接链接到实体类型 |
|
|
函数调用 |
支持: 比较子句中使用函数结果 |
|
不支持: 直接使用函数返回值 |
|
|
IN |
支持 |
|
IS EMPTY 集合 |
支持 |
|
键/值 KEY/VALUE |
不支持 |
|
字面量 |
支持 |
|
不支持: 时间和日期字符串 |
|
|
MEMBER OF |
支持: 字段或者查询结果 |
|
不支持: 字面量 |
|
|
SELECT 中使用 NEW |
支持 |
|
NULLIF/COALESCE |
支持 |
|
order by 中使用 NULLS FIRST, NULLS LAST |
支持 |
|
字符串函数 (CONCAT、 SUBSTRING 、 TRIM 、 LOWER 、 UPPER 、 LENGTH 、 LOCATE) |
支持 |
|
不支持: 带特定字符的 TRIM |
|
|
子查询 |
支持 |
|
不支持: 子查询语句 FROM 中使用路径表达式而不是实体名称 |
|
|
TREAT |
支持 |
|
不支持: WHERE 从句中使用 TREAT |
|
3.4.4.4.2. 不区分大小写的子串搜索
可以在查询参数的值中使用 (?i)
前缀来简单地指定忽略大小写的子串(Substring)搜索。例如这条查询语句:
select c from sales_Customer c where c.name like :name
如果传递字符串 (?i)%doe%
作为 name
参数的值,则查询将返回 John Doe
,如果数据库中存在此类记录,即使字符的大小写不同。出现这种结果是因为 ORM 将执行条件为 lower(C.NAME) like ?
的 SQL 查询。
请注意,这样的查询不能使用在此字段上的索引,即使数据库中已对该字段建立了索引。
3.4.4.4.3. JPQL 中的宏
JPQL 查询文本可以包含宏,这些宏在执行查询之前执行会被转换为可执行的 JPQL,并且还可以修改查询参数集。
宏解决了以下问题:
-
JPQL 有一个限制,这个限制导致条件中不能依赖给定的当前时间字段,(即像“current_date -1”这样的表达式不起作用),宏为这个限制提供一个解决方法。
-
能够将
Timestamp
类型字段(日期/时间字段)与日期进行比较。
下面是更多细节:
- @between
-
格式为
@between(field_name, moment1, moment2, time_unit)
或@between(field_name, moment1, moment2, time_unit, user_timezone)
,其中-
field_name
是要比较的属性的名称。 -
moment1
、moment2
– 开始时间点、结束时间点,field_name
的值在这两个时间点之间。时间点应该使用一个表达式定义,这个表达式包含了now
变量与整数的加减运算。 -
time_unit
– 定义在时间点表达式中now
中增加或减去的时间间隔的单位和时间点精度。下面是可能用到的值:year
、month
、day
、hour
、minute
、second
。 -
user_timezone
- 一个可选参数,用于定义在查询中要使用的当前用户时区。
宏在 JPQL 中转换为以下表达式:
field_name >= :moment1 and field_name < :moment2
例 1. 查询今天创建的 Customer:
select c from sales_Customer where @between(c.createTs, now, now+1, day)
例 2. 查询过去 10 分钟内创建的 Customer:
select c from sales_Customer where @between(c.createTs, now-10, now, minute)
例 3. 查询过去 5 天内的文件,考虑当前用户时区:
select d from sales_Doc where @between(d.createTs, now-5, now, day, user_timezone)
-
- @today
-
格式为
@today(field_name)
或@today(field_name, user_timezone)
,帮助定义检查属性值是否属于当天条件。从本质上讲,这是@between
宏的一个特例。例:查询今天创建的 Customer:
select d from sales_Doc where @today(d.createTs)
- @dateEquals
-
格式为
@dateEquals(field_name, parameter)
或@dateEquals(field_name, parameter, user_timezone)
,允许定义一个检查field_name
(Timestamp
格式)是否落入parameter
传递的日期范围的条件。例如:
select d from sales_Doc where @dateEquals(d.createTs, :param)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales_Doc where @dateEquals(d.createTs, now-1)
- @dateBefore
-
格式为
@dateBefore(field_name, parameter)
或@dateBefore(field_name, parameter, user_timezone)
,允许定义一个条件检查field_name
值(Timestamp
格式)小于parameter
传递的日期。例如:
select d from sales_Doc where @dateBefore(d.createTs, :param, user_timezone)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales_Doc where @dateBefore(d.createTs, now+1)
- @dateAfter
-
格式为
@dateAfter(field_name, parameter)
或@dateAfter(field_name, parameter, user_timezone)
,允许定义条件,即field_name
值的日期 (Timestamp
格式)大于或等于parameter
传递的日期。例如:
select d from sales_Doc where @dateAfter(d.createTs, :param)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales_Doc where @dateAfter(d.createTs, now-1)
- @enum
-
允许使用完全限定的枚举常量名称而不是其数据库标识符。这可以简化在整个应用程序代码中搜索枚举用例的过程。
例如:
select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name
3.4.4.5. 执行 SQL 查询
ORM 允许执行返回个别字段列表或实体实例的 SQL 查询。为此,通过调用 EntityManager.createNativeQuery()
方法创建 Query
或 TypedQuery
对象。
如果选择了个别列,则结果列表中每行的类型为 Object[]
。例如:
Query query = persistence.getEntityManager().createNativeQuery(
"select ID, NAME from SALES_CUSTOMER where NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
Object[] row = (Object[]) it.next();
UUID id = (UUID) row[0];
String name = (String) row[1];
}
如果选择了单个列或聚合函数,结果列表将直接包含这些值:
Query query = persistence.getEntityManager().createNativeQuery(
"select count(*) from SEC_USER where login = #login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();
如果要生成的实体类型与查询语句一起传递给 EntityManager.createNativeQuery()
,则返回 TypedQuery
对象,并且 ORM 尝试将查询结果映射到相应的实体属性。例如:
TypedQuery<Customer> query = em.createNativeQuery(
"select * from SALES_CUSTOMER where NAME like ?1",
Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();
在使用 SQL 时需要注意,对应于 UUID
类型的实体属性的列将以 UUID
类型或 String
类型返回,具体取决于所使用的 DBMS:
-
HSQLDB –
String
-
PostgreSQL –
UUID
-
Microsoft SQL Server –
String
-
Oracle –
String
-
MySQL –
String
此类型的参数也应该以 UUID
或字符串格式传递,具体取决于 DBMS。要确保代码不依赖于 DBMS 细节,请使用 DbTypeConverter
,它提供了在 Java 对象与 JDBC 参数和结果之间转换数据的方法。
原生查询语句支持位置和命名参数。位置参数在查询语句中以 ? 标记,后而跟从 1 开始的参数序号。命名参数用数字符号(#)标记。请参阅上面的示例。
与当前持久化上下文相关的返回实体的 SQL 查询和修改查询(update
、 delete
)的行为类似于上面描述的JPQL 查询。
3.4.4.6. 实体监听器
实体监听器 目的在于响应中间层上的实体实例的生命周期事件。
监听器是一个实现 com.haulmont.cuba.core.listener
包中的一个或多个接口的类。监听器将根据所实现的接口对相应的事件做出响应。
- BeforeDetachEntityListener
-
onBeforeDetach()
方法在事务提交时对象从 EntityManager 分离之前调用。此监听器可用于将非持久化实体属性发送到客户端层之前填充它们。
- BeforeAttachEntityListener
-
onBeforeAttach()
方法在执行了EntityManager.merge()
操作后,对象附加到持久化上下文之前调用。例如,可以使用此监听器在将持久化实体属性保存到数据库之前填充它们。
- BeforeInsertEntityListener
-
onBeforeInsert()
方法在将记录插入数据库之前调用。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterInsertEntityListener
-
onAfterInsert()
方法在将记录插入数据库之后但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用 QueryRunner 实现对数据库的更改。 - BeforeUpdateEntityListener
-
onBeforeUpdate()
方法在记录更新到数据库中之前调用。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterUpdateEntityListener
-
onAfterUpdate()
方法在将记录插入数据库之后但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用 QueryRunner 实现对数据库的更改。 - BeforeDeleteEntityListener
-
onBeforeDelete()
方法在从数据库中删除记录之前调用(在软删除的情况下是在更新记录之前)。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterDeleteEntityListener
-
onAfterDelete()
方法从数据库中删除记录后(在软删除的情况下是在更新记录之后),但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用QueryRunner
实现对数据库的更改。
实体监听器必须是Spring Bean,因此可以在字段和 setters 方法上使用注入。对于特定类的所有实例,一种类型的监听器只会创建一个实例,因此监听器应该是无状态的。
需要知道,对于 BeforeInsertEntityListener
,框架只会保证传入监听器的根实体为托管状态。该实体内对象关系图中其它对象的引用可能是 游离(detached) 状态。所以如果需要更新这些对象,需要用 EntityManager.merge()
方法,或者使用 EntityManager.find()
来访问其所有属性。示例:
package com.company.sample.listener;
import com.company.sample.core.DiscountCalculator;
import com.company.sample.entity.*;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.*;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.math.BigDecimal;
@Component("sample_OrderEntityListener")
public class OrderEntityListener implements
BeforeInsertEntityListener<Order>,
BeforeUpdateEntityListener<Order>,
BeforeDeleteEntityListener<Order> {
@Inject
private DiscountCalculator discountCalculator; // a managed bean of the middle tier
@Override
public void onBeforeInsert(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
@Override
public void onBeforeUpdate(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
@Override
public void onBeforeDelete(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
private void calculateDiscount(Customer customer, EntityManager entityManager) {
if (customer == null)
return;
// Delegate calculation to a managed bean of the middle tier
BigDecimal discount = discountCalculator.calculateDiscount(customer.getId());
// Merge customer instance because it comes to onBeforeInsert as part of another
// entity's object graph and can be detached
Customer managedCustomer = entityManager.merge(customer);
// Set the discount for the customer. It will be saved on transaction commit.
managedCustomer.setDiscount(discount);
}
}
除了 BeforeAttachEntityListener
之外,所有的监听器都在一个数据库事务中工作。也就是说,如果在监听器中抛出异常,当前事务会被回退,所有数据库的改动也会被丢弃。
如果需要在事务提交成功之后做一些操作,可以使用 Spring 的 TransactionSynchronization
回调函数在事务完成之后执行任务。示例:
package com.company.sales.service;
import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Component("sales_CustomerEntityListener")
public class CustomerEntityListener implements BeforeInsertEntityListener<Customer>, BeforeUpdateEntityListener<Customer> {
@Override
public void onBeforeInsert(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
@Override
public void onBeforeUpdate(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
private void printCustomer(Customer customer) {
System.out.println("In transaction: " + customer);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
System.out.println("After transaction commit: " + customer);
}
});
}
}
- 注册实体监听器
-
可以通过两种方式为实体指定实体监听器:
-
静态方式 – 监听器的 bean 名称列在实体类的 @Listeners 注解中。
@Entity(...) @Table(...) @Listeners("sample_MyEntityListener") public class MyEntity extends StandardEntity { ... }
-
动态方式 – 将监听器的 bean 名称传递给
EntityListenerManager
bean 的addListener()
方法。这种方法可以为应用程序组件中的实体添加监听器。在下面的例子中,为框架定义的User
实体添加了一个监听器,监听器由sample_UserEntityListener
bean 实现:package com.company.sample.core; import com.haulmont.cuba.core.global.Events; import com.haulmont.cuba.core.sys.events.AppContextInitializedEvent; import com.haulmont.cuba.core.sys.listener.EntityListenerManager; import com.haulmont.cuba.security.entity.User; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.inject.Inject; @Component("sample_AppLifecycle") public class AppLifecycle { @Inject private EntityListenerManager entityListenerManager; @EventListener(AppContextInitializedEvent.class) // notify after AppContext is initialized @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100) // run after all framework listeners public void initEntityListeners() { entityListenerManager.addListener(User.class, "sample_UserEntityListener"); } }
如果为一个实体声明了几个相同类型的监听器,有来自实体类及其父类的注解,还有动态添加的,则将按以下顺序调用它们:
-
对于每个被继承对象,从最远的父级对象开始,首先调用动态添加的监听器,然后是静态分配的监听器。
-
父类的调用完之后,首先调用给实体类动态添加的监听器,然后调用静态分配的。
-
3.4.5. 事务管理
本节介绍在 CUBA 应用程序中事务管理的各个方面。
3.4.5.1. 编程式事务管理
编程式事务管理使用 com.haulmont.cuba.core.Transaction
接口完成。可以通过Persistence基础接口的 createTransaction()
或 getTransaction()
方法获得对它的引用。
createTransaction()
方法创建一个新事务并返回 Transaction
接口。后续调用此接口的 commit()
、 commitRetaining()
、 end()
方法控制创建的事务。如果在创建时有另一个活动的事务,它将先暂停并在新创建的事务完成后恢复。
getTransaction()
方法要么创建新事务,要么附加到已有事务并返回一个嵌套事务。如果在调用时有一个活动的当前事务,那么该方法会成功完成,但后续调用嵌套事务的 commit()
、 commitRetaining()
、 end()
方法对当前事务没有影响。但是,在没有调用嵌套事务的 commit()
方法的情况下调用 end()
方法,会将当前事务标记为 RollbackOnly
。简单来说,就是只有嵌套事务成功提交了,外层事务才能提交。
编程式事务管理的示例:
@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction()) {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
} finally {
tx.end();
}
Transaction
接口还有 execute()
方法接受 action 类或 lambda 表达式。action 类或 lambda 表达式表示的操作将在事务中执行。这样可以以函数式编程风格组织事务管理,例如:
UUID customerId = persistence.createTransaction().execute((EntityManager em) -> {
Customer customer = metadata.create(Customer.class);
customer.setName("ABC");
em.persist(customer);
return customer.getId();
});
Customer customer = persistence.createTransaction().execute(em ->
em.find(Customer.class, customerId, "_local"));
需要注意,给定 Transaction
实例的 execute()
方法只能调用一次,因为事务在执行完操作代码后结束。
3.4.5.2. 声明式事务管理
中间件Spring bean的任何方法都可以使用 @org.springframework.transaction.annotation.Transactional
进行注解,这将在调用方法时自动创建事务。因此这种方法不需要调用 Persistence.createTransaction()
,可以直接获取 EntityManager
进行使用。
@Transactional
注解支持多个参数,包括:
-
propagation
- 事务创建模式。REQUIRED
值对应于getTransaction()
,REQUIRES_NEW
值对应于createTransaction()
。默认值为REQUIRED
。@Transactional(propagation = Propagation.REQUIRES_NEW) public void doSomething() { }
-
value
- 数据存储名称。如果省略,则使用主数据存储。例如:@Transactional("db1") public void doSomething() { }
声明式事务管理可以减少 脚手架代码 的数量,但它有以下缺点:事务在应用程序代码之外提交,这通常会使调试变得复杂,因为它不会暴露将更改发送到数据库和实体变为Detached的时刻。另外,需要注意,声明性标记仅在容器调用方法时才有效,即从同一对象的另一个方法调用事务方法不会启动事务。
考虑到这一点,我们建议仅对简单的情况使用声明式事务管理,例如服务方法方法读取某个对象并将其返回给客户端。
3.4.5.3. 事务交互示例
- 嵌套事务的回滚
-
如果嵌套事务是通过
getTransaction()
创建并回滚,则无法提交外层事务。例如:void methodA() { Transaction tx = persistence.createTransaction(); try { methodB(); (1) tx.commit(); (4) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.getTransaction(); try { tx.commit(); (2) } catch (Exception e) { return; (3) } finally { tx.end(); } }
1 调用方法创建嵌套事务 2 假设发生异常 3 处理异常并退出 4 在这里将抛出异常,因为事务被标记为仅回滚(rollback only)。 如果使用
createTransaction()
创建methodB()
中的事务,那么回滚它将不会影响methodA()
中的外层事务。 - 在嵌套事务中读取和修改数据
-
首先看一下使用
getTransaction()
创建的依赖嵌套事务:void methodA() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); Employee employee = em.find(Employee.class, id); (1) assertEquals("old name", employee.getName()); employee.setName("name A"); (2) methodB(); (3) tx.commit(); (8) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.getTransaction(); try { EntityManager em = persistence.getEntityManager(); (4) Employee employee = em.find(Employee.class, id); (5) assertEquals("name A", employee.getName()); (6) employee.setName("name B"); tx.commit(); (7) } finally { tx.end(); } }
1 使用 name == "old name" 加载实体 2 给字段设置新值 3 调用方法创建嵌套事务 4 获取与方法 methodA 中相同的 EntityManager 实例 5 使用同样的标识符加载实体 6 字段值是新的,因为我们使用相同的持久化上下文,并且根本没有调用 DB 7 此时不进行实际的提交 8 更改提交到 DB,它将包含 "name B" 现在,看一下使用
createTransaction()
创建的独立嵌套事务的相同示例:void methodA() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); Employee employee = em.find(Employee.class, id); (1) assertEquals("old name", employee.getName()); employee.setName("name A"); (2) methodB(); (3) tx.commit(); (8) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); (4) Employee employee = em.find(Employee.class, id); (5) assertEquals("old name", employee.getName()); (6) employee.setName("name B"); (7) tx.commit(); } finally { tx.end(); } }
1 使用 name == "old name" 加载实体 2 给字段设置新值 3 调用方法创建嵌套事务 4 创建新的 EntityManager 实例, 因为这是一个新事务 5 使用相同的标识符加载一个实体 6 字段值是旧的,因为一个旧的实体实例被从数据库加载了 7 变更被提交到 DB,现在 "name B"值被存储到数据 DB 8 由于启用乐观锁,这里将发生异常,提交失败 在最后一个例子中,只有当实体支持乐观锁时,即只有它实现了
Versioned
接口时,才会发生第(8)点的异常。
3.4.5.4. 事务参数
- 事务超时
-
可以为创建的事务设置超时时限(以秒为单位)。如果发生超时,事务将被中断并回滚。事务超时设置可以有效地限制数据库请求的最大持续时间。
以编程方式管理事务时,通过传递给
Persistence.createTransaction()
方法的参数TransactionParams
对象来指定超时。例如:Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));
在声明式事务管理的情况下,使用
@Transactional
注解的timeout
参数:@Transactional(timeout = 2) public void someServiceMethod() { ...
可以使用cuba.defaultQueryTimeoutSec应用程序属性定义默认超时时限。
- 只读事务
-
如果事务仅用于从数据库读取数据,则可以将事务标记为只读。例如,DataManager的所有
load
方法默认使用只读事务。只读事务会有更好的性能,因为平台不执行处理可能修改实体的代码。BeforeCommit
事务监听器也不会被调用。如果只读事务的持久化上下文包含已修改的实体,则在提交事务时将抛出
IllegalStateException
。这意味着只有在确定不修改任何实体时,才应将事务标记为只读。以编程方式管理事务时,通过将
TransactionParams
对象传递给Persistence.createTransaction()
方法来指定只读标识。例如:Transaction tx = persistence.createTransaction(new TransactionParams().setReadOnly(true));
在声明式事务管理的情况下,使用
@Transactional
注解的readOnly
参数:@Transactional(readOnly = true) public void someServiceMethod() { ...
3.4.5.5. 事务监听器
事务监听器旨在对事务生命周期事件做出响应。与实体监听器不同,它们不与何实体类型绑定,可以被每个事务调用。
监听器是一个Spring bean,它实现了 BeforeCommitTransactionListener
或 AfterCompleteTransactionListener
接口或者同时实现这两个接口。
- BeforeCommitTransactionListener
-
如果事务不是只读的,则在所有实体监听器之后,事务提交之前调用
beforeCommit()
方法。该方法接受当前持久化上下文中的实体集合和当前的EntityManager作为参数。监听器可用于执行涉及多个实体的复杂业务规则。在下面的例子中,
Order
实体的amount
属性必须根据订单中的discount
值计算,OrderLine
实体的price
和quantity
构成订单。@Component("demo_OrdersTransactionListener") public class OrdersTransactionListener implements BeforeCommitTransactionListener { @Inject private PersistenceTools persistenceTools; @Override public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) { // gather all orders affected by changes in the current transaction Set<Order> affectedOrders = new HashSet<>(); for (Entity entity : managedEntities) { // skip not modified entities if (!persistenceTools.isDirty(entity)) continue; if (entity instanceof Order) affectedOrders.add((Order) entity); else if (entity instanceof OrderLine) { Order order = ((OrderLine) entity).getOrder(); // a reference can be detached, so merge it into current persistence context affectedOrders.add(entityManager.merge(order)); } } // calculate amount for each affected order by its lines and discount for (Order order : affectedOrders) { BigDecimal amount = BigDecimal.ZERO; for (OrderLine orderLine : order.getOrderLines()) { if (!orderLine.isDeleted()) { amount = amount.add(orderLine.getPrice().multiply(orderLine.getQuantity())); } } BigDecimal discount = order.getDiscount().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_DOWN); order.setAmount(amount.subtract(amount.multiply(discount))); } } }
- AfterCompleteTransactionListener
-
事务完成后调用
afterComplete()
方法。该方法接受一个参数,该参数表明事务是否已成功提交,以及已完成事务的持久化上下文中包含的已分离实体的集合。用法示例:
@Component("demo_OrdersTransactionListener") public class OrdersTransactionListener implements AfterCompleteTransactionListener { private Logger log = LoggerFactory.getLogger(OrdersTransactionListener.class); @Override public void afterComplete(boolean committed, Collection<Entity> detachedEntities) { if (!committed) return; for (Entity entity : detachedEntities) { if (entity instanceof Order) { log.info("Order: " + entity); } } } }
3.4.6. 实体以及查询语句缓存
- 实体缓存
-
实体缓存由 EclipseLink ORM 框架提供。它将内存中最近读取或写入的实体实例存储,从而最大限度地减少数据库访问并提高应用程序性能
实体缓存仅在根据 ID 检索实体时使用,因此根据其它属性的查询仍在数据库上执行。但是,如果相关实体位于缓存中,则这些查询可以更简单、更快速。例如,如果查询与客户相关的订单并且不使用缓存,则 SQL 查询将包含客户表的 JOIN 关联。如果客户实体被缓存,则 SQL 查询将仅选择订单,并且将从缓存中检索相关客户。
要启用实体缓存,请在 core 模块的 app.properties 文件中设置以下属性:
-
eclipselink.cache.shared.sales_Customer = true
- 启用sales_Customer
实体的缓存。 -
eclipselink.cache.size.sales_Customer = 500
- 将sales_Customer
的缓存大小设置为 500 个实例。默认大小为 100。如果启用了实体缓存,则始终建议增加缓存大小的值。否则,如果查询返回的记录数超过 100,则将对查询结果的每条记录执行大量的获取操作。
实体是否被缓存会影响平台选择的用于加载实体关系图的获取模式。如果引用属性是可缓存的实体,则获取模式始终为
UNDEFINED
,这允许 ORM 从缓存中检索引用,而不是使用 JOIN 执行查询或单独的批量查询。平台在中间件集群中提供实体缓存协调机制。在一个群集节点上更新或删除缓存实体时,其它节点(如果有)上的相同缓存实例将失效,因此使用此实例的下一个操作将从数据库中读取新状态。
-
- 查询缓存
-
查询缓存存储由 JPQL 查询返回的实体实例的标识符,因此它很自然地补充了实体缓存机制。
例如,如果为实体启用了实体缓存(例如,
sales_Customer
),并且首次执行查询语句select c from sales_Customer c where c.grade = :grade
,则会发生以下情况:-
ORM 在数据库上运行查询。
-
已加载的
Customer
实例放置在实体缓存中。 -
查询文本和返回实例的标识符列表参数的映射被放到查询缓存中。
当第二次使用相同的参数执行相同的查询时,平台会在查询缓存中查找查询结果,并通过标识符从实体缓存中加载实体实例。不需要数据库操作。
默认情况下不缓存查询。可以指定应用程序的不同层缓存查询:
-
使用 EntityManager 时,用 Query 接口的
setCacheable()
方法。 -
使用DataManager时,用
LoadContext.Query
接口的setCacheable()
方法。 -
使用数据加载器时,用
CollectionLoader
接口的setCacheable()
方法或cacheable
XML 属性
仅在为返回的实体启用实体缓存时才使用可缓存查询。否则,每个查询实体实例将通过其标识符逐个从数据库中获取。
app-core.cuba:type=QueryCacheSupport
JMX-bean 可用于监视缓存状态并手动释放缓存的查询。例如,如果已直接在数据库中修改了sales_Customer
实体的实例,则应使用带有sales_Customer
参数的evict()
操作释放该实体的所有缓存的查询。以下应用程序属性会影响查询缓存:
-
3.4.7. EntityChangedEvent
参考 使用应用程序事件解耦业务逻辑 指南,学习如何使用 |
EntityChangedEvent
是一个 Spring 的 ApplicationEvent
事件,会在实体保存到数据库时从中间层发送该事件。可以在事务中或者事务完成后处理该事件(使用 @TransactionalEventListener )。
只会为使用了 |
EntityChangedEvent
不包含更改的实体本身,而只包含了其 id。还有,getOldValue(attributeName)
方法也只会返回引用的 id 而不是对象。所以如果需要的话,开发者可以使用合适的视图或者其它参数重新加载实体。
下面的例子展示了在当前事务中和事务后处理 Customer
实体的 EntityChangedEvent
事件。
package com.company.demo.core;
import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.AttributeChanges;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;
@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {
@TransactionalEventListener(
phase = TransactionPhase.BEFORE_COMMIT (1)
)
public void beforeCommit(EntityChangedEvent<Customer, UUID> event) {
Id<Customer, UUID> entityId = event.getEntityId(); (2)
EntityChangedEvent.Type changeType = event.getType(); (3)
AttributeChanges changes = event.getChanges();
if (changes.isChanged("name")) { (4)
String oldName = changes.getOldValue("name"); (5)
// ...
}
}
@TransactionalEventListener(
phase = TransactionPhase.AFTER_COMMIT (6)
)
public void afterCommit(EntityChangedEvent<Customer, UUID> event) {
(7)
}
}
1 | - 该监听器会在当前事务中调用。 |
2 | - 更改实体的 id。 |
3 | - 更改类型: CREATED 、UPDATED 或 DELETED 。 |
4 | - 可以检查是否某个特定属性有变化。 |
5 | - 可以获取变化属性的旧值。 |
6 | - 该监听器在事务提交之后会被调用。 |
7 | - 在事务提交之后,事件包含跟提交之前相同的信息。 |
如果监听器在事务内部调用,可以通过抛出异常的方法回滚事务,这样不会有数据保存至数据库。如果不想用户看到任何错误提示,可以用 SilentException
。
如果一个 "after commit" 监听器抛出了异常,该异常会被日志记录,而不会呈现给客户端(用户不会在 UI 看到该错误)。
如果在当前事务( 在 "after commit" 监听器( |
下面是使用 EntityChangedEvent
更新关联实体的示例。
假设根据 Sales Application ,我们有 Order
、OrderLine
和 Product
实体。但是 Product
还有额外的 special
布尔类型属性,并且 Order
有 numberOfSpecialProducts
整型属性,该属性需要根据每次从 Order
中添加或者删除 OrderLine
时重新计算。
创建下面带有 @EventListener
方法的类,此方法会在事务提交之前, OrderLine
实体发生改变时调用。
package com.company.sales.listener;
import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.TransactionalDataManager;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.util.UUID;
@Component("sales_OrderLineChangedListener")
public class OrderLineChangedListener {
@Inject
private TransactionalDataManager txDm;
@TransactionalEventListener(
phase = TransactionPhase.BEFORE_COMMIT
)
public void beforeCommit(EntityChangedEvent<OrderLine, UUID> event) {
Order order;
if (event.getType() != EntityChangedEvent.Type.DELETED) { (1)
order = txDm.load(event.getEntityId()) (2)
.view("orderLine-with-order") (3)
.one()
.getOrder(); (4)
} else {
Id<Order, UUID> orderId = event.getChanges().getOldReferenceId("order"); (5)
order = txDm.load(orderId).one();
}
long count = txDm.load(OrderLine.class) (6)
.query("select o from sales_OrderLine o where o.order = :order")
.parameter("order", order)
.view("orderLine-with-product")
.list().stream()
.filter(orderLine -> Boolean.TRUE.equals(orderLine.getProduct().getSpecial()))
.count();
order.setNumberOfSpecialProducts((int) count);
txDm.save(order); (7)
}
}
1 | - 如果没有删除 OrderLine ,我们可以使用 id 从数据库加载。 |
2 | - event.getEntityId() 方法返回更改的 OrderLine id。 |
3 | - 使用包含 OrderLine 并带有其关联 Order 的视图。试图必须包含 Order.numberOfSpecialProducts 属性,因为我们之后会更新这个值。 |
4 | - 从加载的 OrderLine 中获取 Order 。 |
5 | - 如果 OrderLine 已经被删除了,则不能从数据库加载,但是 event.getChanges() 方法会返回实体的所有属性,也包含了关联实体的 id。所以我们可以用 id 从中获取关联的 Order 。 |
6 | - 为给定的 Order 加载所有的 OrderLine 实例,使用 Product.special 进行过滤并对它们进行计数。视图必须包含 OrderLine 以及关联的 Product 。 |
7 | - 改变了属性之后再保存 Order 。 |
3.4.8. EntityPersistingEvent
EntityPersistingEvent
是 Spring 的 ApplicationEvent
,是框架在中间层 新实体 保存到数据库之前触发的事件。在触发事件的时候,存在一个活动的数据库事务。
EntityPersistingEvent
可以用来在写入数据库之前初始化实体属性:
package com.company.demo.core;
import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.EntityPersistingEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {
@EventListener
void beforePersist(EntityPersistingEvent<Customer> event) {
Customer customer = event.getEntity();
customer.setCode(obtainNewCustomerCode(customer));
}
// ...
}
3.4.9. 系统身份验证
执行用户请求时,中间件程序代码始终可以通过UserSessionSource接口访问当前用户的信息。这是可能的,因为当从客户端层收到请求时,会自动为当前线程设置相应的SecurityContext对象。
但是,在某些情况下当前线程与任何系统用户都没有关联,例如,从定时任务或通过 JMX 接口调用 bean 的方法时。如果 bean 修改数据库中的实体,则需要实施更改的用户的信息,即身份验证。
这种身份验证称为“系统身份验证”,因为它不需要用户参与 - 应用程序中间层只是创建或使用现有的用户会话,为当前线程设置相应的 SecurityContext
对象。
可以使用以下方法为代码块提供系统身份验证:
-
使用
com.haulmont.cuba.security.app.Authentication
bean:@Inject protected Authentication authentication; ... authentication.begin(); try { // authenticated code } finally { authentication.end(); }
-
在 bean 的方法上添加
@Authenticated
注解:@Authenticated public String foo(String value) { // authenticated code }
第二种情况通过 AuthenticationInterceptor
对象隐式使用 Authentication
bean,该对象拦截所有 @Authenticated
注解 bean 方法。
在上面的示例中,将代表一个用户创建用户会话,该用户的登录名在cuba.jmxUserLogin应用程序属性中指定。如果需要代表另一个用户进行身份验证,请将所需用户的登录名传递给第一种变体的 begin()
方法。
如果当前线程在执行 例如,如果 bean 与用户当前连接的 Web 客户端 block 位于同一 JVM 中,则执行内置JMX console的 Web 客户端对 JMX bean 方法的调用将使用当前登录的用户的信息,而忽略系统身份验证。 |
3.5. 通用用户界面(GUI)
通用用户界面 (Generic UI, GUI) 框架可以使用 Java 和 XML 来创建 UI 界面。XML 是方式是可选的,但是使用这个方式可以声明式的创建界面布局并且减少构建用户界面的代码量。
应用程序的界面包含了以下部分:
应用程序界面的代码跟可视化组件接口(VCL 接口)交互。这些接口通过使用 Vaadin 框架组件实现。
可视化组件库(VCL)包含大量即用型组件。
数据组件为可视化组件绑定到实体以及在界面控制器中处理实体提供统一的接口。
客户端的基础设施包含包含主应用程序窗口和其它的通用客户端机制。
3.5.1. 界面和界面片段(Fragments)
界面(Screen)是通用 UI 的主要部分。它由可视化组件、数据容器和非可视化组件组成。界面可以显示在应用程序主窗口的标签页中,也可以显示为模式对话框。
要显示一个界面,框架会创建一个可视化组件 Window
的新实例,将窗口与界面控制器连接起来,并将界面布局组件作为窗口的子组件加载。最终,界面的窗口将被添加到应用程序主窗口中。
界面片段(fragment)是另一种 UI 构成组件,可以用作界面的一部分或者使用在别的界面片段中。界面片段跟界面本质上非常相似,只不过界面片段有特殊的生命周期;另外在组件树中,片段会作为 Fragment
可视化组件而非 Window
。界面片段也有控制器和 XML 描述。
3.5.1.1. 界面控制器
界面控制器是一个 Java 或 Groovy 类,包含界面初始化和事件处理逻辑。通常,控制器链接到XML 描述,XML 描述中定义了界面布局和数据容器,但也可以以编程方式创建所有可视化组件和非可视化组件。
所有界面控制器都实现了 FrameOwner
标记接口。此接口的名称表示它引用了一个框架(frame),框架是一个在主应用程序窗口中显示界面的可视化组件。框架有两种类型:
-
Window
- 一个独立的窗口,可以显示在应用程序主窗口内的标签页中,也可以显示为模式对话框。 -
Fragment
- 一个轻量级组件,可以被添加到窗口或其它 Fragment。
根据所使用的框架,控制器被分为两个不同的类别:
-
Screen
- 窗口控制器的基类。 -
ScreenFragment
- fragment 控制器的基类。
Screen
类为所有独立界面提供大部分基本的功能。还有其它一些特定的界面基类可用于处理实体:
-
StandardEditor
- 实体编辑界面的基类。 -
StandardLookup
- 实体浏览和查找界面的基类。 -
MasterDetailScreen
- 组合界面,在左侧显示实体列表、在右侧显示所选实体的详细信息。
3.5.1.1.1. 界面控制器注解
控制器上的类级别的注解用于向框架提供界面相关的信息。一部分注解适用于任何类型的界面,也有一部分仅用于实体编辑或查找界面。
以下示例演示了常见的界面注解的用法:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
@UiController("demo_FooScreen")
@UiDescriptor("foo-screen.xml")
@LoadDataBeforeShow
@MultipleOpen
@DialogMode(forceDialog = true)
public class FooScreen extends Screen {
}
-
@UiController
注解表示该类是一个界面控制器。注解的值是界面的 ID,可用于从主菜单或以编程方式打开界面时引用界面。
-
@UiDescriptor
注解将界面控制器连接到界面 XML 描述。注解的值指定描述文件的路径。如果该值仅包含文件名,则假定该文件与控制器类位于同一个包中。
-
@LoadDataBeforeShow
注解表示在显示界面之前应自动触发所有数据加载器。准确的说,数据会在调用所有的 BeforeShowEvent 监听器之后,但是在 AfterShowEvent 监听器之前加载。如果需要在界面显示之前的加载数据时刻执行某些操作,删除此注解或将其值设置为false
,这样可以在BeforeShowEvent
事件监听器中使用getScreenData().loadAll()
方法或个别加载器的load()
方法来手动加载数据。可以考虑使用DataLoadCoordinator facet 来做细致的数据加载控制。
-
@MultipleOpen
注解表示可以从主菜单多次打开界面。默认情况下,当用户点击主菜单项时,框架会检查是否已在主窗口标签页上打开相同类和 ID 的界面。如果找到此类界面,则会关闭该界面,并在新标签页中打开新的界面实例。当存在@MultipleOpen
注解时,不执行任何检查,只在新标签页中打开一个新的界面实例。可以通过覆盖界面控制器中的
isSameScreen()
方法,提供自定义的界面实例同一性检查方法。
-
@DialogMode
注解允许指定界面以对话框窗口方式打开时的尺寸和行为。它对应于界面描述的<dialogMode>
元素,可以替代使用。对于除forceDialog
之外的所有参数,XML 中的设置优先于注解。forceDialog
参数会合并生效,即:只要在注解或 XML 其中之一将其设置为 true,界面就总是以对话框的方式打开。
针对查找界面的注解示例:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LoadDataBeforeShow
// lookup-specific annotations
@LookupComponent("customersTable")
@PrimaryLookupScreen(Customer.class)
public class CustomerBrowse extends StandardLookup<Customer> {
}
-
@LookupComponent
注解指定一个 UI 组件的 ID,这个组件用于获取查找界面的返回值。可以通过覆盖界面控制器的
getLookupComponent()
方法以编程方式指定查找界面,而不是使用注解。
-
@PrimaryLookupScreen
注解表示此界面是指定实体类型的默认查找界面。注解比{entity_name}.lookup / {entity_name}.browse
名称约定具有更高的优先级。
编辑器界面特有的注解示例:
package com.company.demo.web.data.sort;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
@LoadDataBeforeShow
// editor-specific annotations
@EditedEntityContainer("customerDc")
@PrimaryEditorScreen(Customer.class)
public class CustomerEdit extends StandardEditor<Customer> {
}
-
@EditedEntityContainer
注解指定一个数据容器,这个数据容器包含被编辑的实体。不使用注解的话,可以通过覆盖界面控制器的
getEditedEntityContainer()
方法以编程方式指定容器。
-
@PrimaryEditorScreen
注解表示此界面是指定实体类型的默认编辑界面。 注解比{entity_name}.edit
名称约定具有更高的优先级。
3.5.1.1.2. 界面控制器方法
本章节我们介绍一些界面控制器基类的方法,可以直接在代码中调用或者重写。
- 所有界面都可用的方法
-
-
show()
- 展示界面。该方法通常在创建界面之后调用,参阅 打开界面。 -
close()
- 关闭界面,使用StandardOutcome
枚举参数或者CloseAction
对象。示例:@Subscribe("closeBtn") public void onCloseBtnClick(Button.ClickEvent event) { close(StandardOutcome.CLOSE); }
参数值也传递至 BeforeCloseEvent 和 AfterCloseEvent 事件中,因此可以在事件监听器中也能获取界面关闭的原因。参阅 界面关闭后执行代码以及返回值 了解更多使用这些监听器的方法。
-
getScreenData()
- 返回ScreenData
对象,该对象作为所有在界面 XML 描述中定义的 数据组件 的寄存器,因此可以使用loadAll()
方法为界面加载数据:@Subscribe public void onBeforeShow(BeforeShowEvent event) { getScreenData().loadAll(); }
-
getSettings()
- 返回Settings
对象,可以读写当前用户对界面的自定义设置。 -
saveSettings()
- 保存Settings
对象中的设置。如果 cuba.gui.manualScreenSettingsSaving 设置为 false(默认值),则会自动调用该方法。
-
- StandardEditor 的方法
-
-
getEditedEntity()
- 当界面展示时,返回编辑实体的实例。实例是 @EditedEntityContainer 注解内指定的数据容器中的实例。在 InitEvent 和 AfterInitEvent 事件监听器内,该方法会返回 null。在 BeforeShowEvent 事件监听器内,该方法会返回传递给界面用来编辑的实体,之后在界面打开的过程中,实体会重新加载,一个不同的实例会设置给数据容器。
可以用下面的方法关闭编辑界面:
-
closeWithCommit()
- 验证并保存数据,然后用StandardOutcome.COMMIT
关闭界面。 -
closeWithDiscard()
- 忽略任何未保存的数据并用StandardOutcome.DISCARD
关闭界面。
如果界面的 DataContext 有未保存的改动,则会在界面关闭前弹出对应的消息对话框。可以用 cuba.gui.useSaveConfirmation 应用程序属性调整通知类型。如果用
closeWithDiscard()
或close(StandardOutcome.DISCARD)
方法,则会忽略未保存的改动而且没有通知。-
commitChanges()
- 保存数据,但不关闭界面。可以从自定义事件监听器调用该方法或者重写默认的windowCommit
操作监听器,这样能在数据保存后做一些其他的事情,示例:@Override protected void commit(Action.ActionPerformedEvent event) { commitChanges().then(() -> { // this flag is used for returning correct outcome on subsequent screen closing commitActionPerformed = true; // perform actions after the data has been saved }); }
commit()
方法的默认实现展示成功提交的通知消息。可以在界面初始化过程中用setShowSaveNotification(false)
方法关闭。 -
重写
validateAdditionalRules()
方法为保存数据前添加额外的数据验证。方法应该在传入的ValidationErrors
对象中保存验证错误的信息。之后,此信息会与标准验证的错误信息一起展示。示例:
private Pattern pattern = Pattern.compile("\\d"); @Override protected void validateAdditionalRules(ValidationErrors errors) { if (getEditedEntity().getAddress().getCity() != null) { if (pattern.matcher(getEditedEntity().getAddress().getCity()).find()) { errors.add("City name cannot contain digits"); } } super.validateAdditionalRules(errors); }
-
- MasterDetailScreen 的方法
-
-
getEditedEntity()
- 当界面在编辑模式时,返回正在编辑的实体实例。即设置在form
组件数据容器中的实例。如果界面不在编辑模式,此方法会抛出IllegalStateException
。 -
重写
validateAdditionalRules()
方法可以添加额外的数据验证,与上面StandardEditor
介绍的一样。
-
3.5.1.1.3. 界面事件
本节介绍可以在控制器中处理的界面生命周期事件。
参考 使用应用程序事件解耦业务逻辑 指南,了解如何在 UI 层使用事件。 |
- InitEvent
-
InitEvent
在界面控制器及其所有以声明方式定义的组件创建后并完成依赖注入时发送的事件。此时,嵌套的界面 fragment 尚未初始化,某些可视化组件未完全初始化,例如按钮还未与操作关联起来。@Subscribe protected void onInit(InitEvent event) { Label<String> label = uiComponents.create(Label.TYPE_STRING); label.setValue("Hello World"); getWindow().add(label); }
- AfterInitEvent
-
AfterInitEvent
在界面控制器及其所有以声明方式定义的组件被创建并完成依赖注入,并且所有组件都已完成其内部初始化过程时发送此事件。此时,嵌套的界面 fragment(如果有的话)已经发送了InitEvent
和AfterInitEvent
事件。在此事件监听器中,可以创建可视化组件或数据组件并且进行一些依赖于嵌套 fragment 的额外初始化过程。
- InitEntityEvent
-
InitEntityEvent
继承自StandardEditor
和MasterDetailScreen
的界面中,在新实体实例设置给被编辑实体的容器之前发送的事件。使用此事件监听器初始化新实体实例中的默认值,例如:也可参考 初始化实体值 指南,了解如何使用
InitEntityEvent
监听器。用该监听器可以为新实体实例设置初始化值,示例:
@Subscribe protected void onInitEntity(InitEntityEvent<Foo> event) { event.getEntity().setStatus(Status.ACTIVE); }
- BeforeShowEvent
-
BeforeShowEvent
在界面将要展示之前发送的事件,此时,界面尚未被添加到应用程序 UI 中、UI 组件已经应用安全限制、保存的组件设置尚未应用到 UI 组件。对于使用@LoadDataBeforeShow
注解的界面,尚未加载数据。在此事件监听器中,可以加载数据、检查权限和修改 UI 组件。例如:@Subscribe protected void onBeforeShow(BeforeShowEvent event) { customersDl.load(); }
- AfterShowEvent
-
AfterShowEvent
在显示界面之后立即发送此事件,此时,界面已经被添加到应用程序 UI 中。保存的组件设置已应用到 UI 组件。在此事件监听器中,可以显示通知、对话框或其它界面。例如:+
@Subscribe protected void onAfterShow(AfterShowEvent event) { notifications.create().withCaption("Just opened").show(); }
- BeforeCommitChangesEvent
-
BeforeCommitChangesEvent
在继承自StandardEditor
和MasterDetailScreen
的界面中发送,在通过commitChanges()
方法保存数据改动之前发送。在此事件监听器中,可以做一些检查、与用户交互然后通过事件对象的preventCommit()
和resume()
方法退出操作或者继续操作。我们看几个用例:
-
退出保存操作并弹出通知消息:
@Subscribe public void onBeforeCommitChanges(BeforeCommitChangesEvent event) { if (getEditedEntity().getStatus() == null) { notifications.create().withCaption("Enter status!").show(); event.preventCommit(); } }
-
退出保存,显示一个对话框,如果用户选择继续,则继续保存操作:
@Subscribe public void onBeforeCommitChanges(BeforeCommitChangesEvent event) { if (getEditedEntity().getStatus() == null) { dialogs.createOptionDialog() .withCaption("Confirmation") .withMessage("Status is empty. Do you really want to commit?") .withActions( new DialogAction(DialogAction.Type.OK).withHandler(e -> { // resume with default behavior event.resume(); }), new DialogAction(DialogAction.Type.CANCEL) ) .show(); // abort event.preventCommit(); } }
-
退出保存,显示对话框,用户确认之后重试
commitChanges()
:@Subscribe public void onBeforeCommitChanges(BeforeCommitChangesEvent event) { if (getEditedEntity().getStatus() == null) { dialogs.createOptionDialog() .withCaption("Confirmation") .withMessage("Status is empty. Do you want to use default?") .withActions( new DialogAction(DialogAction.Type.OK).withHandler(e -> { getEditedEntity().setStatus(getDefaultStatus()); // retry commit and resume action event.resume(commitChanges()); }), new DialogAction(DialogAction.Type.CANCEL) ) .show(); // abort event.preventCommit(); } }
-
- AfterCommitChangesEvent
-
AfterCommitChangesEvent
在继承自StandardEditor
和MasterDetailScreen
的界面中发送,在通过commitChanges()
方法保存数据改动之后发送。示例:@Subscribe public void onAfterCommitChanges(AfterCommitChangesEvent event) { notifications.create() .withCaption("Saved!") .show(); }
- BeforeCloseEvent
-
BeforeCloseEvent
在界面通过其close(CloseAction)
方法关闭之前发送的事件。此刻界面仍然正常显示并且功能完整。组件设置尚未保存。在此事件监听器中,可以做任何条件检查并使用事件的preventWindowClose()
方法阻止界面关闭,例如:@Subscribe protected void onBeforeClose(BeforeCloseEvent event) { if (Strings.isNullOrEmpty(textField.getValue())) { notifications.create().withCaption("Input required").show(); event.preventWindowClose(); } }
还有一个同名的事件,它定义在
Window
接口内。此事件会在界面被外部(界面控制器之外)操作关闭之前发送,比如点击窗口标签页的关闭按钮或者按下 Esc 键。可以使用getCloseOrigin()
方法获取窗口关闭的类型,此方法返回一个实现了CloseOrigin
接口的对象。CloseOrigin
接口的默认实现是CloseOriginType
,有三个值:-
BREADCRUMBS
- 界面通过点击面包屑链接关闭。 -
CLOSE_BUTTON
- 界面通过窗口顶部的关闭按钮关闭,或者通过窗口标签页的关闭按钮或右键菜单的操作:关闭、关闭全部、关闭其它。 -
SHORTCUT
- 界面通过 cuba.gui.closeShortcut 应用程序属性定义的快捷键关闭。
可以通过在
@Subscribe
注解中指定Target.FRAME
的方法来订阅Window.BeforeCloseEvent
:@Subscribe(target = Target.FRAME) protected void onBeforeClose(Window.BeforeCloseEvent event) { if (event.getCloseOrigin() == CloseOriginType.BREADCRUMBS) { event.preventWindowClose(); } }
-
- AfterCloseEvent
-
AfterCloseEvent
在界面通过close(CloseAction)
方法关闭并且在Screen.AfterDetachEvent
之后发送的事件。组件设置已保存。在此事件监听器中,可以在关闭界面后显示通知或对话框,例如:@Subscribe protected void onAfterClose(AfterCloseEvent event) { notifications.create().withCaption("Just closed").show(); }
- AfterDetachEvent
-
AfterDetachEvent
在用户关闭界面或用户注销时从应用程序 UI 中移除界面后发送的事件。此事件监听器可用于释放界面持有的资源。请注意,在 HTTP 会话过期时不会发送此事件。
- UrlParamsChangedEvent
-
-
UrlParamsChangedEvent
打开的界面对应的浏览器 URL 参数更改时发送的事件。事件在显示界面之前触发,以便能够进行一些准备工作。在此事件监听器中,可以根据新的参数加载一些数据或更改界面控件的状态:
@Subscribe protected void onUrlParamsChanged(UrlParamsChangedEvent event) { Map<String, String> params = event.getParams(); // handle new params }
-
3.5.1.1.4. 界面片段事件
本节介绍界面片段控制器能处理的生命周期事件。
-
InitEvent
会在片段控制器和其所有以声明方式定义的组件创建之后,并且依赖注入完成才触发。此时,嵌套的片段还没有初始化。有些可视化组件没有完全初始化,比如按钮还没有连接到操作。如果该片段是通过 XML 的方式绑定到宿主界面的话,该事件会在宿主控制器的InitEvent事件之后触发。否则会在该片段被添加到宿主组件树的时候触发。
-
AfterInitEvent
会在片段控制器和其所有以声明方式定义的组件创建之后,并且依赖注入完成,左右组件内部的初始化过程也已经结束之后触发。此时,嵌套的界面片段(如果有的话)已经触发了它们自己的InitEvent
和AfterInitEvent
事件。在该事件的监听器中,可以创建可视化和数据组件,并能执行依赖嵌套组件初始化完成的额外初始化过程。
-
AttachEvent
会在片段被添加到宿主的组件树时触发,片段已经完全初始化了,InitEvent
和AfterInitEvent
事件已经触发。在该事件的监听器中,可以通过getHostScreen()
和getHostController()
访问宿主界面的界面和方法。
-
DetachEvent
会在片段以编程的方式从宿主的组件树中移除时触发。在该事件监听器中也能访问宿主界面。
监听界面片段事件的示例:
@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {
private static final Logger log = LoggerFactory.getLogger(AddressFragment.class);
@Subscribe
private void onAttach(AttachEvent event) {
Screen hostScreen = getHostScreen();
FrameOwner hostController = getHostController();
log.info("onAttach to screen {} with controller {}", hostScreen, hostController);
}
@Subscribe
private void onDetach(DetachEvent event) {
log.info("onDetach");
}
}
- 订阅父界面事件
-
在 fragment 控制器中,可以订阅父界面的事件,需要在注解中为
target
属性指定PARENT_CONTROLLER
值,示例:@Subscribe(target = Target.PARENT_CONTROLLER) private void onBeforeShowHost(Screen.BeforeShowEvent event) { // }
这个方法可以处理任何事件,包括实体编辑界面发送的 InitEntityEvent 事件。
3.5.1.2. 界面 XML 描述
示例:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="Sample Screen"
messagesPack="com.company.sample.web.screens.monitor">
<layout>
</layout>
</window>
界面描述具有 window
根元素。
根元素属性:
界面描述的元素:
-
data
− 定义界面的数据组件。 -
dialogMode
- 定义界面作为对话框打开时的尺寸及行为的设置。dialogMode
的属性:-
closeable
- 定义对话框窗口是否有关闭按钮。可选值:true
、false
。 -
closeOnClickOutside
- 当窗口是模式窗口时,定义是否允许通过单击窗口之外的区域来关闭对话框窗口。可选值:true
、false
。 -
forceDialog
- 指定界面应始终以对话框方式打开,无论在调用代码中选择了哪种WindowManager.OpenType
。可选值:true
、false
。 -
height
- 设置对话框窗口的高度。 -
maximized
- 如果设置了true
值,则对话窗口将最大化显示。可选值:true
、false
。 -
modal
-指定对话框窗口是否是模式窗口。可选值:true
、false
。 -
positionX
- 设置对话框窗口左上角的x
坐标。 -
positionY
- 设置对话框窗口左上角的y
坐标。 -
resizable
- 定义用户是否可以更改对话窗口的大小。可选值:true
、false
。 -
width
- 设置对话框窗口的宽度。
例如:
<dialogMode height="600" width="800" positionX="200" positionY="200" forceDialog="true" closeOnClickOutside="false" resizable="true"/>
-
-
actions
– 定义界面的操作列表。 -
timers
– 定义界面的计时器列表。 -
layout
− 界面布局的根元素。
3.5.1.3. 打开界面
- 使用 Screens 接口
-
Screens
接口允许创建和显示任何类型的界面。假设有一个用于显示具有一些特殊格式的消息的界面:
界面控制器@UiController("demo_FancyMessageScreen") @UiDescriptor("fancy-message-screen.xml") @DialogMode(forceDialog = true, width = "300px") public class FancyMessageScreen extends Screen { @Inject private Label<String> messageLabel; public void setFancyMessage(String message) { (1) messageLabel.setValue(message); } @Subscribe("closeBtn") protected void onCloseBtnClick(Button.ClickEvent event) { closeWithDefaultAction(); } }
1 - 一个界面参数 界面描述<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message"> <layout> <label id="messageLabel" value="A message" stylename="h1"/> <button id="closeBtn" caption="Close"/> </layout> </window>
那么可以从另一个界面创建并打开它,如下所示:
@Inject private Screens screens; private void showFancyMessage(String message) { FancyMessageScreen screen = screens.create(FancyMessageScreen.class); screen.setFancyMessage(message); screens.show(screen); }
请注意这里是如何创建界面实例、为其提供参数,然后显示界面。
如果界面不需要来自调用方的任何参数,可以仅用一行代码创建并打开它:
@Inject private Screens screens; private void showDefaultFancyMessage() { screens.create(FancyMessageScreen.class).show(); }
screens
不是 Spring bean,所以只能将它注入到界面控制器或使用ComponentsHelper.getScreenContext(component).getScreens()
静态方法获取。
- 使用 ScreenBuilders bean
-
通过
ScreenBuilders
bean 可以使用各种参数打开所有类型的界面。参考 初始化实体值 指南,了解如何通过
ScreenBuilders
在外部进行初始化。下面是用它打开界面并且在界面关闭之后执行一些代码的例子(更多细节参考这里):
@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(e -> { notifications.create().withCaption("Closed").show(); }) .build() .show(); }
下面我们看看如何操作编辑界面和查找界面。注意,在大多数情况下你会使用 标准操作(比如 CreateAction 或 LookupAction)来打开这类界面,所以并不是非要直接用
ScreenBuilders
API。但是,如果不用标准的操作而是想通过 基础操作 或者 按钮 处理打开界面的话,可以参考下面的例子。为
Customer
实体实例打开默认编辑界面的示例:@Inject private ScreenBuilders screenBuilders; private void editSelectedEntity(Customer entity) { screenBuilders.editor(Customer.class, this) .editEntity(entity) .build() .show(); }
在这种情况下,编辑界面将更新实体,但调用界面将不会接收到更新后的实例。
我们经常需要编辑某些用
Table
或DataGrid
组件显示的实体。那么应该使用以下调用方式,它更简洁且能自动更新表格组件:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void editSelectedEntity() { screenBuilders.editor(customersTable).build().show(); }
要创建一个新的实体实例并打开它的编辑界面,只需在构建器上调用
newEntity()
方法:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void createNewEntity() { screenBuilders.editor(customersTable) .newEntity() .build() .show(); }
默认编辑界面的确定过程如下:
-
如果存在使用@PrimaryEditorScreen注解的编辑界面,则使用它。
-
否则,使用 id 是
<entity_name>.edit
的编辑界面(例如,sales_Customer.edit
)。
界面构建器提供了许多方法来设置被打开界面的可选参数。例如,以下代码以对话框的方式打开的特定编辑界面,同时新建并初始化实体:
@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void editSelectedEntity() { screenBuilders.editor(customersTable).build().show(); } private void createNewEntity() { screenBuilders.editor(customersTable) .newEntity() .withInitializer(customer -> { // lambda to initialize new instance customer.setName("New customer"); }) .withScreenClass(CustomerEdit.class) // specific editor screen .withLaunchMode(OpenMode.DIALOG) // open as modal dialog .build() .show(); }
实体查找界面也能使用不同参数打开。
下面是打开
User
实体的默认查找界面的示例:@Inject private TextField<String> userField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withSelectHandler(users -> { User user = users.iterator().next(); userField.setValue(user.getName()); }) .build() .show(); }
如果需要将找到的实体设置到字段,可使用更简洁的方式:
@Inject private PickerField<User> userPickerField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withField(userPickerField) // set result to the field .build() .show(); }
默认查找界面的确定过程如下:
-
如果存在使用@PrimaryLookupScreen注解的查找界面,则使用它。
-
否则,如果存在 id 为
<entity_name>.lookup
的界面,则使用它(例如,sales_Customer.lookup
)。 -
否则,使用 id 为
<entity_name>.browse
的界面(例如,sales_Customer.browse
)。
与使用编辑界面一样,使用构建器方法设置打开界面的可选参数。例如,以下代码以对话框的方式打开特定的查找界面,在这个界面中查找
User
实体:@Inject private TextField<String> userField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withScreenId("sec$User.browse") // specific lookup screen .withLaunchMode(OpenMode.DIALOG) // open as modal dialog .withSelectHandler(users -> { User user = users.iterator().next(); userField.setValue(user.getName()); }) .build() .show(); }
-
- 为界面传递参数
-
为打开界面传递参数的推荐方式是使用界面控制器的公共 setter 方法,如上面界面接口部分示范。
使用这个方式,可以为任意类型的界面传递参数,包括使用ScreenBuilders或者从主菜单打开的实体编辑和查找界面。带有传参使用
ScreenBuilders
来调用FancyMessageScreen
如下所示:@Inject private ScreenBuilders screenBuilders; private void showFancyMessage(String message) { FancyMessageScreen screen = screenBuilders.screen(this) .withScreenClass(FancyMessageScreen.class) .build(); screen.setFancyMessage(message); screen.show(); }
如果使用类似 CreateAction 的标准操作打开界面,可以用它的
screenConfigurer
处理器通过界面的公共 setters 传递参数。另一个方式是为参数定义一个特殊的类,然后在界面构造器中将该类的实例传递给标准的
withOptions()
方法。参数类必需实现ScreenOptions
标记接口。示例:import com.haulmont.cuba.gui.screen.ScreenOptions; public class FancyMessageOptions implements ScreenOptions { private String message; public FancyMessageOptions(String message) { this.message = message; } public String getMessage() { return message; } }
在打开的
FancyMessageScreen
界面,可以通过InitEvent和AfterInitEvent处理器获取参数:@Subscribe private void onInit(InitEvent event) { ScreenOptions options = event.getOptions(); if (options instanceof FancyMessageOptions) { String message = ((FancyMessageOptions) options).getMessage(); messageLabel.setValue(message); } }
带有传递
ScreenOptions
参数使用ScreenBuilders
来调用FancyMessageScreen
如下所示:@Inject private ScreenBuilders screenBuilders; private void showFancyMessage(String message) { screenBuilders.screen(this) .withScreenClass(FancyMessageScreen.class) .withOptions(new FancyMessageOptions(message)) .build() .show(); }
可以看到,这个方式需要在控制界接收参数的时候进行类型转换,所以需要考虑清楚再使用。推荐还是用上面介绍的类型安全的使用 setter 的方式。
如果使用类似 CreateAction 的标准操作打开界面,可以用它的
screenOptionsSupplier
处理器创建并初始化所需的ScreenOptions
对象。如果界面是基于legacy API从另一个界面打开的,那么使用
ScreenOptions
对象是唯一能获取到参数的方法。此时,参数对象是MapScreenOptions
类型的,可以在打开的界面中按照如下处理:@Subscribe private void onInit(InitEvent event) { ScreenOptions options = event.getOptions(); if (options instanceof MapScreenOptions) { String message = (String) ((MapScreenOptions) options).getParams().get("message"); messageLabel.setValue(message); } }
- 关闭界面后执行代码以及关闭界面返回值
-
每个界面在关闭时都会发送
AfterCloseEvent
事件。可以为界面添加监听器,这样可以在界面关闭时收到通知,示例:@Inject private Screens screens; @Inject private Notifications notifications; private void openOtherScreen() { OtherScreen otherScreen = screens.create(OtherScreen.class); otherScreen.addAfterCloseListener(afterCloseEvent -> { notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show(); }); otherScreen.show(); }
当使用
ScreenBuilders
时,可以在withAfterCloseListener()
方法中提供监听器:@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(afterCloseEvent -> { notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show(); }) .build() .show(); }
事件对象能提供关于界面是如何关闭的信息。信息可以有两种方式获取:一种是检测界面是否通过
StandardOutcome
枚举类型定义的一种标准输出关闭,另一种是获取CloseAction
对象。前一种方法比较简单,但是后一种方法比较灵活。先看看第一种方式:使用标准输出关闭界面然后在调用代码内进行检测。
将调用下面这个界面:
package com.company.demo.web.screens; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.screen.*; @UiController("demo_OtherScreen") @UiDescriptor("other-screen.xml") public class OtherScreen extends Screen { private String result; public String getResult() { return result; } @Subscribe("okBtn") public void onOkBtnClick(Button.ClickEvent event) { result = "Done"; close(StandardOutcome.COMMIT); (1) } @Subscribe("cancelBtn") public void onCancelBtnClick(Button.ClickEvent event) { close(StandardOutcome.CLOSE); (2) } }
1 - 在点击 "OK" 按钮时,设置一些结果状态,并使用 StandardOutcome.COMMIT
枚举值关闭界面。2 - 在点击 "Cancel" 按钮时,使用 StandardOutcome.CLOSE
关闭界面。在
AfterCloseEvent
监听器使用事件的closedWith()
方法检查界面是如何关闭的,并且需要的话可以读取结果:@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(afterCloseEvent -> { OtherScreen otherScreen = afterCloseEvent.getScreen(); if (afterCloseEvent.closedWith(StandardOutcome.COMMIT)) { String result = otherScreen.getResult(); notifications.create().withCaption("Result: " + result).show(); } }) .build() .show(); }
从界面返回值的另一个方法是使用自定义的
CloseAction
实现。重写一下上面的示例,使用如下操作类:package com.company.demo.web.screens; import com.haulmont.cuba.gui.screen.StandardCloseAction; public class MyCloseAction extends StandardCloseAction { private String result; public MyCloseAction(String result) { super("myCloseAction"); this.result = result; } public String getResult() { return result; } }
然后可以使用该操作类关闭界面:
package com.company.demo.web.screens; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.screen.*; @UiController("demo_OtherScreen2") @UiDescriptor("other-screen.xml") public class OtherScreen2 extends Screen { @Subscribe("okBtn") public void onOkBtnClick(Button.ClickEvent event) { close(new MyCloseAction("Done")); (1) } @Subscribe("cancelBtn") public void onCancelBtnClick(Button.ClickEvent event) { closeWithDefaultAction(); (2) } }
1 - 在点击 "OK" 按钮时,创建自定义关闭操作,并设置结果值。 2 - 在点击 "Cancel" 按钮时,使用框架提供的默认操作关闭界面。 在
AfterCloseEvent
监听器从事件获取CloseAction
并读取结果:@Inject private Screens screens; @Inject private Notifications notifications; private void openOtherScreen() { Screen otherScreen = screens.create("demo_OtherScreen2", OpenMode.THIS_TAB); otherScreen.addAfterCloseListener(afterCloseEvent -> { CloseAction closeAction = afterCloseEvent.getCloseAction(); if (closeAction instanceof MyCloseAction) { String result = ((MyCloseAction) closeAction).getResult(); notifications.create().withCaption("Result: " + result).show(); } }); otherScreen.show(); }
可以看到,当使用自定义的
CloseAction
返回值时,调用方不需要知道打开的界面类是什么,因为不会调用具体的界面控制器内的方法。所以界面可以只通过其字符串 id 来创建。当然,在使用
ScreenBuilders
打开界面时,也可以使用相同的方式通过关闭操作返回结果。
3.5.1.4. 使用界面片段
- 声明式使用 fragment
-
假设我们有用来输入地址的 fragment:
AddressFragment.java@UiController("demo_AddressFragment") @UiDescriptor("address-fragment.xml") public class AddressFragment extends ScreenFragment { }
address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <layout> <textField id="cityField" caption="City"/> <textField id="zipField" caption="Zip"/> </layout> </fragment>
然后我们可以在其它界面使用
fragment
元素来包含此 fragment,fragment
元素需要有指向 fragment id 的screen
属性,fragment id 在其@UiController
注解设置:host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <layout> <groupBox id="addressBox" caption="Address"> <fragment screen="demo_AddressFragment"/> </groupBox> </layout> </window>
fragment
元素可以添加在界面任意的 UI 容器中,也包含最顶上的layout
元素。
- 编程式使用 fragment
-
同一个片段也可以通过编程的方式添加到界面,需要在 InitEvent 或 AfterInitEvent 事件处理器中添加:
host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <layout> <groupBox id="addressBox" caption="Address"/> </layout> </window>
HostScreen.java@UiController("demo_HostScreen") @UiDescriptor("host-screen.xml") public class HostScreen extends Screen { @Inject private Fragments fragments; (1) @Inject private GroupBoxLayout addressBox; @Subscribe private void onInit(InitEvent event) { AddressFragment addressFragment = fragments.create(this, AddressFragment.class); (2) addressBox.add(addressFragment.getFragment()); (3) } }
1 - 注入 Fragments
bean,用来实例化界面片段2 - 用 class 创建片段控制器 3 - 从控制器中获取 Fragment
可视化组件,并添加到 UI 容器中。如果该片段有参数,可以在将界面片段添加到界面之前通过公共 setter 方法设置。之后,片段控制器的
InitEvent
和AfterInitEvent
处理方法里面可以访问到这些参数。
- 给界面片段传递参数
-
界面片段控制器可以有公共的 setter 方法用来接收参数,和打开界面一样的做法。如果界面片段是使用编程的方式打开,可以显式的调用 setters:
@UiController("demo_HostScreen") @UiDescriptor("host-screen.xml") public class HostScreen extends Screen { @Inject private Fragments fragments; @Inject private GroupBoxLayout addressBox; @Subscribe private void onInit(InitEvent event) { AddressFragment addressFragment = fragments.create(this, AddressFragment.class); addressFragment.setStrParam("some value"); (1) addressBox.add(addressFragment.getFragment()); } }
1 - 在将片段添加到界面之前先传递参数。 如果片段是通过 XML 以声明的方式添加到界面,使用
properties
元素来传递参数,示例:<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <data> <instance id="someDc" class="com.company.demo.entity.Demo"/> </data> <layout> <textField id="someField"/> <fragment screen="demo_AddressFragment"> <properties> <property name="strParam" value="some value"/> (1) <property name="dataContainerParam" ref="someDc"/> (2) <property name="componentParam" ref="someField"/> (3) </properties> </fragment> </layout> </window>
1 - 传递一个字符串参数给 setStrParam()
方法。2 - 传递一个数据容器给 setDataContainerParam()
方法。3 - 传递 TextField
组件给setComponentParam()
方法。使用
value
属性设置值,用ref
属性指定界面组件的标识符。setters 必须使用合适类型的参数。
- 界面 fragment 中的数据组件
-
界面 fragment 可以有自己的数据容器和数据加载器,通过 XML 元素
data
定义。同时,框架会为界面及其所有 fragments 创建DataContext的单例。因此,所有加载的实体都合并到同一数据上下文,并在父界面提交的时候一起保存更改。下面的例子中,会使用界面 fragment 自己的数据容器和加载器。
假设在 fragment 中有
City
实体,我们希望使用下拉列表框展示可选的城市而不仅仅用文本控件来展示。可以跟普通界面一样,在 fragment 的 XML 描述中定义数据组件。address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <data> <collection id="citiesDc" class="com.company.demo.entity.City" view="_base"> <loader id="citiesLd"> <query><![CDATA[select e from demo_City e ]]></query> </loader> </collection> </data> <layout> <lookupField id="cityField" caption="City" optionsContainer="citiesDc"/> <textField id="zipField" caption="Zip"/> </layout> </fragment>
如果需要在父界面打开时加载 fragment 的数据,需要订阅父界面事件:
AddressFragment.java@UiController("demo_AddressFragment") @UiDescriptor("address-fragment.xml") public class AddressFragment extends ScreenFragment { @Inject private CollectionLoader<City> citiesLd; @Subscribe(target = Target.PARENT_CONTROLLER) (1) private void onBeforeShowHost(Screen.BeforeShowEvent event) { citiesLd.load(); } }
1 - 订阅父界面的 BeforeShowEvent 事件 @LoadDataBeforeShow
对界面 fragments 无效。
- 使用已有的数据容器
-
下一个例子展示了如何在 fragment 中使用父界面的数据容器。
host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <data> <instance id="addressDc" class="com.company.demo.entity.Address"/> (1) </data> <layout> <groupBox id="addressBox" caption="Address"> <fragment screen="demo_AddressFragment"/> </groupBox> </layout> </window>
1 - fragment 将在下面使用的数据容器 address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <data> <instance id="addressDc" class="com.company.demo.entity.Address" provided="true"/> (1) <collection id="citiesDc" class="com.company.demo.entity.City" view="_base"> <loader id="citiesLd"> <query><![CDATA[select e from demo_City e]]></query> </loader> </collection> </data> <layout> <lookupField id="cityField" caption="City" optionsContainer="citiesDc" dataContainer="addressDc" property="city"/> (2) <textField id="zipField" caption="Zip" dataContainer="addressDc" property="zip"/> </layout> </fragment>
1 - provided="true"
表示使用同样 id 的容器必须存在于父界面或者嵌套的 fragment 中,也就是说必须在该 fragment 外部提供2 - UI 组件连接到提供的数据容器 在包含
provided="true"
属性的 XML 元素中,除了id
之外其它所有的属性都会被忽略,但是也可以加上,以便提供设计思路。
3.5.1.5. 界面 Mixins
通过 Mixin 可以创建能在多个UI界面中重复使用的功能,而且不需要从公共基类继承界面。Mixin 通过 Java 接口实现,使用了接口的默认方法。
Mixin 有如下特性:
-
一个界面可以有多个 Mixin。
-
Mixin 接口可以订阅 界面事件。
-
Mixin 可以在界面中保存一些状态,如果需要的话。
-
Mixin 也可以获取界面组件和基础架构 bean,比如 Dialogs,Notifications 等。
-
如果需要参数化 mixin 的行为,mixin 可以依赖界面的注解或者引入抽象方法交由界面实现。
使用 mixin 与在界面控制器中实现特定的接口一样简单。下面的示例中,CustomerEditor
界面使用了由 HasComments
,HasHistory
,HasAttachments
接口实现的 mixin 功能:
public class CustomerEditor extends StandardEditor<Customer>
implements HasComments, HasHistory, HasAttachments {
// ...
}
Mixin 可以使用以下类来处理界面和界面基础架构:
-
com.haulmont.cuba.gui.screen.Extensions
提供静态方法,用来保存和获取 mixin 使用的界面状态,还能访问BeanLocator
,这可以用来获取任何 Spring bean。 -
UiControllerUtils
提供对界面UI和数据组件的访问。
下面是展示如何创建和使用 mixin 的示例。
- DeclarativeLoaderParameters mixin
-
下面这个 mixin 可以帮助在数据容器之间创建主从关系。通常的做法,是需要订阅主容器的
ItemChangeEvent
事件,将改动的主容器内容设置为从容器的数据加载器参数,如数据组件之间的依赖所述。但是如果参数是指向主容器的特殊名称,mixin 能自动完成此功能。Mixin 会使用状态对象在事件处理器之间传递信息。这里为了演示,我们将逻辑分散开,但实际上所有的逻辑都可以在一个
BeforeShowEvent
处理器中完成。首先,为共享状态创建一个类。包含单一字段,用来保存将在
BeforeShowEvent
处理器中触发的一组数据加载器:package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.DataLoader; import java.util.Set; public class DeclarativeLoaderParametersState { private Set<DataLoader> loadersToLoadBeforeShow; public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) { this.loadersToLoadBeforeShow = loadersToLoadBeforeShow; } public Set<DataLoader> getLoadersToLoadBeforeShow() { return loadersToLoadBeforeShow; } }
接下来,创建 mixin 接口:
package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.*; import com.haulmont.cuba.gui.screen.*; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public interface DeclarativeLoaderParameters { Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))"); @Subscribe default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1) Screen screen = event.getSource(); ScreenData screenData = UiControllerUtils.getScreenData(screen); (2) Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>(); for (String loaderId : screenData.getLoaderIds()) { DataLoader loader = screenData.getLoader(loaderId); String query = loader.getQuery(); Matcher matcher = CONTAINER_REF_PATTERN.matcher(query); while (matcher.find()) { (3) String paramName = matcher.group(1); String containerId = matcher.group(2); InstanceContainer<?> container = screenData.getContainer(containerId); container.addItemChangeListener(itemChangeEvent -> { (4) loader.setParameter(paramName, itemChangeEvent.getItem()); (5) loader.load(); }); if (container instanceof HasLoader) { (6) loadersToLoadBeforeShow.add(((HasLoader) container).getLoader()); } } } DeclarativeLoaderParametersState state = new DeclarativeLoaderParametersState(loadersToLoadBeforeShow); (7) Extensions.register(screen, DeclarativeLoaderParametersState.class, state); } @Subscribe default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) { (8) Screen screen = event.getSource(); DeclarativeLoaderParametersState state = Extensions.get(screen, DeclarativeLoaderParametersState.class); for (DataLoader loader : state.getLoadersToLoadBeforeShow()) { loader.load(); (9) } } }
1 - 订阅 InitEvent。 2 - 获取 ScreenData
对象,其中注册了 XML 中定义的所有数据容器和加载器。3 - 检查加载器的参数是否符合 :container$masterContainerId
模式的定义。4 - 从参数名中抽取主容器id,然后为该容器注册一个 ItemChangeEvent
监听器。5 - 使用新的主实体重新加载从实体数据加载器。 6 - 将主加载器添加到集合中,以便之后在 BeforeShowEvent
处理器中能触发。7 - 创建共享状态对象,使用 Extensions
工具类将该对象保存在界面中。8 - 订阅 BeforeShowEvent 事件。 9 - 触发在 InitEvent
处理器中找到的所有主加载器。在界面 XML 描述中定义主从容器以及数据加载器。从加载器需要带有一个参数,其名称类似
:container$masterContainerId
:<collection id="countriesDc" class="com.company.demo.entity.Country" view="_local"> <loader id="countriesDl"> <query><![CDATA[select e from demo_Country e]]></query> </loader> </collection> <collection id="citiesDc" class="com.company.demo.entity.City" view="city-view"> <loader id="citiesDl"> <query><![CDATA[ select e from demo_City e where e.country = :container$countriesDc ]]></query> </loader> </collection>
在界面控制器中,只需要添加 mixin 接口,然后就能自动触发加载器了:
package com.company.demo.web.country; import com.company.demo.entity.Country; import com.company.demo.web.mixins.DeclarativeLoaderParameters; import com.haulmont.cuba.gui.screen.*; @UiController("demo_Country.browse") @UiDescriptor("country-browse.xml") @LookupComponent("countriesTable") public class CountryBrowse extends StandardLookup<Country> implements DeclarativeLoaderParameters { }
3.5.1.6. 根界面
根界面是一个通用 UI 界面,直接展示在 web 浏览器的标签页中。有两种类型的这种界面:登录界面和主界面。其它的组件中,任何根界面都可以包含 WorkArea
组件,这样使得可以在内部的标签页中打开其它的应用程序界面。如果根界面不包含 WorkArea
,应用程序界面只能以 DIALOG
模式打开。
- 登录界面
-
登录界面是在用户登录之前展示的界面。可以通过扩展框架提供的登录界面或者创建全新的登录界面对该界面进行自定义。
如果要扩展已有的界面,在 Studio 界面创建向导中使用 Login screen 模板。Studio 会帮助创建一个扩展了标准登录界面的界面。该界面会替代标准的登录界面,因为它在
@UiController
注解中使用了相同的login
标识符。如果想从头创建一个新的界面,可以使用 Blank screen 模板。一个极简的登录界面源码差不多是这样:
my-login-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Login" messagesPack="com.company.sample.web"> <layout> <label value="Hello World"/> <button id="loginBtn" caption="Login"/> </layout> </window>
MyLoginScreen.javapackage com.company.sample.web; import com.haulmont.cuba.gui.Route; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.screen.*; import com.haulmont.cuba.security.auth.LoginPasswordCredentials; import com.haulmont.cuba.web.App; @UiController("myLogin") @UiDescriptor("my-login-screen.xml") @Route(path = "login", root = true) public class MyLoginScreen extends Screen { @Subscribe("loginBtn") private void onLoginBtnClick(Button.ClickEvent event) { App.getInstance().getConnection().login( new LoginPasswordCredentials("admin", "admin")); } }
为了使用这个界面替代系统默认的界面,需要在
web-app.properties
文件中将cuba.web.loginScreenId
配置项设置为该界面的 id。cuba.web.loginScreenId = myLogin
当然,也可以直接将新界面的 id 设置为
login
,就不需要修改这个配置了。
- 主界面
-
主界面是用户登录之后看到的应用程序的根界面。默认情况下,框架使用带侧边菜单的主界面。
Studio 有一些创建自定义主界面的模板,这些模板都使用相同的
MainScreen
类作为控制器的基类。-
Main screen with side menu 创建标准主界面的扩展,并使用
main
id。带有侧边菜单的主界面,默认可以使用左下角的 Collapse 按钮展开或者收起侧边菜单。-
$cuba-sidemenu-layout-collapse-enabled
启用或禁用侧边菜单收起模式。默认为true
。 -
$cuba-sidemenu-layout-collapsed-width
指定收起后侧边菜单的宽度。 -
$cuba-sidemenu-layout-expanded-width
指定展开后侧边菜单的宽度。 -
$cuba-sidemenu-layout-collapse-animation-time
指定侧边菜单展开收起的动画时间。当
$cuba-sidemenu-layout-collapse-enabled
变量设置为false
时,会隐藏 Collapse 按钮,侧边菜单呈展开状态。
-
-
Main screen with responsive side menu 创建一个类似的界面,但是侧边菜单是响应式的,能在窄的显示环境中自动收起。该界面会带有自己生成的 id,因此,必须在
web-app.properties
里面进行注册:cuba.web.mainScreenId = respSideMenuMainScreen
-
Main screen with top menu 创建一个带有顶部菜单栏的界面,并且能在左侧显示 文件夹面板。该界面会带有自己生成的 id,因此,必须在
web-app.properties
里面进行注册:cuba.web.mainScreenId = topMenuMainScreen
除了标准 UI 组件之外,下面这些特殊的组件也可以用在主界面:
-
SideMenu
- 应用程序菜单,以垂直树的形势展示。 -
AppMenu
– 应用程序菜单栏。 -
AppWorkArea
– 工作区,如果需要以THIS_TAB
、NEW_TAB
和NEW_WINDOW
模式打开界面,则需要该组件。 -
FoldersPane
– 应用程序和搜索文件夹的面板。 -
UserIndicator
– 显示当前用户的控件,也包括选择替代用户的功能。使用
setUserNameFormatter()
方法可以设置不同于User
实例名称的用户名称展示:userIndicator.setUserNameFormatter(value -> value.getName() + " - [" + value.getEmail() + "]");
-
NewWindowButton
– 在单独的浏览器标签页打开新主界面的按钮。
-
UserActionsButton
– 如果用户会话没有认证,显示登录界面的链接。否则,显示一个菜单:用户设置界面的链接和登出操作。可以在主界面控制器装载
LoginHandler
或LogoutHandler
以实现自定义逻辑:@Install(to = "userActionsButton", subject = "loginHandler") private void loginHandler(UserActionsButton.LoginHandlerContext ctx) { // do custom logic } @Install(to = "userActionsButton", subject = "logoutHandler") private void logoutHandler(UserActionsButton.LogoutHandlerContext ctx) { // do custom logic }
-
LogoutButton
– 应用程序登出按钮。 -
TimeZoneIndicator
– 显示当前用户时区的标签。 -
FtsField
– 全文搜索控件。
下列应用程序属性可能影响主界面:
-
cuba.web.appWindowMode – 设置主窗口的默认模式:标签页式的还是单独界面式(
TABBED
或SINGLE
)。用户可以使用 UserActionsButton 提供的 Settings - 设置 界面进行修改。 -
cuba.web.maxTabCount – 当主界面在标签页式时,使用该属性设置用户能打开的标签页最多个数。默认值为 20。
-
cuba.web.foldersPaneEnabled - 为使用 Main screen with top menu 模板创建的界面启用显示文件夹面板。
-
cuba.web.defaultScreenId - 设置主窗口自动打开的默认界面。
-
cuba.web.defaultScreenCanBeClosed - 定义用户是否可以关闭默认界面。
-
cuba.web.useDeviceWidthForViewport - 处理 viewport 宽度。如果需要使用设备的宽度作为 viewport 宽度,设置为
true
。cuba.web.pageInitialScale 属性也可以参考设置。
-
3.5.1.7. 界面中的验证
ScreenValidation
bean 可以用来运行界面中的验证逻辑,有如下方法:
-
ValidationErrors validateUiComponents()
,默认在StandardEditor
,InputDialog
和MasterDetailScreen
提交改动时使用。该方法接收一组界面组件或者一个组件容器作为参数,返回这些组件中的验证错误(ValidationErrors
对象)。validateUiComponents()
方法还可以用在其他任意的界面中。示例:@UiController("demo_DemoScreen") @UiDescriptor("demo-screen.xml") public class DemoScreen extends Screen { @Inject private ScreenValidation screenValidation; @Inject private Form demoForm; @Subscribe("validateBtn") public void onValidateBtnClick(Button.ClickEvent event) { ValidationErrors errors = screenValidation.validateUiComponents(demoForm); if (!errors.isEmpty()) { screenValidation.showValidationErrors(this, errors); return; } } }
-
showValidationErrors()
- 显示所有的错误和有问题的组件。该方法接收界面和ValidationErrors
对象作为参数。默认在StandardEditor
,InputDialog
和MasterDetailScreen
中使用。 -
validateCrossFieldRules()
- 接收界面和实体作为参数,返回ValidationErrors
对象。执行跨字段验证规则。如果编辑界面的约束中包含UiCrossFieldChecks
并且所有的属性级别验证通过,则会在提交时,进行类级别约束的验证(更多信息参考 自定义约束 章节)。可以使用控制器的setCrossFieldValidate()
方法禁用此类验证。默认情况下,在StandardEditor
,MasterDetailScreen
以及DataGrid
的编辑界面使用。另外,validateCrossFieldRules()
方法也可以用在任意界面中。举个例子,对于
Event
实体,我们可以定义类级别注解@EventDate
来检查 Start date 必须小于 End date。Event entity@Table(name = "DEMO_EVENT") @Entity(name = "demo_Event") @NamePattern("%s|name") @EventDate(groups = {Default.class, UiCrossFieldChecks.class}) public class Event extends StandardEntity { private static final long serialVersionUID = 1477125422077150455L; @Column(name = "NAME") private String name; @Temporal(TemporalType.TIMESTAMP) @Column(name = "START_DATE") private Date startDate; @Temporal(TemporalType.TIMESTAMP) @Column(name = "END_DATE") private Date endDate; ... }
注解定义如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EventDateValidator.class) public @interface EventDate { String message() default "The Start date must be earlier than the End date"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
EventDateValidatorpublic class EventDateValidator implements ConstraintValidator<EventDate, Event> { @Override public boolean isValid(Event event, ConstraintValidatorContext context) { if (event == null) { return false; } if (event.getStartDate() == null || event.getEndDate() == null) { return false; } return event.getStartDate().before(event.getEndDate()); } }
然后可以在任意界面中使用
validateCrossFieldRules()
方法。@UiController("demo_DemoScreen") @UiDescriptor("demo-screen.xml") public class DemoScreen extends Screen { @Inject protected Metadata metadata; @Inject protected ScreenValidation screenValidation; @Inject protected TimeSource timeSource; @Subscribe("validateBtn") public void onValidateBtnClick(Button.ClickEvent event) { Event event = metadata.create(Event.class); event.setName("Demo event"); event.setStartDate(timeSource.currentTimestamp()); // We make the endDate earlier than the startDate event.setEndDate(DateUtils.addDays(event.getStartDate(), -1)); ValidationErrors errors = screenValidation.validateCrossFieldRules(this, event); if (!errors.isEmpty()) { screenValidation.showValidationErrors(this, errors); } } }
-
showUnsavedChangesDialog()
- 为没保存的数据显示标准对话框("Do you want to discard unsaved changes?" - “您要放弃更改吗?”),带有 Yes - 是 和 No - 否 按钮。用在StandardEditor
中。showUnsavedChangesDialog()
带有处理用户操作(点击的按钮)的处理器:screenValidation.showUnsavedChangesDialog(this, action) .onDiscard(() -> result.resume(closeWithDiscard())) .onCancel(result::fail);
-
showSaveConfirmationDialog()
- 为确认保存更改的数据显示标准对话框("Do you want to save changes before close?" - “关闭页面前需要保存修改吗?”)带有 Save - 保存,Do not save - 不保存 和 Cancel - 取消 按钮。用在StandardEditor
中。showSaveConfirmationDialog()
带有处理用户操作(点击的按钮)的处理器:screenValidation.showSaveConfirmationDialog(this, action) .onCommit(() -> result.resume(closeWithCommit())) .onDiscard(() -> result.resume(closeWithDiscard())) .onCancel(result::fail);
可以用 cuba.gui.useSaveConfirmation 应用程序属性修改对话框类型。
3.5.2. 可视化组件库
3.5.2.1. 可视化组件
菜单 |
|
按钮 |
|
文本 |
|
文本输入 |
|
日期输入 |
|
选择 |
|
上传 |
|
表格和树 |
|
其它 |
|
3.5.2.1.1. 应用程序菜单
AppMenu
应用程序菜单组件提供了在主界面中自定义主菜单的方式,通过它可以动态管理菜单项。
CUBA Studio 基于标准的 MainScreen
主窗口界面提供了一些界面模板。下面的例子中,这个模板扩展了 MainScreen
类,通过它可以直接访问 AppMenu
实例:
public class ExtMainScreen extends MainScreen implements Window.HasFoldersPane {
@Inject
private Notifications notifications;
@Inject
private AppMenu mainMenu;
@Subscribe
public void onInit(InitEvent event) {
AppMenu.MenuItem item = mainMenu.createMenuItem("shop", "Shop");
AppMenu.MenuItem subItem = mainMenu.createMenuItem("customer", "Customers", null, menuItem -> {
notifications.create()
.withCaption("Customers menu item clicked")
.withType(Notifications.NotificationType.HUMANIZED)
.show();
});
item.addChildItem(subItem);
mainMenu.addMenuItem(item, 0);
}
}
AppMenu
- 应用程序菜单接口的方法有:
-
addMenuItem()
- 往根菜单列表末尾指定位置添加菜单项目。
-
createMenuItem()
- 是一个创建新菜单项目的工厂方法。并不会把菜单项目添加到菜单里。id
必须在菜单中唯一。
-
createSeparator()
- 创建菜单分隔线。 -
getMenuItem()/getMenuItemNN()
- 根据菜单树中的id
返回菜单项目。 -
getMenuItems()
- 返回菜单根菜单项列表。 -
hasMenuItems()
- 如果该菜单包含菜单项则返回true
。
MenuItem
- 菜单项接口的方法有:
-
addChildItem() / removeChildItem()
- 在子菜单项列表末尾或者指定位置添加/删除菜单项。 -
getCaption()
- 返回菜单项标题。 -
getChildren()
- 返回子菜单项列表。
-
setCommand()
- 设置菜单项命令或点击菜单项目需要执行的动作。 -
setDescription()
- 设置菜单项的描述,会在弹出提示中显示。 -
setIconFromSet()
- 设置菜单项的图标。 -
getId()
- 返回菜单项的 id。 -
getMenu()
- 返回菜单项所属菜单。 -
setStylename()
- 设置一个或多个自定义的样式,会替换已有的所有自定义样式。多个样式之间用空格分开。样式名称必须是有效的 CSS 类名称。 -
hasChildren()
- 该菜单项有子菜单项时返回true
。 -
isSeparator()
- 如果该菜单项是分隔线的话返回true
。 -
setVisible()
- 设置菜单项是否可见。
AppMenu
的展示可以使用带 $cuba-menubar-*
和 $cuba-app-menubar-*
前缀的 SCSS 变量进行自定义。可以在创建一个 主题扩展 或者一个 自定义主题 之后在可视化编辑器里修改这些变量的值。
- API
3.5.2.1.2. 浏览器框架
BrowserFrame
是用来显示嵌入的网页,跟 HTML 里面的 iframe
元素的效果是一样的。
该组件对应的 XML 名称: browserFrame
下面是界面 XML 描述中这个组件定义的一个例子:
<browserFrame id="browserFrame"
height="280px"
width="600px"
align="MIDDLE_CENTER">
<url url="https://www.cuba-platform.com/blog/cuba-7-the-new-chapter"/>
</browserFrame>
跟 Image 组件类似, BrowserFrame
组件也可以显示不同来源的图片。可以用下面提到的 browserFrame
的 XML 元素来声明式的设置图片来源的类型:
-
classpath
- classpath 中能访问的资源。<browserFrame> <classpath path="com/company/sample/web/screens/myPic.jpg"/> </browserFrame>
-
file
- 文件系统的资源。<browserFrame> <file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/> </browserFrame>
-
relativePath
- 应用程序目录中的资源。<browserFrame> <relativePath path="VAADIN/images/myImage.jpg"/> </browserFrame>
-
theme
- 主题中用到的资源,比如:<browserFrame> <theme path="../halo/com.company.demo/myPic.jpg"/> </browserFrame>
-
url
- 可从指定 URL 加载的资源。<browserFrame> <url url="http://www.foobar2000.org/"/> </browserFrame>
browserFrame
的属性:
-
allow
- 指定组件的“功能政策”。属性的值可以是空格隔开的下面值的组合:-
autoplay
– 控制当前网页是否允许自动播放接口的请求的媒体。 -
camera
– 控制当前网页是否允许使用视频输入设备。 -
document-domain
– 控制当前网页是否允许设置document.domain
。 -
encrypted-media
– 控制当前网页是否允许使用加密媒体扩展 API(EME)。 -
fullscreen
– 控制当前网页是否允许使用Element.requestFullScreen()
。 -
geolocation
– 控制当前网页是否允许使用地理位置接口。 -
microphone
– 控制当前网页是否允许使用音频输入设备。 -
midi
– 控制当前网页是否允许使用 Web MIDI API。 -
payment
– 控制当前网页是否允许使用付款请求 API。 -
vr
– 控制当前网页是否允许使用 WebVR API。
-
-
alternateText
- 如果 frame 中没有设置内容源或者内容源不可用的情况下,作为默认显示的文字。
browserFrame
内容源的配置信息:
-
referrerpolicy
- 设置当获取 frame 的资源时,发送给哪个 referrer。ReferrerPolicy
– 该属性的标准值枚举:-
no-referrer
– 不会发送 referer header。 -
no-referrer-when-downgrade
– 如果没有 TLS(HTTPS),则不会发送 referer header 给 origins。 -
origin
– 发送的 referrer 限制在 referrer page 的 origin:scheme、host、port。 -
origin-when-cross-origin
– 发送给其它 origins 的 referrer 会被限制在 scheme、host 和 port。同源浏览也仍会包含 path。 -
same-origin
– 同源则会发送 referrer,但是跨域的请求不会包含 referrer 信息。 -
strict-origin
– 在协议的安全级别相同(HTTPS->HTTPS)时,将网页的 orign 作为 referrer 发送,但是不会发送给低安全级别的目的地(HTTPS->HTTP)。 -
strict-origin-when-cross-origin
– 当发起同源请求时发送全部 URL,在协议的安全级别相同(HTTPS->HTTPS)时,只发送 origin, 如果是低安全级别的目的地(HTTPS->HTTP),则不会发送 header。 -
unsafe-url
– referrer 会包含 origin 和 path。该值不安全,因为从 TLS 保护的资源转到了不安全的 origins,从而泄露了 origins 和 paths。
-
-
sandbox
- 对 frame 的内容使用更多的限制。该属性的值如果是空则使用所有限制,或者设置为空格分隔的标记升级特殊的限制。Sandbox
– 该属性的标准值枚举:-
allow-forms
– 允许资源提交表单。 -
allow-modals
– 资源可以打开模态窗。 -
allow-orientation-lock
– 资源可以锁住屏幕朝向。 -
allow-pointer-lock
– 资源可以使用 Pointer Lock API. -
allow-popups
– 允许弹窗(比如window.open()
、target="_blank"
或showModalDialog()
)。 -
allow-popups-to-escape-sandbox
– 允许沙盒内的网页打开新窗口,并且这些新窗口不继承当前的沙盒。 -
allow-presentation
– 允许资源开启一个展示会话。 -
allow-same-origin
– 允许iframe
的内容被当做同源处理。 -
allow-scripts
– 允许资源运行脚本。 -
allow-storage-access-by-user-activation
– 允许资源请求访问父网页的存储能力,能使用 Storage Access API。 -
allow-top-navigation
– 允许资源浏览最顶级网页(以_top
命名)浏览的内容。 -
allow-top-navigation-by-user-activation
– 允许资源浏览最顶级网页浏览的内容,但是只有在该网页是通过用户交互产生。 -
allow-downloads-without-user-activation
– 允许在没有用户交互的情况下进行下载。 -
""
– 应用所有的限制。
-
-
srcdoc
– 可嵌入的行内 HTML,会覆盖src
属性。IE 和 Edge 浏览器不支持该属性。也可以在 xml 中使用srcdocFile
属性指定 HTML 代码的文件。
-
srcdocFile
– 文件的路径,文件的内容会被设置到srcdoc
属性。文件内容通过 classPath 资源获取。只能在 XML 描述中设置该属性。
-
bufferSize
- 下载资源时的缓存大小,以字节为单位。<browserFrame> <file bufferSize="1024" path="C:/img.png"/> </browserFrame>
-
cacheTime
- 缓存的失效时间,以毫秒为单位.<browserFrame> <file cacheTime="2400" path="C:/img.png"/> </browserFrame>
-
mimeType
- 资源的 MIME 类型。<browserFrame> <url url="https://avatars3.githubusercontent.com/u/17548514?v=4&s=200" mimeType="image/png"/> </browserFrame>
BrowserFrame
定义的接口方法:
-
addSourceChangeListener()
- 添加一个监听器,当 frame 的内容源发生变化时触发。@Inject private Notifications notifications; @Inject BrowserFrame browserFrame; @Subscribe protected void onInit(InitEvent event) { browserFrame.addSourceChangeListener(sourceChangeEvent -> notifications.create() .withCaption("Content updated") .show()); }
-
setSource()
- 设置 frame 的内容源。这个方法接受一个描述资源类型的参数,然后返回相应的资源对象,以便可以继续使用流式操作对资源进行更多的设置。每种资源类型有其独特的设置方法,比如,ThemeResource
具有的setPath()
方法、StreamResource
具有setStreamSupplier()
方法等:BrowserFrame frame = uiComponents.create(BrowserFrame.NAME); try { frame.setSource(UrlResource.class).setUrl(new URL("http://www.foobar2000.org/")); } catch (MalformedURLException e) { throw new RuntimeException(e); }
也可以使用用于
Image
组件的资源类型。
-
createResource()
- 用资源类型自己实现的方法创建资源对象,然后这个对象可以传递给setSource()
方法使用。UrlResource resource = browserFrame.createResource(UrlResource.class) .setUrl(new URL(fromString)); browserFrame.setSource(resource);
- 在 BrowserFrame 中使用 HTML 标记:
-
BrowserFrame
可以集成 HTML 标记到应用程序中。比如,可以在运行时根据用户的输入生成 HTML 内容。<textArea id="textArea" height="250px" width="400px"/> <browserFrame id="browserFrame" height="250px" width="500px"/>
textArea.addTextChangeListener(event -> { byte[] bytes = event.getText().getBytes(StandardCharsets.UTF_8); browserFrame.setSource(StreamResource.class) .setStreamSupplier(() -> new ByteArrayInputStream(bytes)) .setMimeType("text/html"); });
- 用 BrowserFrame 预览 PDF:
-
除了 HTML,
BrowserFrame
还可以用来展示 PDF 文件的内容。需要配置使用文件类型的资源和相应的 MIME 类型:@Inject private BrowserFrame browserFrame; @Inject private Resources resources; @Subscribe protected void onInit(InitEvent event) { browserFramePdf.setSource(StreamResource.class) .setStreamSupplier(() -> resources.getResourceAsStream("/com/company/demo/" + "web/screens/CUBA_Hands_on_Lab_6.8.pdf")) .setMimeType("application/pdf"); }
- BrowserFrame 的属性
-
align - allow - alternateText - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - colspan - css - description - descriptionAsHtml - enable - box.expandRatio - height - htmlSanitizerEnabled - icon - id - referrerpolicy - responsive - rowspan - sandbox - srcdoc - srcdocFile - stylename - visible - width
- BrowserFrame 资源的属性
- browserFrame 的 XML 元素
-
classpath - file - relativePath - theme - url
- API
3.5.2.1.3. 按钮
当用户点击一个按钮,就会执行一个操作。
该组件对应的 XML 名称: button
按钮上可以有标题、图标、或者两者皆有。下面这个图列举了一些不同类型的按钮。
下面是从本地化消息包获取文本显示到按钮和提示上的例子:
<button id="textButton" caption="msg://someAction" description="Press me"/>
按钮上的标题是用 caption 属性来设置,弹出提示用 description 来设置。
如果 disableOnClick
属性设置成 true
这个按钮在点击之后就会变成不可点击的状态,主要用来防止多次(意外的)点击这个按钮。之后可以通过调用 setEnabled(true)
把按钮恢复成可点击状态。
创建带有图标的按钮的例子:
<button id="iconButton" caption="" icon="SAVE"/>
按钮的主要功能是在点击的时候执行一个动作(action)。点击之后调用的控制器方法可以通过 invoke
属性来定义。这个属性的值需要是控制器的方法名,这个方法需要满足下面的条件:
-
方法应该是
public
。 -
方法返回值是
void
。 -
方法不能有任何参数, 或者只能有一个
Component
组件类型的参数。 如果方法带有Component
参数, 那么这个组件就是调用此方法的按钮实例。
以下是按钮调用 someMethod
的例子:
<button invoke="someMethod" caption="msg://someButton"/>
在界面控制器里需要定义名称为 someMethod
的方法:
public void someMethod() {
//some actions
}
如果设置了 action
属性,那么就会忽略 invoke
属性。action
属性包含了按钮中相应操作的名称。
带有 action
属性的按钮的例子:
<actions>
<action id="someAction" caption="msg://someAction"/>
</actions>
<layout>
<button action="someAction"/>
</layout>
实现了 Component.ActionsHolder
接口的组件中的任何操作都可以指定给按钮。表格、 分组表格、 树形表格、 树中的操作都可以指定给按钮。有两种添加操作的方法,一种是在 XML 描述中以声明的方式添加,另一种是在界面控制器里以编程的方式添加,这两种方式没有区别。不管使用哪种方式,在使用操作的时候,组件的名称和操作的标识符必须定义在 action
属性中,并且它们之间用 .
分隔。比如,下面的例子中,将 coloursTable
的 create
操作指定给一个按钮:
<button action="coloursTable.create"/>
按钮的操作也可以通过编程创建,方法是在界面控制器中创建继承自BaseAction的类。
如果给 如果 |
shortcut
属性用来为按钮指定一个快捷键组合。可选的功能键为:ALT
、CTRL
、SHIFT
,使用 "-" 与其他键分隔。示例:
<button id="button" caption="msg://shortcutButton" shortcut="ALT-C"/>
- 按钮样式
-
primary
属性用来将按钮设置为高亮显示,默认情况下,如果这个按钮调用的操作的primary属性为true
,这个按钮会被设置为高亮显示。<button primary="true" invoke="foo"/>
这个高亮样式在 Hover 主题中是默认开启的;如果希望在 Halo-based 主题中使用 primary 样式,可以通过设置
$cuba-highlight-primary-action
样式变量为true
来开启。接下来,在使用了 Halo-based 主题的 Web Client 里,可以通过
stylename
属性来给按钮组件设置一些预定义的样式,可以通过 XML 或者编程的方法设置:<button id="button" caption="Friendly button" stylename="friendly"/>
如果使用编程的方式来设置样式, 可以直接用
HaloTheme
主题类里面的以BUTTON_
开头的一些主题常量:button.setStyleName(HaloTheme.BUTTON_FRIENDLY);
- 按钮的属性
-
action - align - caption - captionAsHtml - css - description - descriptionAsHtml - disableOnClick - enable - box.expandRatio - htmlSanitizerEnabled - icon - id - invoke - shortcut - stylename - tabIndex - visible - width
- 按钮预定义的样式
-
borderless - borderless-colored - danger - friendly - huge - icon-align-right - icon-align-top - icon-only - large - primary - quiet - small - tiny
3.5.2.1.4. 批量编辑器
该组件对应的 XML 名称: bulkEditor
|
要使用 BulkEditor
, 相应的表格或者树组件的 multiselect
属性需设置为 "true"
。
批量实体编辑界面是基于定义的 view(view 里一般包括实体的字段和引用)、实体动态属性和用户权限自动生成的。系统属性不会显示在生成的界面里。
实体属性名称会按字母排序。默认情况下,值都为空,界面提交的时候,非空值会更新到所有的实体对象中。
批量实体编辑界面也支持批量删除值 - 实体对象的对应字段会设置为空( null
)。操作方法是点击字段旁边的 按钮,点击之后,该字段变为不可编辑, 再次点击该按钮则该字段恢复可编辑。
以下为在表格中使用 bulkEditor
批量编辑器的例子:
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<!-- ... -->
</actions>
<buttonsPanel>
<!-- ... -->
<bulkEditor for="invoiceTable"
exclude="customer"/>
</buttonsPanel>
-
bulkEditor
批量编辑器的属性有 -
-
属性
exclude
标识需要在批量编辑界面排除的字段,它可以包含一个正则表达式。比如:date|customer
-
includeProperties
- 定义批量编辑界面需要包含的字段;设置它以后,其它字段会被忽略。includeProperties
不会应用到动态属性。以声明的方式设置时,多个属性之间应该用逗号隔开:
<bulkEditor for="ordersTable" includeProperties="name, description"/>
这些属性也可以在界面控制器中以编程的方式设置:
bulkEditor.setIncludeProperties(Arrays.asList("name", "description"));
-
loadDynamicAttributes
定义实体的动态属性是否在批量编辑界面显示。默认为true
。
-
useConfirmDialog
定义保存之前是否弹出确认对话框,默认为true
。
-
columnsMode
− 定义批量编辑界面列的数量,是ColumnsMode
枚举的值。默认为TWO_COLUMNS
。示例:<groupTable id="customersTable" width="100%"> <actions>...</actions> <columns>...</columns> <buttonsPanel id="buttonsPanel" alwaysVisible="true"> ... <bulkEditor for="customersTable" columnsMode="ONE_COLUMN"/> </buttonsPanel> </groupTable>
-
- bulkEditor 批量编辑器的属性
-
align - caption - captionAsHtml - columnsMode - css - description - descriptionAsHtml - enable - exclude - box.expandRatio - for - htmlSanitizerEnabled - icon - id - includeProperties - loadDynamicAttributes - openType - stylename - tabIndex - useConfirmDialog - visible -