1 业务模型
为了说明 Hibernate 的领域模型与库表结构,这里举一个在线拍卖系统的例子。
1.1 层级架构
一般的应用系统都会采用层级架构,因为有如下好处:
- 每一个层只依赖于下一层定义的接口。所以只要下一层定义的接口不变,就对她没有影响。
- 每一个层并不知道其他层的存在,所以其他层的变化,不会影响她。
- 表现层包含用户的使用逻辑,位于顶层。在有些架构中,表现层不能直接调用业务逻辑实体,因为表现层的代码与用业务逻辑的代码位于不同的服务器中。在这种情况下,表现层通常还需要特别的数据传输模型。
- 业务逻辑层包含业务规则或者问题领域中能被用户理解的系统需求。
- 持久层中,这里采用的是实现了 JPA 的 Hibernate。
- 拦截器与工具类:会被应用中的每一层类所调用,比如异常类啦,或者错误处理类啦。
1.2 分析业务模型
在业务模型中,工程师与架构师会先创建一个面向对象的模型,她仍然是属于概念层面的。在我们的拍卖系统中,包含分类、拍卖物以及用户,这是真实世界的抽象:
1.3 典型拍卖系统的领域模型
假设这个拍卖系统拥有许多种拍卖物,涵盖了电子设备到飞机票。拍卖规则是用户可以不断对一个拍卖物竞拍,直到这个拍卖物的竞拍时间结束为止,价高者得。每一个拍卖物竞拍成功后,就不能再参与竞拍了。。每一个拍卖物都至少属于每一个分类:
之所以搞得这么复杂,是为了以后演示 Hibernate 的许多特性。
2 实现领域类模型
2.1 注意事项
- 分层设计使得在领域类模型上进行单元测试,变得很容易。
- Hibernate 框架是一个实现了 JPA 规范的持久层框架,她只关注持久层。
- 实践中的领域类模型一般是 POJO(Plain Old Java Object,简单 Java 对象)
2.2 编写具有持久化能力的类
2.2.1 使用 POJO 实现 User 类
package net.deniro.hibernate.model.simple;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* @author Deniro Li
* 2017/2/4
*/
@Entity
@Table(name = "USERS")
public class User implements Serializable {
protected String username;
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public BigDecimal calcShippingCosts(Address fromLocation) {
//Empty implementation of business method
return null;
}
}
JPA 并没有要求持久化类必须要实现 java.io.Serializable
,但如果类实例可能被存在在 HttpSession 中,或者被 RMI 作为传输对象,那么序列化就是必须的。
具有持久化能力的类可以是抽象类,也可以继承一个非持久化类或者实现一个接口,但不能是嵌套类。JPA 规范要求她的任何方法都不能是 final。
具有持久化能力的类必须要有一个没有任何参数的构造函数,因为 Hibernate 会使用 Java 反射的 API,即通过这个构造函数来创建类的实例。构造函数可以不是 public,但她必须至少是包级别可见,因为 Hibernate 为了保证性能,使用的是运行时生成的类代理实际的类。
一般来说,会把类成员变量设置为 private 或者 protected,然后再使用 getter 和 setter 方法来存取属性。
Hibernate 会利用这些成员变量的存取器,来操作这些值。get 的命名方式也适用于 Boolean 类型的值。
在 Hibernate 的配置中,把存取类成员变量的方式改为基于 field 的方式,这样就可以把这些存取器设定为非 public 甚至完全移除她们咯 O(∩_∩)O~。
建议把类成员变量设定为 protected,这样不仅避免了外部类直接操作的危险,而且允许继承的子类可以直接操作一些特别的类成员变量。
2.2.2 User 类中使用逻辑存取器
public class User{
protected String firstname;
protected String lastname;
public String getName(){
return firstname + ' ' + lastname;
}
public void setName(String name){
StringTokenizer t = new StringTokenizer(name);
firstname = t.nextToken();
lastname = t.nextToken();
}
}
使用自定义的类型转换器可以很好地适应各种情况。
2.2.3 检测脏数据
Hibernate 会自动检测实例状态,这样在实例中的值发生改变时,会自动同步到数据库。Hibernate 比较的是实例的值而不是实例对象本身,通过比较来觉得实例的值是否需要更新。因此使用 setter 方法设置的值,可以在 getter 方法返回各种各样的值。
下面的这个 getter 方法不会受到数据库数据更新的影响:
public String getFirstname(){
return new String(firstname);
}
注意: 下面说的集合类型是一个例外,她们比较的实例本身,即是否是同一实例。
因此,如果类成员变量是一个集合类型时,必须在 getter 方法中返回之前在 setter 中设置的变量。如果不这样做,Hibernate 将会自动同步到数据库!这是完全没有必要的操作,每次调用 getter 方法都会更新数据库,想想都觉得可怕!所以一定要避免在代码中这样写:
protected String[] names = new String[0];
public void setNames(List<String> names){
this.names = names.toArray(new String[name.size()]);
}
public List<String> getNames(){
return Arrays.asList(names);//如果 Hibernate 是通过 getter 获取属性值的情况下,千万不要这样做!!!
}
如果 Hibernate 配置的是以 field 模式对类成员变量进行存取的话,就不会发生上面所说的问题。
如果 Hibernate 在使用类的存取器时出现问题,那么她将会抛出一个 RuntimeException,当前的事务会被回滚,你可以捕获这些异常。
2.3 实现 POJO 的关联
假设每一个拍卖物(Item)可能存在 0 或者 多个的竞拍价(Bid),即拍卖物与竞拍价之间是整体与部分的关系,所以这里采用组合关系:
在 Bid 中添加 Item 属性,这样就把 Bid 与其对应的 Item 关联起来了:
public class Bid {
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
}
Item 中也关联了 Bid 集合,这里展示了最佳实践,关联的类型采用了 java.util.Set
接口类型:
public class Item {
protected Set<Bid> bids = new HashSet<Bid>();
public Set<Bid> getBids() {
return bids;
}
public void setBids(Set<Bid> bids) {
this.bids = bids;
}
}
我们在 Bid 与 Item 之间设置了双向关联关系。
这里也可以使用 java.util.List
,对 Bid 进行排序。
关联的存取器必须声明为 public。
任何建立了双向关联连接的对象,都必须在新建时完成以下动作:
- Item 中的 bids 中新增一个 Bid 对象。
- 新增的 Bid 中再设置 Item 属性。
anItem.getBids().add(aBid);
aBid.setItem(anItem);
JPA 本身不会管理持久化关联关系,所以在双向关联关系上,我们必须通过代码把它们之间的关系关联上。
建议把这些操作集中起来,作为一个方法,这样可以实现代码复用:
protected Set<Bid> bids = new HashSet<Bid>();
public Set<Bid> getBids() {
return bids;
}
public void addBid(Bid bid) {
if (bid == null)//Be defensive
throw new NullPointerException("Can't add null Bid");
if (bid.getItem() != null)
throw new IllegalStateException("Bid is already assigned to an Item");
getBids().add(bid);
bid.setItem(this);
}
上面的代码中,通过参数验证,保证了数据完整性。
有了 addBid() 方法,setBids() 方法就可以改为 private ,甚至直接移除咯(这个前提是 Hibernate 被配置为直接通过 field 方式存取数据)。Bid#setItem() 方法因为同样的原因也可以直接移除咯。
Item#getBids() 如果返回的是可修改的 Collection,就存在被调用者修改的情况,所以可以这样返回一个不可变的集合(注意, Hibernate 必须被配置为直接通过 field 方式存取数据,因为 getter 方法与 setter 方法返回的已经不是同一个集合对象咯):
Collections.unmodifiableCollection(c);
Collections.unmodifiableSet(s);
另一种策略是使用不可变实例。可以在 Bid 的构造函数中强制传入一个 Item 参数:
public class Bid {
protected Item item;
public Bid(Item item){
this.item=item;
item.getBids().add(this);//Bidirectional
}
public Item getItem() {
return item;
}
}
这种策略有一些问题:
- Hibernate 无法调用类的构造函数,所以必须新增一个无参数的构造函数以供 Hibernate 调用,它必须至少是包可见的。
- 因为没有 SetItem() 方法,所以 Hibernate 必须被配置为直接通过 field 方式存取数据。这也意味着 field 无法被设置为 final,也就是说无法保证类是不可变的。
基于上述原因,所以不推荐这么做。
3 领域类模型的元数据
ORM 工具需要在映射文件中定义元数据,来说明类与表、属性与列,关联与外键以及Java 类型与 SQL 类型之间的映射关系。创建与维护这种映射关系是工程师的工作。
JPA 定义了两种元数据的定义规范:
- 在 Java 代码中的注解方式
- 在 XML 中的标签方式
我们也将讨论 Bean Validation 规范,它为领域模型类提供了声明验证机制。Hibernate Validator 项目实现了 Bean Validation。
如今的工程师更喜欢采用 Java 注解方式来定义元数据。
3.1 基于注解定义元数据
注解方式的最大好处是可以把元数据直接放在领域模型类中:
@Entity
public class Item {
}
这种的注解方式是类型安全的,JPA 的元数据定义直接包含在编译后的 class 文件中。我们用的 IDE 也可以对 JPA 注解进行验证和高亮提示,因为它们是标准 Java 注解的一部分。
因为 Hibernate 会在运行时读取你定义的元数据,所以 JPA 的依赖包必须放在 classpath 中。
3.1.1 使用 Hibernate 定义额外的注解
如果标准的 JPA 规范无法满足业务要求,可以考虑使用 Hibernate 定义额外的注解。比如某些应用有高性能的要求,那么可以使用 Hibernate 定义的性能选项注解:
@Entity
@org.hibernate.annotations.Cache(
usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE
)
3.1.2 使用全局注解来定义元数据
我们使用一个特别的文件来存储定义的具有包级别范围的元数据。这个文件名为 package-info.java ,它被放在需要应用这些元数据规则包内:
@org.hibernate.annotations.NamedQueries({
@org.hibernate.annotations.NamedQuery(
name = "findItemsOrderByName",
query = "select i from Item i order by i.name asc"
),
@org.hibernate.annotations.NamedQuery(
name = "findItemBuyNowPriceGreatorThan",
query = "select i from Item i where i.buyNowPrice > :price",
timeout = 60,//seconds
comment = "Custom SQL comment"
)
}
) package net.deniro.hibernate.model.querying;
这个文件只包含 Hibernate 的注解声明。
3.2 添加 Bean 的验证规则
大多数的应用系统会使用多种数据验证机制,从而确保数据完整性。用户接口层用于显示错误消息。业务逻辑层和持久化层对传入的值进行验证。数据库是最后一级的验证器,这些都为了确保用于持久化的数据的完整性。
在 Bean 中声明属性的规则,可以在我们系统中的每一层对数据进行验证,你可以认为 Bean 验证器是一种额外的 ORM 元数据定义。
Item.java:
@NotNull
@Size(
min = 2,
max = 255,
message = "Name is required, maximum 255 characters."
)
protected String name;
@Future
protected Date auctionEnd;
如果你字段添加了验证注解,那么验证器引擎将通过字段级别读取这些配置。如果你更喜欢采用存取器的方式进行验证,那么可以把这些注解放在 getter 方法中。
我们还可以自定义验证器注解,甚至自定义出应用于类级别的注解,以及可以在一个类实例中同时验证多个属性的值是否正确:
/**
* 使用 Bean 验证器
*/
@Test
public void validateItem() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setName("Some Item");
item.setAuctionEnd(new Date());
Set<ConstraintViolation<Item>> violations = validator.validate(item);
// We have one validation error, auction end date was not in the future!
ConstraintViolation<Item> violation = violations.iterator().next();
String failedPropertyName = violation.getPropertyPath().iterator().next().getName();
assertEquals(failedPropertyName, "auctionEnd");
if (Locale.getDefault().getLanguage().equals("zh"))
assertEquals(violation.getMessage(), "需要是一个将来的时间");
}
在 pom.xml 中引入 Hibernate 的验证器就可以拥有以下功能啦:
- 在把数据写入库表之前,就会自动对传入的 Bean 数据进行验证。
- 当验证失败,Hibernate 会抛出
ConstraintViolationException
,里面包含详细的错误信息。 - Hibernate 工具集通过 Bean 定义的约束注解,会自动更新 SQL 的库表结构。比如
@NotNull
会把字段设置为 Not Null。
<!-- Bean 验证器 API 与实现-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate.validator.version}</version>
</dependency>
可以通过 <validation-mode>
设置 Hibernate 的验证模式:
- AUTO:默认,只有在 classpath 中添加了 Bean 验证实现的包(比如 hibernate-validator)才会进行验证。
- CALLBACK:如果忘记在classpath 中添加了 Bean 验证实现的包,就会直接抛出错误。
- NONE:关闭 Bean 验证。
3.3 在 XML 中配置元数据
有些特别的场景下,比如开发和测试的部署包,配置的元数据并不一样,这时就可以在 XML 中进行特别的配置。
你可以在 XML 中替换或覆盖掉原来用 JPA 注解配置的信息。
3.3.1 JPA 方式下的元数据配置 XML
可以在配置中指定只覆盖某些类或者类中的某些属性。
Mappings.xml:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings
version="2.1"
xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm">
<!-- First, global metadata-->
<persistence-unit-metadata>
<!-- Ignore all annotations, all mapping metadata in XML files-->
<xml-mapping-metadata-complete/>
<!-- Some default settings-->
<persistence-unit-defaults>
<!-- Escape all SQL column/table/etc. names,
e.g. if your SQL names are actually keywords (a "USER" table for example)-->
<delimited-identifiers/>
</persistence-unit-defaults>
</persistence-unit-metadata>
<entity class="net.deniro.hibernate.model.simple.Item" access="FIELD">
<attributes>
<id name="id">
<generated-value strategy="AUTO"/>
</id>
<basic name="name"/>
<basic name="auctionEnd">
<temporal>TIMESTAMP</temporal>
</basic>
<transient name="bids"/>
<transient name="category"/>
</attributes>
</entity>
</entity-mappings>
配置了 <xml-mapping-metadata-complete/>
元素,就会忽略之前在 Bean 中所配置的所有的 JPA 注解,也就是只有这里的 XML 配置才有效。
persistence.xml:
<!-- xml 配置-->
<persistence-unit name="SimpleXMLCompletePU">
<jta-data-source>deniroDS</jta-data-source>
<mapping-file>simple/Mappings.xml</mapping-file>
<mapping-file>simple/Queries.xml</mapping-file>
<properties>
<!-- Ignore hbm.xml files and annotated classes-->
<property name="hibernate.archive.autodetection" value="none"/>
</properties>
</persistence-unit>
注意: Bean 上配置的验证注解,对于自动验证以及更新表结构的功能,仍然是有效的。
3.3.2 Hiberante 原生方式下的元数据配置 XML
配置文件一般被命名为 xxx.hbm.xml:
<?xml version="1.0"?>
<hibernate-mapping
xmllns="http://www.hibernate.org/xsd/orm/hbm"
package="net.deniro.hibernate.model.simple"
default-access="field"
>
<!-- Entity class mapping-->
<class name="Item">
<id name="id">
<generator class="native"/>
</id>
<property name="name"/>
<property name="auctionEnd" type="timestamp"/>
</class>
<!-- Externalized queries-->
<query name="findItemHibernate">select i from Item</query>
<!-- Auxiliary schema DDL-->
<database-object>
<create>create index ITEM_NAME_IDX on ITEM(NAME)</create>
<drop>drop index if exists ITEM_NAME_IDX</drop>
</database-object>
</hibernate-mapping>
- 这个配置文件为所有元素声明了一个命名空间(package),这是 Hibernate 5 新增的特性。
- 可以在这个配置文件中新增多个映射类(class),老的 Hibernate 项目一般是一个类配置一个映射文件。
- Hiberante 原生方式的 XML 配置与 JPA 的 XML 配置不能并存。
- 如果类中一个属性没有被配置,则 Hibernate 会把它认为是 transient 状态。
现今的程序员更喜欢使用注解进行映射文件的配置。
3.4 运行时改变元数据
3.4.1 使用动态的元数据模型 API
在一个通用的可配置框架下,有时候需要直接编写一个类的持久性元数据,下面的代码展示了如何读取元数据:
Metamodel mm = entityManagerFactory.getMetamodel();
Set<ManagedType<?>> managedTypes = mm.getManagedTypes();
assertEquals(managedTypes.size(), 1);
ManagedType itemType = managedTypes.iterator().next();
assertEquals(itemType.getPersistenceType(), Type.PersistenceType.ENTITY);
SingularAttribute nameAttribute = itemType.getSingularAttribute("name");
assertEquals(nameAttribute.getJavaType(), String.class);
assertEquals(nameAttribute.getPersistentAttributeType(), Attribute
.PersistentAttributeType.BASIC);
assertFalse(nameAttribute.isOptional());//NOT NULL
SingularAttribute auctionEndAttribute = itemType.getSingularAttribute("auctionEnd");
assertEquals(auctionEndAttribute.getJavaType(), Date.class);
assertFalse(auctionEndAttribute.isCollection());
assertFalse(auctionEndAttribute.isAssociation());
3.4.2 使用静态的元数据模型 API
Java (包括 java 8)只能根据属性名,来存取一个类的属性,这其实不是类型安全的方式,特别是在写数据库查询的情况下。 JPA 提供了类型安全的查询方式:
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
//This query is the equivalent of "select i from Item i"
CriteriaQuery<Item> query = cb.createQuery(Item.class);
Root<Item> fromItem = query.from(Item.class);
query.select(fromItem);
List<Item> items = entityManager.createQuery(query).getResultList();
assertEquals(items.size(), 2);
上面的代码返回了 Item 的所有数据,当然也可以加上查询条件:
//"Where i.name like :pattern"
Path<String> namePath = fromItem.get("name");
query.where(cb.like(namePath,//Has to be a Path<String> for like() operator!
cb.parameter(String.class, "pattern")));
items = entityManager.createQuery(query).setParameter("pattern", "%some item%") //Wildcards!
.getResultList();
assertEquals(items.size(), 1);
assertEquals(items.iterator().next().getName(), "This is some item");
更好的方式是代码能够在编译时就能检测类型是否安全:
query.where(cb.like(fromItem.get(Item_.name),// Static Item_ metamodel
cb.parameter(String.class, "pattern")));
要这么做,必须现在 Item.java 所在的包内新增一个 Item_.java ,这是一个元数据类,里面列出了 Item 中所有的属性类型:
@javax.persistence.metamodel.StaticMetamodel(Item.class)
public abstract class Item_ {
public static volatile SingularAttribute<Item,Long> id;
public static volatile SingularAttribute<Item,String> name;
public static volatile SingularAttribute<Item,Date> auctionEnd;
}
你可以手写这个类,或者使用注解生成工具(apt)直接自动生成。Hibernate JPA2 的元数据生成器也可以生成。
推荐使用注解来配置元数据,XML 的配置方式只在必要的情况下才使用。