说说 Hibernate 领域模型与库表结构设计

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 的配置方式只在必要的情况下才使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值