数据类型
字符串格式本地化
许多标准数据类型使用 消息包 中定义的字符串格式。可以根据当前用户的区域设置进行格式化和解析。框架中默认的字符串格式如下:
# 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
文件中:
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;
不能使用 如果需要的话,注入 |
自定义 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);
}
}