Datatype 接口

每个非引用类型的实体属性都与一个 Datatype 接口的实现相关联。此接口定义了在 用户界面(UI) 中显示实体和在 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 org.springframework.lang.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;

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

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

DatatypeFormatter

DatatypeFormatter 是一个工具 bean,用于在编程式对不同的数据类型值进行格式化和解析。会自动应用当前用户语言环境的 本地化格式字符串

示例:

import io.jmix.core.metamodel.datatype.DatatypeFormatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.time.LocalDate;

@Component
public class DataFormatterService {

    private final DatatypeFormatter datatypeFormatter;

    public DataFormatterService(DatatypeFormatter datatypeFormatter) {
        this.datatypeFormatter = datatypeFormatter;
    }

    public String formatDate (LocalDate date) {
        return datatypeFormatter.formatLocalDate(date);
    }

    public LocalDate parseDate(String value) throws ParseException {
        return datatypeFormatter.parseLocalDate(value);
    }

}

自定义 Java 类型

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

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

import java.io.Serializable;

public record GeoPoint(double latitude, double longitude) implements Serializable {
}

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

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

import com.company.demo.entity.GeoPoint;
import jakarta.persistence.AttributeConverter;
import jakarta.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 com.company.demo.entity.GeoPoint;
import io.jmix.core.metamodel.annotation.DatatypeDef;
import io.jmix.core.metamodel.annotation.Ddl;
import io.jmix.core.metamodel.datatype.Datatype;
import org.springframework.lang.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), 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 注解,表示 Studio 应该在数据库为该类型的属性生成 varchar(255) 类型的列。参考 后续内容
5 默认的格式化(没有区域设置时)。进行系统级转换时使用此方法。
6 基于区域设置的格式化。在 UI 中时使用此方法。
7 默认解析方法。进行系统级转换时使用此方法。
8 基于区域设置的解析。在 UI 中时使用此方法。

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

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

@Ddl 注解

datatype 类的 @Ddl 注解中可以指定实体属性应该使用的 SQL 类型。Studio 会按照注解的配置生成 数据库迁移 脚本。

注解的 value 属性可以设置 SQL 类型,示例:

@DatatypeDef(id = "foo", javaClass = Foo.class, defaultForClass = true)
@Ddl("varchar(255)")
public class FooDatatype implements Datatype<Foo> {

如果需要为不同的数据库配置不同的 SQL 类型,可以添加多个 @Ddl 注解,通过 dbms 属性设置数据库类型。下面的示例中,同一个实体属性在 PostgreSQL 中使用 text 类型,在 Oracle 中使用 varchar2(255),而在其他数据库中,使用 varchar(255) 类型:

@DatatypeDef(id = "bar", javaClass = Bar.class, defaultForClass = true)
@Ddl("varchar(255)")
@Ddl(value = "text", dbms = "postgres")
@Ddl(value = "varchar2(255)", dbms = "oracle")
public class BarDatatype implements Datatype<Bar> {

Studio 支持以下 dbms 属性值:

  • hsql

  • postgres

  • mysql

  • mssql

  • mssql-2005

  • mssql-2008

  • mssql-2012

  • oracle