Embedded马上就要露出庐山真面目了. 本质上也是映射成为值类型, 来看看如何把一个类加上其中属性都映射成为值类型.

  1. Embedded 内嵌类 – @Embeddable
  2. 多个同类型内嵌类
  3. 内嵌类中还有内嵌类
  4. 类型转换 – 基本类型和数值
  5. 类型转换 – 字符类型
  6. 类型转换 – 时间类型
  7. 类型转换 – 二进制和大数字
  8. 类型转换 – 将类转换成一个值
  9. 类型转换 – 转换继承体系的类

Embedded 内嵌类

像之前分析的一样, User类型中的Address不需要共享. 意味着在数据库层面, 实际上只要添加若干列来保存一个对应某个User数据的类型就可以.

然后Address为了方便起见, 也是一个Java类, 并没有直接将其拆分为几个成员变量, 直接加到User类型上. 我们甚至需要在Java代码中通过User来访问Address类型.

这个时候我们来看看Address 和 User类到底应该如何映射. 先看Address类:

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.validation.constraints.NotNull;

//被@Embeddable注解的类, 内部不需要一个@Id注解
@Embeddable
public class Address {

    //验证所用, 创建表格时候不看@NotNull
    @NotNull

    //@Column建表的时候参考这个注解, 这个相当于 not null 约束
    @Column(nullable = false)
    protected String street;

    @NotNull
    //length控制长度, 这里相当于varchar(5), 覆盖了默认的varchar(255)的设定
    @Column(nullable = false, length = 5)
    protected String zipcode;

    @NotNull
    @Column(nullable = false)
    protected String city;

    protected Address() {
    }

    public Address(String street, String zipcode, String city) {
        this.street = street;
        this.zipcode = zipcode;
        this.city = city;
    }

    ......
}

可以看到, Address是一个单独的Java类, 但是我们要将其映射成为值类型, 把一个类映射成为值类型, 就需要在其上添加@Embeddable注解, 使用这个注解, 就不需要@Id了,
说明其和@Entity是两种类型的映射.

这里要注意的是, @NotNull约束仅仅在运行时才有效, 如果需要创建表的时候就带上not null约束, 需要使用@Column, 毕竟@NotNull是验证器注解, 而不是持久化注解, 这点要搞清楚.

然后是关键的User类型, 看代码:

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.math.BigDecimal;

@Entity
@Table(name = "USERS")
public class User implements Serializable {

    @Id
    @GeneratedValue()
    protected Long id;

    protected String username;

    protected Address homeAddress;

    public User() {
    }

    public Long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Address getHomeAddress() {
        return homeAddress;
    }

    public void setHomeAddress(Address homeAddress) {
        this.homeAddress = homeAddress;
    }

    ......

}

可以看到, Address已经被注解了@Embeddable, User类中的Address类型的成员变量, 压根不需要任何注解, 因为Hibernate会检测到Address对应的持久化类型, 然后会将Address中的属性,
映射到User表中.

Address这样就被映射成一个User类的Embedded内嵌类, 而且Hibernate对Address的访问类型会继承User类的访问类型(@Access), 除非明确指定.

在集成的时候, 时刻要注意, Address的各个值最好不是null, 否则取出来的三个属性都是null, 和Java类中本身address都是null, 会无法在概念上区分开来, 因为在内嵌类变量上加不加@NotNull都没用,
只看字段是不是null. 所以这要求, 持久化的时候一定要获取足够的信息.

现在运行一个测试看看究竟生成了什么语句:

@Test
public void test() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    //创建Address对象
    Address address = new Address("owlroad", "12345", "Shanghai");

    //创建User对象并设置地址
    User user = new User();
    user.setUsername("cony");
    user.setHomeAddress(address);

    em.persist(user);

    tx.commit();

    //查询
    tx.begin();
    List<User> result = em.createQuery("SELECT u FROM User u",User.class).getResultList();
    System.out.println(result);
    tx.commit();
}

建表和插入数据库的语句是:

Hibernate:
    create table USERS (
        id int8 not null,
        city varchar(255) not null,
        street varchar(255) not null,
        zipcode varchar(5) not null,
        username varchar(255),
        primary key (id)
    )

insert
    into
        USERS
        (city, street, zipcode, username, id)
    values
        (?, ?, ?, ?, ?)

可以发现, 对于数据库来讲, 并没有address表, Address对象的所有属性都嵌入到了User对象中, 但是取出来的时候, 又自动映射到Java类型上, 确实棒.

多个同类型内嵌类

现在已经知道了User和Address之间是组合关系, 如果此时还想在User中再添加一个Address属性, 用于保存这个用户的额外地址, 比如账单地址.

很显然, 在Java层面要再添加一个Address类型的成员变量, 但问题接踵而至, 列名怎么办. 如果都是自动映射到Address上, 在Address类中更改列名解决不了冲突问题.

这个时候就需要使用@AttributeOverrides来覆盖默认设置, 代码如下:

@Entity
@Table(name = "USERS")
public class User implements Serializable {

    ......

    @NotNull
    @AttributeOverrides({
        @AttributeOverride(name = "street",
                column = @Column(name = "BILLING_STREET")),
        @AttributeOverride(name = "zipcode",
                column = @Column(name = "BILLING_ZIPCODE", length = 10)),
        @AttributeOverride(name = "city",
                column = @Column(name = "BILLING_CITY"))
    })
    protected Address billingAddress;

    ......
}

很显然,@AttributeOverrides中包含所有要覆盖的属性的条目, 每一个条目都是一个@AttributeOverride注解, @AttributeOverride中的name是要覆盖的列名,
要与Address类中的列名对应.

然后在@AttributeOverride写要覆盖的属性名称和设置, 注意@AttributeOverride的覆盖是完整覆盖,
也就是说Address上的@Column中的内容会被完全抛弃.

通过测试看一下:

@Test
public void test2() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    //创建Address对象和billingAddress对象
    Address homeAddress = new Address("owlroad", "12345", "Shanghai");
    Address billingAddress = new Address("timberroad", "66666", "Shanghai");

    //创建User对象并设置地址
    User user = new User();
    user.setUsername("cony");
    user.setHomeAddress(homeAddress);
    user.setBillingAddress(billingAddress);

    em.persist(user);

    tx.commit();

    tx.begin();
    List<User> result = em.createQuery("SELECT u FROM User u", User.class).getResultList();

    for (User u : result) {
        System.out.println(u);
    }

    tx.commit();
}

实际的语句是:

Hibernate:
create table USERS (
   id int8 not null,
    BILLING_CITY varchar(255),
    BILLING_STREET varchar(255),
    BILLING_ZIPCODE varchar(10),
    city varchar(255) not null,
    street varchar(255) not null,
    zipcode varchar(5) not null,
    username varchar(255),
    primary key (id)
)

打印查询结果是:

User{id=40, username='cony', homeAddress=Address{street='owlroad', zipcode='12345', city='Shanghai'},
    billingAddress=Address{street='timberroad', zipcode='66666', city='Shanghai'}}

可以看到, 实际还是映射了和Address一样类型的数值, 但是列名不同, 取出来的时候自动映射到两个不同的成员变量上. 确实好用.

内嵌类中还有内嵌类

目前Address中都是基本类型, 而且User类型虽然组合了两个Address类, 但都是直接组合.

现在考虑这样一种情况, 即Address类中只有一个基本类型street, 然后还有一个City类, City类有cityName和zipcode两个基本类型. 即内嵌类中还有内嵌类.

其实处理很简单, 只要将City也标记为@Embeddable, 就行了, 对于City的属性覆盖, 则在其上一层的Address中完成就可以了.

修改一下上一个例子中的类. 首先是City类:

import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.validation.constraints.NotNull;

@Embeddable
public class City {

    @NotNull
    @Column(nullable = false)
    private String cityName;

    @NotNull
    @Column(length = 5)
    private String zipcode;

    ......

}

City类比较简单, 现在要将Address类中加入City引用:

@Embeddable
public class Address {

    @NotNull
    @Column(nullable = false)
    protected String street;

    @NotNull
    @AttributeOverrides({
            @AttributeOverride(name = "cityName",
                    column = @Column(name = "addname")),
            @AttributeOverride(name = "zipcode",
                    column = @Column(name = "addzip", length = 10))
    })
    protected City city;

    ......
}

可以在Address里对City进行属性覆盖, 比如这里就将City类的cityName名称, 覆盖成了addname名称, 注意覆盖的名称不能和User类中其他列名称相同, 注解中也可以写成name =
“city.cityName”. 如果不进行任何覆盖, 就会使用City类中的列名.

如果有多层嵌套, 就可以这样一直嵌套映射下去, 直到最后一个内嵌类的所有成员变量都是基本类型.

使用的方法与原来一样, 创建City, Address类, 最后给User类设置上:

@Test
public void test3() {
    EntityManagerFactory emf =
            Persistence.createEntityManagerFactory("HelloWorldPU");
    EntityManager em = emf.createEntityManager();
    EntityTransaction tx = em.getTransaction();
    tx.begin();

    City city = new City();
    city.setCityName("shanghai");
    city.setZipcode("66666");

    Address address = new Address();
    address.setStreet("kiwiroad");
    address.setCity(city);

    User user = new User();
    user.setUsername("cony");
    user.setHomeAddress(address);

    em.persist(user);

    tx.commit();
}

不过这里我试验了, 如果再有一个billingAddress成员, 也映射到address类, 这时候配置就算在billingAddress上覆盖列名, 也会提示重复, 还暂时不知道原因. 不过目前应该是够用了.

到目前为止, 将一个类型映射成为Entity或者Embedded类型都会了, 映射的问题初步解决了. 现在就来看看映射的过程中如何使用类型转换器.

类型转换 – 基本类型和数值

映射的本质就是类型转换, 从Java类型到SQL类型的互相转换. 到目前为止, 不论什么样的属性, 似乎添加几个注解就可以发现Hibernate将其转换成了某种数据库类型. 这就是类型转换器的功劳, 即将一个Java类(和属性)转换成SQL类型. 可以来总结一下类型转换了.

类型转换器主要有两种, 一种是内建转换器, 一种是自定义转换器. 之前都是用的基本类型转换器, 即使是Embedded类型, 其实也是一个拆解, 最后还是落到实际的基本类型上.

JPA标准提供了一个Java类型到SQL类型的转换列表, 要求JPA provider必须实现这些转换. 凡是Java成员变量属于这些类型, 不加任何注释的话, 都会使用列表中的映射关系.

基本类型和数值
Hibernate类型名称 Java类型 SQL标准类型
integer int, java.lang.Integer INTEGER
long long, java.lang.Long BIGINT
short short, java.lang.Short SMALLINT
float float, java.lang.Float FLOAT
double double, java.lang.Double DOUBLE
byte byte, java.lang.Byte TINYINT
boolean boolean, java.lang.Boolean BOOLEAN
big_decimal java.math.BigDecimal NUMERIC
big_integer java.math.BigInteger NUMERIC

由于历史原因, 很多数据库的数据类型的名称并不是表中的ANSI名称, 但是Hibernate可以通过配置方言来自动使用对应的名称, 所以不用太关心. 这里唯一特殊的是NUMERIC类型, 需要指定两个参数. 使用@Column的presision和scale参数即可.

如果想知道具体对应关系, 可以查看Hibernate的方言类源代码或者JavaDoc.

类型转换 – 字符类型

字符类型也是非常常用的类型.

字符类型
Hibernate类型名称 Java类型 SQL标准类型
integer int, java.lang.Integer INTEGER
string java.lang.String VARCHAR
character char[], Character[], java.lang.String CHAR
yes_no boolean, java.lang.Boolean CHAR(1), 'Y' or 'N'
true_false boolean, java.lang.Boolean CHAR(1), 'T' or 'F'
locale java.util.Locale VARCHAR
timezone java.util.TimeZone VARCHAR
currency java.util.Currency VARCHAR
class java.lang.Class VARCHAR

关于字符类型有几个特别之处:

  1. 纯粹存放字符的话, 一般数据库都有若干种实现, Hibernate是根据长度和方言来确定使用何种字符类型的. Hibernate默认的所有字符串映射是varchar(255), 可以在@Column中设置length.
  2. 很多数据库都提供了UTF-8支持, 如果想细粒度的控制, 尤其是NVARCHAR, NCHAR等要求使用UTF-8的类型, 可使用@org.hibernate.annotations.Nationalized. 不过如今一些数据库默认编码就是UTF-8, 也可以不使用该设置.
  3. 很多数据库比如PgSQL对于布尔类型可以使用多种方式表示, 结合方言和这里的规则具体使用.

类型转换 – 时间类型

时间类型对于SQL而言有三种, 但Java中的类型很多:

时间类型
Hibernate类型名称 Java类型 SQL标准类型
integer int, java.lang.Integer INTEGER
date java.util.Date, java.sql.Date DATE
time java.util.Date, java.sql.Time TIME
timestamp java.util.Date, java.sql.Timestamp TIMESTAMP
calendar java.util.Calendar TIMESTAMP
calendar_date java.util.Calendar DATE
duration java.time.Duration BIGINT
instant java.time.Instant TIMESTAMP
localdatetime java.time.LocalDateTime TIMESTAMP
localdate java.time.LocalDate DATE
localtime java.time.LocalTime TIME
offsetdatetime java.time.OffsetDateTime TIMESTAMP
offsettime java.time.OffsetTime TIME
zoneddatetime java.time.ZonedDateTime TIMESTAMP

虽然时间的对应有很多, 但其实不复杂, TIMESTAMP是一个信息最全的对象, 而TIME的信息最少, 如果不是追求细节, 都用TIMESTAMP对象也未尝不可. 要点如下:

  1. java.sql.3种类型可以直接对应具体数据库类型.
  2. 一般使用java.util.Date对象也可以, 通过TemporalType.DATE/TIME/TIMESTAMP三种类型指定具体映射关系.
  3. java.util.Date类型精确到毫秒, 而java.sql.Timestamp精确到纳秒. Hibernate在将TIMESTAMP转换成Java的Date类型的时候会保留到纳秒的信息, 结果就是不能简单的通过.equals()比较两个时间. 而是要通过取出时间日期属性的时候来比较.

所以对于时间的处理, 只要牢记使用时间处理函数, 而不是Java对象自己的比较方式就可以.

类型转换 – 二进制和大数字

二进制和大数字
Hibernate类型名称 Java类型 SQL标准类型
integer int, java.lang.Integer INTEGER
binary byte[], java.lang.Byte[] VARBINARY
text java.lang.String CLOB
clob java.sql.Clob CLOB
blob java.sql.Blob BLOB
serializable java.io.Serializable VARBINARY

二进制的要点如下:

  1. 如果一个Java属性是byte[], 会映射到VARBINARY, 但是实际采用的SQL属性还是需要结合方言和数据库决定. 比如在PgSQL中, 是BYTEA类型.
  2. 通常情况下String映射到VARCHAR,但是长度太长的时候, 就会映射为text, 从而使用CLOB.
  3. 如果想强制byte[]使用CLOB或者BLOB属性, 就采用@Lob注解即可:
        @Entity
            public class Item {
            @Lob
            protected byte[] image;
            @Lob
            protected String description;
            // ...
        }
    

    这会让byte[]变成BLOB, 而对String采用CLOB, 这也是将String映射成为text类型最简单的方法.

  4. 对于LOB系列, JDBC会延迟加载, 即在加载对象的时候, LOB类型的属性只是一个占位符, 需要的时候可以采取流的方式读出来, 或者复制到一个输出流中. 比如:
        Item item = em.find(Item.class, ITEM_ID);
        InputStream imageDataStream = item.getImageBlob().getBinaryStream();
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        StreamUtils.copy(imageDataStream, outStream);
        byte[] imageBytes = outStream.toByteArray();
    
  5. 对于Hibernate来说, 提供了一些辅助工具用于方便的读取流, 比如:
        Session session = em.unwrap(Session.class);
        Blob blob = session.getLobHelper().createBlob(imageInputStream, byteLength);
        someItem.setImageBlob(blob);
        em.persist(someItem);
    
  6. Hibernate对于无法映射的二进制类型, 如果是类型是Serializable, 会将其按照序列化之后的二进制, 以VARBINARY类型保存在数据库中. 但是一般来说不要如此使用, 因为这会让Java对象的生存周期超过应用的周期, 将来谁也不知道数据库里存了什么东西.

如今需要存储文件的场景不太多, 额外的文件一般都提供文件服务器, 不过类似于头像之类的也可以在测试代码中简单尝试一下.

还有一个类型适配器的概念. 在上边可以发现, 一种Java类型可以映射为很多类型. 比如boolean可以映射成BOOLEAN, 也可以映射成CHAR(1), 这就很有意思.

如果想覆盖默认的Hibernate替你选择的类, 可以使用上边所有表中的Hibernate的名称, 以及@Type注解来指定具体的类型:

@Entity
public class Item {
@org.hibernate.annotations.Type(type = "yes_no")
    protected boolean verified = false;
}

如果不使用@Type注解, 这个boolean将会被映射成为BOOLEAN类型, 现在指定了”yes_no”属性, 就不会被映射成很多数据库中的BIT或者BOOLEAN类型, 而是会映射成为CHAR(1).

类型转换 – 将类转换成一个值

这个类型转换器很有意思. 将一个类转换成一列. 书里的例子很有意思, 就是这个拍卖数据库中的所有价格, 都可以国际化, 采用的方法就是在Java的层面, 用一个MonetaryAmount类型来取代BigDecimal类型.

但是这个类型和之前的Embedded类不同, 不是仅仅为类增加额外的属性, 而是同一个属性代表不同的意思, 如下:

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Currency;

public class MonetaryAmount implements Serializable {

    protected final BigDecimal value;
    protected final Currency currency;

    public MonetaryAmount(BigDecimal value, Currency currency) {
        this.value = value;
        this.currency = currency;
    }

    public BigDecimal getValue() {
        return value;
    }

    public Currency getCurrency() {
        return currency;
    }

    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof MonetaryAmount)) return false;

        final MonetaryAmount monetaryAmount = (MonetaryAmount) o;

        if (!value.equals(monetaryAmount.value)) return false;
        if (!currency.equals(monetaryAmount.currency)) return false;

        return true;
    }

    public int hashCode() {
        int result;
        result = value.hashCode();
        result = 29 * result + currency.hashCode();
        return result;
    }

    public String toString() {
        return getValue() + " " + getCurrency();
    }

    public static MonetaryAmount fromString(String s) {
        String[] split = s.split(" ");
        return new MonetaryAmount(
            new BigDecimal(split[0]),
            Currency.getInstance(split[1])
        );
    }
}

这个类有两个成员变量, 其中一个还是一个Currency类, 然后编写了hashcode()和equals()用于比较相等, 这个属性映射成的类都需要编写此方法, 因为Hibernate使用这个来判断是否需要更新, veli . 之后还编写了一个从String 中生成这个实例的方法.

这个类的类型是Serializable, 意味着Hibernate会在二级缓存中保存其二进制代码.

如果现在用这个类替换掉所有的BigDecimal类型, 很显然, 没办法将其保存到数据库中, 因为按照之前学到的内容, 将其映射成一个@Entity的话, 似乎不太对, 因为只是一个价格.

如果映射成一个@Embedded类型倒是可以, 但是还需要对Currency来映射.

牛逼的是, 我们可以采用JPA标准提供的类型转换器, 将一个类转换成一个SQL基本类型. 具体的说, 是将Java类在保存的时候转换成SQL类型的值, 在取出的时候, 调用合理的方法将值转换成Java类. 这样无论这个类有多复杂, 只要实现了对象和一个值之间的转换, 就可以用一列来保存一个对象.

JPA提供的接口是javax.persistence.AttributeConverter:

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

@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<MonetaryAmount, String> {

    @Override
    public String convertToDatabaseColumn(MonetaryAmount monetaryAmount) {
        return monetaryAmount.toString();
    }

    @Override
    public MonetaryAmount convertToEntityAttribute(String s) {
        return MonetaryAmount.fromString(s);
    }
}

@Converter注解表示这是一个类型转换器, 设置autoApply为true表示所有持久化类中, 无论是Entity还是Embedded, 只要类型是MonetaryAmount, 没有加上特别的注解, 都会使用这个转换器来持久化.

这个泛型接口有两个泛型, 第一个泛型表示Java类型, 第二个泛型表示存放到数据库中的类型. 然后有两个方法需要覆盖, 从名字就能看出来, 一个是把Java类型转换成数据库中类型的, 一个恰好相反.

在MonetaryAmount中已经编写好了为了这两个接口的函数. 下一步就是要在使用到这个MonetaryAmount类型的持久化类上进行设置. 这里就自建一个简单的类加测试(访问器方法都省略):

import org.junit.Test;

import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
import java.util.Currency;

@Entity
public class Sr {

    @Id
    @GeneratedValue
    private long id;

    @NotNull
    @Column(name = "PRICE", length = 63)
    @Convert(converter = MoneyConverter.class, disableConversion = false)
    private MonetaryAmount monetaryAmount;


    @Override
    public String toString() {
        return "Sr{" +
                "id=" + id +
                ", monetaryAmount=" + monetaryAmount +
                '}';
    }

    @Test
    public void test() {
        EntityManagerFactory emf =
                Persistence.createEntityManagerFactory("HelloWorldPU");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        Sr sr = new Sr();

        MonetaryAmount monetaryAmount = new MonetaryAmount(new BigDecimal("25.00"), Currency.getInstance("CNY"));

        sr.setMonetaryAmount(monetaryAmount);

        em.persist(sr);

        tx.commit();
    }
}

@Convert(converter = MoneyConverter.class, disableConversion = false)指定了转换器, 并且这个还提供了一个简单的选项用于控制是否启用转换器. 虽然启用了autoApply, 但是推荐还是完整的写上注解.

生成表的语句是:

Hibernate:

    create table Sr (
    id int8 not null,
    PRICE varchar(63),
    primary key (id)
)

可以看到, 由于发现了要转换成String类型, 所以PRICE列被持久化成为varchar类型. 查看数据库可以发现, 其中写入了"25.00 CNY" 这样的字符串. 从其中查询出来的肯定是字符串, 转换的过程发生在应用级别.

更方便的是, 如果MonetaryAmount类型本身都是基本类型, 如果哪一天需要将其转换成为Embedded类(比如在数据库中添加了额外的两行保存金额和货币名称, 只需要在Sr类上去掉Convert等注解, 然后把MonetaryAmount注解为@Embedded就可以了.

Hibernate的方便之处又一次感受到了. 到目前为止, 知道了基本类型的映射, Embedded类的属性拆解到主体类中的映射, 以及将一个类映射成为一个值的手段, 可以根据需要灵活选用.

类型转换 – 转换继承体系的类

这个就不用写代码了. 简述一下.

假设Sr类又多了一个属性, 类型是PostCode, 这个PostCode是一个抽象类, 有两个子类ChinaPostCode和USAPostCode, 二者的不同是ChinaPostCode有6位长的邮编, USAPostCode内部的邮编字符串是5位.

这个时候编写的转换类可以是这样:

@Converter
public class ZipcodeConverter implements AttributeConverter<PostCode, String> {

    @Override
    public String convertToDatabaseColumn(PostCode postcode) {
        return attribute.getValue();
    }

    @Override
    public Zipcode convertToEntityAttribute(String s) {
        if (s.length() == 5)
            return new GermanZipcode(s);
        else if (s.length() == 4)
            return new SwissZipcode(s);
        else
        throw new IllegalArgumentException(
            "Unsupported zipcode in database: " + s
        );
    }
}

其他的配置都类似, 这样就可以像多态一样来转换, 相比刚才一个具体的类转换成一个值, 现在数据库中的一个列可以对应一个体系中的类, 更加扩展了类型转换的范围.

这里留下一个问题, 也就是Hibernate的UserType的使用, 待以后再看.