实体

Jmix 实体支持以下几种类型:

实体特性使用 实体属性 来定义。

JPA 和 DTO 实体由 Java 类定义,用 Jmix 特定的注解 进行标注。

可以使用 Studio 的实体设计器 来创建 JPA 和 DTO 实体。

JPA 实体

JPA 实体是按照 JPA 规则标注的 Java 类。JPA 实体将持久化到关系型数据库,数据库通过数据存储连接,可以是主数据存储,也可以是附加数据存储。

JPA 注解定义了数据库表字段和实体属性之间的映射。Jmix 对映射注解有以下限制:

  • 属性注解只能放在字段(AccessType.FIELD)上。

  • 不支持的注解:@IdClass@ElementCollection

下面是一个典型的 JPA 实体类的示例:

Customer.java
@JmixEntity (1)
@Table(name = "CUSTOMER") (2)
@Entity (3)
public class Customer {

    @JmixGeneratedValue (4)
    @Id (5)
    @Column(name = "ID", nullable = false) (6)
    private UUID id;

    @Version (7)
    @Column(name = "VERSION")
    private Integer version;

    @InstanceName (8)
    @NotNull (9)
    @Column(name = "NAME", nullable = false)
    private String name;

    @Email (9)
    @Column(name = "EMAIL", unique = true) (10)
    private String email;

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    // other getters and setters
1 必须有的 @JmixEntity 注解。
2 @jakarta.persistence.Table 注解,指定数据库表名。
3 @jakarta.persistence.Entity 注解,表明该类是一个 JPA 实体。
4 @JmixGeneratedValue 注解,表示创建实体实例时必须由 Jmix 生成主键。
5 @jakarta.persistence.Id 注解,表示主键。
6 @jakarta.persistence.Column 注解,指定对应的数据库表字段。nullable = false 表明 数据库变更 机制应该为该字段创建具有 NOT NULL 约束的字段。
7 @jakarta.persistence.Version 注解,表示必须基于该属性进行乐观锁操作。类型必须为 Integer。如果你在 Studio 中为实体选择了 Versioned 特性,Studio 会自动创建这样一个的属性。
8 @InstanceName 注解,表示选择一个属性作为实例名称。
9 javax.validation.constraints 包中的 @NotNull@Email 注解,是使用 Bean 验证 注解的示例。
10 @Column 注解中,unique = true 表示数据库变更机制应该为该字段添加唯一性约束。

表名和实体名都可以通过前缀避免与其他模块中的实体名称冲突。如果在 build.gradle 中设置了 jmix.projectId 属性,Studio 会用它做前缀。

特性

特性是一组属性,赋予实体一些特定的系统级行为。这些属性由框架处理,不能由用户或你的应用程序代码修改。

Studio 的实体设计器能帮助你将可用的特性指定给实体。你也可以手动实现,创建相应的属性并按照下面的方法进行标注:

Has UUID 特性

Has UUID 特性提供在内存中创建实例时自动分配一个全局唯一标识符的功能。用于使用 @JmixGeneratedValue 注解的 UUID 类型的属性上。

在 Studio 中创建实体时,如果你为字段的 Id type 选择了 UUID,则会自动使用 Has UUID 特性。如果你选择了其他类型并且 Id valueGenerated by Jmix 不同,则需要手动添加 Has UUID 特性。

如果你将 UUID 类型作为实体标识符,则 Has UUID 特性中不再需要添加额外的属性。

如果你的实体在内存中创建时不能马上分配一个标识属性,则强烈建议使用 Has UUID 特性。不论是映射到标识列的 LongInteger 标识符,还是用户指定的任何类型,都建议这样做。如果这样的实体没有 @JmixGeneratedValue 属性,则其 hashCode() 方法总是返回一个常量值,这会影响基于哈希表的集合的性能。

版本特性

Versioned(版本)特性提供了 JPA 级别的乐观锁。由一个用 @Version 注解的 integer 属性实现。

永远不要在应用程序代码中修改 @Version 属性的值;否则将导致无法更新数据库中的实例。

审查特性

Audit of creationAudit of modify 特性用来追踪创建和修改实体实例的用户和时间。由 Spring Data 的 @CreatedBy@CreatedDate@LastModifiedBy@LastModifiedDate 标注的合适类型的属性实现。

例如:

@CreatedBy
@Column(name = "CREATED_BY")
private String createdBy;

@CreatedDate
@Temporal(TemporalType.DATE)
@Column(name = "CREATED_DATE")
private Date createdDate;

@LastModifiedBy
@Column(name = "LAST_MODIFIED_BY")
private String lastModifiedBy;

@LastModifiedDate
@Temporal(TemporalType.DATE)
@Column(name = "LAST_MODIFIED_DATE")
private Date lastModifiedDate;

框架保存实体实例时会自动设置 Audit 相关属性。

软删除特性

Soft Delete 特性提供了实体实例的软删除功能。由 @DeletedDate@DeletedBy 标注的一对属性实现,例如:

@DeletedBy
@Column(name = "DELETED_BY")
private String deletedBy;

@DeletedDate
@Temporal(TemporalType.DATE)
@Column(name = "DELETED_DATE")
private Date deletedDate;

更多详细信息参见 软删除 章节。

实体继承

Jmix 框架使用 JPA 的继承原则。Studio 实体设计器会根据所选的继承策略自动生成必要的注解。下面是一些主要的注解:

@DiscriminatorColumn

在选择 SINGLE_TABLEJOINED 继承策略时,用该注解定义一个数据库列,此列负责区分实体的类型。

参数:

  • name - 区分列的名称。

  • discriminatorType - 区分列的类型。

  • length - 字符串类型区分列的长度。

示例:

@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)

@DiscriminatorValue

定义实体的区分列值。

示例:

@DiscriminatorValue("0")

@Inheritance

定义实体类层级结构中的继承策略。该注解在实体类层级结构的根实体上进行指定。

参数:

  • strategy - 继承策略。默认为 SINGLE_TABLE

@MappedSuperclass

定义该类为某些实体的祖先,其属性必须在后代实体中使用。这种类型的类不关联任何特定的数据库表。

@PrimaryKeyJoinColumn

用在 JOINED 继承策略中,指定实体的外键列,指向祖先实体的主键。

参数:

  • name - 实体外键列的名称。

  • referencedColumnName - 祖先实体的主键列名称。

示例:

@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")

DTO 实体

应用程序的数据模型可能包含仅存在于内存中的实体,或者使用非 JPA 机制映射的一些外部数据。我们称此类实体为 DTO,因为它们通常被用作参数中的数据传输对象,也会在 REST API 中或与外部 API 通信时被用做返回值。

一个最简单的 DTO 实体示例:

OperationResult.java
@JmixEntity (1)
public class OperationResult {

    private String result; (2)

    private Integer errorCode; (2)

    private String errorMessage; (2)

    public String getResult() {
        return result;
    }

    public void setResult(String result) {
        this.result = result;
    }

    // other getters and setters
1 必须的 @JmixEntity 注解。
2 所有对象属性( 带存取方法的字段 )都是实体属性。

也可以用注解为实体属性指定一些细节:

ProductPart.java
@JmixEntity (1)
public class ProductPart {

    @JmixProperty(mandatory = true) (2)
    @InstanceName (3)
    private String name;

    private Integer quantity; (4)

    // getters and setters
1 @JmixEntity 注解显式定义实体名称。
2 带有 mandatory = true 参数的 @JmixProperty 注解表示该属性是必需的,即必须有值。
3 这里的 @InstanceName 注解表示选择一个属性作为实例名称。
4 一个没有任何注解的属性。

DTO 实体可以与 自定义数据存储 关联,通过 DataManager 进行通用的 CRUD 操作,并能自动解析 JPA 实体对 DTO 实体的引用。

在下面的示例中,你还可以看到如何将某些对象属性排除在实体属性之外(在 实体属性 章节有更多介绍):

Metric.java
@Store(name = "inmem") (1)
@JmixEntity(annotatedPropertiesOnly = true) (2)
public class Metric {

    @JmixProperty(mandatory = true) (3)
    @JmixId (4)
    @JmixGeneratedValue (5)
    private UUID id;

    @JmixProperty (6)
    private String name;

    @JmixProperty (6)
    private Double value;

    private Object ephemeral; (7)

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    // other getters and setters
1 @Store 注解指定了一个自定义数据存储。
2 @JmixEntity 注解的 annotatedPropertiesOnly = true 参数表示未使用 @JmixProperty 标注的对象属性不是实体属性。
3 @JmixProperty 注解中的 mandatory = true 参数表示该属性是必需的,即它必须有值。
4 @JmixId 注解表示用该属性作为实体标识。
5 @JmixGeneratedValue 注解表示在内存中创建实体实例时必须由 Jmix 生成并分配实体标识属性的值。
6 这里的 @JmixProperty 注解表示它是一个实体属性。
7 由于 @JmixEntity 注解设置了 annotatedPropertiesOnly = true 参数,没有注解的属性则不是实体属性。

Key-Value 实体(键值实体)

KeyValueEntity 允许你将任意数量的命名值集合作为实体,用来处理不能使用 Java 类(JPA 或 DTO 实体)表示的数据。

比如这个例子:你的数据模型中有 Order(订单)实体,你需要计算按客户汇总的订单金额总和并在 UI 展示。可以执行一个 JPQL 查询并使用 DataManager 将结果集加载为一个 KeyValueEntity 实例列表:

List<KeyValueEntity> entities = dataManager.loadValues(
            "select e.customer, sum(e.amount) from Order_ e group by e.customer")
        .properties("customer", "total")
        .list();

返回的 KeyValueEntity 实例具有你在 properties() 方法中指定的两个属性:customer 属性值为查询结果集中第一个字段的值,而 total 为第二个字段的值。可以按如下方式读取:

for (KeyValueEntity entity : entities) {
    Customer customer = entity.getValue("customer");
    BigDecimal totalAmount = entity.getValue("total");
    // ...
}

Jmix UI 有特殊的 key-value 数据容器 用于将 UI 组件与 KeyValueEntity 实例进行绑定。

实体属性

实体属性的英文术语除了 Entity Attributes,还有另一个术语:Entity Properties。经常在 Jmix 代码中使用,比如这些注解:@JmixProperty@DependsOnProperties 等。

每个实体属性都应具有对应的类型。Jmix 开箱支持以下类型:

  • java.lang.String

  • java.lang.Character

  • java.lang.Boolean

  • java.lang.Integer

  • java.lang.Long

  • java.lang.Double

  • java.math.BigDecimal

  • java.util.Date

  • java.time.LocalDate

  • java.time.LocalTime

  • java.time.LocalDateTime

  • java.time.OffsetTime

  • java.time.OffsetDateTime

  • java.sql.Date

  • java.sql.Time

  • java.util.UUID

  • java.net.URI

  • byte[](字节数组)

  • 枚举

  • 实体或实体集合(引用属性)

如果你需要的类型不在上述列表,你可以实现一个对应的 数据类型 并确保 数据存储 支持该类型,然后就可以作为实体属性的类型使用。

请注意,Java 原始类型(intboolean 等)不能作为实体属性的类型。

在 JPA 和 DTO 实体中,属性有两种类型:

  • Field-based attribute,基于字段的属性,对应一个字段和该字段的一对存取方法(getter / setter)。字段名称作为属性名称。

    setter 可以省略,表示该属性为只读。

    基于字段的属性示例:

    User.java
    @Column(name = "FIRST_NAME")
    protected String firstName;
    
    public String getFirstName() {
        return firstName;
    }
    
    public void setFirstName(final String firstName) {
        this.firstName = firstName;
    }
  • Method-based attribute,基于方法的属性,对应一个不带参数的方法,它返回支持的类型,并且名称以 get 开头,比如 getCustomer()。方法名去掉 get,剩下的部分首字母改为小写则为属性名:getFullName()fullName

    基于方法的属性示例:

    User.java
    @JmixProperty
    @DependsOnProperties({"firstName", "lastName"})
    public String getFullName() {
        return this.firstName + " " + this.lastName;
    }

实体类的某些属性(字段 + getter/setter)和非实体属性的类方法,可以不包含在实体元数据中。因此,虽然可以在应用程序代码中使用它们,但框架无法识别这些字段和方法,也不会在 UI 中显示或通过 REST API 传输。

一个属性或限定方法是否成为实体属性是基于以下规则判定的:

  • 如果 @JmixEntity 注解的 annotatedPropertiesOnly 参数为 false(默认值),则以下对象属性为实体属性:

    • 对于 JPA 实体:除了 @jakarta.persistence.Transient 标注的所有其他属性。

    • 对于 DTO 实体:所有属性。

    • 对于两者:使用 @JmixProperty 标注的所有属性和方法。

  • 如果 annotatedPropertiesOnly 参数设置为 true,则只有用 @JmixProperty 标注的属性和方法为实体属性。

实体关系

引用属性定义了实体间的关系。引用可以是单个值(对一),也可以是集合(对多)。

默认情况下,实体之间的关系是 association(关联),也就是说两个实体可以不依赖彼此独立存在,互相没有所有权。例如,在一个 Customer - Order(客户-订单)关系中,Order 有一个属性是对 Customer 的引用。用户分别创建 Customer 和 Order,为 order 选择 customer,甚至在需要的时候改为对另一个 customer 的引用。

Jmix 还支持一种更强的实体关联关系,叫做 composition(组合)组合 意味着存在所有权,一个实体实例只能作为其所有者实体实例的一部分存在。例如,在 Order - OrderLine 关系中,Order 有一个属性,是 OrderLine 实例的集合。每个 OrderLine 实例都是为特定的 Order 创建,是 Order 的一部分,不能属于另一个 Order。

组合 关系的实体在 UI 中一并编辑。例如,用户打开一个 Order 编辑界面,可以在其单独的编辑界面中创建和编辑 OrderLines,但是 Order 及其所有的 OrderLines 更改会在同一个事务中一起保存到数据库中,并且是只在用户确认保存所有者实体(即 Order)的时候。

组合 关系由引用属性上的 @Composition 注解指定。

Studio 实体设计器支持在 Attribute type 字段中选择关系类型。

跨数据库引用

DataManager 可以自动处理来自不同 数据存储 的单一实体引用。

当从不同的数据存储中选择实体做关联时,Studio 实体设计器会自动定义跨数据存储引用的属性集。

例如,在主数据存储中有 Customer 实体,在附加数据存储中有 Address 实体,并且你希望有从 CustomerAddress 的引用。那么 Customer 实体应该包含以下两个属性:

@SystemLevel
@Column(name = "ADDRESS_ID")
private UUID addressId; (1)

@Transient
@JmixProperty
@DependsOnProperties("addressId")
private Address address; (2)

public UUID getAddressId() {
    return addressId;
}

public void setAddressId(UUID addressId) {
    this.addressId = addressId;
}

public Address getAddress() {
    return address;
}

public void setAddress(Address address) {
    this.address = address;
}
1 addressId 属性存储 Address 的标识符。带 @SystemLevel 注解,提示框架该属性不应显示给用户。
2 address 属性包含对 Address 实体的引用。这个属性是 @Transient(不会存储在数据库),使用 @DependsOnProperties 注解,提示框架该属性值依赖于另一个属性。

之后,当你使用包含 address 属性的 fetch plan 加载 Customer 时,DataManager 会自动从附加数据存储中加载相关的 Address。本框架优化了集合的加载性能:加载客户列表后,会从附加数据存储中批量加载引用。批量的数目由 jmix.core.cross-data-store-reference-loading-batch-size 应用程序属性定义。

当保存带 AddressCustomer 实体图时,DataManager 通过相应的 DataStore 保存实例,然后将地址的标识符保存在 customer 的 addressId 属性中。

实体实例化

创建 JPA 和 DTO 实例时,请使用适当的框架接口,不要使用 new 运算符调用类构造函数;这样可以保证框架正确的初始化 @JmixGeneratedValue 标注的字段并调用 @PostConstruct 方法。

实体实例化最常用的核心方法是 Metadata.create()

@Autowired
private Metadata metadata;

Order createOrder() {
    return metadata.create(Order.class);
}

如果你在编写业务逻辑并且在代码中已经有 DataManager,可以使用其 create() 方法,这样可以避免额外注入 Metadata bean。例如:

@Autowired
private DataManager dataManager;

Order createAndSaveOrder(String number) {
    Order order = dataManager.create(Order.class);
    order.setNum(number);
    dataManager.save(order);
    return order;
}

在 UI 视图中,上述两种方法都可以使用。但是你可能还希望将创建的实例通过视图的 DataContext 自动保存,此时,请使用 DataContext.create() 方法,会创建实例并立即合并到数据上下文,开始跟踪其变化。在下面的示例中,我们创建了一个 ProductPart 实体实例,合并到 DataContext 中,并添加到一个数据容器以显示在 UI 表格中:

@ViewComponent
private DataContext dataContext;

@ViewComponent
private CollectionPropertyContainer<ProductPart> partsDc;

@Subscribe("createProductPart")
public void onCreateProductPartClick(final ClickEvent<JmixButton> event) {
    ProductPart part = dataContext.create(ProductPart.class);
    partsDc.getMutableItems().add(part);
}

唯一性

Jmix 依赖数据库的唯一约束来管理实体实例的唯一性。因此,如果需要设置实体的某个属性或者一组属性唯一,则需要为数据库表创建相应的索引。

Studio 实体设计器 包含 Indexes 标签页,可以定义唯一索引。索引定义保存在实体的 @Table 注解中,后续 Liquibase 用来创建数据库结构中的索引。

Jmix 不会以任何方式解析 @Column 注解的 unique = true 属性。

如需为带有软删除特性的实体定义唯一属性,参阅 软删除 章节。

Jmix 实体注解

Jmix 实体注解按字母顺序描述如下。

Jmix 实体也可以有 JPA 映射、审查特性软删除 的注解。

@Composition

@Composition 注解用于引用属性,表示该关系是一种 composition(组合关系)。例如:

@Composition
@OneToMany(mappedBy = "order")
private List<OrderLine> lines;

@DbView

@DbView 注解表示一个 JPA entity 实体是否映射到数据库视图。这类实体不会生成数据库变更脚本。

@DdlGeneration

@DdlGeneration 注解表示是否需要开发工具为该实体生成 DDL 脚本。

脚本生成模式通过 DbScriptGenerationMode 枚举设置:

  • CREATE_AND_DROP - 完整的初始化和更新脚本;

  • CREATE_ONLY - 完整的初始化脚本。更新脚本不包含删除列的语句;

  • DISABLED - 初始化和更新脚本都不生成。

默认值:CREATE_AND_DROP

此外,你可以使用以下属性微调脚本生成逻辑:

  • unmappedColumns - 存在于数据库中但不应映射到实体的列。不会生成这些列的删除脚本;

  • unmappedConstraints - 存在于数据库中但不应映射到实体的约束和索引。不会生成这些列的删除脚本。

@DependsOnProperties

@DependsOnProperties 注解指定该属性所依赖的实体属性。在构建 fetch plans 以及加载/保存来自不同数据存储的实体的引用时,会考虑这些属性。此外,如果被标注的属性是只读的(没有 setter 方法),则在更改它时会发送 EntityPropertyChangeEvent 事件。

只能标注直接属性。不支持像 customer.name 这样的路径属性。

@InstanceName

Instance name 是表示一个实体实例的可阅读文本。可以将它理解为应用程序级别的 toString() 方法。广泛用于在界面的单个控件或表格控件的单元格中显示实体实例。还可以用编程的方式通过 MetadataTools.getInstanceName() 方法获取实例名称。

@InstanceName 注解可以出现在单个字段或方法上。

在前一种情况下,被标注的属性值将作为实例名称。例如:

@InstanceName
@Column(name = "NAME")
private String name;

如果想生成比单个属性值更复杂的结果,可以在实体类中创建一个返回类型为 String 的方法。例如:

@JmixEntity(name = "sample_GeoPoint")
@Embeddable
public class GeoPointEntity {

    @Column(name = "LAT")
    protected Double latitude;

    @Column(name = "LON")
    protected Double longitude;

    @InstanceName
    @DependsOnProperties({"latitude", "longitude"})
    public String getDisplayName(Messages messages) {
        return messages.formatMessage(
                getClass(), "GeoPointEntity.instanceName", this.latitude, this.longitude);
    }

该方法可以接受任何 Spring beans 作为参数。在上面的示例中,Messages bean 用来根据当前用户的区域设置格式化实例名称。

实例名称方法的 @DependsOnProperties 注解是必要的,因为它指定了内置的 _instance_name fetch plan 需要的属性。

@JmixEntity

@JmixEntity 是一个强制注解,表示该类是一个 Jmix 实体。

如果类带有 @jakarta.persistence.Entity 注解,框架会从中获取元数据的实体名称,这时 @JmixEntity 不能指定 name 参数。如果没有被 @javax.persistence.Entity 标注,则在 @JmixEntityname 参数中指定实体名称。如果 @JmixEntity@javax.persistence.Entity 都没有 name 参数,则实体名称等于其 Java 类的简单名称。

annotatedPropertiesOnly 参数指定哪些对象属性会成为实体属性,请参阅 实体属性 了解更多详细信息。

@JmixGeneratedValue

@JmixGeneratedValue 注解表示在内存中创建实体实例时必须由框架生成并分配实体属性值。

被标注的属性必须是 LongIntegerUUID 类型,并且一个实体不能有多个带此注解的 UUID 属性。

请注意,如果你使用 new 运算符创建实体实例,@JmixGeneratedValue 注解不会生效。有关创建新实例的正确方法,请参阅 实例化实体

@JmixId

@JmixId 注解为 DTO 实体 指定实体标识。如果你的 DTO 实体映射到一些外部数据并且你需要重复加载/保存其实例,则应该显式地选择一个标识符,因为在这种情况下,你需要在实体生命周期中维护对象标识。

如果某个属性值唯一,你可以将它用作标识,例如:

@JmixId
private String code;

如果没有这样的自然存在的唯一属性值,可以创建一个并用 @JmixGeneratedValue 注解在创建实例时分配唯一值:

@JmixId
@JmixGeneratedValue
private UUID id;

@JmixProperty

@JmixProperty 注解表示该属性或方法是实体属性。更多详细信息,请参阅 实体属性

使用 mandatory 参数来表示该属性需要有值,如果对象字段没有 JPA @Column 注解,你可以设置 nullable = false

@NumberFormat

Number 类型(可以是 BigDecimalIntegerLongDouble)的属性指定数字格式。这些类型的属性值会按照注解参数指定的格式进行格式化:

  • pattern - DecimalFormat 中描述的格式。

  • decimalSeparator - 小数点符号(可选)。

  • groupingSeparator - 千分位符号(可选)。

如果未设置 decimalSeparatorgroupingSeparator,则对于依赖语言环境的格式化,这些值从 当前用户的 locale 中获取;对于不依赖语言环境的格式化,则从服务器的默认 locale 中获取。

示例:

@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;

@PostConstruct

jakarta.annotation.PostConstruct 标注新实体实例初始化的方法。例如:

@PostConstruct
void init() {
    setGrade(CustomerGrade.BRONZE);
}

被标注的方法接受任何 Spring bean。在下面的示例中,我们使用 TimeSource bean 来初始化一个 date 属性:

@PostConstruct
void init(TimeSource timeSource) {
    setDate(timeSource.now().toLocalDate());
}
如果你使用 new 运算符创建实体实例,@PostConstruct 注解的方法不会被调用到。有关正确的创建新实例的方法,请参阅 实例化实体

@PropertyDatatype

如果实体属性的 Java 类型有多个 数据类型,则 @PropertyDatatype 注解可以通过其 id 显式指定 Datatype 实现。例如:

@PropertyDatatype("year")
@Column(name = "YEAR_")
private Integer productionYear;

@Store

在实体类上使用 @Store 注解将实体与附加的 数据存储 关联。

@SystemLevel

@SystemLevel 标注的实体或属性是底层的,不会在界面中显示。