1 实体类与值类型
1.1 细粒度级别的领域模型
细粒度级别的领域模型意味着,实体类的数量会比库表的数量来得多。
一般设计中,会把与地址相关的三个属性(城市、街道、门牌号)作为 User 类中属性。但更好的方式是,设计一个 Address 类,User 类里面有一个 Address 类属性。这样的设计不仅提高了类的内聚性,而且更容易被重用,也更清楚。
1.2 定义应用模型
一个账号就代表一个 User 实例。User 就是一个实体类,这样你才可以获取、保存或者删除这些 User 实例。通过这种方式,就可以很容易找出哪些是实体类。
User 类有一个 homeAddress 属性,它关联到一个 Address 类。这里有两种设计方式:
* 两个 User 实例共享一个 Address 实例
这样做的问题时,当删除一个用户实例时,需要判断它所引用的地址实例是否还有被其他用户实例所引用,如果有,则不能删除;如果没有,才删除。这在实际的实践中,很麻烦。
- 每一个 User 实例都有一个专属的 Address 实例
这种方式下,如果要删除 John 实例,那么就可以安全地删除它的 Address 实例。
一个类的所有简单类型属性(比如 String,Integer 以及其他的原生类型),都属于这个类的实例。如果这个类的实例被销毁了,那么这些简单类型的属性也就跟着消亡了。也就是说,它们的生命周期就是这个类实例的生命周期。
1.3 区分实体类和值类型
实体类就是需要持久化的类,而值类型其实是实体类的一部分,它有可能是另一个类(比如这里的 Address),值类型是由实体类统一管理的。
很容易判断出这里的 Address 类其实是值类型,因为 Address 类实例是作为 User 实例的一部分而存在的。
在讨论现实世界中的实际对象时,应该首先考虑把某些对象作为某个实体类的值类型,只有在必要的情况下,才把某些对象改为实体类型。
接下来准备开始画领域类型图咯,主要要注意以下几个方面:
- 作为主类值类型的类,要避免被主类的实例所共享。就像我们上面的例子一样,一个用户实例拥有一个地址实例。这可以在 User 的构造函数中强制关联一个 Address 类,同时把 setUser() 方法设为 非 public。
- 生命周期管理。当删除一个用户实例时,作为它所对应的 Address 实例也可以被安全删除。
- 所有的实体类都需要一个标识符属性。
2 映射实体类中的标识符
2.1 Java 中的同一性和相等性
== 比较的是,两个对象所在的内存地址是否相同。而 equals() 比较的是两个对象是否在内容上是否相等。
总结如下:
- 如果两个对象在 JVM 中占用了同一个内存地址,那么它们是同一个对象。
- 如果两个对象(比如 a 和 b),a.equals(Object b) 返回 true,则表示这两个对象在内容上是相等的。
- 如果两个对象在库表中,是同一张表同一个主键下的记录,那么它们就是在数据库级别上的同一个对象。
2.2 第一个实体类和映射定义
@Entity
public class Item {
@Id
@GeneratedValue(generator = "ID_GENERATOR")
protected Long id;
public Long getId(){//Optional but useful
return id;
}
}
- 每一个实体类都必须有一个
@Id
标注的属性,而且最好直接标注在属性上。 - 不要设置 setter 方法,因为主键值是永远不会被改变的,也不允许被改变。
2.3 选择一个主键
一个候选键要想成为一个主键,必须满足以下条件:
- 值不能为 null。
- 对于任意一行的记录,值必须唯一。
- 值不能被改变。
因为这些要求,所以强烈建议使用一个非自然主键,它没有实际的业务逻辑上的含义,是由数据库或者应用程序生成的。
2.4 配置主键生成器
如果没有为主键的属性配置 @GeneratedValue
注解,那么框架就会认为主键是由程序员写代码直接赋值的(在保存实例之前),可以叫做由应用程序直接赋值的主键。
@GeneratedValue
里面的 strategy
有以下这里选项:
GeneratedValue.AUTO
:由 Hibernate 框架来自行选择最佳策略,依赖于底层配置的 SQL 方言。这是默认值。GeneratedValue.SEQUENCE
:数据库中需要事先配置一个名叫HIBERNATE_SEQUENCE
的序列,每一项记录被保存时,会先从这一序列中取出一个值作为主键。GeneratedValue.IDENTITY
:使用库表定义的自增长主键。GeneratedValue.TABLE
:使用一张名叫HIBERNATE_SEQUENCES
表,表中包含两个字段(SEQUENCE_NAME
、SEQUENCE_NEXT_HI_VALUE
),框架会自动维护这张主键表。
不要偷懒直接使用 GeneratedValue.AUTO
,最好明确指定一个其他的主键生成策略。
推荐这样指定主键生成策略:
@GeneratedValue(generator = "ID_GENERATOR")
然后再在包级别上配置元数据(package-info.java):
@org.hibernate.annotations.GenericGenerator(
name = "ID_GENERATOR",
strategy = "enhanced-sequence",//使用一个序列值,如果数据库不支持序列,Hibernate 会使用一张表来模拟序列行为。
parameters = {
@org.hibernate.annotations.Parameter(
name = "sequence_name",//支持序列的数据库中,表示的是序列名;不支持序列的数据库中,表示的是模拟序列行为的表名
value = "JPWH_SEQUENCE"
),
@org.hibernate.annotations.Parameter(
name = "initial_value",//用于单元测试,测试前会事先生成的序列数,这样在单元测试代码中就可以直接写 id 值了,id 值的范围从 1 到 999。这是 DDL 选项。
value = "1000"
)
}
)
JPA 有两个注解(@javax.persistence.SequenceGenerator
和 @javax.persistence.TableGenerator
),通过它们可以自定义序列名和表名,但它们只能定义在类的顶部,不能放在 package-info.java 中。
所有的实体领域类都可以共享一个数据库序列。
最后要说明的是主键字段的类型,一般是设定为整型。这里又分为 Int 和 Long 类型的整型。假设每一微秒生成一个主键值,如果主键字段设置为 Int ,那么 2 个月后就会溢出; 如果主键字段设置为 Long ,那么可以用 3 亿年之久。因此,强烈建议把主键值设置为 Long 类型(就是 MySQL 中的 BigInt)。
2.5 主键生成策略
如果需要在记录保存之前事先就准备好主键值,那么最好使用之前介绍的 enhanced-sequence
策略,它是易用的、适应性强的策略,而且本身还提供了针对大表的优化选项。
下面列出 JPA 标准策略与 Hibernate 原生策略之间的关系:
JPA | Hibernate | 说明 |
---|---|---|
native | GenerationType.AUTO | 自动选择生成策略,依赖于底层配置的数据库 |
sequence | - | 使用一个名为 HIBERNATE_SEQUENCE 的序列,保存记录前调用 |
sequence-identity | - | 使用一个名为 HIBERNATE_SEQUENCE 的序列,保存记录时调用,形如 HIBERNATE_SEQUENCE.nextval |
enhanced-sequence | GenerationType.SEQUENCE (推荐) | 使用一个序列值,如果数据库不支持序列,Hibernate 会使用一张表来模拟序列行为,默认名称是 HIBERNATE_SEQUENCE |
seqhilo | - | 使用一个名为 HIBERNATE_SEQUENCE 的序列,可以设定返回主键的低位值数量(默认是 9)。如果序列返回的高位值是 1,那么接下来会返回 11,12,13,14,15,16,17,18,19(最高位都是 1) |
hilo | - | 使用一张名叫 HIBERNATE_UNIQUE_KEY 表来模拟 seqhilo 策略 |
enhanced-table | GenerationType.TABLE | 使用一张名叫 HIBERNATE_SEQUENCE 表来模拟序列行为 |
identity | GenerationType.IDENTITY | 自动生成主键值(DB2、MySQL、MSSQL、Sybase) |
increment | - | 保存之前,Hibernate 会读取表中的最大主键值,然后加 1 作为新的主键值 |
select | - | 由 DBMS 在插入数据时,定义一个值(schema 或者 触发器),作为主键值,然后在通过 select 语句返回这个主键值(基本不用) |
uuid2 | - | 生成唯一的 128 位的 UUID,当在跨数据库时,需要全球唯一 ID 的情况下有用 |
guid | - | 由数据库生成全球唯一的标识符(Oracle、Ingres、MS-SQL、MySQL) |
可以在 persistence.xml 中切换这两种策略名称(hibernate.id.new_generator_mappings),默认是使用 Hibernate 策略名称。
推荐使用在插入数据前,可以预先生成主键的策略,比如 enhanced-sequence 策略。这样更容易编写插入子表的代码。
3 实体映射可选项
3.1 名称映射
使用 @Entity
来注解实体类,那么默认就会把类名作为实际的表名。
可以使用 JPA 的 @Table
注解来自定义需要映射的表名(表名没有大小写区分,即大小写不敏感)。
@Entity
@Table(name = "USERS")
public class User implements Serializable {
...
}
@javax.persistence.Table.annotation
还有 catalog 和 schema 选项,如果需要,也可以自定义。
3.1.1 引号标注 SQL 关键字
Hibernate 5 会根据底层配置的数据库方言,自动把数据库的关键字字符串添加引号。这需要在 persistence 的 unit 配置中添加 hibernate.auto_quote_keyword=true
。
如果因为某些原因,把表名添加了倒引号,像这样 @Table(name = "
USER")
,Hibernate 可以正确识别;而在 JPA 2.0 标准中应该使用反斜杠,像这样:@Table(name = "\"USER\"")
3.1.2 自定义命名转换
假设所有映射的表名前缀都是 CE_
,形如 CE_<table name>
。要想实现自动命名转换映射,可以直接继承 PhysicalNamingStrategyStandardImpl
类并覆盖 toPhysicalTableName
方法即可:
public class CENamingStrategy extends PhysicalNamingStrategyStandardImpl {
/**
* 自定义 entity 名称与 table 名称的映射关系
*
* @param name
* @param context
* @return
*/
@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment context) {
return new Identifier("CE_" + name.getText(), name.isQuoted());
}
}
通过 PhysicalNamingStrategyStandardImpl
类,也可以自定义列或序列等命名转换功能。
要想生效,必须在 persistence.xml 中配置一下:
<persistence-unit name="CaveatEmptorPU">
<properties>
<!-- 自定义 entity 名称与 table 名称的映射关系-->
<property name="hibernate.physical_naming_strategy"
value="net.deniro.hibernate.shared.CENamingStrategy"/>
</properties>
</persistence-unit>
3.1.3 用于查询的实体类名字
默认情况下,所有实体类的名称会导入查询引擎的命名空间。这也意味着,可以直接使用简短的类名称作为查询语句的一部分:
List result = em.createQuery("select i from Item i").getResultList();
如果实体类很多,那么有可能有遇到命名冲突,比如有两个类都叫 Item,只是它们在不同的包中,那么可以对其中一个类重命名(当然也可以直接使用包含包路径的全类名):
@javax.persistence.Entity(name="AuctionItem")
public class Item{
//...
}
3.2 动态维护表结构
默认情况下,当持久化单元被创建时,Hibernate 会为每个类更新相应的表数据,比如 CRUD 操作,如果连接的是内存数据库,这样做就会很方便。
Hibernate 会更新所有的字段,无论这个字段是否有改变过。这可能会造成性能上的问题,比如更新了许多表,其实只有一个字段有更新。在这种情况下,可以通过配置关闭 Hibernate 的表维护功能,关闭后,可能需要在某些类上使用 Hibernate 维护功能,这时可以在类上加上 Hibernate 的维护注解,就像这样:
@Entity
@org.hibernate.annotations.DynamicInsert
@org.hibernate.annotations.DynamicUpdate
public class Item{
//...
}
只有值发生变动的列才会被更新,非 null 的列才会被插入。
3.3 使一个实体类不可变
在我们的例子中,一个 来自拍卖物(Item)的出价(Bid)就是一个实际的不可变的实体类。因此可以直接把这个类注解为不可变:
@Entity
@org.hibernate.annotations.Immutable
public class Bid{
//...
}
一个 POJO 类,如果它所有的属性都是私有的,并且都没有 setter 方法,这些属性都是通过构造函数传入的,那么这个类就是一个不可变的类。
3.4 把子查询映射进实体类
有时候 DBA 会创建一个数据库视图共应用程序使用,这时就可以利用 Hibernate 提供的注解,创建一个包含子查询的不可变类:
@Entity
@org.hibernate.annotations.Immutable
@org.hibernate.annotations.Subselect(
value = "select i.ID as ITEMID, i.ITEM_NAME as NAME," +
"count(b.ID) as NUMBEROFBIDS " +
"from ITEM i left outer join BID b on i.ID = b.ITEM_ID " +
"group by i.ID, i.ITEM_NAME"
)
@org.hibernate.annotations.Synchronize({"Item", "Bid"})//注意这里表名区分大小写
public class ItemBidSummary {
@Id
protected Long itemId;
protected String name;
protected long numberOfBids;
public ItemBidSummary() {
}
public Long getItemId() {
return itemId;
}
public String getName() {
return name;
}
public long getNumberOfBids() {
return numberOfBids;
}
}
当 ItemBidSummary 类的实例被加载时,我们定义的子查询就会被执行:
ItemBidSummary itemBidSummary = em.find(ItemBidSummary.class, ITEM_ID);
assertEquals(itemBidSummary.getName(), "AUCTION:Some item");
底层生成的 SQL 类似这样:
select * from(
select i.id as itemid, i.item_name as name,...
) where itemid = ?
加入 @org.hibernate.annotations.Synchronize
注解后,如果 Item 和 Bid 实例发生变动,ItemBidSummary 实例会同步更新。至于表名区分大小写,好像是 Hibernate 的 bug,以后应该会被修复吧 O(∩_∩)O~。
Item item = em.find(Item.class, ITEM_ID);
item.setName("New name");
//No flush before retrieval by identifier!
//Automatic flush before queries if synchronized tables are affected!
Query query = em.createQuery("select ibs from ItemBidSummary ibs where ibs" +
".itemId=:id");
ItemBidSummary itemBidSummary = (ItemBidSummary) query.setParameter("id",
ITEM_ID).getSingleResult();
assertEquals(itemBidSummary.getName(), "AUCTION:New name");
只有实际查询 ItemBidSummary 时,才会触发同步更新 ItemBidSummary 动作。