数据类型

每个非引用类型的实体属性都与一个 Datatype 接口的实现相关联。此接口定义了在 用户界面 中显示实体和在 REST API 中序列化时将属性值与字符串转换(格式化和解析)的方法。

本框架提供了一组对应于 实体属性 标准数据类型的 Datatype 实现。

字符串格式本地化

许多标准数据类型使用 消息包 中定义的字符串格式。可以根据当前用户的区域设置进行格式化和解析。框架中默认的字符串格式如下:

# Date/time formats
dateFormat = dd/MM/yyyy
dateTimeFormat = dd/MM/yyyy HH:mm
offsetDateTimeFormat = dd/MM/yyyy HH:mm Z
timeFormat = HH:mm
offsetTimeFormat = HH:mm Z

# Number formats
integerFormat = #,##0
doubleFormat = #,##0.###
decimalFormat = #,##0.##

# Number separators
numberDecimalSeparator = .
numberGroupingSeparator = ,

# Booleans
trueString = True
falseString = False

如需提供自定义的字符串格式,需要将相应的消息添加到应用程序的消息包中。例如,要为英语区域设置美国日期格式,将以下行添加到你的 messages_en.properties 文件中:

messages_en.properties
dateFormat = MM/dd/yyyy
dateTimeFormat = MM/dd/yyyy HH:mm
offsetDateTimeFormat = MM/dd/yyyy HH:mm Z

或者,定义一个单独的 en_US 区域环境并在 messages_en_US.properties 文件中设置字符串格式。

你可以使用 Studio 配置字符串格式:打开 Project Properties 窗口的 Locales 选项卡,然后勾选 Show data format strings 复选框。

定制格式和解析

你可以将自己创建的数据类型分配给属性,实现特定实体属性值的自定义格式和解析。

假设你的应用程序中的某些实体属性存储日历的年份,由 integer 表示。用户应该能够查看和编辑年份,如果用户只输入两位数字,应用程序应将其转换为 2000 到 2100 之间的年份。否则,将输入的整个数字视为年份。

首先,实现 Datatype 接口并用 @DatatypeDef 注解:

import com.google.common.base.Strings;
import io.jmix.core.metamodel.annotation.DatatypeDef;
import io.jmix.core.metamodel.annotation.Ddl;
import io.jmix.core.metamodel.datatype.Datatype;

import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;

@DatatypeDef(
        id = "year", (1)
        javaClass = Integer.class (2)
)
@Ddl("int")
public class YearDatatype implements Datatype<Integer> {

    private static final String PATTERN = "##00";

    @Override
    public String format(@Nullable Object value) { (3)
        if (value == null)
            return "";
        DecimalFormat format = new DecimalFormat(PATTERN);
        return format.format(value);
    }

    @Override
    public String format(@Nullable Object value, Locale locale) { (4)
        return format(value);
    }

    @Nullable
    @Override
    public Integer parse(@Nullable String value) throws ParseException { (5)
        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 { (6)
        return parse(value);
    }
}
1 数据类型的唯一标识。
2 该数据类型处理的 Java 类。
3 默认的格式化(没有区域设置时)。进行系统级转换时使用此方法。
4 基于区域设置的格式化。在 UI 中时使用此方法。
5 默认解析方法。进行系统级转换时使用此方法。
6 基于区域设置的解析。在 UI 中时使用此方法。

创建 Datatype 实现后,你可以使用 @PropertyDatatype 注解指定给实体属性:

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

不能使用 @Autowired 将其他 beans(例如 Messages) 直接注入到数据类型类中,因为数据类型在启动过程的早期就已初始化,而且此类注入将导致循环依赖。

如果需要的话,注入 ApplicationContext 并使用它的 getBean() 方法来获取所需的 bean。

自定义 Java 类型

你可以使用自定义的 Java 类作为实体属性的类型。

假设你创建了一个表示地理坐标的 Java 类:

import java.io.Serializable;
import java.util.Objects;

public class GeoPoint implements Serializable {

    public final double latitude;
    public final double longitude;

    public GeoPoint(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GeoPoint that = (GeoPoint) o;
        return Double.compare(that.latitude, latitude) == 0 &&
                Double.compare(that.longitude, longitude) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(latitude, longitude);
    }
}

现在你想将它用作 JPA 实体属性的类型。

首先,为你的类创建一个 JPA 转换器:

import datamodel.ex1.entity.GeoPoint;

import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter(autoApply = true) (1)
public class GeoPointConverter implements AttributeConverter<GeoPoint, String> {

    @Override
    public String convertToDatabaseColumn(GeoPoint attribute) {
        if (attribute == null)
            return null;
        return attribute.latitude + "|" + attribute.longitude;
    }

    @Override
    public GeoPoint convertToEntityAttribute(String dbData) {
        if (dbData == null)
            return null;
        String[] strings = dbData.split("\\|");
        return new GeoPoint(Double.parseDouble(strings[0]), Double.parseDouble(strings[1]));
    }
}
1 使用 autoApply = true 以后,你不需要在每个属性上都指定转换器。转换器将应用于对应类型的所有属性。

然后为 GeoPoint 实现 Datatype 接口,并用 @DatatypeDef 标注:

import io.jmix.core.metamodel.annotation.DatatypeDef;
import io.jmix.core.metamodel.annotation.Ddl;
import io.jmix.core.metamodel.datatype.Datatype;
import datamodel.ex1.entity.GeoPoint;

import javax.annotation.Nullable;
import java.text.ParseException;
import java.util.Locale;

@DatatypeDef(
        id = "geoPoint", (1)
        javaClass = GeoPoint.class, (2)
        defaultForClass = true (3)
)
@Ddl("varchar(255)") (4)
public class GeoPointDatatype implements Datatype<GeoPoint> {

    @Override
    public String format(@Nullable Object value) { (5)
        if (value instanceof GeoPoint) {
            return ((GeoPoint) value).latitude + "|" + ((GeoPoint) value).longitude;
        }
        return null;
    }

    @Override
    public String format(@Nullable Object value, Locale locale) { (6)
        return format(value);
    }

    @Nullable
    @Override
    public GeoPoint parse(@Nullable String value) throws ParseException { (7)
        if (value == null)
            return null;
        String[] strings = value.split("\\|");
        try {
            return new GeoPoint(Double.parseDouble(strings[0]), Double.parseDouble(strings[1]));
        } catch (Exception e) {
            throw new ParseException(String.format("Cannot parse %s as GeoPoint: %s", value, e.toString()), 0);
        }
    }

    @Nullable
    @Override
    public GeoPoint parse(@Nullable String value, Locale locale) throws ParseException { (8)
        return parse(value);
    }
}
1 数据类型的唯一标。
2 该数据类型所处理的 Java 类。
3 defaultForClass = true 表示数据类型将自动应用于所有 GeoPoint 类型的实体属性。
4 使用`@Ddl`注解,你可以指定实体属性对应的 SQL 类型。Studio 在生成 数据库变更 脚本时会考虑此注解。
5 默认的格式化(没有区域设置时)。进行系统级转换时使用此方法。
6 基于区域设置的格式化。在 UI 中时使用此方法。
7 默认解析方法。进行系统级转换时使用此方法。
8 基于区域设置的解析。在 UI 中时使用此方法。

之后,当你定义 GeoPoint 类型的实体属性时,框架将使用你定制的 JPA 转换器和数据类型:

@Column(name = "GEO_POINT")
private GeoPoint geoPoint;

转换错误信息

当 UI 组件使用一个数据类型解析字符串输入时,有可能会产生解析异常。UI 组件会处理这个异常并展示 对用户友好的信息。这些信息在框架的 消息包 中通过 databinding.conversion.error.<datatype-id> 键值定义。例如:

databinding.conversion.error.boolean=Must be Boolean

消息的完整列表,请参阅 messages.properties 文件,选择你项目使用的 Jmix 版本对应的分支查看。

如果某个数据类型的消息不存在,则使用下面这个默认的通用消息:

databinding.conversion.error.defaultMessage=Wrong format

在项目中提供相同键值的消息可以覆盖默认的消息。此外,也可以为自定义数据类型提供错误消息,例如:

databinding.conversion.error.year=Incorrect year format

直接使用数据类型

大多数时候,Datatype 的实现类仅在框架内部使用,提供对实体属性的解析和格式化功能。但有时候可能需要在代码中直接使用数据类型。

假设你有一个 TextField 组件,并未绑定任何实体属性:

<textField id="amountField"/>

如需在组件内部输入小数值,可以在界面控制器内通过 DatatypeRegistry bean 获取数据类型并分配给该组件:

@Autowired
private TextField<BigDecimal> amountField;

@Autowired
private DatatypeRegistry datatypeRegistry;

@Subscribe
public void onInit(InitEvent event) {
    Datatype<BigDecimal> datatype = datatypeRegistry.get(BigDecimal.class);
    amountField.setDatatype(datatype);
}
事实上,在 XML 中可以更容易为文本控件设置数据类型,参考组件的 datatype 属性。

如果需要获取实体属性的数据类型,可以通过 metadata 实现。下面是一个稍微复杂的示例,使用实体属性的对应数据类型解析小数值:

@Autowired
private Metadata metadata;

private BigDecimal parseAmountValue(String stringValue) {
    MetaClass metaClass = metadata.getClass(Order.class);
    Datatype<BigDecimal> amountDatatype = metaClass.getProperty("amount")
            .getRange().asDatatype();
    assert amountDatatype instanceof BigDecimalDatatype;
    try {
        return amountDatatype.parse(stringValue);
    } catch (ParseException e) {
        throw new RuntimeException("Cannot parse amount", e);
    }
}