说说 Hibernate 如何映射持久化类

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_NAMESEQUENCE_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 值的范围从 1999。这是 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 原生策略之间的关系:

JPAHibernate说明
nativeGenerationType.AUTO自动选择生成策略,依赖于底层配置的数据库
sequence-使用一个名为 HIBERNATE_SEQUENCE 的序列,保存记录前调用
sequence-identity-使用一个名为 HIBERNATE_SEQUENCE 的序列,保存记录时调用,形如 HIBERNATE_SEQUENCE.nextval
enhanced-sequenceGenerationType.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-tableGenerationType.TABLE使用一张名叫 HIBERNATE_SEQUENCE 表来模拟序列行为
identityGenerationType.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 动作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值