Embeddables是简单的Java对象。它们提供了一种简单的方法来定义和分组一组成为实体一部分的属性。开发人员经常使用它们来创建可重用的映射信息,并使用同一段业务代码来处理它们。
不幸的是,JPA规范和Hibernate直到6.0.0版本都要求你的embeddable有一个默认构造函数。如果你要从数据库中获取一个实体,这可能是可以的。Hibernate会在向业务代码提供对象之前自动设置所有属性。但默认构造函数并不总是一个好主意。例如,如果一些属性是强制性的,并且你在业务代码或前端实例化了可嵌入的对象。在这种情况下,一个设置所有强制性属性的构造函数会更合适。
自Hibernate 6.0.0以来,你可以轻松地定义Hibernate如何实例化和初始化你的可嵌入物。你可以利用这一点,例如,删除默认构造函数的要求。我将在本文中向你展示如何做到这一点。
内容
- 1什么是Embeddable以及如何定义它
- 2实现一个EmbeddableInstantiator
- 3 使用带有自定义EmbeddableInstantiator的Embeddable
- 4结论
什么是Embeddable以及如何定义它
一个Embeddable是由多个属性和它们的映射定义组成的。你可以在一个或多个实体类中使用它作为一个属性类型。这样做的时候,嵌入的所有属性都会成为实体对象的一部分,并跟随其生命周期。
这里你可以看到Address embeddable的定义。如果你想依靠Hibernate对其所有属性的默认映射,你只需要用 @Embeddable注解来注释这个类。如果你还不熟悉这种映射,我建议你查看Hibernate的文档或观看Persistence Hub中高级Hibernate课程的Embeddable讲座。
@Embeddable
public class Address {
private String street;
private String city;
private String postalCode;
// getter and setter methods
}
在你定义了embeddable之后,你可以在你的实体类中把它作为一个属性类型,并以与其他实体属性相同的方式使用它。在这个例子中,地址 可嵌入的所有属性成为作者实体的一部分,并被映射到作者 表中。
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private int version;
private String firstName;
private String lastName;
@Embedded
private Address address;
// getter and setter methods
}
实现一个EmbeddableInstantiator
默认情况下,Hibernate会调用地址 可嵌入器的默认构造函数。之后,当你从数据库中获取一个作者 实体时,它会使用反射来设置它的所有属性。从Hibernate 6开始,你可以通过提供一个EmbeddableInstantiator来定制embeddable的实例化。
让我们用它来避免Address embeddable的默认构造函数,而使用一个设置所有属性的构造函数。这需要对地址类做两个小改动。我需要添加额外的构造函数,并且我需要注册我的EmbeddableInstantiator。你可以通过用*@EmbeddableInstantiator*注解可嵌入类或你的可嵌入类型的实体属性来做到这一点。
@Embeddable
@EmbeddableInstantiator(AddressInstantiator.class)
public class Address {
private String street;
private String city;
private String postalCode;
public Address(String street, String city, String postalCode) {
this.street = street;
this.city = city;
this.postalCode = postalCode;
}
// getter methods
}
在下一步,你必须实现EmbeddableInstantiator 接口。这并不复杂。该接口只定义了3个方法。一个方法检查一个对象是否是被处理的可嵌入类的一个实例。另一个方法检查一个对象是否与可嵌入的类相同。最后一个方法是实例化可嵌入的对象。
这里你可以看到我在前面的代码片断中的*@EmbeddableInstantiator注解中引用的AddressInstantiator* 类。
public class AddressInstantiator implements EmbeddableInstantiator {
Logger log = LogManager.getLogger(this.getClass().getName());
public boolean isInstance(Object object, SessionFactoryImplementor sessionFactory) {
return object instanceof Address;
}
public boolean isSameClass(Object object, SessionFactoryImplementor sessionFactory) {
return object.getClass().equals( Address.class );
}
public Object instantiate(Supplier<Object[]> valuesAccess, SessionFactoryImplementor sessionFactory) {
final Object[] values = valuesAccess.get();
// valuesAccess contains attribute values in alphabetical order
final String city = (String) values[0];
final String postalCode = (String) values[1];
final String street = (String) values[2];
log.info("Instantiate Address embeddable for "+street+" "+postalCode+" "+city);
return new Address( street, city, postalCode );
}
}
正如你在代码片断中所看到的,instantiate 方法包含了实例化和初始化可嵌入对象的代码。当然,这个方法的实现是特定于应用程序的。
但有一件事我需要指出。方法参数Supplier<Object[]> valuesAccess包含了从数据库中按属性名称的字母顺序选择的属性值。在代码片段中,你可以看到我从Supplier那里获得了3个值。我把它们分配给命名的变量,并把每个变量都投给了String。这种映射可能会导致未来的可维护性问题,但它至少使代码更容易理解。如果你对此有更好的想法,我很想在评论中读到。
使用带有自定义EmbeddableInstantiator的Embeddable
在你定义和注册了EmbeddableInstantiator之后,你可以像其他的embeddable一样使用你的embeddable。
你可以在你的一个实体类上把它作为一个属性类型,并用*@Embedded*来注释这个属性。
@Entity
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Version
private int version;
private String firstName;
private String lastName;
@Embedded
private Address address;
...
}
之后,你可以像其他属性一样在你的业务代码中使用该属性:
Author a = new Author();
a.setFirstName("firstName");
a.setLastName("lastName");
Address home = new Address("homeStreet", "homeCity", "12345");
a.setAddress(home);
em.persist(a);
当我执行这段代码时,你可以在日志输出中看到,Hibernate将地址可嵌入的所有属性映射到作者 表中,并使用AddressInstantiator 来实例化地址 对象。
结论
正如你在这篇文章中所看到的,Hibernate 6中引入的EmbeddableInstantiator契约在处理可嵌入对象时给了你更多的灵活性。它让你完全控制了可嵌入对象的实例化和初始化。你可以利用这一点做各种事情。你可以在实例化可嵌入对象之前执行额外的业务逻辑或转换或计算属性值。或者你可以避免使用默认的构造函数,而是调用一个完全初始化可嵌入对象的构造函数。