Skip to content

2. 实体JPA

2.1. 示例 1 - 单张表的对象表示

2.1.1. 表 [personne]

假设有一个数据库,其中包含一个名为 [personne] 的表,其作用是存储一些关于个人的信息:

 
ID
表的主键
VERSION
表中该行的版本号。每当
修改该人员时,其版本号都会递增。
NOM
人员姓名
PRENOM
其名字
DATENAISSANCE
出生日期
MARIE
整数 0(未婚)或 1(已婚)
NBENFANTS
该人的子女数

2.1.2. 实体 [Personne]

我们处于以下运行环境中:

JPA [5] 层必须在 [7] 数据库的关系世界与由 Java 程序 [4] 操作的对象世界之间建立桥梁228ZQX所处理的对象世界之间搭建桥梁。这种连接是通过配置实现的,主要有两种方式:

  1. 使用 XML 配置文件。在 JDK 1.5 版本推出之前,这几乎是唯一的方法
  1. 自 JDK 1.5 起,使用 Java 注解

在本文档中,我们将几乎完全采用第二种方法。

前文所述的 [personne] 表的 [Personne] 映像对象可能如下所示:


...

@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // 构造器
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }

    // toString
    public String toString() {
...
    }

    // 获取器和设置器
...
}

配置通过 Java @Annotation 注解实现。 Java注解可在编译时由编译器处理,也可在运行时由专用工具处理。除第3行面向编译器的注解外,此处所有注解均面向所使用的JPA实现(即Hibernate或Toplink)。因此,这些注解将在运行时被利用。 若缺乏能够解析这些注解的工具,这些注解将被忽略。因此,上文中的 [Personne] 类可在非 JPA 环境中被调用。

在关联表 T 的 C 类中使用 JPA 注解时,需区分以下两种情况:

  1. 表 T 已存在:此时 JPA 注释必须复现现有内容(列名与定义、完整性约束、外键、主键等)
  2. 表 T 不存在,并将根据类 C 中找到的注解进行创建。

情况 2 最容易处理。借助 JPA 注解,我们可以指定所需的表 T 结构。情况 1 通常更为复杂。表 T 可能是在很久以前、脱离任何 JPA 上下文的情况下构建的。 因此,其结构可能与JPA的关系型/对象桥接机制不匹配。为简化说明,我们假设处于情况2:即与类C关联的表T将根据类C的注解JPA进行创建。

下面对类[Personne]的注解进行说明:

  • 第 4 行:@Entity 注解是第一个必不可少的注解。它位于声明类的行之前,表明该类应由持久层 JPA 管理。如果没有此注解,所有其他注解 JPA 都将被忽略。
  • 第 5 行:@Table 注解指定了该类所表示的数据库表。其主要参数是 name,用于指定表名。若缺少此参数,表将采用类名,即此处的 [Personne]。因此,在本例中,@Table 注解是多余的。
  • 第 8 行:@Id 注解用于指定类中作为表主键映射的字段。该注解是必填的。此处表明第 11 行中的 id 字段是表主键的映射。
  • 第 9 行:@Column 注解用于建立类中字段与该字段所映射的表列之间的关联。name 属性指定表中列的名称。若未指定该属性,则列名与字段名相同。因此,在本例中,name 参数并非必需。 nullable=false 参数表示与该字段关联的列不能为空(即不能取值为 NULL),因此该字段必须有值。
  • 第 10 行:注释 @GeneratedValue 说明了当主键由 SGBD 自动生成时,其生成方式。在所有示例中均采用此方式。 这并非强制要求。因此,我们的“person”实体可能拥有一个用作主键的学生编号,该编号并非由 SGBD 生成,而是由应用程序设定。在这种情况下,注解 @GeneratedValue 将不存在。 strategy参数指定了当主键由SGBD生成时,其生成方式。并非所有SGBD都采用相同的主键值生成技术。例如:
Firebird
在每次插入前调用一个名为的值生成器
SQL server
主键字段被定义为类型 Identity。其效果与 Firebird 的值生成器类似,只是主键的值要等到行插入完成后才确定。
Oracle
使用名为 SEQUENCE 的对象,该对象同样充当值生成器

JPA 层必须根据不同的 SGBD 生成不同的 SQL 命令,以创建值生成器。 通过配置,向其指定了需要管理的 SGBD 类型。因此,它能够识别该 SGBD 通常的主键值生成策略。 参数 strategy = GenerationType.AUTO 指示 JPA 层必须使用该常规策略。在本文档的所有示例中,该技术对所使用的七个 SGBD 均有效。

  • 第 14 行:@Version 注解指定了用于管理对表中同一行并发访问的字段。

为理解[personne]表中同一行数据并发访问的问题,假设某个Web应用程序允许更新某人的信息,并分析以下情况:

在时间点 T1,用户 U1 进入某人 P 的编辑界面。此时,子女数量为 0。 他将该数值改为1,但在提交修改前,用户U2进入同一人员P的编辑界面。由于U1尚未提交修改, U2 在屏幕上看到子女数为 0。U2 将人员 P 的姓名改为大写。随后,U1 和 U2 按此顺序提交了修改。 最终生效的是 U2 的修改:在数据库中,姓名将变为大写,而子女数量仍保持为零,尽管 U1 认为自己已将其改为 1。

“人员版本”的概念有助于解决此问题。我们继续使用相同的用例:

在时间点 T1,用户 U1 进入对人员 P 的修改界面。此时,子女数量为 0,版本号为 V1。 他将子女数量改为 1,但在提交修改之前,用户 U2 进入同一人员 P 的编辑界面。由于 U1 尚未提交修改, U2 看到的子女数为 0,版本号为 V1。U2 将人员 P 的姓名改为大写。 随后,U1 和 U2 按此顺序提交了修改。在提交修改前,系统会验证修改人员 U1 是否持有与当前已记录人员 P 相同的版本。该用户确实持有相同版本。 因此其修改被接受,随后将该人员的版本号从 V1 更改为 V2,以标记该人员已发生变更。在验证 U2 的修改时, 将发现 U2 持有人员 P 的版本 V1,而当前该人员的版本为 V2。 此时,我们可以告知用户U2,有人在他之前进行了操作,他必须基于人员P的新版本重新开始。他将照做,获取版本为V2的人员P(该人员现在已有一个孩子),将姓名改为大写,并提交。 如果已保存的P仍处于版本V2,则其修改将被接受。最终,U1和U2所做的修改都将被保留,而在没有版本控制的用例中,其中一项修改会丢失。

客户端应用程序的 [dao] 层可以自行管理 [Personne] 类的版本。每当对象 P 发生修改时,该对象在表中的版本号将递增 1。 通过 @Version 注解,可以将此管理职责转移至 JPA 层。相关字段完全不必像示例中那样命名为 version,可以是任意名称。

与注解 @Id@Version 对应的字段是因持久化需求而存在的。如果类 [Personne] 无需持久化,则无需这些字段。由此可见,根据对象是否需要持久化,其表示形式会有所不同。

  • 第 17 行:再次使用 @Column 注解,用于提供与类 Personne 的字段 nom 关联的表 [personne] 列的相关信息。此处出现了两个新参数:
    • unique=true 表示人员姓名必须唯一。这将在数据库中体现为:在表 [personne] 的列 NOM 上添加唯一性约束。
    • length=30 将 NOM 列的字符数限制为 30。这意味着该列的类型将为 VARCHAR(30)。
  • 第 24 行:注解 @Temporal 用于指定日期/时间类型的列/字段应采用哪种 SQL 类型。类型 TemporalType.DATE 表示仅包含日期而不包含时间。 其他可能的类型包括:TemporalType.TIME(用于编码时间)和 TemporalType.TIMESTAMP(用于编码带时间日期)。

现在我们来分析 [Personne] 类的其余代码:

  • 第 6 行:该类实现了 Serializable 接口。对象的序列化(sérialisation)是指将其转换为一串二进制位。反序列化(désérialisation)则是该操作的逆过程。 序列化/反序列化主要应用于客户端/服务器应用程序,其中对象通过网络进行交换。 客户端或服务器端应用程序无需了解此操作,该操作由 JVM 透明地完成。但要实现这一功能,交换对象的类必须使用关键字 Serializable 进行“标记”。
  • 第 37 行:该类的构造函数。请注意,id version 字段不属于参数。实际上,这两个字段由 JPA 层管理,而非由应用程序管理。
  • 第51行及之后:类中各个字段的get和set方法。需要注意的是,JPA注解可以添加到字段的get方法上,而非直接添加到字段本身。 注解的位置决定了 JPA 访问字段时应采用的模式:
    • 如果注解位于字段级别,JPA 将直接访问字段进行读写
    • 如果注解位于 get 方法级别,JPA 将通过 get/set 方法访问字段以进行读写

正是 @Id 注解的位置决定了类中 JPA 注解的位置。 若置于字段级别,则表示直接访问字段;若置于get级别,则表示通过get和set访问字段。其他注解应与@Id注解采用相同的放置方式。

2.1.3. 测试的 Eclipse 项目

我们将使用前文提到的 [Personne] 实体进行初步实验。实验将基于以下架构进行:

  • 生成 [7]:该数据库将基于实体 [Personne] 的注解,以及在名为 [persistence.xml] 的文件中进行的补充配置
  • [5, 6]:由Hibernate实现的JPA层
  • 在 [4] 中:实体 [Personne]
  • [3]:一个控制台类型的测试程序

我们将进行各种实验:

  • 基于 Ant 脚本和 Hibernate Tools 工具生成 BD 的模式
  • 生成 BD 并用少量数据对其进行初始化
  • 利用 BD 表,对 [personne] 表执行四种基本操作(插入、更新、删除、查询)

所需工具如下:

  • Eclipse 及其在第 5.2 节中描述的插件。
  • 位于 <exemples>/hibernate/direct/personnes-entites 文件夹中的 [hibernate-personnes-entites] 项目
  • 附录(第 5 节及后续内容)中描述的各类 SGBD 文件。

Eclipse 项目如下:

  • [1]:Eclipse 项目文件夹
  • 在 [2] 中:导入到 Eclipse 中的项目(文件 / 导入)
  • 在 [3] 中:作为测试对象的 [Personne] 实体
  • 在 [4] 中:测试程序
  • 在 [5] 中:[persistence.xml] 是 JPA 层的配置文件
  • 在 [6] 中:所使用的库。这些库已在第 1.5 节中描述。
  • 在 [8] 中:一个 Ant 脚本,将用于生成与实体 [Personne] 关联的表
  • 在 [9] 中:用于每个 SGBD 的 [persistence.xml] 文件
  • 在 [10] 中:为每个使用的 SGBD 生成的数据库模式

我们将依次描述这些元素。

2.1.4. 实体 [Personne] (2)

我们对之前关于实体 [Personne] 的描述进行了一点修改,并补充了以下信息:


package entites;

...

@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // 构造函数
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
....
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // 获取器和设置器
...
}
  • 第 7 行:我们将与实体 [Personne] 关联的表命名为 [jpa01_personne]。 在本文档中,将在始终命名为 jpa 的模式下创建多个表。本教程结束时,jpa 模式将包含大量表。为了便于读者辨识,相互关联的表将使用相同的前缀 jpaxx_
  • 第 45 行:一个 [toString] 方法,用于在控制台上显示 [Personne] 对象。

2.1.5. 数据访问层的配置

在上述 Eclipse 项目中,JPA 层的配置由文件 [META-INF/persistence.xml] 负责:

运行时,系统会在应用程序的 classpath 中查找 [META-INF/persistence.xml] 文件。 在我们的 Eclipse 项目中,[/src] 和 [1] 文件夹中的所有内容都被复制到了 [/bin] 和 [2] 文件夹中。 该文件夹属于项目中的 classpath。正因如此,当 JPA 层进行配置时,系统将能找到 [META-INF/persistence.xml]。

默认情况下,Eclipse 不会将源代码放在项目的 [/src] 文件夹中,而是直接放在项目文件夹下。 我们将按照第 5.2.1 节的说明,将所有 Eclipse 项目配置为:源代码位于 [/src] 文件夹中,编译后的类文件位于 [/bin] 文件夹中。

让我们来查看项目中 [persistence.xml] 文件中对 JPA 层的配置:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  提供者 -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- 持久化类 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- 日志SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- 连接JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  自动创建模式 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!-- 属性 DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

要理解这一配置,我们需要回顾应用程序的数据访问架构:

  • 文件 [persistence.xml] 将配置图层 [4, 5, 6]
  • [4]:JPA的Hibernate实现
  • [5]:Hibernate 通过连接池访问数据库。连接池是一组已与 SGBD 建立连接的连接。 一个 SGBD 实例可能被多个用户访问,但出于性能考虑,其同时打开的连接数不能超过上限 N。 编写良好的代码会在最短时间内与 SGBD 建立连接:它发送 SQL 命令,然后关闭连接。每当需要操作数据库时,它都会重复这一过程。 建立和关闭连接的开销不容忽视,而连接池的作用正体现在此。在应用程序启动时,连接池会与 SGBD 建立 N1 个连接。 当应用程序需要连接时,将向该池请求一个已建立的连接。一旦应用程序不再需要该连接,便会将其归还给连接池,且应尽可能快地归还。该连接不会被关闭,而是保持可用状态,供下一位用户使用。因此,连接池是一种已建立连接的共享系统。
  • [6]:SGBD所使用的驱动程序JDBC

现在让我们看看文件 [persistence.xml] 是如何配置上述 [4, 5, 6] 层的:

  • 第 2 行:XML 文件的根标签是 <persistence>。
  • 第 3 行:<persistence-unit> 用于定义持久化单元。可以存在多个持久化单元。每个单元都有一个名称(name 属性)和一个事务类型(transaction-type 属性)。应用程序将通过其名称(此处为 jpa)访问该持久化单元。 事务类型 RESOURCE_LOCAL 表示应用程序将自行管理与 SGBD 相关的事务。本例中即采用此方式。当应用程序在 EJB3 容器中运行时,可使用该容器的事务服务。 在此情况下,应设置 transaction-type=JTA(Java Transaction API)。当 transaction-type 属性缺失时,默认值为 JTA。
  • 第 5 行:<provider> 标签用于定义一个实现 [javax.persistence.spi.PersistenceProvider] 接口的类,该接口允许应用程序初始化持久层。 由于使用了 JPA / Hibernate 实现,此处使用的类是 Hibernate 类。
  • 第 6 行:<properties> 标签引入了针对所选特定 provider 的专属属性。因此,根据所选的 Hibernate、Toplink、Kodo 等实现,属性会有所不同。以下属性专属于 Hibernate。
  • 第 8 行:要求 Hibernate 扫描项目中的 classpath,查找带有 @Entity 注解的类以便进行管理。 @Entity类也可以通过<class>nom_de_la_classe</class>标签进行声明,该标签直接位于<persistence-unit>标签之下。这就是我们在provider JPA / Toplink中将要采用的做法。
  • 第 10-12 行(此处已注释)用于配置 Hibernate 的控制台日志:
  • 第 10 行:用于控制是否显示 Hibernate 在 SGBD 上发出的 SQL 命令。这在学习阶段非常有用。 由于关系型/对象型的桥梁作用,应用程序在持久化对象上运行,并对这些对象执行 [persist, merge, remove] 类型的操作。了解这些操作实际生成的 SQL 命令非常有意义。 通过研究这些命令,我们逐渐能够推测出当对持久化对象执行特定操作时,Hibernate将生成的SQL命令,从而在脑海中逐渐形成对关系/对象桥接机制的清晰认知。
  • 第 11 行:控制台上显示的 SQL 命令可以进行美观的格式化,以便于阅读
  • 第 12 行:显示的 SQL 命令还将添加注释
  • 第15-19行定义了JDBC层(在架构中为[6]层)
  • 第 15 行:SGBD 的驱动程序类 JDBC,此处为 MySQL5
  • 第 16 行:所用数据库的 URL
  • 第 17、18 行:连接用户及其密码
  • 此处使用的元素已在附录第 5.5 节中进行说明。建议读者阅读关于 MySQL5 的该章节。
  • 第22行:Hibernate需要知道其所面对的SGBD。事实上,所有SGBD都带有专有扩展名SQL,这是一种管理主键值自动生成的独特方式, ……这导致 Hibernate 必须知道其正在处理的 SGBD,以便向其发送该对象能够理解的 SQL 指令。 [MySQL5InnoDBDialect] 指代 SGBD MySQL5,其中包含支持事务的 InnoDB 类型表。
  • 第24-28行配置连接池c3p0(架构中的[5]层):
  • 第24、25行:连接池中的最小(默认3)和最大连接数(默认15)。默认初始连接数为3。
  • 第26行:等待客户端连接请求的最大时长(以毫秒为单位)。超过此时间,c3p0将向客户端抛出异常。
  • 第27行:为了访问BD,Hibernate使用预编译的SQL命令(PreparedStatement),c3p0可以将其缓存。 这意味着,如果应用程序再次请求已缓存的预编译命令 SQL,则无需重新预编译(预编译 SQL 会产生开销),而是直接使用缓存中的命令。 此处指定了缓存中可容纳的已准备 SQL 命令的最大数量,涵盖所有连接(一个已准备的 SQL 命令属于一个连接)。
  • 第28行:以毫秒为单位,检查连接有效性的频率。连接池中的连接可能因各种原因失效(例如驱动程序JDBC因连接超时而使其失效,或驱动程序JDBC存在“错误”等)。
  • 第20行:此处要求在持久化单元初始化时,生成@Entity对象的数据库映射。Hibernate现已具备所有工具,可发出生成数据库表的命令:
  • @Entity 对象的配置使其能够确定需要生成的表
  • 第 15-18 行和第 24-28 行使其能够建立与 SGBD 的连接
  • 第 22 行使其能够确定生成表时应使用的 SQL 方言

因此,此处使用的 [persistence.xml] 文件会在每次应用程序重新运行时重建一个全新的数据库。如果表已存在,则先将其删除(drop table),然后重新创建(create table)。需要注意的是,这显然不适用于生产环境中的数据库……

测试表明,表的drop/create阶段可能会失败。特别是在同一测试中,当从JPA/Hibernate层切换到JPA/Toplink层,或反之亦然时,这种情况尤为明显。 基于相同的 @Entity 对象,这两种实现生成的表、生成器、序列等并非完全一致,有时会导致删除/创建阶段失败,从而不得不手动删除表。 “附录”部分第5段及后续内容介绍了可用于手动完成此工作的应用程序。值得注意的是,在数据库初始内容创建阶段,JPA/Hibernate 实现方案表现最为高效:几乎未发生崩溃。

JPA / Hibernate 层所使用的工具位于 [jpa-hibernate] 库中,该库在第 8 页的 1.5 节中进行了介绍。 访问 SGBD 所需的 JDBC 驱动程序位于 [jpa-divers] 库中。这两个库已被放入本文研究项目的 classpath 中。 现将它们的内容概述如下:

2.1.6. 使用 Ant 脚本生成数据库

如前所述,Hibernate 提供了用于生成应用程序中 @Entity 对象映射数据库的工具。Hibernate 可以:

  • 生成用于创建数据库的命令文本文件 SQL。此时仅使用 [persistence.xml] 中的方言。
  • 在 [persistence.xml] 中定义的目标数据库中创建 @Entity 对象的表。此时将使用 [persistence.xml] 文件的全部内容。

我们将介绍一个能够生成数据库模式及 @Entity 对象映射的 Ant 脚本。该脚本并非本人所写:它借鉴了 [ref1] 中的类似脚本。Ant(Another Neat Tool)是一款用于批处理 Java 任务的工具。 对于初学者而言,Ant脚本并不容易理解。我们将仅使用其中一个,即我们现在要讲解的这个:

  • 在 [1] 中:本教程示例的目录结构。
  • [2]:当前正在研究的 Eclipse 项目的 [personnes-entites] 文件夹
  • [3]:包含第1.5节中定义的五个JAR库的<lib>文件夹
  • 在 [4] 中:[hibernate-tools.jar] 压缩包,这是我们将要学习的脚本 [ant-hibernate.xml] 中某项任务所需的。
  • 在 [5] 中:Eclipse 项目及脚本 [ant-hibernate.xml]
  • 在 [6] 中:项目的 [src] 文件夹

脚本 [ant-hibernate.xml] [5] 将使用 <lib> 文件夹 [3] 中的 JAR 文件, 特别是 [lib/hibernate] 文件夹中的 [hibernate-tools.jar] 和 [4] 归档文件。 我们重现了文件夹结构,以便读者看到:要从脚本 [ant-hibernate.xml] 中的 [personnes-entites] [2] 文件夹中找到 [lib] 文件夹,需要遵循以下路径:../../../lib

让我们来看看脚本 [ant-hibernate.xml]:


<project name="jpa-hibernate" default="compile" basedir=".">

    <!-- 项目名称和版本 -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />

    <!-- 全局属性 -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />

    <!-- 项目的类路径 -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <!-- 必须位于类路径中的配置文件-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>

    <!-- 项目清理 -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>

    <!-- 项目编译 -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>

    <!-- 将配置文件复制到类路径中 -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>

    <!-- Hibernate 工具 -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />

    <!-- 生成数据库的 DDL -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- 使用 META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- 导出 -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>

    <!-- 生成数据库 -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- 使用 META-INF/persistence.xml -->
            <jpaconfiguration />
            <!-- 导出 -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • 第 1 行:项目 [ant] 名为“jpa-hibernate”。它包含一组任务,其中之一是默认任务:此处名为“compile”的任务。 调用脚本 ant 来执行任务 T。如果未指定任务,则执行默认任务。 basedir="." 表示对于脚本中所有相对路径,其起点均为包含脚本 ant 的文件夹,此处即 <exemples>/hibernate/direct/personnes-entites 文件夹。
  • 第 3-11 行:使用 <property name="nomVariable" value="valeurVariable"/> 标签定义脚本变量。随后可在脚本中使用 ${nomVariable} 这种形式调用该变量。 变量名称可以任意设定。让我们重点关注第9-11行定义的变量:
    • 第 9 行:定义了一个名为 "src.java.dir" 的变量(名称可自定义),该变量将在后续脚本中指代包含 Java 源代码的文件夹。其值为 "src",这是一个相对于 basedir 属性(第 1 行)所指定文件夹的相对路径。 因此,该路径为“./src”,其中 . 代表 <exemples>/hibernate/direct/personnes-entites 文件夹。Java 源代码确实位于 <personnes-entites>/src 文件夹中(参见上文的 [6])。
    • 第 10 行:定义了一个名为“lib.dir”的变量,该变量将在后续脚本中指代包含脚本中 Java 任务所需 JAR 存档的文件夹。其值 ../../../lib 指向 <exemples>/lib 文件夹(参见上文 [3])。
    • 第 11 行:定义了一个名为“build.dir”的变量,在后续脚本中,该变量将指向用于生成 .java 源代码编译后生成的 .class 文件的文件夹。其值“bin”指向 <personnes-entites>/bin 文件夹。 我们之前已经说明过,在所研究的 Eclipse 项目中,<bin> 文件夹就是生成 .class 文件的位置。Ant 也会采用同样的做法。
    • 第14-18行:<path>标签用于定义classpath中的元素,这些元素将被ant任务所调用。 此处,路径“project.classpath”(名称可自定义)汇集了<exemples>/lib文件夹树中的所有.jar存档文件。
    • 第 21-24 行:<patternset> 标签用于通过命名模式指定一组文件。 此处,名为 confpatternset 指代所有后缀为 .xml .properties 的文件。该 patternset 将用于指定 <src> 文件夹中的 .xml 和 .properties 文件 (persistence.xml、log4j.properties)(参见 [6]),这些是应用程序的配置文件。 在执行某些任务时,这些文件必须复制到<bin>文件夹中,以便将其纳入项目的classpath中。届时将使用patternset conf来指定它们。
    • 第27-30行:<target>标签表示脚本中的一个任务。这是我们遇到的第一个任务。此前所有内容均属于ant脚本运行环境的配置。 该任务名为 clean。其执行分为两个步骤:先删除 <bin> 文件夹(第 28 行),随后重新创建该文件夹(第 29 行)。
    • 第 33-35 行:compile 任务是脚本的默认任务(第 1 行)。它依赖于(attribute dependsclean 任务。 这意味着在执行 compile 任务之前,ant 必须先执行 clean 任务(c.a.d),以清理 <bin> 文件夹。此处 compile 任务的目的是编译 <src> 文件夹中的 Java 源代码。
    • 第 34 行:调用 Java 编译器,并传入三个参数:
      • srcdir:包含 Java 源代码的文件夹,此处为 <src> 文件夹
      • destdir:用于存放生成的 .class 文件的目录,此处为 <bin> 目录
      • classpathref:编译时使用的类路径,此处为<lib>目录树下的所有jar文件
  • (续)
    • 第38-45行:copyconf任务,其目的是将<src>目录下的所有.xml和.properties文件复制到<bin>目录中。
    • 第48行:使用<taskdef>标签定义任务。此类任务旨在在脚本的其他位置重复使用,这是一种编码便利。由于该任务在脚本的多个位置被使用,因此我们使用<taskdef>标签将其定义一次,随后在需要时通过其名称进行复用。
      • 该任务名为 hibernatetoolname 属性)。
      • 其类由 classname 属性定义。在此,指定的类将位于我们之前提到的 [hibernate-tools.jar] 归档文件中。
      • classpathref 属性指示 ant 在何处查找前一个类
  • (续)
    • 第 51-60 行涉及我们此处关注的任务,即生成我们 Eclipse 项目中 @Entity 对象的图像数据库模式。
      • 第 51 行:该任务名为 DDL(取自数据定义语言 Data Definition Language,即与数据库对象创建相关的 SQL)。它依序依赖于 compile copyconf 任务。 因此,任务 DDL 将依次触发 cleancompile copyconf 任务的执行。 当任务 DDL 启动时,<bin> 文件夹中包含 .java 源代码生成的 .class 文件(特别是 @Entity 对象),以及用于配置 JPA / Hibernate 层的 [META-INF/persistence.xml] 文件。
      • 第 53-59 行:调用第 48 行定义的任务 [hibernatetool]。除了第 48 行已定义的参数外,还向其传递了多个参数:
      • 第 53 行:该任务生成的结果输出目录将设为当前目录。
      • 第 54 行:该任务的 classpath 文件夹将设置为 <bin> 文件夹
      • 第 56 行:告知任务 [hibernatetool] 如何识别其运行环境: <jpaconfiguration/> 标签告知该任务,它处于 JPA 环境中,因此应使用其 classpath 目录中找到的 [META-INF/persistence.xml] 文件。
      • 第58行规定了数据库的生成条件: drop=true 表示在创建表之前必须执行 SQL drop table 命令,create=true 表示必须创建用于创建数据库的 SQL 命令文本文件,outputfilename 指定该 SQL 文件的名称 ——此处为 schema.sql,位于 Eclipse 项目的 <ddl> 文件夹中;export=false 表示生成的 SQL 命令不应在 SGBD 连接中执行。 这一点很重要:这意味着执行该任务时,目标 SGBD 无需启动。delimiter 用于设定生成的模式中两个 SQL 命令之间的分隔符,format=true 则要求对生成的文本进行基础格式化。
  • (续)
    • 第 63-72 行定义了名为 BD 的任务。它与之前的 DDL 任务完全相同,只是这次它会生成数据库(第 70 行的 export="true")。 该任务使用 [persistence.xml] 中获取的信息连接到 SGBD,以便在其中执行 SQL 模式并生成数据库。 因此,要执行任务 BD,必须先启动 SGBD。

2.1.7. 执行任务 ,然后是DDL

要执行脚本 [ant-hibernate.xml],我们需要首先在 Eclipse 中进行一些配置。

  • 在 [1] 中:选择 [External Tools]
  • 在 [2] 中:创建一个新的配置 ant
  • 在 [3] 中:为配置命名 ant
  • 在 [5] 中:使用按钮指定脚本 ant [4]
  • 在 [6] 中:应用更改
  • 在 [7] 中:已创建配置 ant DDL
  • 在 [8] 中:在 JRE 选项卡中,定义要使用的 JRE。 [10]字段通常会预先填入Eclipse使用的JRE。因此,通常无需在此面板上进行任何操作。 不过,我曾遇到过一种情况,即脚本 ant 无法找到 <javac> 编译器。该编译器并非位于 JRE(Java 运行时环境)中,而是位于 JDK(Java 开发工具包)中。 Eclipse 的 ant 工具是通过环境变量 JAVA_HOME 定位该编译器的(“开始”/“控制面板”/ 性能和维护 / 系统 / 高级选项卡 / 环境变量按钮)[A]。 如果该变量未被定义,可以通过在 [10] 中设置 JDK(而非 JRE)来让 ant 找到 <javac> 编译器。 该文件位于与 JRE 和 [B] 相同的文件夹中。 我们将使用 [9] 按钮,在可用的 JRE 和 [C] 中申报 JDK,以便随后在 [10] 中选中它。
  • 在 [12] 中:在 [Targets] 选项卡中,选择任务 DDL。 因此,我们命名为 DDL 和 [7] 的配置 ant 将对应于名为 DDL 和 [12] 的任务的执行, 如我们所知,该任务会生成应用程序中 @Entity 对象图像数据库的 DDL 模式。
  • 在 [13] 中:验证配置
  • 在 [14] 中:执行该配置

在视图 [console] 中,可查看任务 ant 的执行日志 DDL:


Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
   [delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
    [mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
    [javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
     [copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
  • 请注意,任务 DDL 的名称为 [hibernatetool](第 10 行),且它依赖于任务 clean(第 2 行)、 compile(第 5 行)和 copyconf(第 7 行)。
  • 第 10 行:任务 [hibernatetool] 处理来自配置 JPA 的文件 [persistence.xml]
  • 第 11 行:任务 [hbm2ddl] 将生成数据库的模式 DDL
  • 第 12-22 行:数据库的 DDL 模式

我们记得曾要求任务 [hbm2ddl] 在特定位置生成模式 DDL:


<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
  • 第 74 行:该模式应生成在文件 ddl/schema.sql 中。让我们验证一下:
  • 在 [1] 中:ddl/schema.sql 文件确实存在(执行 F5 以刷新目录结构)
  • 在 [2] 中:其内容。该内容是数据库 MySQL5 的模式。 JPA 层的配置文件 [persistence.xml] 确实指定了 SGBD 和 MySQL5(见下文第 8 行):


            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!--  自动创建模式 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  属性DataSource c3p0 -->
...

让我们通过检查 @Entity Personne 对象的配置以及生成的 DDL 模式,来分析此处建立的对象/关系映射:

需要注意以下几点:

  • A1-B1:A1中指定的表名确实与B1中使用的表名一致。 请注意,在 B1 中,drop 位于 create 之前。
  • A2-B2:展示了主密钥的生成模式。 在 A2 中指定的 AUTO 模式,转化为 MySQL5 特有的 autoincrement 属性。 主密钥的生成模式通常专属于 SGBD。
  • A3-B3:显示了 SQL 类型,该类型具有 MySQL5 特有的,用于表示 boolean Java 类型。

让我们用另一个 SGBD 重新进行此测试:

  • 文件夹 [conf] [1] 包含用于各种 SGBD 的 [persistence.xml] 文件。 以Oracle的[2]为例,将其放入[META-INF]和[3]文件夹中,替换原有的文件。其内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  提供程序 -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- 持久类 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- 日志 SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  自动生成模式 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  属性 DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

建议读者查阅附录中关于 Oracle 的章节(第 5.7 节),特别是为了理解 JDBC 的配置。

此处仅第25行真正重要:该行告知Hibernate,SGBD现已被定义为Oracle的SGBD。 执行 Ant 任务 DDL 会得到上文中的 [4] 结果。 值得注意的是,该 Oracle 模式与 MySQL5 模式不同。这是 JPA 的一个优势:开发人员无需关注这些细节,从而大大提高了其开发的移植性。

2.1.8. 执行任务 与BD

大家可能还记得,名为 BD 的任务 ant 与任务 ant DDL 功能相同,但还会额外生成数据库。 因此必须启动 SGBD。 我们将以 SGBD 和 MySQL5 为例,并建议读者将文件 [conf/mysql5/persistence.xml] 复制到文件夹 [src/META-INF] 中。为了检查任务的运行情况, 我们将使用 SQL Explorer 插件(参见第 5.2.6 节)来检查 BD JPA 在执行任务 ant BD 之前和之后的状态。

首先,我们需要创建一个新的配置 ant 来执行任务 BD。 请读者参照第 2.1.7 节中针对配置 DDL 所描述的步骤进行操作。新的配置 ant 将命名为 BD:

  • 为 [1]:复制名为 DDL 的先前配置
  • 为 [2]:将新配置命名为 BD。 它执行任务 ant BD [3],这些任务实际生成数据库。
  • 完成此操作后,启动 SGBD 和 MySQL5(第 5.5 节)。

现在,我们使用 SQL Explorer 插件来浏览由 SGBD 管理的数据库。读者如有需要,应先熟悉该插件(参见第 5.2.6 节)。

  • [1]:打开 SQL Explorer 视图 [Window / Open Perspective / Other]
  • [2]:如有需要,创建连接 [mysql5-jpa](参见第 5.5.5 节,第 252 页)并打开它
  • [3]:使用 jpa / jpa 进行身份验证
  • [4]:已连接到 MySQL5。
  • 在 [5] 中:BD jpa 仅有一个表:[articles]
  • 在 [6] 中:我们启动任务 ant BD 的执行。 由于当前处于 [SQL Explorer] 视图中,因此无法看到显示任务日志的 [Console] 视图。 我们可以显示视图 [Window / Show View / ...],或者返回 Java 视图 [Window / Open Perspective / ...]。
  • 在 [7] 中:一旦 Ant 任务 BD 完成,可返回 [SQL Explorer] 视图并刷新 BD JPA 树结构。
  • 在 [8] 中:可以看到已创建的 [jpa01_personne] 表。

请读者尝试使用其他 SGBD 文件重新生成 BD。操作步骤如下:

  • 将文件 [conf/<sgbd>/persistence.xml] 复制到文件夹 [src/META-INF] 中,其中 <sgbd> 代表已测试的 SGBD
  • 按照附录中关于<sgbd>的说明启动该数据库
  • 在 SQL Explorer 视图中,建立与 <sgbd> 的连接。附录中针对每个 SGBD 文件也对此进行了说明
  • 重新执行之前的测试

至此,我们已掌握以下要点:

  • 我们对对象/关系桥接的概念有了更深入的理解。此处通过 Hibernate 实现了该功能。后续我们将使用 Toplink。
  • 我们知道该对象/关系桥接在两个位置进行配置:
  • @Entity 对象中,这里指定了对象字段与 BD 表中列之间的关联
  • [META-INF/persistence.xml] 中,我们向 JPA 实现提供了关于对象/关系桥接的两个组成部分的信息:@Entity 对象(对象)和数据库(关系)。
  • 我们创建了两个名为 DDL 和 BD 的 Ant 任务,这些任务使我们能够根据之前的配置创建数据库,甚至在编写任何 Java 代码之前。

现在,我们的应用程序的 JPA 层已正确配置,我们可以开始使用 Java 代码探索 API 和 JPA。

2.1.9. 应用程序的持久化上下文

让我们详细说明一下 JPA 客户端的运行环境:

我们知道,JPA [2] 层建立了一个对象 [3] 与关系型 [4] 之间的桥梁。 在该对象/关系桥接框架下,由 JPA 层管理的所有对象统称为“持久化上下文”。 要访问持久化上下文中的数据,JPA [1] 客户端必须通过 JPA [2] 层:

  1. 它可创建一个对象,并请求 JPA 层将其持久化。此时,该对象即成为持久化上下文的一部分。
  2. 它可向 [JPA] 层请求现有持久化对象的引用。
  3. 他可以修改从 JPA 层获取的持久化对象。
  4. 它可以请求 JPA 层从持久化上下文中删除一个对象。

JPA 层向客户端提供了一个名为 [EntityManager] 的接口,顾名思义,该接口用于管理持久化上下文中的 @Entity 对象。下面介绍该接口的主要方法:

void persist(Object entity)
entity 放入持久化上下文
void remove(Object entity)
entity 从持久化上下文中移除
<T> T merge(T entity)
将持久化上下文未管理的客户端对象 entity
与持久化上下文中具有相同主键的对象 entity 合并。
生成的结果是持久化上下文中的对象 entity
<T> T find(Class<T> entityClass,
 Object primaryKey)
将数据库中通过主键检索到的对象
。该对象的类型 T 使
JPA 层确定应查询哪个表。
由此创建的持久化对象被返回给客户端。
Query createQuery(String queryText)
根据 JPQL 查询
(Java持久化查询语言)。查询 JPQL 与
与 SQL 查询类似,只是查询的是对象而非表。
Query createNativeQuery(String queryText)
与前一个方法类似,只是 queryText 属于
命令为 SQL 而非 JPQL。
Query createNamedQuery(String name)
createQuery 的方法相同,只是 JPQL queryText 的命令
已外包至配置文件并关联了一个名称。
该名称即为该方法的参数。

EntityManager 对象的生命周期未必与应用程序的生命周期一致。它有开始和结束。因此,JPA 客户端可以依次与不同的 EntityManager 对象进行交互。 与 EntityManager 关联的持久化上下文与其具有相同的生命周期。二者密不可分。当 EntityManager 对象被关闭时,其持久化上下文会在必要时与数据库同步,随后便不复存在。 若要重新获得持久化上下文,必须创建一个新的 EntityManager

客户端 JPA 可以通过以下语句创建一个 EntityManager,从而建立一个持久化上下文:


        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  • javax.persistence.Persistence 是一个静态类,用于获取 EntityManager 对象的工厂。该工厂与特定的持久化单元相关联。 需要提醒的是,配置文件 [META-INF/persistence.xml] 用于定义持久化单元,且这些单元都有一个名称:

    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">

上文中的持久化单元名为 jpa。它附带了一整套专属配置,其中包括与其协同工作的 SGBD。 指令 [Persistence.createEntityManagerFactory("jpa")] 创建了一个 EntityManagerFactory 类型的对象工厂,该工厂能够提供 EntityManager 对象,用于管理与名为 jpa 的持久化单元相关的持久化上下文。 获取 EntityManager 对象(即持久化上下文)需基于 EntityManagerFactory 对象,具体操作如下:

        EntityManager em = emf.createEntityManager();

[EntityManager] 接口中的以下方法可用于管理持久化上下文的生命周期:

void close()
关闭持久化上下文。强制持久化上下文与数据库同步:
  • 如果持久化上下文中的某个对象不在数据库中,则通过操作 SQL INSERT 将其插入数据库
  • 如果上下文中的某个对象已存在于数据库中,且自读取以来已被修改,则执行操作 SQL UPDATE 以保存该修改
  • 如果上下文中的某个对象在执行 remove 操作后被标记为“已删除”,则执行 SQL DELETE 操作将其从数据库中删除。
void clear()
持久化上下文中的所有对象已被清空,但尚未关闭。
void flush()
持久化上下文将按照 close() 中描述的方式与数据库同步

客户端 JPA 可通过方法 [EntityManager] 强制将持久化上下文与数据库同步(如前所述的 flush)。 同步可以是显式的,也可以是隐式的。在第一种情况下,由客户端在需要同步时执行 flush 操作;否则,同步将在我们后续说明的特定时刻自动进行。同步模式由 [EntityManager] 接口的以下方法管理:

void setFlushMode(FlushModeType flushMode)
flushmode有两种可能的取值:
FlushModeType.AUTO(默认):在每次向数据库发出 SELECT 请求之前进行同步。
FlushModeType.COMMIT:仅在数据库事务结束时进行同步。
FlushModeType getFlushMode()
返回当前的同步模式

总结如下。在默认模式 FlushModeType.AUTO 下,持久化上下文将在以下时间点与数据库进行同步:

  1. 每次对数据库执行 SELECT 操作之前
  2. 数据库事务结束时
  3. 在持久化上下文上执行 flush close 操作之后

FlushModeType.COMMIT 模式下,情况相同,但操作 1 不会发生。与 JPA 层交互的标准模式是事务模式。 客户端在事务内部对持久化上下文执行各种操作。在此情况下,持久化上下文与数据库的同步时机在 AUTO 模式下对应上述情况 1 和 2,而在 COMMIT 模式下仅对应情况 2。

最后介绍查询接口(Query)中的API,该接口可用于向持久化上下文发出JPQL命令,或直接向数据库发出SQL命令以检索数据。 Query接口如下:

我们将使用上述方法 1 至 4:

  • 1 - 方法 getResultList 执行 SELECT,该操作返回多个对象。这些对象将存储在 List 对象中。 该对象是一个接口。该接口提供了一个 Iterator 对象,可用于以以下形式遍历列表 L 中的元素:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
            // 处理表示列表中当前元素的对象 iterator.next()
...
}

列表 L 也可通过 for 进行操作:


        for (Object o : L) {
            // 利用对象 o
}
  • 2 - 方法 getSingleResult 执行命令 JPQL / SQL / SELECT,该命令返回一个单一对象。
  • 3 - 方法 executeUpdate 执行 SQL 命令(更新删除),并返回受该操作影响的行数。
  • 4 - 方法 setParameter(String, Object) 允许为带参数的 JPQL 命令中的命名参数赋值
  • 5 - 方法 setParameter(int, Object) 中的参数并非通过名称指定,而是通过其在 JPQL 命令中的位置指定。

2.1.10. 首个 JPA 客户端

让我们从Java的角度重新审视该项目:

 

现在我们对该项目已了如指掌,唯独尚未了解[src/tests]文件夹的内容,接下来我们将对此进行分析。该文件夹包含两个用于测试JPA层的测试程序:

  • [InitDB.java] 是一个将几行数据插入数据库表 [jpa01_personne] 的程序。其代码将为我们提供 JPA 层的首批数据。
  • [Main.java] 是一个对表 [jpa01_personne] 执行 CRUD 操作的程序。通过研究其代码,我们将能够探讨持久化上下文的基本概念以及该上下文中对象的生命周期。

2.1.10.1. 代码

程序 [InitDB.java] 的代码如下:


package tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import entites.Personne;

public class InitDB {
    // 常量
    private final static String TABLE_NAME = "jpa01_personne";

    public static void main(String[] args) throws ParseException {
        // 持久化单元
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // 从持久化单元中检索 EntityManagerFactory
        EntityManager em = emf.createEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 从人员表中删除条目
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // 创建两个人员
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 人员持久化
        em.persist(p1);
        em.persist(p2);
        // 显示人员
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 结束事务
        tx.commit();
        // 结束 EntityManager
        em.close();
        // 结束 EntityManagerFactory
        emf.close();
        // 日志
        System.out.println("terminé ...");
    }
}

阅读此代码时,需结合第 2.1.9 节的说明进行理解。

  • 第19行:为持久化单元jpa(在persistence.xml中定义)请求一个EntityManagerFactory emf对象。此操作通常在应用程序的生命周期中仅执行一次。
  • 第 21 行:请求一个 EntityManager em 对象来管理持久化上下文。
  • 第 23 行:请求一个 Transaction 对象来管理事务。在此提醒,对持久化上下文的操作必须在事务内部进行。 虽然这并非强制要求,但若不遵循可能会遇到问题。如果应用程序在 EJB3 容器中运行,则对持久化上下文的操作必须始终在事务内部进行。
  • 第 24 行:事务开始
  • 第 26 行:对表“jpa01_personne”(nativeQuery)执行 SQL 删除命令。 这样做是为了清空表中的所有内容,从而更清楚地查看应用程序 [InitDB] 的执行结果
  • 第 28-29 行:创建了两个 Personne 对象 p1 p2。它们是普通对象,目前与持久化上下文无关。 就持久化上下文而言,Hibernate 将这些对象定义为“瞬态”(transient)对象,以区别于由持久化上下文管理的“持久”(persistent)对象。 我们将更倾向于使用非持久化对象”(非法语表达)来指代尚未由持久化上下文管理的对象,并使用“持久化对象”来指代由持久化上下文管理的对象。此外,我们还会遇到第三类对象,即“脱离对象”(detached),它们是先前曾为持久化对象,但其持久化上下文已被关闭的对象。 客户端可能持有此类对象的引用,这解释了为何在持久化上下文关闭时,它们未必会被销毁。此时称其处于“脱离”状态。操作 [EntityManager].merge 可将其重新关联到新创建的持久化上下文中。
  • 第 31-32 行:通过操作 [EntityManager].persist,人员 p1 p2 被纳入持久化上下文。此时,它们成为持久化对象。
  • 第 35-37 行:执行 JPQL 语句“select p from Personne p order by p.nom asc”。 Personne 并非表(表名为 jpa01_personne),而是与该表关联的 @Entity 对象。 这里是一个针对持久化上下文的 JPQL(Java 持久化查询语言)查询,而不是针对数据库的 SQL 命令。 话虽如此,除了 Personne 对象取代了 jpa01_personne 表之外,两者的语法是完全相同的。 一个 for 循环遍历 select 查询返回的(人员)列表,并将每个元素显示在控制台上。此处旨在验证第 31-32 行放入持久化上下文中的元素是否确实存在于表中。 在此过程中,持久化上下文将与数据库进行透明同步。实际上,系统会发出一个 select 请求,而我们之前提到过,这正是进行同步操作的场景之一。因此,就在此时,后台会执行 JPA / Hibernate 将发出两个命令 SQL insert,将这两个人插入到表 jpa01_personne 中。 操作 persist 并未执行此操作。该操作将对象添加到持久化上下文中,但不会对数据库产生影响。实际操作发生在同步过程中,此处即在数据库上执行 select 之前。
  • 第 39 行:结束第 24 行开始的事务。将再次进行同步。由于自上次同步以来持久化上下文未发生变化,此处不会发生任何操作。
  • 第 41 行:关闭持久化上下文。
  • 第 43 行:关闭 EntityManager 工厂。

2.1.10.2. 代码执行

  • 启动 SGBD MySQL5
  • 如有需要,将 conf/mysql5/persistence.xml 放入 META-INF/persistence.xml 中
  • 运行应用程序 [InitDB]

将得到以下结果:

  • 在 [1] 中:Java 视图中的控制台显示。结果符合预期。
  • 在 [2] 中:通过 SQL Explorer 视图(如第 2.1.8 节所述)验证 [jpa01_personne] 表的内容。可以注意到两点:
    • 主键 ID 是在无需人工干预的情况下自动生成的
    • 版本号也是如此。可以看到第一个版本的编号为 0..

至此,我们已掌握了JPA框架的基础要素。我们已成功将数据插入表中。我们将以此为基础编写第二个测试,但在那之前,先来谈谈日志。

2.1.11. 实现 Hibernate 日志

我们可以了解SQL层/Hibernate向数据库发出的JPA命令。 了解这些日志很有意义,这样可以验证 JPA 层是否与开发人员亲自编写 SQL 语句同样高效。

使用 JPA / Hibernate,可在文件 [persistence.xml] 中查看日志 SQL:


            <!-- 持久类 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- 日志SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- 连接JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />

  • 第 4-6 行:SQL 日志目前尚未启用。现在通过移除第 3 行和第 7 行的注释标记来启用它们。

重新运行应用程序 [InitDB]。此时控制台显示如下:

Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
[personnes]
Hibernate: 
    select
        personne0_.ID as ID0_,
        personne0_.VERSION as VERSION0_,
        personne0_.NOM as NOM0_,
        personne0_.PRENOM as PRENOM0_,
        personne0_.DATENAISSANCE as DATENAIS5_0_,
        personne0_.MARIE as MARIE0_,
        personne0_.NBENFANTS as NBENFANTS0_ 
    from
        jpa01_personne personne0_ 
    order by
        personne0_.NOM asc
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
terminé ...
  • 第 2-4 行:来自以下指令的 SQL delete 命令:

        // 删除人员表中的条目
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • 第5-18行:来自以下指令的命令 SQL insert

        // 人员数据持久化
        em.persist(p1);
        em.persist(p2);
  • 第 21-32 行:来自以下指令的 SQL select 命令:

        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) 

如果进行中间控制台输出,会发现 Java 代码中 I 语句的日志 SQL 是在 I 语句执行时写入的。这并不意味着此时数据库上执行了显示的 SQL 命令。 实际上,该指令已被缓存,将在下次持久化上下文与数据库同步时执行。

还可以通过文件 [src/log4j.properties] 获取其他日志:

  • 在 [1] 中,文件 [log4j.properties] 由名为 LOG4j 的工具生成的归档文件 [log4j-1.2.13.jar] [2] 所调用 (Java日志)所包含的[2]文件,该工具可通过网址[http://logging.apache.org/log4j/docs/index.html]获取。 将其放置在 Eclipse 项目的 [src] 文件夹中,我们知道 [log4j.properties] 将自动复制到 [3] 项目的 [bin] 文件夹中。 完成此操作后,该文件现已位于该项目的 classpath 文件夹中,而 [2] 归档文件将从该处获取它。

文件 [log4j.properties] 允许我们监控某些 Hibernate 日志。在之前的执行中,其内容如下:


# 将日志消息重定向到标准输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

# 根日志器选项
log4j.rootLogger=ERROR, stdout

# Hibernate 日志选项(INFO 仅显示启动消息)
#log4j.logger.org.hibernate=INFO

# 记录 JDBC 绑定参数的运行时参数
#log4j.logger.org.hibernate.type=DEBUG

对于此配置我将简要说明,因为我从未花时间认真研究过 LOG4j。

  • 第 1-8 行出现在我遇到过的所有 log4j.properties 文件中
  • 第10-14行出现在Hibernate示例中的log4j.properties文件中。
  • 第11行:控制Hibernate的通用日志。由于该行已被注释,因此此处的日志功能被禁用。 日志级别有多种:INFO(关于Hibernate运行情况的一般信息)、WARN(Hibernate提示可能存在的问题)、DEBUG(详细日志)。 INFO级别日志最简洁,DEBUG模式则最详尽。启用第11行可了解Hibernate的运行情况,尤其是在应用程序启动时。这通常很有参考价值。
  • 第 12 行(如果启用)可显示在执行带参数的 SQL 查询时实际使用的参数。

首先,让我们取消第 14 行的注释


# 记录 JDBC 绑定参数的运行时参数
log4j.logger.org.hibernate.type=DEBUG

并重新执行 [InitDB]。此修改产生的新日志如下(部分视图):

Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 1
07:20:03,843 DEBUG StringType:80 - binding 'Durant' to parameter: 2
07:20:03,843 DEBUG StringType:80 - binding 'Sylvie' to parameter: 3
07:20:03,843 DEBUG DateType:80 - binding '05 juillet 2001' to parameter: 4
07:20:03,843 DEBUG BooleanType:80 - binding 'false' to parameter: 5
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 6
  • 第 8-10 行是因启用 [log4j.properties] 的第 14 行而产生的新日志。它们显示了分配给第 2-7 行带参数查询中形式参数 ? 的 5 个值。 由此可见,VERSION 列将接收值 0(第 8 行)。

现在我们启用 [log4j.properties] 的第 11 行:

# Hibernate 日志选项(INFO 仅显示启动消息)
log4j.logger.org.hibernate=INFO

然后重新执行 [InitDB]:

07:50:23,937  INFO Version:15 - Hibernate EntityManager 3.2.0.CR3
07:50:23,968  INFO Version:15 - Hibernate Annotations 3.2.0.CR3
07:50:23,984  INFO Environment:500 - Hibernate 3.2.0.cr5
07:50:23,984  INFO Environment:533 - hibernate.properties not found
07:50:23,984  INFO Environment:667 - Bytecode provider name : cglib
07:50:24,000  INFO Environment:584 - using JDK 1.4 java.sql.Timestamp handling
07:50:24,375  INFO AnnotationBinder:387 - Binding entity from annotated class: entites.Personne
07:50:24,421  INFO EntityBinder:340 - Bind entity entites.Personne on table jpa01_personne
07:50:24,609  INFO C3P0ConnectionProvider:50 - C3P0 using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/jpa
07:50:24,609  INFO C3P0ConnectionProvider:51 - Connection properties: {user=jpa, password=****, autocommit=true, release_mode=auto}
07:50:24,609  INFO C3P0ConnectionProvider:54 - autocommit mode: true
07:50:25,296  INFO SettingsFactory:81 - RDBMS: MySQL, version: 5.0.37-community-nt
07:50:25,296  INFO SettingsFactory:82 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
07:50:25,312  INFO Dialect:141 - Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect
07:50:25,312  INFO TransactionFactoryFactory:34 - Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
07:50:25,312  INFO TransactionManagerLookupFactory:33 - No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
07:50:25,328  INFO SettingsFactory:134 - Automatic flush during beforeCompletion(): disabled
07:50:25,328  INFO SettingsFactory:138 - Automatic session close at end of transaction: disabled
07:50:25,328  INFO SettingsFactory:145 - JDBC batch size: 15
07:50:25,328  INFO SettingsFactory:148 - JDBC batch updates for versioned data: disabled
07:50:25,328  INFO SettingsFactory:153 - Scrollable result sets: enabled
07:50:25,328  INFO SettingsFactory:161 - JDBC3 getGeneratedKeys(): enabled
07:50:25,328  INFO SettingsFactory:169 - Connection release mode: auto
07:50:25,328  INFO SettingsFactory:193 - Maximum outer join fetch depth: 2
07:50:25,328  INFO SettingsFactory:196 - Default batch fetch size: 1
07:50:25,328  INFO SettingsFactory:200 - Generate SQL with comments: disabled
07:50:25,328  INFO SettingsFactory:204 - Order SQL updates by primary key: disabled
07:50:25,328  INFO SettingsFactory:369 - Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
07:50:25,328  INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTranslatorFactory
07:50:25,328  INFO SettingsFactory:212 - Query language substitutions: {}
07:50:25,328  INFO SettingsFactory:217 - JPA-QL strict compliance: enabled
07:50:25,328  INFO SettingsFactory:222 - Second-level cache: enabled
07:50:25,328  INFO SettingsFactory:226 - Query cache: disabled
07:50:25,328  INFO SettingsFactory:356 - Cache provider: org.hibernate.cache.NoCacheProvider
07:50:25,328  INFO SettingsFactory:241 - Optimize cache for minimal puts: disabled
07:50:25,328  INFO SettingsFactory:250 - Structured second-level cache entries: disabled
07:50:25,343  INFO SettingsFactory:270 - Echoing all SQL to stdout
07:50:25,343  INFO SettingsFactory:277 - Statistics: disabled
07:50:25,343  INFO SettingsFactory:281 - Deleted entity synthetic identifier rollback: disabled
07:50:25,343  INFO SettingsFactory:296 - Default entity-mode: pojo
07:50:25,468  INFO SessionFactoryImpl:161 - building session factory
07:50:25,750  INFO SessionFactoryObjectFactory:82 - Not binding factory to JNDI, no JNDI name configured
07:50:25,765  INFO SchemaExport:154 - Running hbm2ddl schema export
07:50:25,765  INFO SchemaExport:179 - exporting generated schema to database
07:50:25,968  INFO SchemaExport:196 - schema export complete
Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    ... 

阅读这些日志可以获得许多有用的信息:

  • 第 7 行:Hibernate 指出了它找到的一个 @Entity 类的名称
  • 第 8 行:表明类 [Personne] 将与表 [jpa01_personne] 建立关联
  • 第 9 行:指明了将要使用的连接池 C3P0、JDBC 驱动程序名称以及待管理的数据库 URL
  • 第 10 行:提供 Jdbc 连接的其他特性:所有者、提交类型等
  • 第 14 行:用于与 SGBD 进行交互的方言
  • 第 15 行:使用的事务类型。JDBCTransactionFactory 表示应用程序自行管理事务。它并非在 EJB3 容器中运行,该容器本应提供自己的事务服务。
  • 后续几行涉及我们尚未涉及的Hibernate配置选项。感兴趣的读者可查阅Hibernate文档。
  • 第 37 行:SQL 命令将显示在控制台上。这是在 [persistence.xml] 中要求的:

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="use_sql_comments" value="true" />
  • 第43-45行:将数据库结构导出至SGBD和c.a.d文件,随后清空数据库并重新创建。 此机制源于在 [persistence.xml] 中的配置(见下文第 4 行):

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!--  自动创建模式 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- 方言 -->
            ...

当应用程序因无法理解的 Hibernate 异常而“崩溃”时,应首先在 [log4j.properties] 中将 Hibernate 日志启用为 DEBUG 模式,以便更清晰地查看问题:


# 根日志器选项
log4j.rootLogger=ERROR, stdout

# Hibernate 日志选项(INFO 仅显示启动消息)
log4j.logger.org.hibernate=DEBUG

在本文档的后续部分中,日志默认处于禁用状态,以便获得更易读的控制台显示。

2.1.12. 通过 Hibernate 控制台探索 JPQL / HQL 中的 语言

注意:本节需要 Hibernate Tools 插件(第 5.2.5 节)。

在应用程序代码 [InitDB] 中,我们使用了一个 JPQL 查询。JPQL(Java 持久化查询语言)是一种用于查询持久化上下文的语言。遇到的查询如下:

select p from Personne p order by p.nom asc

该查询选取了与 @Entity [Personne] 关联的表中所有元素,并按名称升序返回。在上述查询中,p.nom 是 [Personne] 类的一个实例 p 的 name 字段。 因此,JPQL 查询是在持久化上下文中的 @Entity 对象上操作,而不是直接在数据库表上操作。 JPA 层将把该 JPQL 查询转换为适合其所处理的 SGBD 的 SQL 查询。 因此,在 JPA / Hibernate 实现,且该实现关联了 SGBD 和 MySQL5,则前一个查询 JPQL 将被转换为下一个查询 SQL:

select
  personne0_.ID as ID0_,
  personne0_.VERSION as VERSION0_,
  personne0_.NOM as NOM0_,
  personne0_.PRENOM as PRENOM0_,
  personne0_.DATENAISSANCE as DATENAIS5_0_,
  personne0_.MARIE as MARIE0_,
  personne0_.NBENFANTS as NBENFANTS0_ 
 from
  jpa01_personne personne0_ 
 order by
  personne0_.NOM asc

JPA 层利用 @Entity 对象 [Personne] 的配置,生成了正确的 SQL 订单。此处实现的是对象/关系映射。

[Hibernate Tools] 插件(第 5.2.5 节)提供了一个名为“Hibernate 控制台”的工具,可用于

  • 在持久化上下文中发出 JPQL 命令或其超集 HQL(Hibernate 查询语言)
  • 获取结果
  • 了解在数据库上实际执行的等效 SQL 语句

Hibernate 控制台是学习 JPQL 语言并熟悉 JPQL / SQL 桥接机制的绝佳工具。 众所周知,JPA 深受 Hibernate 或 Toplink 等 ORM 工具的启发。 JPQL 与 Hibernate 的 HQL 语言非常接近,但并未完全继承其所有功能。 在 Hibernate 控制台中,可以发出 HQL 命令,这些命令将在控制台中正常执行,但不属于 JPQL 语言,因此无法在 JPA 客户端中使用。 遇到此类情况时,我们将予以说明。

现在为当前的 Eclipse 项目创建一个 Hibernate 控制台:

  • [1]:切换到 [Hibernate Console] 视图(Window / Open Perspective / Other)
  • [2]:我们在 [Hibernate Configuration] 窗口中创建一个新配置
  • 通过按钮 [4],我们选择要为其创建 Hibernate 配置的 Java 项目。其名称显示在 [3] 中。
  • 在 [5] 中,为该配置指定所需名称。此处我们采用了 [3]。
  • 在 [6] 中,我们指定使用 JPA 配置,以便工具知道应调用 [META-INF/persistence.xml] 文件
  • 在 [7] 中:我们指定在该 [META-INF/persistence.xml] 文件中,需使用名为 jpa 的持久化单元。
  • 在 [8] 中,我们验证配置。

接下来,需要启动 SGBD。此处指的是 MySQL5。

  • 在 [1] 中:创建的配置呈现为三叉树结构
  • 在 [2] 中:分支 [Configuration] 列出了控制台用于配置自身的对象:此处为 @Entity Personne
  • 在 [3] 中:Session Factory 是 Hibernate 中的一个概念,与 EntityManager 中的 JPA 类似。它通过 [Configuration] 分支中的对象实现对象与关系数据库的桥接。 在 [3] 中介绍了持久化上下文的对象,这里再次出现了 @Entity Personne
  • 在 [4] 中:通过 [persistence.xml] 中的配置访问的数据库。其中包含表 [jpa01_personne]。
  • 在 [1] 中,创建编辑器 HQL
  • 在编辑器 HQL 中,
    • 在 [2] 中,若存在多个配置,则选择要使用的 Hibernate 配置
    • 在 [3] 中,输入要执行的命令 JPQL
    • 在 [4] 中,执行该命令
  • 在 [5] 中,可在 [Hibernate Query Result] 窗口中获取查询结果。此处可能遇到两种情况:
    • 没有任何输出(没有一行数据)。Hibernate 控制台使用 [persistence.xml] 的内容与 SGBD 建立了连接。然而,该配置中有一项属性要求清空数据库:

            <property name="hibernate.hbm2ddl.auto" value="create" />

因此,在重新执行上述 JPQL 命令之前,必须先重新运行 [InitDB] 应用程序。

  • (待续)
    • 没有[Hibernate Query Result]窗口。通过[Window / Show View / ...]调用该窗口

窗口 [Hibernate Dynamic SQL preview](下文中的 [1])用于查看即将执行的请求 SQL,该请求将用于执行当前正在编写的命令 JPQL。 只要 JPQL 命令的语法正确,相应的 SQL 命令就会出现在此窗口中:

  • 在 [2] 中,删除之前的命令 HQL
  • 在 [3] 中,执行一个新命令
  • 在 [4] 中,结果
  • 在 [5] 中,基于

编辑器 HQL 提供 HQL 命令的编写帮助:

  • 在 [1] 中:一旦编辑器识别出 p Personne 对象,它便可在输入时自动补全 p 的字段。
  • 在 [2] 中:HQL 命令有误。应输入 where p.marie=true
  • 在 [3] 中:错误已在 [SQL Preview] 窗口中报告

建议读者在该数据库上执行其他 HQL / JPQL 命令。

2.1.13. 第二个客户端 JPA

让我们从 Java 角度重新审视该项目:

 
  • [InitDB.java] 是一个将几行数据插入数据库表 [jpa01_personne] 的程序。通过研究其代码,我们掌握了 API 和 JPA 的初步信息。
  • [Main.java] 是一个对表 [jpa01_personne] 执行 CRUD 操作的程序。对其代码的研究将使我们能够回顾持久化上下文的基本概念以及该上下文中对象的生命周期。

2.1.13.1. 代码结构

[Main.java] 将执行一系列测试,每个测试旨在展示 JPA 的某个特定方面:

 

方法 [main]

  • 依次调用 test1test11 方法。我们将分别介绍这些方法的代码。
  • 此外,该方法还使用了以下私有辅助方法:cleandumploggetEntityManagergetNewEntityManager

本文将介绍方法 main 以及所谓的辅助方法:


package tests;

...
import entites.Personne;

@SuppressWarnings("unchecked")
public class Main {

    // 常量
    private final static String TABLE_NAME = "jpa01_personne";

    // 持久化上下文
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;

    // 共享对象
    private static Personne p1, p2, newp1;

    public static void main(String[] args) throws Exception {
        // 数据库清理
        log("clean");clean();

        // 转储表
        dump();

        // test1
        log("test1");test1();

...
        // 测试11
        log("test11");test11();

        // 持久化上下文结束
        if (em.isOpen())
            em.close();

        // 关闭 EntityManagerFactory
        emf.close();
    }

    // 检索当前的 EntityManager
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // 获取一个新的 EntityManager
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }

    // 显示表内容
    private static void dump() {
        // 当前持久化上下文
        EntityManager em = getEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 显示人员
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 事务结束
        tx.commit();
    }

    // 清空 BD
    private static void clean() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除表中的元素 PERSONNES
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // 事务结束
        tx.commit();
    }

    // 日志
    private static void log(String message) {
        System.out.println("main : ----------- " + message);
    }

    // 创建对象
    public static void test1() throws ParseException {
...
    }

    // 修改上下文对象
    public static void test2() {
...
    }

    // 请求对象
    public static void test3() {
...
    }

    // 删除持久化上下文中的对象
    public static void test4() {
....
    }

    // 分离、重新附加和修改
    public static void test5() {
...
    }

    // 删除不属于持久化上下文的对象
    public static void test6() {
...
    }

    // 修改不属于持久化上下文的对象
    public static void test7() {
...
    }

    // 将对象重新附加到持久化上下文
    public static void test8() {
...
    }

    // SELECT 语句触发同步
    // 数据库与持久化上下文的同步
    public static void test9() {
....
    }

    // 版本控制(乐观锁)
    public static void test10() {
...
    }

    // 事务回滚
    public static void test11() throws ParseException {
...
    }

}
  • 第 13 行:基于 [persistence.xml] 中定义的 JPA 持久化单元构建的 EntityManagerFactory emf 对象。它将使我们能够在应用程序运行过程中创建各种持久化上下文。
  • 第 14 行:一个尚未初始化的持久化上下文 EntityManager
  • 第 17 行:三个由测试共享的 [Personne] 对象
  • 第 21 行:清空表 jpa01_personne,然后在第 24 行显示该表,以确保从空表开始。
  • 第 27-31 行:测试序列
  • 第 34-35 行:若持久化上下文 em 处于打开状态,则将其关闭。
  • 第 38 行:关闭 EntityManagerFactory emf 对象。
  • 第 42-47 行:方法 [getEntityManager] 将 EntityManager(即持久化上下文)设为当前上下文,若不存在则创建一个新的(第 43-44 行)。
  • 第 50-56 行:方法 [getNewEntityManager] 创建一个新的持久化上下文。如果之前已存在一个,则将其关闭(第 51-52 行)
  • 第 59-72 行:方法 [dump] 显示表 [jpa01_personne] 的内容。该代码已在 [InitDB] 中出现过。
  • 第 75-85 行:方法 [clean] 清空表 [jpa01_personne]。该代码已在 [InitDB] 中出现过。
  • 第 88-90 行:方法 [log] 将作为参数传递的消息显示在控制台上,以便引起注意。

现在我们可以开始研究测试用例了。

2.1.13.2. 测试 1

测试1的代码如下:


// 创建对象
    public static void test1() throws ParseException {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 创建人员
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 人员持久化
        em.persist(p1);
        em.persist(p2);
        // 事务结束
        tx.commit();
        // 显示表
        dump();

}

该代码在 [InitDB] 中已出现过:它创建了两个人物,并将它们放入持久化上下文中。

  • 第4行:获取当前持久化上下文
  • 第 6-7 行:创建这两个人
  • 第 9-15 行:将这两个人在事务内放入持久化上下文中。
  • 第 15 行:由于事务提交,持久化上下文与数据库进行了同步。这两个人将被添加到表 [jpa01_personne] 中。
  • 第17行:显示表格

此首次测试的控制台输出如下:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]

2.1.13.3. 测试 2

测试2的代码如下:


// 修改上下文中的对象
    public static void test2() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 增加 p1 的子女数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 修改其婚姻状况
        p1.setMarie(false);
        // 对象 p1 被自动保存(脏数据检查)
        // 在下一次同步时(提交或查询)
        // 事务结束
        tx.commit();
        // 显示新表
        dump();
    }
  • 测试2的目标是修改持久化上下文中的一个对象,然后显示表中的内容以查看是否发生了修改
  • 第4行:获取当前持久化上下文
  • 第6-7行:操作将在事务中进行
  • 第 9、11 行:修改人员 p1 的子女数量及其婚姻状况
  • 第15行:事务结束,因此将持久化上下文与数据库同步
  • 第17行:显示表

测试 2 的控制台输出如下:

1
2
3
4
5
6
7
8
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
  • 第4行:修改前的p1
  • 第8行:修改后的p1。请注意其版本号已变为1。该版本号在每次更新该行时会递增1。

2.1.13.4. 测试3

测试3的代码如下:


    // 查询对象
    public static void test3() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 查询人员 p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // 由于 p1 已存在于持久化上下文中,因此未访问数据库
        // p1b 和 p1 是相同的引用
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        // 请求一个不存在的对象会使指针变为空
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        // 事务结束
        tx.commit();
}
  • 测试3关注方法[EntityManager.find],该方法用于从数据库中检索对象并将其放入持久化上下文中。除非出现非常规用法,否则我们不再对所有测试中涉及的事务进行说明。
  • 第 9 行:向持久化上下文查询与 p1 具有相同主键的对象。有两种情况:
    • p1 已存在于持久化上下文中。此处即为这种情况。因此无需访问数据库。方法 find 仅返回对该持久化对象的引用。
    • p1 不在持久化上下文中。此时将通过给定的主键访问数据库。检索到的记录被放入持久化上下文中,而 find 返回该新持久化对象的引用。
  • 第 12 行:验证 find 是否已返回位于持久化上下文中的 p1 对象的引用
  • 第 14 行:请求一个既不在持久化上下文中也不在数据库中存在的对象。方法 find 随后返回指针 null。这一点在第 15 行进行了验证。

测试 3 的控制台输出如下:

1
2
3
main : ----------- test3
p1==p1b ? true
px==null ? true

2.1.13.5. 测试 4

测试4的代码如下:


    // 删除属于持久化上下文的对象
    public static void test4() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除持久化对象 p2
        em.remove(p2);
        // 事务结束
        tx.commit();
        // 显示新表
        dump();
}
  • 测试4关注方法[EntityManager.remove],该方法用于从持久化上下文(进而从数据库)中删除一个元素。
  • 第 9 行:将人员 p2 从持久化上下文中移除
  • 第 11 行:将上下文与数据库同步
  • 第 13 行:显示表。通常情况下,人员 p2 应该已不存在。

测试4的控制台输出如下:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test3
p1==p1b ? true
px==null ? true
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
  • 第 3 行:test1 中的 p2
  • 第12-14行:在test4执行完毕后,该用户已不存在。

2.1.13.6. 测试5

测试5的代码如下:


// 分离、重新附加并修改
    public static void test5() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 已分离
        Personne oldp1=p1;
        // 将 p1 重新附加到新上下文
        p1 = em.find(Personne.class, p1.getId());
        // 验证
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        // 事务结束
        tx.commit();
        // 增加 p1 的子节点数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 显示新表
        dump();
    }
  • 测试5关注对象在多个连续持久化上下文中的生命周期。此前,我们在不同测试中始终使用同一个持久化上下文。
  • 第4行:请求了一个新的持久化上下文。方法[getNewEntityManager]关闭了之前的上下文并打开了一个新的。这导致应用程序持有的对象p1和p2不再处于持久化状态。它们原本属于一个已被关闭的上下文。 这被称为“脱离状态”。它们不属于新的持久化上下文。
  • 第 6-7 行:事务开始。此处将以非典型方式使用该事务。
  • 第 9 行:记录现已脱离的对象 p1 的地址。
  • 第11行:向持久化上下文查询人员p1(使用p1的主键)。由于该上下文是新的,人员p1并不存在其中。因此将进行数据库访问。返回的对象将被放入新上下文中。
  • 第13行:验证上下文中的持久化对象p1是否不同于对象oldp1(该对象即原先已脱离的对象p1)。
  • 第15行:事务结束
  • 第17行:在事务之外修改新的持久化对象p1。这种情况下会发生什么?我们需要了解这一点。
  • 第 19 行:请求显示表。需要提醒的是,由于方法 dump 发出了 select,因此会自动执行持久化上下文与数据库的同步。

测试 5 的控制台输出如下:

1
2
3
4
5
6
7
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • 第5行:方法find确实访问了数据库,否则这两个指针的值会相等
  • 第7行和第3行:p1的子节点数量确实增加了1。因此,在事务外进行的修改已被纳入考虑。这实际上取决于所使用的SGBD。 在 SGBD 中,SQL 命令始终在事务内执行。 如果 JPA 客户端未自行启动显式事务,则 SGBD 将启动隐式事务。通常有两种情况:
    • 1 - 每个单独的 SQL 命令都属于一个事务,该事务在命令执行前打开,并在命令执行后关闭。 这被称为自动提交模式。因此,其运行效果就如同 JPA 客户端为每个 SQL 命令分别执行事务一样。
    • 2 - SGBD 未处于自动提交模式,并在客户 JPA 于非事务状态下发出的首个订单 SQL 时启动隐式事务,并由该客户负责关闭该事务。 因此,客户端 JPA 发出的所有 SQL 命令都属于该隐式事务。该事务可能因各种事件而结束:客户端关闭连接、开始新事务等。

这种情况取决于 SGBD 的配置。因此,这段代码不具备可移植性。稍后我们将展示一段不使用事务的代码,并会发现并非所有 SGBD 在处理该代码时都表现一致。因此,我们将认为在非事务环境下工作是一种编程错误。

  • 第7行:请注意版本号已更新为2。

2.1.13.7. 测试 6

测试6的代码如下:


// 删除不属于持久化上下文的对象
    public static void test6() {
        // 新建持久化上下文
        EntityManager em = getNewEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除不属于新持久化上下文的 p1
        try {
            em.remove(p1);
            // 事务结束
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // 回滚事务
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // 显示新表
        dump();
    }
  • 测试6试图删除一个不属于持久化上下文的对象。
  • 第4行:请求创建新的持久化上下文。因此旧的上下文被关闭,其中包含的对象变为脱离状态。前一个测试5中的对象p1即属于这种情况。
  • 第6-7行:开始事务。
  • 第10行:删除脱离上下文的对象p1。由于知道这会引发异常,因此用try/catch语句包裹了该操作。
  • 第 12 行:提交操作将不会发生。
  • 第16-21行:事务必须以commit(事务中的所有操作均被提交)或rollback(事务中的所有操作均被回滚)结束。 由于发生了异常,因此对事务执行 rollback。由于事务中的唯一操作已失败,无需回滚任何内容,但 rollback 会终止该事务。 这是我们首次使用 [EntityTransaction].rollback 操作。其实从最初的示例开始就应该这样做。为了保持代码简洁,我们之前才没有这样做。不过,读者必须牢记,代码中必须始终预留处理事务 rollback 的情况。
  • 第 24 行:显示该表。通常情况下,该表内容应未发生变化。

测试6的控制台输出如下:

1
2
3
4
5
6
7
8
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • 第 6 行:删除 p1 失败。异常消息说明试图删除一个脱离上下文的对象,因此无法执行。
  • 第8行:p1仍然存在。

2.1.13.8. 测试7

测试7的代码如下:


// 修改不属于持久化上下文的对象
    public static void test7() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 增加不属于新上下文的 p1 的子节点数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 事务结束
        tx.commit();
        // 显示新表——它应该没有变化
        dump();
    }
  • 测试7试图修改一个不属于持久化上下文的对象,并观察其对数据库的影响。可以推测这不会产生任何影响。测试结果也证实了这一点。
  • 第4行:请求了一个新的持久化上下文。因此,我们获得了一个不包含任何持久化对象的新上下文。
  • 第6-7行:事务开始。
  • 第9行:修改脱离上下文的对象p1。此操作不涉及持久化上下文em,因此不应引发异常或类似情况。这是对POJO对象进行的基本操作。
  • 第 11 行:提交操作会触发上下文与数据库的同步。该上下文为空,因此数据库未被修改。
  • 第24行:显示该表。通常情况下,该表不应发生变化。

测试 7 的控制台输出如下:

1
2
3
4
5
6
7
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • 第 7 行:数据库中 p1 的信息未发生变化。但在接下来的测试中,需注意内存中其子女数量现已变为 5。

2.1.13.9. 测试8

测试8的代码如下:


    // 将对象重新关联到持久化上下文
    public static void test8() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 将已分离的对象 p1 重新关联到新上下文
        newp1 = em.merge(p1);
        // 现在是 newp1 属于上下文,而不是 p1
        // 事务结束
        tx.commit();
        // 显示新表——p1的子节点数量应已发生变化
        dump();
}
  • 测试8将一个已分离的对象重新附加到持久化上下文中。
  • 第4行:请求了一个新的持久化上下文。因此,我们得到一个不包含任何持久化对象的新上下文。
  • 第6-7行:开始事务。
  • 第 9 行:将已脱离的对象 p1 重新附加到持久化上下文中。merge 操作可能涉及多个操作:
    • 情况1:在持久化上下文中,存在一个名为ps1的持久化对象,其主键与已脱离的对象p1相同。 p1 的内容被复制到 ps1 中,而 merge 引用了 ps1
    • 情况 2:在持久化上下文中,不存在与已分离对象 p1 具有相同主键的持久化对象 ps1。 此时将查询数据库以确认所查找的对象是否存在。若存在,则将该对象引入持久化上下文,使其成为持久化对象 ps1,并回到前述情况 1。
    • 情况 3:持久化上下文和数据库中均不存在与脱离对象 p1 具有相同主键的对象。此时将创建一个新对象 [Personne](new),并将其放入持久化上下文中。随后又回到情况 1。
    • 最终结果:脱离对象 p1 保持脱离状态。操作 merge 返回了一个引用(此处为 newp1),指向由 merge 派生的持久化对象 ps1。 客户端应用程序现在必须与持久化对象 ps1 进行交互,而不是与已分离的对象 p1 进行交互。
    • 需要注意的是,在情况1和情况3中,针对merge所安排的SQL的顺序存在差异: 在情况 1 和 2 中,该命令为 UPDATE,而在情况 3 中,该命令为 INSERT。
  • 第12行:提交操作会触发上下文与数据库的同步。该上下文不再为空,其中包含对象newp1。该对象将被持久化到数据库中。
  • 第24行:显示表以进行验证。

测试 8 的控制台输出如下:

main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test8
[personnes]
[1,3,Martin,Paul,31/01/2000,false,5]
  • 在测试 6 中(第 4 行),p1 的子节点数量为 4,随后在测试 7 中变为 5,但未被持久化到数据库中(第 7 行)。 在 merge 之后,newp1 已保存到数据库中:第 10 行,确实有 5 个子节点。
  • 第10行:newp1的版本号已更新为3。

2.1.13.10. 测试 9

测试9的代码如下:


// 一个 SELECT 查询会触发同步
    // 数据库与持久化上下文之间的同步
    public static void test9() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 增加 newp1 的子女数量
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // 人员显示 - newp1 的子女数量应已更改
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 事务结束
        tx.commit();
    }
  • 测试9旨在展示在执行select之前自动发生的上下文同步机制。
  • 第5行:未更改持久化上下文。因此newp1位于其中。
  • 第7-8行:事务开始。
  • 第10行:持久化对象newp1的子节点数量增加1(5 -> 6)。
  • 第12-15行:通过SELECT语句显示表。在执行select之前,上下文将与数据库进行同步。
  • 第 17 行:事务结束

为观察同步过程,我们启用 DEBUG(log4j.properties)模式下的 Hibernate 日志显示:


# 根日志记录器选项
log4j.rootLogger=ERROR, stdout

# Hibernate 日志记录选项(INFO 仅显示启动消息)
log4j.logger.org.hibernate=DEBUG

测试 9 的控制台输出如下:

main : ----------- test9
14:27:27,250 DEBUG JDBCTransaction:54 - begin
14:27:27,250 DEBUG ConnectionManager:415 - opening JDBC connection
14:27:27,250 DEBUG JDBCTransaction:59 - current autocommit status: true
14:27:27,250 DEBUG JDBCTransaction:62 - disabling autocommit
14:27:27,250 DEBUG JDBCContext:210 - after transaction begin
[personnes]
14:27:27,250 DEBUG QueryPlanCache:76 - located HQL query plan in cache (select p from Personne p order by p.nom asc)
14:27:27,250 DEBUG AbstractFlushingEventListener:58 - flushing session
...
14:27:27,250 DEBUG AbstractEntityPersister:3116 - entites.Personne.nbenfants is dirty
14:27:27,250 DEBUG DefaultFlushEntityEventListener:229 - Updating entity: [entites.Personne#1]
14:27:27,250 DEBUG Versioning:27 - Incrementing: 3 to 4
...
14:27:27,250 DEBUG AbstractFlushingEventListener:85 - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects
...
14:27:27,250 DEBUG ConnectionManager:463 - registering flush begin
14:27:27,250 DEBUG AbstractEntityPersister:2274 - Updating entity: [entites.Personne#1]
14:27:27,265 DEBUG AbstractEntityPersister:2276 - Existing version: 3 -> New version: 4
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - update jpa01_personne set VERSION=?, NOM=?, PRENOM=?, DATENAISSANCE=?, MARIE=?, NBENFANTS=? where ID=? and VERSION=?
14:27:27,265 DEBUG AbstractBatcher:476 - preparing statement
14:27:27,265 DEBUG AbstractEntityPersister:1927 - Dehydrating entity: [entites.Personne#1]
14:27:27,265 DEBUG IntegerType:80 - binding '4' to parameter: 1
14:27:27,265 DEBUG StringType:80 - binding 'Martin' to parameter: 2
14:27:27,265 DEBUG StringType:80 - binding 'Paul' to parameter: 3
14:27:27,265 DEBUG DateType:80 - binding '31 janvier 2000' to parameter: 4
14:27:27,265 DEBUG BooleanType:80 - binding 'false' to parameter: 5
14:27:27,265 DEBUG IntegerType:80 - binding '6' to parameter: 6
14:27:27,265 DEBUG IntegerType:80 - binding '1' to parameter: 7
14:27:27,265 DEBUG IntegerType:80 - binding '3' to parameter: 8
14:27:27,265 DEBUG AbstractBatcher:366 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
14:27:27,265 DEBUG AbstractBatcher:525 - closing statement
14:27:27,265 DEBUG ConnectionManager:472 - registering flush end
14:27:27,265 DEBUG HQLQueryPlan:150 - find: select p from Personne p order by p.nom asc
14:27:27,265 DEBUG QueryParameters:277 - named parameters: {}
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - select personne0_.ID as ID0_, personne0_.VERSION as VERSION0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.DATENAISSANCE as DATENAIS5_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_ from jpa01_personne personne0_ order by personne0_.NOM asc
...
14:27:27,265 DEBUG Loader:1164 - result row: EntityKey[entites.Personne#1]
...
14:27:27,265 DEBUG Loader:839 - total objects hydrated: 0
14:27:27,265 DEBUG StatefulPersistenceContext:748 - initializing non-lazy collections
[1,4,Martin,Paul,31/01/2000,false,6]
14:27:27,265 DEBUG JDBCTransaction:103 - commit
14:27:27,265 DEBUG SessionImpl:337 - automatically flushing session
...
14:27:27,265 DEBUG AbstractFlushingEventListener:91 - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
...
14:27:27,296 DEBUG JDBCTransaction:116 - committed JDBC Connection
...
  • 第 1 行:测试 9 开始
  • 第 2-6 行:Jdbc 事务开始。SGBD 的自动提交模式已禁用(第 5 行)
  • 第 7 行:由 Java 代码第 12 行触发的输出。Java 代码的后续行将触发 select,从而使持久化上下文与数据库同步。
  • 第 8 行:我们想要发出的 JPQL 命令已经发出过。Hibernate 在其“预编译查询”缓存中找到了它。
  • 第 9 行:Hibernate 通知将对持久化上下文进行刷新
  • 第 11-12 行:Hibernate(Hb)发现实体 Personne#1(主键为 1)已被修改(dirty)。
  • 第 12-13 行:Hb 通知将更新该实体,并将版本号从 3 更新为 4。
  • 第 15 行:上下文同步将导致 0 次插入、1 次更新 (update)、0 次删除 (delete)
  • 第 17-34 行:执行上下文同步(flush)。 需注意:版本号的递增(第19行)、已准备好的SQL更新命令(第21行)、update命令的参数值(第24-31行)。
  • 第 35 行:select 开始
  • 第 38 行:即将执行的 SQL 命令
  • 第 40 行:select 仅返回一行
  • 第42行:Hb发现其持久化上下文中已存在实体Personne#1,该实体正是SELECT语句从数据库中检索回来的。因此,它不会将从数据库获取的行复制到上下文中,这一操作被称为“数据填充”。
  • 第 43 行:它检查由 select 返回的对象是否存在依赖关系(通常为外键),这些依赖关系也需要加载(非延迟加载的集合)。此处没有依赖关系。
  • 第 44 行:由 Java 代码触发的显示操作
  • 第 45 行:由 Java 代码请求的 Jdbc 事务结束
  • 第 46 行:在 commit 期间进行的上下文自动同步开始。
  • 第 48 行:Hb 发现自上次同步以来上下文未发生变化。
  • 第 50 行:commit 结束。

再次,DEBUG模式下的Hibernate日志对于准确了解Hibernate的具体操作非常有用。

2.1.13.11. 测试 10

测试10的代码如下:


// 版本控制(乐观锁)
    public static void test10() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 直接在数据库中递增 newp1 的版本号(原生查询)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        // 事务结束
        tx.commit();
        // 开始新事务
        tx = em.getTransaction();
        tx.begin();
        // 增加 newp1 的子节点数量
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        // 事务结束 - 该事务必须失败,因为 newp1 已不再具有正确的版本
        try {
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
            // 回滚事务
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // 关闭已过时的上下文
        em.close();
        // 导出表数据 - p1 的版本可能已发生变化
        dump();
    }
  • 测试 10 旨在展示 @Entity Personne 中的 version 字段所引入的机制,该字段带有 @Version 属性。 我们曾解释过,该注解的作用是:每当对所属行执行一次update操作时,数据库中与@Version注解关联的列的值就会递增。 这种机制也被称为乐观锁optimistic locking),它要求想要修改数据库中对象 O 的客户端必须拥有该对象的最新版本。如果客户端没有最新版本,则说明自其获取该对象以来,该对象已被修改,因此必须向其发出警告。
  • 第4行:不更改持久化上下文。因此 newp1 仍处于该上下文中。
  • 第6-7行:事务开始。
  • 第 9 行:直接在数据库中将对象 newp1 的版本号增加 1(4 -> 5)。 类型为 nativeQuery 的查询会绕过持久化上下文,直接写入数据库。结果是持久化对象 newp1 及其在数据库中的映射不再具有相同的版本号。
  • 第 10 行:第一个事务结束
  • 第 13-14 行:开始第二个事务
  • 第 16 行:持久化对象 newp1 的子节点数量增加 1(6 -> 7)。
  • 第 19 行:事务结束。因此会进行同步。这将导致数据库中 newp1 的子节点数量被更新。 该更新将失败,因为持久化对象 newp1 的版本为 4,而数据库中待更新的对象版本为 5。系统将抛出异常,这正是代码中使用 try/catch 语句的原因。
  • 第 21 行:显示异常及其原因。
  • 第 25 行:回滚事务
  • 第 33 行:显示表信息:应可见数据库中 newp1 的版本为 5。

测试 10 的控制台输出如下:

1
2
3
4
5
6
7
main : ----------- test9
[personnes]
[1,4,Martin,Paul,31/01/2000,false,6]
main : ----------- test10
Erreur lors de la mise à jour de newp1 [javax.persistence.RollbackException,Error while commiting the transaction,org.hibernate.StaleObjectStateException,Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [entites.Personne#1]]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • 第 5 行:提交操作确实抛出了异常。异常类型为 [javax.persistence.RollbackException]。 相关消息较为模糊。若关注该异常(Exception.getCause)的成因,可见这是由于在未持有正确版本的情况下试图修改数据库行,从而引发的Hibernate异常。
  • 第7行:可以看到数据库中newp1的版本确实已被nativeQuery更新为5。

2.1.13.12. 测试 11

测试11的代码如下:


// 回滚事务
    public static void test11() throws ParseException {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // 从数据库中检索 p1 并将其重新关联到上下文
            p1 = em.find(Personne.class, p1.getId());
            // 增加 p1 的子女数量
            p1.setNbenfants(p1.getNbenfants() + 1);
            // 显示人员信息——p1的子女数量应已发生变化
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // 创建了两名同名人员,这违反了DDL的规定
            Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            // 人员数据持久化
            em.persist(p3);
            em.persist(p4);
            // 事务结束
            tx.commit();
        } catch (RuntimeException e1) {
            // 出现问题
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // 放弃当前上下文
            em.clear();
        }
        // 转储 - 由于回滚,该表不应发生变化
        dump();
    }
  • 测试11关注事务中rollback的机制。 事务遵循全有或全无原则:其中包含的 SQL 操作要么全部成功执行(提交),要么在其中任何一个操作失败时全部回滚。
  • 第4行:继续使用相同的持久化上下文。读者可能还记得,在上一个测试崩溃后,该上下文已被关闭。在这种情况下,[getEntityManager]会返回一个全新的、因此为空的上下文。
  • 第 7-27 行:使用单个 try/catch 块来处理可能遇到的问题
  • 第8-9行:开始一个将包含多个操作的交易 SQL
  • 第 11 行:在数据库中查询 p1 并将其放入上下文
  • 第13行:增加p1的子节点数量(6→7)
  • 第15-18行:显示数据库内容,这将强制执行上下文同步。此时数据库中p1的子节点数量将变为7,控制台输出应能验证这一点。
  • 第20-21行:创建了两个同名的实体p3和p4。 然而,@Entity Personne 中的 nom 字段具有 unique=true 属性,这导致在表 [jpa01_personne] 的 NOM 列上生成了一条唯一性约束。
  • 第 23-24 行:人员 p3 p4 被放入持久化上下文中。
  • 第26行:事务提交。随后进行第二次上下文同步,第一次同步发生在处理select时。 JPA 将为 p3 p4 发出两条命令:SQL 和 insert。 p3将被插入。对于p4,SGBD将抛出异常,因为p4与p3名称相同。 因此 p4 未被插入,Jdbc 驱动程序向客户端抛出异常。
  • 第 27 行:处理异常
  • 第 29-31 行:显示该异常及其在导致当前异常的异常链中之前的两个原因。
  • 第 34 行:回滚当前活动的事务。该事务始于 Java 代码的第 9 行。 此前已执行操作 update 来修改 p1 的子女数量,随后又执行了针对人员 p3 的操作 insert。回滚操作将撤销所有这些变更。
  • 第 39 行:清空持久化上下文
  • 第 42 行:显示表 [jpa01_personne]。需验证 p1 仍拥有 6 个子节点,且表中既不包含 p3,也不包含 p4

测试 11 的控制台输出如下:


main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • 第 3 行:数据库中 p1 的子节点数量从 6 变为 7,p1 的版本号更新为 6。
  • 第 4 行:事务提交时捕获的异常。仔细阅读可知,原因在于键 X(名称)重复。是插入 p4 导致了此错误,而此前已插入的 p3 同样具有名称 X。
  • 第7行:回滚后的表。p1恢复了版本5和6个子节点,p3和p4未被插入。

2.1.13.13. 测试 12

测试12的代码如下:


    // 重复相同操作,但不包含交易
    // 结果与之前使用 SGBD 时相同: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    // 配合 SQLSERVER 时,会导致表为空。连接将处于一种无法重新执行
    // 导致程序无法重新执行。此时必须重启服务器。
    // 与 SGBD Derby 情况相同
    // HSQL 插入第一人称 - 没有回滚

    public static void test12() throws ParseException {
        // 重新关联 p1
        p1 = em.find(Personne.class, p1.getId());
        // 增加 p1 的子女数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 显示人员 - p1的子女数量应已发生变化
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 创建了两名同名人员,这违反了DDL的规定
        Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        // 人员数据持久化
        em.persist(p3);
        em.persist(p4);
        // 该转储将导致 EM 上下文与 BD 进行同步
        try {
            dump();
        } catch (RuntimeException e3) {
            System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
                    .getCause().getMessage());
        }
        // 关闭当前上下文
        em.close();
        // 转储
        dump();
}
  • 测试 12 与测试 11 操作相同,但不在事务中进行。我们想看看这种情况下会发生什么。
  • 第 1-6 行:给出了使用不同 SGBD 进行的测试结果:
  • 使用若干 SGBD(Firebird、Oracle、MySQL5、Postgres)时,结果与测试 11 相同。 这表明这些 SGBD 实例自行发起了一项事务,该事务涵盖了截至引发错误的那个 SQL 命令之前收到的所有 SQL 命令,并且它们还自行发起了一个 rollback
  • 若与其他 SGBD(SQL 服务器,Apache Derby)同时存在,则会导致应用程序和/或 SGBD 崩溃。
  • 在 SGBD 和 HSQLDB 存在时,SGBD 打开的事务似乎处于 autocommit 模式: p1 的子节点数量修改以及 p3 的插入操作均已永久保存。仅 p4 的插入操作失败。

因此,结果依赖于 SGBD,这导致应用程序无法移植。需注意,对持久化上下文的操作必须始终在事务内进行。

2.1.14. 更改 SGBD

让我们回顾一下当前项目的测试架构:

客户端应用程序 [3] 仅能看到接口 JPA [5]。它既看不到该接口的实际实现,也看不到目标 SGBD。 因此,我们应当能够在不修改客户端 [3] 的情况下,更改链中的这两个元素。这就是我们现在试图验证的内容,首先从更改 SGBD 开始。此前我们一直使用的是 MySQL5。 我们在附录(第5段)中还介绍了另外六个版本,希望其中能包含读者最青睐的 SGBD。

无论如何,在Eclipse项目中进行的修改都很简单(见下文): 将 JPA 层的配置文件 persistence.xml [1] 替换为项目中 conf [2] 文件夹中的任意一个。 这些 JDBC 和 SGBD 的驱动程序已存在于 [jpa-divers]、[3] 和 [4] 库中。

2.1.14.1. Oracle 10g Express

Oracle 10g Express 介绍见附录第 5.7 节。Oracle 的 persistence.xml 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!-- 提供程序 -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- 持久类 -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- 日志SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- 连接JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  自动创建模式 -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!-- 属性 DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

此配置与 SGBD 和 MySQL5 的配置完全相同,仅在以下细节上有所不同:

  • 第 15-18 行配置了 JDBC 与数据库的连接
  • 第22行:指定要使用的SQL方言

在接下来的示例中,我们将仅标注发生变更的行。 关于配置的说明,请参阅专门介绍所用 SGBD 的附录。其中每次都会在 [SQL Explorer] 插件的背景下,给出 JDBC 连接的使用示例。 借助附录中的信息,读者可重现第2.1.10.2节中对[InitDB]应用程序结果的验证操作。

我们按照上述段落所述进行操作:

  • 启动 Oracle SGBD
  • 将 conf/oracle/persistence.xml 放入 META-INF/persistence.xml
  • 运行应用程序 [InitDB]

控制台显示以下结果:

此后,我们将不再展示这始终相同的屏幕截图。更有趣的是通过 SQL 视图探索 JDBC 与 SGBD 之间的关联。我们将遵循第 2.1.8 节中所述的步骤。

  • 在 [1] 中:与 Oracle 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请运行应用程序 [Main],然后停止 SGBD。

2.1.14.2. PostgreSQL 8.2

PostgreSQL 8.2 列于第 5.6 节的附录中。其 persistence.xml 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

要运行 [InitDB]:

  • 运行 SGBD PostgreSQL
  • 将 conf/postgres/persistence.xml 放入 META-INF/persistence.xml 目录中
  • 运行应用程序 [InitDB]

SQL 视图中,JDBC 与 SGBD 的关联如下:

  • 在 [1] 中:与 PostgreSQL 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树结构
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请运行应用程序 [Main],然后停止 SGBD

2.1.14.3. SQL Server Express 2005

SQL Server Express 2005 详见附录第 5.8 节,第 270 页。其 persistence.xml 文件内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
            <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

要运行 [InitDB]:

  • 运行 SGBD SQL Server
  • 将 conf/sqlserver/persistence.xml 放入 META-INF/persistence.xml
  • 运行应用程序 [InitDB]

SQL 视图中,JDBC 与 SGBD 的关联如下:

  • 在 [1] 中:与 SQL Server 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树结构
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请运行应用程序 [Main],然后停止 SGBD

2.1.14.4. Firebird 2.0

Firebird 2.0 介绍见附录第 5.4 节。其 persistence.xml 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
            <property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
            <property name="hibernate.connection.username" value="sysdba" />
            <property name="hibernate.connection.password" value="masterkey" />
...
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

要运行 [InitDB]:

  • 运行 SGBD Firebird
  • 将 conf/firebird/persistence.xml 放入 META-INF/persistence.xml 目录下
  • 运行应用程序 [InitDB]

SQL 视图中,JDBC 与 SGBD 之间的关联如下:

  • 在 [1] 中:与 Firebird 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请运行应用程序 [Main],然后停止 SGBD。

2.1.14.5. Apache Derby

Apache Derby 在附录第 5.10 节中进行了介绍。其 persistence.xml 文件如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
            <property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- 方言 -->
...
    </persistence-unit>
</persistence>

要运行 [InitDB]:

  • 运行 Apache Derby 的 SGBD
  • 将 conf/derby/persistence.xml 放入 META-INF/persistence.xml 目录中
  • 运行应用程序 [InitDB]

SQL 视图中,JDBC 与 SGBD 的关联如下:

  • 在 [1] 中:与 Apache Derby 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树结构。 值得注意的是,JPA / Hibernate 创建了表 [HIBERNATE_UNIQUE_KEY],用于自动生成主键 ID 的连续值。 我们此前已指出,这种机制通常属于专有机制。这一点在此处表现得尤为明显。得益于 JPA,开发人员无需深入了解 SGBD 的这些细节。
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请运行应用程序 [Main],然后停止 SGBD。

2.1.14.6. HSQLDB

HSQLDB 列于第 5.9 节的附录中。其文件 persistence.xml 如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- 连接 JDBC -->
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
            <property name="hibernate.connection.username" value="sa" />
            <!-- 
                <property name="hibernate.connection.password" value="" />
            -->
...
            <!-- 方言 -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

要运行 [InitDB]:

  • 运行 SGBD HSQL
  • 将 conf/hsql/persistence.xml 放入 META-INF/persistence.xml 目录中
  • 运行应用程序 [InitDB]

SQL 视图中,JDBC 与 SGBD 的关联如下:

  • 在 [1] 中:与 HSQL 的连接
  • 在 [2] 中:执行 [InitDB] 后的连接树结构。
  • 在 [3] 中:表 [jpa01_personne] 的结构
  • 在 [4] 中:其内容。

完成上述操作后,请执行应用程序 [Main],然后停止 SGBD。

2.1.15. 更改实现 JPA

让我们回顾一下当前项目的测试架构:

前文研究表明,我们已成功将 SGBD 替换为 [7],且未对客户端代码 [3] 进行任何修改。 现在,我们将修改 JPA 和 [6] 的实现,并再次证明这对客户端代码 [3] 而言是透明的。 我们以 TopLink 和 [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html] 的实现为例:

2.1.15.1. Eclipse 项目

在更换 JPA 实现时,我们创建了一个新的 Eclipse 项目,以免污染现有项目。因为新项目使用的持久化库可能会与 Hibernate 的持久化库发生冲突:

  • 在 [1] 中:文件夹 [<exemples>/toplink/direct/personnes-entites] 包含该 Eclipse 项目。请导入该项目。
  • 在 [2] 中:已导入的 [toplink-personnes-entites] 项目。该项目与 [hibernate-personne-entites] 项目完全相同(通过复制获得),仅在两个细节上有所不同:
    • 文件 [META-INF/persistence.xml] [3] 现在配置了 JPA / Toplink 层
    • 库文件 [jpa-hibernate] 已被库文件 [jpa-toplink]、[4] 和 [5] 取代(参见第 1.5 节)。
  • 在 [6] 中:文件夹 [conf] 包含每个 SGBD 对应的 [persistence.xml] 文件版本。
  • 在 [7] 中:文件夹 [ddl] 将包含用于生成数据库架构的脚本 SQL。

我们知道,JPA层由文件[META-INF/persistence.xml]进行配置。 该文件现配置了一个 JPA / Toplink 实现。其针对与 SGBD 和 MySQL5 进行接口连接的 JPA 层的配置内容如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  提供者 -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- 持久化类 -->
        <class>entites.Personne</class>
        <!-- 持久化单元的属性 -->
        <properties>
            <!-- 连接JDBC -->
            <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="MySQL4" />
            <!--  应用服务器 -->
            <property name="toplink.target-server" value="None" />
            <!--  模式生成 -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- 日志 -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>
  • 第 3 行:未作更改
  • 第5行:提供商现为Toplink。此处命名的类可在库[jpa-toplink](下文中的[1])中找到:
  • 第7行:<class>标签用于列出项目中所有@Entity类,此处仅列出了Personne类。Hibernate曾有一个配置选项,可让我们无需手动列出这些类。它会自动扫描项目中的classpath文件,从中查找@Entity类
  • 第 9 行:<properties> 标签用于引入所用实现(此处为 Toplink)特有的属性。
  • 第 11-14 行:配置 SGBD 与 MySQL5 之间的 JDBC 连接
  • 第 15-18 行:配置由 Toplink 原生管理的 JDBC 连接池:
  • 第 15、16 行:读取连接池中的最大和最小连接数。默认值 (2,2)
  • 第 17、18 行:写入连接池中的最大和最小连接数。默认值 (10,2)
  • 第 20 行:目标 SGBD。可用的 SGBD 列表可在 [oracle.toplink.essentials.platform.database] 包中找到(参见上文的 [2])。 SGBD MySQL5 未出现在 [2] 列表中,因此选择了 MySQL4。 Toplink 对 SGBD 的支持程度略逊于 Hibernate。因此,在我们示例中使用的七个 SGBD 中,Firebird 未被支持。列表中也找不到 Oracle。 实际上它位于另一个包中(即上文提到的 [3])。如果在这两个包中,目标 SGBD 由类 <Sgbd>Platform.class 指定,则标签将写为:

            <property name="toplink.target-database" value="<Sgbd>" />
  • 第22行:若应用程序在应用服务器上运行,则在此处指定应用服务器。当前可选值包括(None、OC4J_10_1_3、SunAS9)。默认值为(None)。
  • 第24-28行:当JPA层初始化时,要求其清理由第11-14行Jdbc连接定义的数据库。这样将从一个空数据库开始。
    • 第24行:要求Toplink对数据库模式中的表执行drop操作,随后执行create操作
    • 第25行:要求Toplink为操作dropcreate生成SQL脚本。application-location用于指定生成这些脚本的文件夹。 默认值:(当前目录)。
    • 第 26 行:操作 create. 的脚本 SQL 的名称。默认值:createDDL.jdbc
    • 第 27 行:操作 drop. 的脚本 SQL 的名称。默认值:dropDDL.jdbc
    • 第 28 行:模式生成模式(默认:both):
      • both:脚本和数据库
      • database:仅数据库
      • sql-script:仅脚本
  • 第30行:禁用(OFF)Toplink日志。可用的不同日志级别如下:OFF、SEVERE、WARNING、 INFO、CONFIG、FINE、FINER、FINEST。 默认值:INFO。

有关可与 Toplink 一起使用的 <property> 标签的完整定义,请参阅网址 [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html]。

2.1.15.3. 测试 [InitDB]

无需进行其他操作。我们已准备好执行首次测试 [InitDB]:

  • 启动 SGBD,此处为 MySQL5
  • 执行 [InitDB]
  • 在 [1] 中:控制台显示。这里显示的结果与之前通过 JPA / Hibernate 获得的结果一致。
  • 在 [3] 中:打开 [SQL Explorer] 视图,然后打开 [mysql5-jpa] 连接
  • 在 [4] 中:jpa 数据库的树形结构。 发现执行 [InitDB] 创建了两个表:预期的 [jpa01_personne] 以及出乎意料的 [sequence]。
  • 在 [5] 中:[jpa01_personne] 表的结构,以及在 [6] 中其内容
  • 在 [7] 中:表 [sequence] 的结构,以及在 [8] 中其内容。

配置文件 [persistence.xml] 要求生成 DDL 的脚本:


            <!--  生成模式 -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />

让我们看看 [ddl/mysql5] 文件夹中生成了什么:

 

create.sql


CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • 第 1 行:来自表 [jpa01_personne] 的 DDL。 可以发现,Toplink 并未为主键 ID 使用 autoincrement 属性。这导致在插入行时,该主键不会自动递增。
  • 第 2 行:表 [sequence] 中的 DDL。其名称似乎表明 Toplink 使用该表来生成主键 ID 的值。
  • 第 3 行:向 [SEQUENCE] 表中插入一条记录

drop.sql


DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
  • 第 1 行:删除表 [jpa01_personne]
  • 第2行:删除表[SEQUENCE]中的一条特定记录。该表本身不会被删除,其中可能包含的其他记录也不会被删除。

若要进一步了解表 [SEQUENCE] 的作用,需在 [persistence.xml] 中启用 Toplink 日志记录功能,并将其级别设置为 FINE——该级别会记录 Toplink 发出的 SQL 指令:


            <!-- 日志 -->
<property name="toplink.logging.level" value="FINE" />

重新执行 InitDB。下文仅保留了控制台显示的一部分:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--Connected: jdbc:mysql://localhost:3306/jpa
    User: jpa@localhost
    Database: MySQL  Version: 5.0.37-community-nt
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--连接(19255406)--线程(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--线程(Thread[main,5,main])--异常 [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (2007年3月30日))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--连接(19255406)--线程(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--连接(30708295)--线程(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) 值 ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--连接(14069849)--Thread(Thread[main,5,main])--delete from jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--连接(14069849)--线程(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ? WHERE SEQ_NAME = ?
    bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--连接(14069849)--线程(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--连接(14069849)--线程(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--连接(14069849)--线程(Thread[main,5,main])--SELECT ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--连接(30708295)--线程(Thread[main,5,main])--断开
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Thread(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa 注销成功
...
terminé ...
  • 第 2-5 行:连接到 SGBD 及其参数。实际上,日志显示 Toplink 实际创建了 3 个连接到 SGBD 的连接。需要确认该数字是否与 Jdbc 连接池所用的配置值之一相关:

            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
  • 第 7 行:删除表 [jpa01_personne]。这是正常的,因为文件 [persistence.xml] 要求清理 jpa 数据库。
  • 第 8 行:创建表 [jpa01_personne]。注意到主键 ID 缺少属性 autoincrement
  • 第 9 行:创建表 [SEQUENCE],该表已存在,是在上次执行时创建的。
  • 第 10-13 行:Toplink 报告创建表 [SEQUENCE] 时发生错误。
  • 第 15-18 行:Toplink 清理了表 [SEQUENCE]。 清理完成后,表 [SEQUENCE] 包含一行(SEQ_NAME, SEQ_COUNT),其值为 ('SEQ_GEN', 1)。
  • 第 18 行:表 [jpa01_personne] 被清空。
  • 第 19-20 行:Toplink 将表 [SEQUENCE] 中唯一一行(其中 SEQ_NAME='SEQ_GEN')的值从 ('SEQ_GEN', 1) 转换为 ('SEQ_GEN', 51)
  • 第 21 行:Toplink 从表 [SEQUENCE] 的行 ('SEQ_GEN', 51) 中获取值 51。
  • 第24-27行:Toplink将“Martin”和“Durant”这两个人插入到表[jpa01_personne]中。这里存在一个谜团:这两行记录的主键被赋予了2和3这两个值,但我们并不知道这些值是如何得出的。 目前尚不清楚第21行生成的值SEQ_COUNT (51)是否起到了什么作用。需要注意的是,行版本号的值为1,而Hibernate的默认起始值为0。
  • 第 28 行:Toplink 生成 SELECT 以获取表 [jpa01_personne] 中的所有行
  • 第29-30行:Java客户端显示的行
  • 第 31-32 行:Toplink 关闭一个连接。它将针对最初打开的每个连接重复此操作。

最终,我们虽不清楚表 [SEQUENCE] 的确切作用,但它似乎在生成主键 ID 的值方面起着一定作用。 通过查看最详细的日志级别(FINEST),我们可以进一步了解表 [SEQUENCE] 的作用。


            <!-- 日志 -->
            <property name="toplink.logging.level" value="FINEST" />

下文仅保留了关于将这两个人插入该表的日志。从中可以看出主键值生成的机制:

[TopLink Finest]: 2007.05.28 03:05:04.046--ClientSession(30617157)--线程(Thread[main,5,main])--执行查询 ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--连接(13301441)--线程(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ?
    bind => [SEQ_GEN]
[TopLink Finest]: 2007.05.28 03:05:04.062--ClientSession(30617157)--连接(13301441)--线程(Thread[main,5,main])--SEQ_GEN的本地排序预分配:对象:50,起始:2,结束:51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--线程(Thread[main,5,main])--为对象分配序列 (2 -> [null,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--线程(Thread[main,5,main])--执行查询 DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--线程(Thread[main,5,main])--PERSIST 调用操作:[null,0,Durant,Sylvie,05/07/2001,false,0]。
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--线程(Thread[main,5,main])--为对象分配序列号 (3 -> [null,0,Durant,Sylvie,05/07/2001,false,0])
[personnes]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--线程(Thread[main,5,main])--执行查询 InsertObjectQuery([3,0,Durant,Sylvie,05/07/2001,false,0])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--线程(Thread[main,5,main])--分配返回行 DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--连接(13301441)--线程(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--线程(Thread[main,5,main])--执行查询 InsertObjectQuery([2,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--线程(Thread[main,5,main])--分配返回行 DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--连接(13301441)--线程(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?, ?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
  • 第 4 行:可以看到,从 [SEQUENCE] 表第 2 行获取的数字 51 用于限定主键的取值范围:[2,51]
  • 第5行:第1个人被分配主键值为2
  • 第8行:第二个人被分配主键值3
  • 第12行:显示第一人的版本管理
  • 第17行:第二位人员的操作同上

日志级别 [FINEST] 还显示了 Toplink 发出的事务边界。研究这些日志可以了解 Toplink 的运作机制,这是理解对象/关系桥接机制的重要途径。

综上所述:

  • 不同的 JPA 实现会生成不同的数据库模式。在此示例中,Hibernate 和 Toplink 生成的模式并不相同。
  • 当需要了解 Toplink 的具体操作时,应使用 Toplink 的 FINE、FINER 和 FINEST 日志级别。

2.1.15.4. 测试 [Main]

现在我们执行测试 [Main]:

  • 在 [1] 中:除第 11 个测试 [2] 外,所有测试均通过
  • 在 [3] 中:第 376 行,即发生异常的代码行

引发异常的代码如下:


} catch (RuntimeException e1) {
            // 我们遇到一个问题
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
            ...
  • [3] 行:异常发生的位置。我们有一个 NullPointerException,这表明第 4 行和第 5 行中的某个方法 getCause 返回了一个指针 null。 诸如 [e1.getCause().getCause()] 这样的表达式假设异常链包含 3 个 [e1.getCause().getCause(), e1.getCause(), e1] 元素。如果异常链只有两个元素,第一个表达式将引发异常。

我们将上述代码修改为仅显示异常链中的最后两个异常:


        } catch (RuntimeException e1) {
            // 出现了一个问题
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

运行后,结果如下:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- 测试11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]

这次,测试 11 通过了。 关于异常的显示(第6-10行)是由Java代码(上述代码的第3行)调用的。需要说明的是,测试11在同一事务中串联了多个SQL操作,其中一个操作失败并导致事务回滚。 测试前(第3行)和测试后(第12行)[jpa01_personne]表的状态完全一致,表明回滚已成功执行。

这里需要注意一个重要问题:JPA / Hibernate 和 JPA / Toplink 的实现并非完全互换。 在此示例中,我们需要修改 JPA 客户端的代码,以避免出现 NullPointerException 的情况。我们将在后续内容中再次遇到这个问题,并且是在异常处理的背景下。

让我们回顾一下当前项目的测试架构:

此前,在[7]中使用的SGBD是MySQL5。我们将结合Oracle演示如何将其更改为SGBD。无论如何, 在 Eclipse 项目中进行的修改非常简单(参见下文):将 JPA 层的配置文件 persistence.xml 或 [1] 替换为项目中 conf ([2] 和 [3])中的任意一个。

2.1.16.1. Oracle 10g Express

Oracle 10g Express 介绍见附录第 5.7 节。Oracle 针对 Toplink 的文件 persistence.xml 如下:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  提供商 -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- 持久化类 -->
        <class>entites.Personne</class>
        <!-- 持久化单元的属性 -->
        <properties>
            <!-- 连接JDBC -->
            <property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
            <property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="Oracle" />
            <!--  应用服务器 -->
            <property name="toplink.target-server" value="None" />
            <!--  模式生成 -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/oracle" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- 日志 -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>

此配置与 SGBD 和 MySQL5 的配置完全相同,仅以下细节有所不同:

  • 第 11-14 行配置了 JDBC 与数据库的连接
  • 第 20 行:指定目标 SGBD
  • 第25行:指定DDL生成的SQL脚本的生成目录

要执行 [InitDB] 测试:

  • 运行 Oracle 脚本 SGBD
  • 将 conf/oracle/persistence.xml 放入 META-INF/persistence.xml
  • 运行应用程序 [InitDB]

在控制台和 [SQL Explorer] 视图中将显示以下结果:

  • [1]:控制台显示
  • [2]:[oracle-jpa] 在 SQL Explorer 中的连接
  • [3]:jpa数据库
  • [4]:InitDB 创建了两个表:JPA01_PERSONNE 和 SEQUENCE,与 MySQL5 类似。 有时在 [4] 中,会出现 [BIN*] 表。这些表对应已被删除的表。要观察这一现象,只需重新执行 [InitDB] 即可。 JPA层的初始化阶段包含对jpa数据库的清理,在此过程中[JPA01_PERSONNE]表会被删除:

在 [A] 中,会出现一个名为 [BIN] 的表。Oracle 不会永久删除经历过 drop 的表,而是将其放入回收站 [Recycle Bin] 中。 该回收站可通过第 5.7.4 节所述的 SQL Developer 工具在 [B] 中查看。 在 [B] 中,可以清空回收站 [JPA01_PERSONNE] 中的表。这将清空回收站 [C]。 如果在 SQL Explorer 中刷新(右键单击 / Refresh)表,会发现表 BIN 已不复存在 [D]。

  • [5, 6]:表 [JPA01_PERSONNE] 的结构和内容
  • [7, 8]:表 [SEQUENCE] 的结构和内容

好了!现在请读者在 Oracle 上运行应用程序 [Main]。

2.1.16.2. 其他 SGBD

关于其他 SGBD,我们将简要说明。只需复制适用于 Oracle 的操作流程即可。请注意以下几点:

  • 无论SGBD为何,Toplink生成表[JPA01_PERSONNE]的主键ID值时始终采用相同的技术:即使用上文所述的表[SEQUENCE]。
  • Toplink 不支持 Firebird 的 SGBD。针对此类情况,存在一个通用数据库:
                <property name="toplink.target-database" value="Auto" />

使用名为 [Auto] 的通用数据库时,Firebird 测试因 SQL 语法错误而失败。 Toplink 用于主键 ID 的类型 SQL Number(10) 无法被 Firebird 识别。 因此需要选择一个与 Firebird 具有相同数据类型的 SGBD(以本例为例)。Apache Derby 便是如此:


            <!-- 连接JDBC -->
            <property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
            <!-- SGBD -->
            <!-- 
            TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
            -->
            <property name="toplink.target-database" value="Derby" />
...
  • Toplink无法为SGBD和HSQLDB生成原始数据库模式。也就是说,以下指令:

            <!--  生成模式 -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

在 HSQLDB 上执行失败。原因在于创建表 [jpa01_personne] 时出现语法错误:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--连接(29775659)--线程(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Connection(29775659)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--线程(Thread[main,5,main])--异常 [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (2007年3月30日))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]

第 4 行,语法 NOM VARCHAR(30) UNIQUE NOT NULL 该语法不被 HSQL 接受。 Hibernate 曾使用以下语法: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM)。

总体而言,在识别本文档测试中使用的 SGBD 方面,Hibernate 比 Toplink 更有效。

2.1.17. 结论

对 @Entity [Personne] 的研究到此结束。 从概念上讲,我们所做的工作并不多:我们研究了最简单的对象/关系映射场景:一个 @Entity 对象 <--> 一张表。不过,这项研究让我们得以介绍本文中将要使用的工具。这将使我们今后在研究其他对象/关系映射场景时能够更快地推进:

  • 在之前的 @Entity [Personne] 上,我们将添加一个由类 [Adresse] 建模的字段 adresse。 在数据库端,我们将探讨两种可能的实现方案。对象 [Personne] 和 [Adresse] 将生成
  • 一个包含地址的单一表 [personne]
  • 两张表 [personne] 和 [adresse],它们通过一对一的外键关系关联。
  • 一个一对多关系的示例,其中表 [article] 通过外键与表 [categorie] 相关联
  • 多对多关系的示例,其中两个表 [personne] 和 [activite] 通过连接表 [personne_activite] 相关联。

2.2. 示例 2:通过包含关系建立的一对一关系

2.2.1. 数据库模式

 
1
2

    drop table if exists jpa02_personne;

    create table jpa02_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

  • 在 [1] 中:数据库(Azurri Clay 插件)
  • 在 [2] 中:由 Hibernate 为 MySQL5 生成的 DDL

表 [jpa02_personne] 是之前分析过的表 [jpa01_personne],其中添加了一个地址(DDL 的第 12-18 行)。

2.2.2. 代表数据库的 @Entity 对象

一个人的地址将由以下 [Adresse] 类表示:


package entites;

...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {

    // 字段
    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    // 构造函数
    public Adresse() {

    }

    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }

    // 获取器和设置器
...

    // toString
    public String toString() {
        return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
    }
}
  • 主要创新在于第5行的@Embeddable注解。[Adresse]类并非用于生成表,因此未添加@Entity注解@Embeddable 注解表明该类旨在被嵌入到 @Entity 对象中,从而被包含在与其关联的表中。 因此,在数据库模式中,[Adresse] 类并未作为独立的表出现,而是作为 @Entity [Personne] 关联表的一部分存在。

@Entity [Personne] 与前一版本相比变化不大:仅向其添加了一个字段 adresse


package entites;

...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @Embedded
    private Adresse adresse;

    // 构造函数
    public Personne() {
    }
...
}
  • 修改位于第 33-34 行。对象 [Personne] 现在拥有一个类型为 Adresse 的字段 adresse。这是针对 POJO 的。 @Embedded注解用于对象/关系映射。它表示字段[Adresse adresse]应封装在与对象[Personne]相同的表中。

2.2.3. 测试环境

我们将进行与之前所学非常相似的测试。这些测试将在以下环境中进行:

所使用的实现为 JPA / Hibernate [6]。测试的 Eclipse 项目如下:

Eclipse项目[1]与前一个项目的区别仅在于其Java代码[2]。 该环境(库 – persistence.xml – 数据库管理系统 – 配置文件、DDL 文件 – Ant 脚本)与之前(特别是第 2.1.5 节)所研究的相同。对于后续的 Hibernate 项目,情况也将如此,除特殊情况外,我们将不再赘述该环境。 特别是,用于为不同SGBD配置JPA/Hibernate层的persistence.xml文件,正是此前已研究过的、位于<conf>文件夹中的文件。

如果对操作步骤有疑问,建议读者回顾前文中的相关操作流程。

Eclipse 项目位于示例文件夹 [4] 中。我们将导入该项目。

2.2.4. 生成数据库的 DDL

按照第 2.1.7 节的说明,针对 SGBD 和 MySQL5 生成的 DDL 如下:


    drop table if exists jpa02_hb_personne;

    create table jpa02_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

Hibernate 正确识别出该人员的地址应被整合到与 @Entity Personne 关联的表中(第 11-17 行)。

2.2.5. InitDB

[InitDB] 的代码如下:


package tests;
...

public class InitDB {

    // 常量
    private final static String TABLE_NAME = "jpa02_hb_personne";

    public static void main(String[] args) throws ParseException {

        // 持久化上下文
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // 从之前的 EntityManagerFactory 获取 EntityManager
        em = emf.createEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 查询
        Query sql1;
        // 从表 PERSONNE 中删除条目
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // 创建人员
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 创建地址
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // 人员 <--> 地址关联
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // 人员数据持久化
        em.persist(p1);
        em.persist(p2);
        // 显示人员
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 交易结束
        tx.commit();
        // 结束 EntityManager
        em.close();
        // 结束 EntityManagerFactory
        emf.close();
        // 日志
        System.out.println("terminé...");

    }
}

该代码中没有新内容。所有内容都已出现过。将 [InitDB] 与 MySQL5 一起执行,结果如下:

  • [1]:控制台输出
  • [2]:在 SQL Explorer 视图中的 [jpa02_hb_personne] 表
  • [3] 和 [4]:其结构和内容。

2.2.6.

类 [Main] 如下:


package tests;

...
import entites.Adresse;
import entites.Personne;

@SuppressWarnings( { "unused", "unchecked" })
public class Main {

    // 常量
    private final static String TABLE_NAME = "jpa02_hb_personne";

    // 持久化上下文
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = null;

    // 共享对象
    private static Personne p1, p2, newp1;

    private static Adresse a1, a2, a3, a4, newa1, newa4;

    public static void main(String[] args) throws Exception {
        // 从 EntityManagerFactory 获取 EntityManager
        em = emf.createEntityManager();

        // 清理数据库
        log("clean");clean();

        // 转储表
        dumpPersonne();

        // 测试1
        log("test1"); test1();

        // 测试2
        log("test2"); test2();

        // 测试3
        log("test3"); test3();

        // 测试4
        log("test4"); test4();

        // 测试5
        log("test5");test5();

        // 持久化上下文结束
        if (em != null && em.isOpen())
            em.close();

        // 关闭 EntityManagerFactory
        emf.close();
    }

    // 获取当前的 EntityManager
    private static EntityManager getEntityManager() {
...
    }

    // 获取一个新的 EntityManager
    private static EntityManager getNewEntityManager() {
...
    }

    // 显示“人员”表内容
    private static void dumpPersonne() {
...
    }

    // 清空 BD
    private static void clean() {
    ...
    }

    // 日志
    private static void log(String message) {
...
    }

    // 对象创建
    public static void test1() throws ParseException {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 人员创建
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 地址创建
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // 人员 <--> 地址关联
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 人员持久化
        em.persist(p1);
        em.persist(p2);
        // 事务结束
        tx.commit();
        // 转储
        dumpPersonne();
    }

    // 修改上下文对象
    public static void test2() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 增加 p1 的子女数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 修改其婚姻状况
        p1.setMarie(false);
        // 对象 p1 被自动保存(脏数据检查)
        // 在下一次同步时(提交或查询)
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
    }

    // 删除属于持久化上下文的对象
    public static void test4() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除关联对象 p2
        em.remove(p2);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
    }

    // 解绑、重新绑定并修改
    public static void test5() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 将 p1 重新关联到新上下文
        p1 = em.find(Personne.class, p1.getId());
        // 事务结束
        tx.commit();
        // 更改 p1 的地址
        p1.getAdresse().setVille("Paris");
        // 显示新表
        dumpPersonne();
    }

}

再次,没有任何新内容。控制台显示如下:

main : ----------- clean
[personnes]
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,0,Martin,Paul,31/01/2000,true,2,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test5
[personnes]
P[1,2,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Paris,null,France]]

请读者自行将结果与代码建立联系。

现在我们使用 JPA / Toplink 实现:

新的 Eclipse 测试项目如下:

Java代码与之前的Hibernate项目完全一致。 环境(库 – persistence.xml – 数据库管理系统 – 配置文件、DDL 文件 – Ant 脚本)即第 2.1.15.2 节中已探讨过的环境。未来的 Toplink 项目也将沿用此环境,除特殊情况外,我们将不再赘述该环境。 特别是,用于为不同 SGBD 配置 JPA/Toplink 层的 persistence.xml 文件,正是之前已研究过的、位于 <conf> 文件夹中的那些文件。

如果对操作步骤有疑问,建议读者回顾前文中的相关操作流程。

Eclipse 项目 [3] 位于示例文件夹 [4] 中。我们将导入该项目。

使用 SGBD 和 MySQL5 运行 [InitDB] 后,结果如下:

  • [1]:控制台显示
  • [2]:SQL Explorer视图中的[jpa02_tl_personne]和[SEQENCE]表
  • [3] 和 [4]:[jpa02_tl_personne] 的结构和内容。

在 ddl/mysql5 目录中生成的 SQL 脚本如下:

create.sql


CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.3. 示例 3:通过外键建立一对一关系

2.3.1. 数据库模式

1
2

    alter table jpa03_hb_personne
        drop
        foreign key FKFBBBFDD05FE379D0;

    drop table if exists jpa03_hb_adresse;

    drop table if exists jpa03_hb_personne;

    create table jpa03_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa03_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa03_hb_personne
        add index FKFBBBFDD05FE379D0 (adresse_id),
        add constraint FKFBBBFDD05FE379D0
        foreign key (adresse_id)
references jpa03_hb_adresse (id);
  • 在 [1] 中:数据库。这次,该人的地址被放入了一个专门的表 [adresse] 中。表 [personne] 通过外键与该表相关联。
  • 在 [2] 中:由 Hibernate 为 MySQL5 生成的 DDL:
    • 第 9-20 行:表 [adresse],该表将与已作为 @Entity 对象的类 [Adresse] 建立关联。
    • 第 10 行:表 [adresse] 的主键
    • 第 30 行:在表 [personne] 中,不再存储完整的地址,而是存储该地址的标识符 [adresse_id]。
    • 第 34-38 行:person(adresse_id)是 address(id)的外键。

2.3.2. 代表数据库的 @Entity 对象

现在,拥有地址的人员由以下 [Personne] 类表示:


package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
...
}
  • 第 32-34 行:该人的地址
    • 第 32 行:注解 @OneToOne 表示一对一关系:一个人员至少且至多有一个地址。 属性 cascade = CascadeType.ALL 表示对 @Entity [Personne] 执行的任何操作(persist、merge、remove)都必须对 @Entity [Adresse] 进行级联。 从持久化上下文 em 的角度来看,这意味着以下内容。如果 p 是一个人,a 是其地址:
      • 显式的 em.persist(p) 操作将引发隐式的 em.persist(a) 操作
      • 显式操作 em.merge(p) 将引发隐式操作 em.merge(a)
      • 显式操作 em.remove(p) 将引发隐式操作 em.remove(a)

实践表明,这些隐式级联并非万能良方。开发人员最终会忘记它们的作用。在代码中,可能更倾向于使用显式操作。级联有多种类型。注解 @OneToOne 本可以写成如下形式:


//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)

此处的 cascade 属性允许其值为一个常量数组,该数组指定所需的级联类型。

fetch=FetchType.LAZY 属性要求 Hibernate 在最后一刻加载依赖关系。当将人员列表放入持久化上下文时,我们未必希望同时放入他们的地址。 例如,可能仅希望在用户通过 Web 界面选定特定人员时才加载其地址。而 fetch=FetchType.EAGER 属性则要求立即加载依赖项。

  • (续)
    • 第 33 行:注解 @JoinColumn 定义了 @Entity [Personne] 表在 @Entity [Adresse] 表上的外键。 name 属性定义了用作外键的列名。unique=true 属性强制建立一对一关系:[adresse_id] 列中不能出现相同的值。nullable=false 属性强制要求每个人必须拥有一个地址。

现在,一个人的地址由以下 @Entity [Adresse] 表示:


package entites;

...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {

    // 字段
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
    private Personne personne;

    // 构造函数
    public Adresse() {

    }
...
}
  • 第 4 行:[Adresse] 类成为 @Entity 对象。因此,它将在数据库中对应一张表。
  • 第 9-12 行:与所有 @Entity 对象一样,[Adresse] 具有主键。该主键被命名为 Id,并具有与 @Entity [Personne] 的主键 Id 相同的(标准)注解。
  • 第 39-40 行:与 @Entity [Personne] 建立的一对一关系。这里有几个细节需要注意:
    • 首先,字段 personne 并非必填。它允许我们根据一个地址追溯到拥有该地址的唯一个人。如果我们不需要这种便利,字段 personne 便不存在,系统依然能正常运行。
    • 将两个实体 [Personne] 和 [Adresse] 关联的一对一关系已在 @Entity [Personne] 中配置:

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;

为避免两个一对一配置相互冲突,其中一个被视为 principale,另一个被视为 inverse。 由对象/关系桥管理的是所谓的 principale 关系。另一条名为 inverse 的关系并非直接管理:它是通过 principale 关系间接管理的。 在 @Entity [Adresse] 中:


@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

正是属性 mappedBy 构成了上述一对一关系, inverse 关系与 principale 的一对一关系,是由 @Entity [Personne] 的字段 adresse 定义的。

2.3.3. Eclipse / Hibernate 1 项目

此处使用的 JPA 实现来自 Hibernate。测试的 Eclipse 项目如下:

该项目位于示例文件夹 [4] 中,文件名为 [3]。我们将导入该项目。

2.3.4. 生成数据库的 DDL

按照第 2.1.7 节的说明,针对 SGBD 和 MySQL5 生成的 DDL 即为本节开头所示的文件。

2.3.5. InitDB

[InitDB] 的代码如下:


package tests;
...
import entites.Adresse;
import entites.Personne;

public class InitDB {

    // 常量
    private final static String TABLE_PERSONNE = "jpa03_hb_personne";

    private final static String TABLE_ADRESSE = "jpa03_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // 持久化上下文
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // 从之前的 EntityManagerFactory 获取 EntityManager
        em = emf.createEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 查询
        Query sql1;
        // 从表 PERSONNE 中删除元素
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // 删除表中的条目 ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // 创建人员
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 创建地址
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // 人员 <--> 地址关联
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // 人员及其地址的级联持久化
        em.persist(p1);
        em.persist(p2);
        // 以及未与人员关联的地址 a3 和 a4
        em.persist(a3);
        em.persist(a4);
        // 显示人员
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 地址显示
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }

        // 交易结束
        tx.commit();
        // 结束 EntityManager
        em.close();
        // 结束 EntityManagerFactory
        emf.close();
        // 日志
        System.out.println("terminé...");

    }
}

我们仅对相较于已研究内容具有新意义的部分进行说明:

  • 第31-32行:创建了两个人物
  • 第34-37行:创建了四个地址
  • 第39-42行:将人员(p1,p2)与地址(a1,a2)关联。地址(a3,a4)为孤立地址,没有任何人员引用它们。DDL 允许这种情况。虽然人员必然拥有地址,但反之则不成立。
  • 第44-45行:保存人员(p1,p2)。 由于我们在连接人员与其地址的一对一关系上设置了 cascade = CascadeType.ALL 属性,因此这两名人员的地址 (a1,a2) 也应应用 persist。这就是我们要验证的内容。 对于孤立的地址 (a3,a4),我们必须显式地进行处理(第 47-48 行)。
  • 第51-53行:显示人员表
  • 第56-57行:显示地址表

同时执行 [InitDB] 和 MySQL5 会得到以下结果:

  • [1]:控制台显示
  • [2]:在 SQL 资源管理器视图中的 [jpa03_hb_*] 表
  • [3]:人员表
  • [4]:地址表。所有数据均已完整显示。 此外,还需注意 [3] 表中的 [adresse_id] 列与 [4] 表中的 [id] 列之间的关联(外键)。

2.3.6. 主表

类 [Main] 包含六个测试,我们将逐一审查。

2.3.6.1. Test1

该测试如下:


// 对象创建
    public static void test1() throws ParseException {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 人员创建
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 地址创建
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // 人员 <--> 地址关联
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 人员持久化
        em.persist(p1);
        em.persist(p2);
        // 以及未与人员关联的地址 a3 和 a4
        em.persist(a3);
        em.persist(a4);
        // 事务结束
        tx.commit();
        // 显示表
        dumpPersonne();
        dumpAdresse();
    }

该代码摘自 [InitDB]。其结果如下:

1
2
3
4
5
6
7
8
9
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]

两张表均已填满。

2.3.6.2. Test2

该测试如下:


    // 修改上下文中的对象
    public static void test2() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 增加 p1 的子女数量
        p1.setNbenfants(p1.getNbenfants() + 1);
        // 修改其婚姻状况
        p1.setMarie(false);
        // 对象 p1 被自动保存(脏数据检查)
        // 在下一次同步时(提交或查询)
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
}

其结果如下:

1
2
3
4
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
  • 第4行:人物p1的子女数增加了1,其版本号从0变为1

2.3.6.3. Test4

该测试如下:


    // 删除属于持久化上下文的对象
    public static void test4() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除关联对象 p2
        em.remove(p2);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpAdresse();
}
  • 第9行:删除人员p2。该人员与地址a2存在级联关系。因此地址a2也应被删除。

测试4的结果如下:

main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • 测试1第3行中出现的人物p2在测试4中已不存在
  • 其地址 a2 亦是如此,在测试 1 的第 7 行存在,但在测试 4 中已不存在。

2.3.6.4. Test5

该测试结果如下:


// 解绑、重新绑定并修改
    public static void test5() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 将 p1 重新关联到新上下文
        p1 = em.find(Personne.class, p1.getId());
        // 更改 p1 的地址
        p1.getAdresse().setVille("Paris");
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpAdresse();
    }
  • 第4行:有一个新的持久化上下文,因此为空。
  • 第9行:将人员p1放入其中。由于p1不在上下文中,因此会在数据库中进行查找。 而依赖于 p1(即其地址)的元素,则不会从数据库中检索回来,因为我们写的是:

    @OneToOne(..., fetch=FetchType.LAZY)

这就是“加载”(lazy loading)的概念:持久化对象的依赖项仅在需要时才会被加载到内存中。

  • 第11行:修改了p1地址的“城市”字段。由于getAdresse的存在,且如果p1的地址尚未处于持久化上下文中,系统将通过读取数据库将其加载进来。
  • 第13行:提交事务,这将导致持久化上下文与数据库进行同步。持久化上下文将检测到人员p1的地址已被修改,并将其保存。

执行 test5 产生以下结果:

main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • 人员 p1(第 3 行 test4,第 10 行 test5)的居住城市确实已从昂热(第 5 行 test4)更改为巴黎(第 12 行 test5)。

2.3.6.5. Test6

该测试内容如下:


// 删除一个地址对象
    public static void test6() {
        EntityTransaction tx = null;
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        tx = em.getTransaction();
        tx.begin();
        // 将地址 a3 重新关联到新上下文
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // 将其删除
        em.remove(a3);
        // 事务结束
        tx.commit();
        // 导出地址表
        dumpAdresse();
    }
  • 第5行:当前处于一个新的持久化上下文中,因此为空。
  • 第10行:将地址a3放入持久化上下文
  • 第13行:将其删除。这是一个孤立地址(未与任何人关联)。因此可以进行删除。

执行结果如下:

main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • 测试 5(第 6 行)中的地址 a3 已从测试 6(第 11-12 行)的地址列表中消失

2.3.6.6. Test7

该测试内容如下:


// 回滚
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // 新的持久化上下文
            EntityManager em = getNewEntityManager();
            // 开始事务
            tx = em.getTransaction();
            tx.begin();
            // 将地址 a1 重新绑定到新上下文
            newa1 = em.find(Adresse.class, a1.getId());
            // 将地址 a4 重新关联到新上下文
            newa4 = em.find(Adresse.class, a4.getId());
            // 尝试删除它们——应抛出异常,因为无法删除与人员关联的地址,而 newa1 正是这种情况
            em.remove(newa4);
            em.remove(newa1);
            // 事务结束
            tx.commit();
        } catch (RuntimeException e1) {
            // 出现问题
            System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
                    .getCause());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // 放弃当前上下文
            em.clear();
        }
        // 转储 - 由于回滚,地址表应该没有发生变化
        dumpAdresse();
    }
  • test7:测试事务回滚
    • 第6行:当前处于一个新的持久化上下文中,因此为空。
    • 第11行:将地址a1放入持久化上下文中,作为引用newa1
    • 第13行:将地址 a4 放入持久化上下文中,作为 newa4 的子项
    • 第15-16行:删除两个地址 newa1 newa4newa1 p1 该人的地址,因此在数据库中,p1 通过外键引用 newa1。 因此,在事务提交时(第18行)同步持久化上下文时,删除 newa1 将失败并抛出异常。该事务将遭遇回滚(第25行),从而导致事务中的两项操作均被撤销。 因此,我们应能观察到本可合法删除的地址 newa4 并未被删除。

执行结果如下:


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • 测试 7 的地址表(第 12-13 行)与测试 6 的地址表(第 4-5 行)完全一致。回滚似乎已成功执行。不过,第 9 行的错误信息令人费解,值得深入探究。似乎发生的异常并非预期中的那种。 需将log4j.properties中的Hibernate日志切换至DEBUG模式以查明原因:

# 根日志记录器选项
log4j.rootLogger=ERROR, stdout

# Hibernate 日志记录选项(INFO 仅显示启动消息)
log4j.logger.org.hibernate=DEBUG

由此可见,当地址 a1 被放入持久化上下文时,Hibernate 同时也放入了人员 p1,这很可能是由于 @Entity [Adresse] 的“一对一”关系所致:


    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

尽管此处请求的是“LazyLoading”,但依赖项 [Personne] 却被立即加载。这可能意味着 fetch=FetchType.LAZY 属性在此处没有意义。 随后发现,在提交事务时,Hibernate 不仅准备了删除 a1 a4 这两个地址的操作,还准备了保存 p1 这个人的操作。 此时便发生了异常:由于 p1 该实体在其地址上设置了级联关系,Hibernate 试图同时持久化地址 a1,而该地址刚刚被销毁。是 Hibernate 抛出了异常,而非 JDBC 驱动程序。 因此才出现了上面第 9 行中的错误信息。此外,我们可以发现第 25 行中的 rollback 从未被执行,因为事务已变为非活动状态。因此,第 24 行中的测试阻止了 rollback 的执行。

因此,我们未能达到预期目标:演示回滚。实际上,数据库中并未发出任何 SQL 命令。需要记住几点:

  • 启用详细日志以了解ORM的具体行为有何意义
  • 虽然 ORM 可能为开发人员带来便利,但也可能因掩盖了开发人员需要了解的行为而增加其工作难度。此处涉及 @Entity 依赖项的加载方式。

2.3.7. Eclipse / Hibernate 2 项目

我们将 Eclipse / Hibernate 项目复制/粘贴过来,以便对 @Entity 对象的配置进行微调:

该项目位于示例文件夹 [4] 中,文件名为 [3]。我们将导入该项目。

我们仅修改 @Entity [Adresse],使其不再与 @Entity [Personne] 建立一对一的反向关系:


package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {

    // 字段
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    ...

    @Column(length = 20, nullable = false)
    private String pays;

//    @OneToOne(mappedBy = "地址", fetch=FetchType.LAZY)
//    private Person person;

    // 构造函数
    public Adresse() {

    }
  • 第 25-26 行:删除了 @OneToOne 的反向关系。 必须清楚地认识到,反向关系绝非必不可少。只有主关系才是必需的。反向关系仅出于便利而使用。在此,它能让我们简单地获取地址的所有者。反向关系总能被 JPQL 查询所替代。我们将在接下来的示例中展示这一点。

测试程序保持完全一致。我们仅关注测试7,即之前演示过一对一反向关系实际应用的那个测试。此外,我们新增了测试8,以展示在没有“地址 -> 人员”反向关系的情况下,如何仍能获取拥有特定地址的人员。

测试7保持不变。其执行结果如下(日志已禁用):


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- 测试7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • 这次确实出现了预期的异常:这是由Jdbc驱动程序抛出的,因为我们试图在表[adresse]中删除一行,而该行被表[personne]中某行的外键引用。 [10] 行明确指出了错误原因。
  • 回滚已成功执行:在测试7结束时,表[adresse](第12-13行)与测试6结束时的状态(第4-5行)一致。

这与之前 Eclipse 项目的第 7 次测试有何不同? 为何此处会出现 Jdbc 异常,而之前的测试中却未出现?因为 @Entity [Adresse] 不再与 @Entity [Personne] 保持一对一的反向关系,因此由 Hibernate 独立管理。 当地址 newa1 被引入持久化上下文时,Hibernate 并未将拥有该地址的人员 p1 同时放入该上下文中。 因此,在删除 newa1 newa4 这两个地址时,上下文中并不包含 Personne 实体。

现在,如何从地址 newa1 推导出拥有该地址的人员 p1?这是一个合理的问题。以下测试 8 给出了答案:


// 一对一反向关系
    // 通过查询实现 JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        tx = em.getTransaction();
        tx.begin();
        // 将地址 a1 重新关联到新上下文
        newa1 = em.find(Adresse.class, a1.getId());
        // 检索该地址的所有者
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // 显示相关信息
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // 交易结束
        tx.commit();
    }
  • 第 6 行:新的空持久化上下文
  • 第8-9行:事务开始
  • 第 11 行:地址 a1 被引入持久化上下文,并由 newa1 引用。
  • 第13行:通过查询JPQL,检索到拥有地址newa1的人员p1。 已知 [Personne] 和 [Adresse] 通过外键关系相关联。 在类 [Personne] 中,字段 [adresse] 带有注解 @OneToOne,该注解实现了这一关系。 语句 JPQL "select p from Personne p join p.adresse a" 实现了 [personne] 表与 [adresse] 表之间的连接。 在 Hibernate 控制台中生成的等效代码 SQL(参见第 2.1.12 节的示例)如下:
SQL #0 类型:entites.Personne
-----------------
select
  personne0_.id as id1_,
  personne0_.version as version1_,
  personne0_.nom as nom1_,
  personne0_.prenom as prenom1_,
  personne0_.datenaissance as datenais5_1_,
  personne0_.marie as marie1_,
  personne0_.nbenfants as nbenfants1_,
  personne0_.adresse_id as adresse8_1_ 
 from
  jpa04_hb_personne personne0_ 
 inner join
  jpa04_hb_adresse adresse1_ 
on personne0_.adresse_id=adresse1_.id

可以清楚地看到两张表的连接。现在,每个人都与其地址相关联。需要说明的是,我们只关注地址 newa1。 查询语句变为“select p from Personne p join p.adresse a where a.id=:adresseId”。 请注意别名 pa 的使用。JPQL 查询中大量使用了这些别名。 因此,表达式“from Personne p join p.adresse a”使得某个人由别名 p 表示,其地址(p.adresse)则由别名 a 表示。 限制条件“where a.id=:adresseId”将查询结果限定为仅包含那些将值adresseId作为其地址标识符a:adresseId 被称为参数,而命令 JPQL 则是一个带参数的命令 JPQL。在执行时,该参数必须被赋予一个值。这是通过方法

Query setParameter(String nomParamètre, Object valeurParamètre)

该方法用于为通过名称标识的参数赋值。需要注意的是,setParameter 会返回一个 Query 对象,这与方法 createQuery 相同。 因此,我们可以连续调用 [em.createQuery(...).setParameter(...).getSingleResult(...)] 方法,因为 [setParameter, getSingleResult] 方法是 Query 接口的方法。 方法 [getSingleResult] 用于处理仅返回单一结果的 Select 查询。本例即属此类。

  • 第16-17行:显示地址newa1以及拥有该地址的人员p1,以便核对。

所得结果如下:

1
2
3
main : ----------- test8
adresse=A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
personne=P[1,1,Martin,Paul,31/01/2000,false,3,1]

结果正确。从这个例子中可以看出,@entity [Adresse] 到 @entity [Personne] 的反向一对一关系并非必不可少。实践表明,删除该关系使代码的行为更加可预测。这种情况经常发生。

2.3.8. Hibernate 控制台

前面的测试 8 使用了 JPQL 命令,对 Personne Adresse 这两个实体进行了连接。 尽管与 SQL 语言类似,但 Hibernate 的 JPQL、JPA 或 HQL 语言需要学习,而 Hibernate 控制台非常适合用于此目的。 我们在第 2.1.12 节中已经使用过它来操作单个表。现在,我们将再次使用它来操作两个通过外键关系关联的表。

为当前的 Eclipse 项目创建一个 Hibernate 控制台:

  • [1]:切换到 [Hibernate Console] 视图(Window / Open Perspective / Other)
  • [2]:我们创建一个新配置
  • 通过按钮[4],我们选择需要创建Hibernate配置的Java项目。其名称显示在[3]中。
  • 在 [5] 中,为该配置指定所需名称。此处我们采用了 Java 项目的名称。
  • 在 [6] 中,我们指定使用 JPA 配置,以便工具知道应使用 [META-INF/persistence.xml] 文件
  • 在 [7] 中:我们在此文件中指定,[META-INF/persistence.xml] 必须使用名为 jpa 的持久化单元。
  • 在 [8] 中,我们确认配置。

接下来,需要运行 SGBD。此处指的是 MySQL5。

  • 在 [1] 中:创建的配置呈现为三叉树结构
  • 在 [2] 中:分支 [Configuration] 列出了控制台用于配置自身的对象:此处为 @Entity Personne Adresse
  • 在 [3] 中:Session Factory 是 Hibernate 中的一个概念,与 EntityManager 中的 JPA 类似。它通过 [Configuration] 分支中的对象实现对象与关系数据库的桥接。 在 [3] 中介绍了持久化上下文的对象,这里再次出现 @Entity Personne Adresse
  • 在 [4] 中:通过 [persistence.xml] 中的配置访问的数据库。其中包含由我们当前的 Eclipse 项目生成的 [jpa04_hb_*] 表。
  • 在 [1] 中,创建了一个编辑器 HQL
  • 在编辑器 HQL 中,
    • 在 [2] 中,若存在多个 Hibernate 配置(此处即为这种情况),则选择要使用的配置
    • 在 [3] 中,输入要执行的命令 JPQL,此处为测试 8 的命令 JPQL
    • 在 [4] 中,执行该命令
    • 在 [5] 中,可在 [Hibernate Query Result] 窗口中获取查询结果。
    • 在 [6] 中,[Hibernate Dynamic SQL preview] 窗口可查看已执行的 SQL 查询。

另一种获得相同结果的方法:

  • 在 [1] 中:命令 JPQL 执行对实体 Personne Adresse 的连接操作。 [ref1] 将这种形式称为“theta 连接”。
  • 在 [2] 中:等效于 SQL
  • 在 [3] 中:结果

仅 Hibernate 支持的第三种形式(HQL):

  • 转换为 [1]:命令 HQL。JPQL 不支持 p.adresse.id 这种写法。它仅支持一层间接引用。
  • 在 [2] 中:等效于 SQL。可以看出它避免了表之间的连接。
  • 在 [3] 中:结果

以下是其他示例:

  • 在 [1] 中:人员及其地址的列表
  • 在 [2] 中:即 SQL 的对应内容。
  • 在 [3] 中:结果
  • 在 [1] 中:地址列表及其所有者(如有),否则为空 (右外连接:实体 Adresse 将提供与 Personne 无关的行,位于关键字 join右侧)。
  • 在 [2] 中:等同于 SQL。
  • 在 [3] 中:结果

需注意,仅实体 Personne 与实体 Adresse 存在关联。 自从在实体 Adresse 中删除了名为 personne 的反向一对一关系后,反向关系便不再成立。如果该反向关系存在,我们可以写成:

  • 在 [1] 中: 地址列表及其所有者(如有所有者)或无所有者(否则)(左外连接:实体 Adresse 将提供与 Personne 无关联的行,位于关键字 join左侧)。
  • 在 [2] 中:等同于 SQL。
  • 在 [3] 中:结果

我们强烈建议读者使用 Hibernate 控制台练习 JPQL 语言。

现在我们使用 JPA / Toplink 实现:

新的 Eclipse 测试项目如下:

Java代码与之前的Hibernate项目完全一致。环境(库 – persistence.xml – 数据库管理系统 – 配置文件夹、DDL – Ant脚本)与第2.1.15.2节中所述的环境相同。 该Eclipse项目[3]位于示例文件夹[4]中。我们将导入该项目。

文件<persistence.xml>仅在声明实体处进行了一处修改:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  提供商 -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- 持久化类 -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- 持久化单元的属性 -->
...
  • 第 5 行和第 6 行:两个管理的实体

使用 SGBD 和 MySQL5 执行 [InitDB] 后,结果如下:

在 [1] 中, 控制台显示;在 [2] 中,显示生成的两个表 [jpa04_tl];在 [3] 中,显示生成的脚本 SQL。其内容如下:

create.sql


CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.4. 示例 4:一对多关系

2.4.1. 数据库模式

1
2

    alter table jpa06_article
        drop
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article
        add index FKFFBDD9D8ECCE8750 (categorie_id),
        add constraint FKFFBDD9D8ECCE8750
        foreign key (categorie_id)
references jpa06_categorie (id);
  • 在 [1] 数据库中,以及在 [2] 中,其 DDL(MySQL5)

一个商品 A(id, version, name) 仅属于一个类别 C(id, version, name)。一个类别 C 可以包含 0、1 或多个商品。这里存在一对多关系(类别 -> 商品)及其逆向的多对一关系(商品 -> 类别)。 该关系通过表 [article] 对表 [categorie] 的外键实现(参见 DDL 的第 24-28 行)。

2.4.2. 表示数据库的 @Entity 对象

一个商品由以下 @Entity [Article] 表示:


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // 字段
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // 主关系 Article(多)-> Category(一)
    // 通过 Article 中的外键 (categorie_id) 实现
    // 1 个 Article 必须有 1 个 Category(可为空=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

    // 构造函数
    public Article() {
    }

    // 获取器和设置器
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }

}
  • 第 9-11 行:@Entity 的主键
  • 第 13-15 行:其版本号
  • 第 17-18 行:文章名称
  • 第 20-25 行:将 @Entity Article 与 @Entity Categorie 关联的多对一关系
    • 第23行:注解 ManyToOne“Many” 指代当前所在的 @Entity Article,而“One” 指代 @Entity Categorie(第 25 行)。一个类别(One)可以包含多个文章(Many)。
    • 第 24 行:注解 ManyToOne 定义了表 [article] 中的外键列。 该列名为 categorie_id,且每行都必须在此列中包含一个值(nullable=false)。
    • 第 25 行:商品所属的类别。当商品被放入持久化上下文时,要求其类别不要立即被放入(fetch=FetchType.LAZY,第 23 行)。尚不清楚此要求是否有意义。我们拭目以待。

一个类别由以下 @Entity [Categorie] 表示:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // 字段
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // 关系“文章(多)→ 分类(一)”的反向关系“分类(一)→ 文章(多)”
    // 级联插入:分类 -> 插入文章
    // 级联更新 分类 -> 更新文章
    // 类别 -> 文章的删除级联
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

    // 构造函数
    public Categorie() {
    }

    // 获取器和设置器
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // 类别与文章的双向关联
    public void addArticle(Article article) {
        // 将文章添加到该类别的文章集合中
        articles.add(article);
        // 文章更改分类
        article.setCategorie(this);
    }
}
  • 第 8-11 行:@Entity 的主键
  • 第12-14行:其版本号
  • 第16-17行:类别名称
  • 第 19-24 行:该类别的文章集合(set)
    • 第 23 行:注解 @OneToMany 表示一对多关系。 “One”指代当前所在的@Entity [Categorie],“Many”指代第24行的类型[Article]:一个(One)类别包含多个(Many)文章。
    • 第23行:该注解是注解ManyToOne的反向关系(mappedBy),该注解位于@Entity Article字段categorie上mappedBy=categorie。位于 @Entity Articlecategorie 字段上的关系 ManyToOne 是主关系。它是必不可少的。 它实现了将 @Entity Article 与 @Entity Categorie 关联的外键关系。 定义在 @Entity Categoriearticles 字段上的 OneToMany 关系是反向关系。该关系并非必不可少。 这是为了方便获取某类别的文章。如果没有这一便利,这些文章将通过查询 JPQL 来获取。
    • 第 23 行:cascadeType.ALL 要求对 @Entity Categorie 执行的操作(persist、merge、remove)应级联到其文章上。
    • 第 24 行:某类别的商品将被放入一个 Set<Article> 类型的对象中。Set 类型不接受重复项。因此,不能将同一件商品放入 Set<Article> 对象中两次。什么是“同一件商品”? 为了表示商品 a 与商品 b 相同,Java 使用表达式 a.equals(b)。在所有类的父类 Object 中,如果 a==b,则 a.equals(b) 为真,c.a.d。 如果对象 a b 具有相同的内存地址。我们可能希望表示,如果物品 a 和 b 具有相同的名称,则它们是相同的。在这种情况下,开发人员必须在 [Article] 类中重定义两个方法:
      • equals:当两个项目名称相同时,该方法应返回 true
      • hashCode:对于两个被 equals 方法视为相等的 [Article] 对象,该方法应返回相同的整数值。在此,该值将根据文章名称生成。 hashCode 返回的值可以是任意整数。该值用于各种对象容器中,特别是字典(Hashtable)。

关系 OneToMany 可以使用除 Set 以外的其他类型来存储“多”方,例如 List 对象。本文档中将不讨论这些情况。读者可在 [ref1] 中查阅相关内容。

  • 第 38 行:方法 [addArticle] 允许我们将商品添加到类别中。 该方法会自动更新连接 [Categorie] 与 [Article] 的 OneToMany 关系的两端。

2.4.3. Eclipse / Hibernate 1 项目

此处使用的 JPA 实现来自 Hibernate。测试的 Eclipse 项目如下:

该项目位于示例文件夹 [4] 中,文件名为 [3]。我们将导入该项目。

2.4.4. 生成数据库的 DDL

按照第 2.1.7 节的说明操作,针对 SGBD 和 MySQL5 生成的 DDL 即为本示例开头第 2.4.1 节中展示的文件。

2.4.5. InitDB

[InitDB]的代码如下:


package tests;

...
public class InitDB {

    // 常量
    private final static String TABLE_ARTICLE = "jpa05_hb_article";

    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";

    public static void main(String[] args) {
        // 持久化上下文
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // 从之前的 EntityManagerFactory 检索到 EntityManager
        em = emf.createEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 请求
        Query sql1;
        // 从表 ARTICLE 中删除元素
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // 删除表中的元素 CATEGORIE
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // 创建三个类别
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        // 创建 3 篇文章
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // 将其关联到所属分类
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // 保存分类,并级联(插入)文章
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        // 显示分类
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 显示文章
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 结束交易
        tx.commit();
        // 结束 EntityManager
        em.close();
        // 结束 EntityMangerFactory
        emf.close();
        // 日志
        System.out.println("terminé...");

    }
}
  • 第22-27行:清空了表[article]和[categorie]。 需注意必须从包含外键的表开始。若从表 [categorie] 开始,将删除由表 [article] 行引用的类别,而 SGBD 会拒绝此操作。
  • 第29-34行:创建三个类别A、B、C
  • 第36-41行:创建三个商品条目 A1、A2、B1(字母表示类别)
  • 第43-45行:将这3个商品放入各自的类别中
  • 第47-49行:将3个类别放入持久化上下文。由于存在“类别 -> 商品”的级联关系,其对应的商品也将被放入其中。因此,所有创建的对象现在都位于持久化上下文中。
  • 第50-59行:向持久化上下文查询以获取类别和文章列表。我们知道这将触发上下文与数据库的同步。此时,类别和文章将被分别保存到各自的表中。

同时执行 [InitDB] 和 MySQL5 会得到以下结果:

  • [1]:控制台显示
  • [2]:[jpa05_hb_*] 表在 SQL Explorer 视图中的显示
  • [3]:类别表
  • [4]:文章表。 请注意 [categorie_id] 在 [4] 中的链接与 [id] 在 [3] 中的链接(外键)。

2.4.6.

类 [Main] 串联了一系列测试,我们将逐一审查,但测试 1 和 2 除外,它们复用了 [InitDB] 中的代码来初始化数据库。

2.4.6.1. Test3

该测试如下:


    // 搜索特定元素
    public static void test3() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 交易
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 加载分类
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // 显示类别及其相关商品
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        // 结束交易
        tx.commit();
}
  • 第 4 行:这是一个新的持久化上下文,因此为空
  • 第6-7行:开始事务
  • 第9行:将类别A从数据库加载到持久化上下文中
  • 第11行:显示类别A
  • 第12-14行:显示类别A下的商品。此处展示了反向关联OneToMany(@Entity Categorie商品)的价值。 该关系的引入使我们无需再执行 JPQL 查询来获取类别 A 的商品。要获取这些商品,我们使用字段 articles 的方法 get

结果如下:

main : ----------- test1
[categories]
Categorie[1,0,A]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[1,0,A1,1]
Article[2,0,A2,1]
Article[3,0,B1,2]
main : ----------- test2
3 categorie(s) trouvée(s) :
A
B
C
3 article(s) trouvé(s) :
A1
A2
B1
main : ----------- test3
Articles de la catégorie Categorie[1,0,A] :
Article[2,0,A2,1]
Article[1,0,A1,1]
  • 第 20 行:类别 A
  • 第 21-22 行:A 类别的两件商品

2.4.6.2. Test4

该测试如下:


    // 删除商品
    @SuppressWarnings("unchecked")
    public static void test4() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 加载商品 A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // 删除商品 A1(当前未加载任何类别)
        em.remove(newarticle1);
        // toplink:必须将商品从其类别中移除,否则测试6会崩溃
        // hibernate:没有必要
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // 事务结束
        tx.commit();
        // 导出商品
        dumpArticles();
}
  • 测试4将删除商品A1
  • 第5行:从一个新的、空的上下文开始
  • 第10行:将商品A1引入持久化上下文。该商品将通过newarticle1在此上下文中被引用。
  • 第 12 行:该条目从上下文中删除
  • 第15行:类别A、B和C,以及商品A1、A2和B1,即使不再持久化,仍保留在内存中。它们只是从持久化上下文中解耦。 属于A类别的A1条目已被移出该类别。这将使A类别日后能够重新关联到持久化上下文。如果不这样做,A类别将与一组条目重新关联,而其中一个条目已被删除。这似乎不会影响Hibernate,但会导致Toplink崩溃。
  • 第 19 行:显示所有文章以验证 A1 是否已消失。

结果如下:

1
2
3
4
main : ----------- test4
[articles]
Article[2,0,A2,1]
Article[3,0,B1,2]

文章 A1 确实已消失。

2.4.6.3. Test5

测试步骤如下:


// 修改1个商品
    public static void test5() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 修改 articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 被放回持久化上下文
        em.merge(articleA2);
        // 事务结束
        tx.commit();
        // 导出商品
        dumpArticles();
    }
  • 测试5更改了商品A2的名称
  • 第 4 行:从一个全新的、空的上下文开始
  • 第9行:将独立条目A2的名称更改为“A2-”。
  • 第11行:已从持久化上下文中分离的条目A2被重新添加回持久化上下文。需要注意的是,A2仍然是一个已分离的对象。 现在属于持久化上下文的对象是 em.merge(articleA2)。该对象并未像通常那样被存储在变量中,因此无法访问。
  • 第13行:将持久化上下文与数据库同步。条目A2将在数据库中被修改,其版本号将从N变为N+1。脱离内存的版本articleA2不再有效。 代表类别 A 的脱离对象亦是如此,因为该对象的条目中包含 articleA2
  • 第 15 行:显示所有项目以验证项目 A2 的名称变更

结果如下:

1
2
3
4
main : ----------- test5
[articles]
Article[2,1,A2-,1]
Article[3,0,B1,2]

物料 A2 的名称已成功更改。

2.4.6.4. Test6

测试内容如下:


// 修改1个类别及其商品
    public static void test6() {
        // 新的持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 加载类别
        categorieA = em.find(Categorie.class, categorieA.getId());
        // 类别 A 的商品列表
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        // 修改类别名称
        categorieA.setNom(categorieA.getNom() + "-");
        // 交易结束
        tx.commit();
        // 导出类别和商品
        dumpCategories();
        dumpArticles();
}
  • 测试6更改了类别A及其所有文章的名称
  • 第4行:从一个全新的、空的上下文开始
  • 第9行:从数据库中检索类别A。我们不会对分离对象categorieA执行merge操作,因为我们知道它引用了已过时的文章A2。因此,我们从零开始。
  • 第11-12行:更改类别A中所有商品的名称。再次通过getArticles方法使用反向关联OneToMany
  • 第15行:类别名称也随之更改
  • 第17行:事务结束。执行上下文与数据库的同步。上下文中所有被修改的对象都将更新到数据库中。
  • 第21-22行:显示商品和类别以供核对

结果如下:

1
2
3
4
5
6
7
8
main : ----------- test6
[categories]
Categorie[1,2,A-]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[2,2,A2--,1]
Article[3,0,B1,2]

商品 A2 的名称已再次更改,类别 A 亦已更新。

2.4.6.5. Test7

本次测试如下:


// 删除类别
    public static void test7() {
        // 新建持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 持久化 catégorieB 并级联(合并)相关商品
        Categorie mergedcategorieB = em.merge(categorieB);
        // 删除类别并级联(delete)相关商品
        em.remove(mergedcategorieB);
        // 结束交易
        tx.commit();
        // 导出分类和商品
        dumpCategories();
        dumpArticles();
    }
  • 测试7删除了类别B,并随之删除了其下的文章
  • 第 4 行:从一个全新的空上下文开始
  • 第9行:类别B作为脱离持久化上下文的对象存在于内存中。将其重新整合(merge)到持久化上下文中。 随之,其文章(文章 B1)将执行 merge 操作,从而重新纳入持久化上下文。
  • 第 11 行:现在 B 类已处于上下文中,可以将其删除(remove)。 通过级联效应,其商品也将经历 remove 操作。正是由于第 9 行中的 merge 操作已将它们重新纳入持久化上下文,才使得此操作成为可能。
  • 第13行:事务结束。上下文将被同步。已执行remove操作的上下文对象将从数据库中删除。
  • 第15-16行:显示商品和类别以供核对

结果如下:

1
2
3
4
5
6
main : ----------- test7
[categories]
Categorie[1,2,A-]
Categorie[3,0,C]
[articles]
Article[1,2,A2--,1]

类别 B 和商品 B1 已成功删除。

2.4.6.6. Test8

测试内容如下:


// 查询
    @SuppressWarnings("unchecked")
    public static void test8() {
        // 新建持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 类别 A 的商品列表
        List articles = em
                .createQuery(
                        "select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
                .getResultList();
        // 商品浏览
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        // 事务结束
        tx.commit();
    }
  • 测试7展示了如何在不通过反向关系的情况下检索某分类下的商品。这表明反向关系并非必不可少。
  • 第4行:从一个全新的空上下文开始
  • 第10行:执行查询JPQL,获取名称以字母A开头的某分类下的所有文章
  • 第15-17行:显示查询结果。

结果如下:

1
2
3
main : ----------- test8
Articles de la catégorie A
Article[2,2,A2--,1]

2.4.7. Eclipse / Hibernate 2 项目

我们将 Eclipse / Hibernate 项目复制/粘贴过来,以便阐明围绕 @ManyToOne (主关系)与 @Entity [Article] 以及 @Entity [Categorie] 的反向关系 @OneToMany(反向)所建立的关系概念。 我们想说明,如果后者未被声明为与前者的反向关系,那么生成的数据库模式将与之前生成的截然不同。

在 [1] 中是新的 Eclipse 项目。 [2] 包含 Java 代码,[3] 包含脚本 ant,该脚本将生成数据库模式 SQL。 该项目位于示例文件夹 [5] 中。我们将导入该项目。

我们仅修改 @Entity [Categorie]使其与 @[Article] 实体之间的关系不再被声明为与 @ManyToOne 关系(即 @[Article] 与 @[Categorie] 实体之间的关系)相反:


...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // 字段
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // 非反向关系 OneToMany(无 mappedby) 类别(one) -> 商品(many)
    // 通过连接表 Categorie_Article 实现,以便从一个类别
    // 可访问该分类下的文章
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

    // 制造商
...
  • 第18-22行:我们仍希望保留通过第21行的关系@OneToMany查找特定类别下文章的功能。 但我们需要了解 mappedBy 属性的影响,该属性将一个关系定义为在另一个 @Entity 中定义的主关系的逆关系。在此,mappedBy 已被移除。

我们使用 SGBD 和 MySQL5 执行 ant-DLL 任务(参见第 2.1.7 节)。得到的模式如下:

请注意以下几点:

  • 创建了一个新的表 [categorie_article] [1]。该表此前并不存在。
  • 这是一张连接表,用于连接表 [categorie] [2] 与 [article] [3]。 如果对象 Article a1、a2 属于类别 c1,则在连接表中将找到以下行:
[c1,a1]
[c1,a2]

其中 c1a1a2 是对应对象的主键。

  • Hibernate 创建了关联表 [categorie_article] [1],以便从 Categorie 对象 c 开始,能够查找属于 cArticle 对象 a。 正是 @OneToMany 关系迫使创建了该表。 由于未将其声明为 @Entity Article 的主关系 @ManyToOne 的反向关系,Hibernate 无法识别 可利用该主关系来检索 c 类别下的文章。因此它采用了其他方式。
  • 通过这个示例,我们可以更好地理解 principale inverse 这两种关系的概念。其中一种(逆向关系)利用了另一种(主关系)的属性。

该数据库中 SQL 与 MySQL5 之间的关系图如下:


    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D26D17756;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D424C61C9;

    alter table jpa06_hb_article 
        drop 
        foreign key FK4547168FECCE8750;

    drop table if exists jpa05_hb_categorie;

    drop table if exists jpa05_hb_categorie_jpa06_hb_article;

    drop table if exists jpa06_hb_article;

    create table jpa05_hb_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa05_hb_categorie_jpa06_hb_article (
        jpa05_hb_categorie_id bigint not null,
        articles_id bigint not null,
        primary key (jpa05_hb_categorie_id, articles_id),
        unique (articles_id)
    ) ENGINE=InnoDB;

    create table jpa06_hb_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id), 
        add constraint FK79D4BA1D26D17756 
        foreign key (jpa05_hb_categorie_id) 
        references jpa05_hb_categorie (id);

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D424C61C9 (articles_id), 
        add constraint FK79D4BA1D424C61C9 
        foreign key (articles_id) 
        references jpa06_hb_article (id);

    alter table jpa06_hb_article 
        add index FK4547168FECCE8750 (categorie_id), 
        add constraint FK4547168FECCE8750 
        foreign key (categorie_id) 
references jpa05_hb_categorie (id);
  • 第19-24行,创建表[categorie];第33-39行,创建表[article]。请注意,它们与前一个示例中的内容完全相同。
  • 第 26-31 行:由于 @Entity Categorie 存在非反向关系 @OneToMany,因此创建了连接表 [categorie_article]。 该表的行类型为 [c,a],其中 c 是类别 c 的主键,a 是属于类别 c 的商品 a 的主键。该连接表的主键由两个主键 [c,a] 拼接而成(第 29 行)。
  • 第 41-45 行:表 [categorie_article] 指向表 [categorie] 的外键约束
  • 第 47-51 行:表 [categorie_article] 到表 [article] 的外键约束
  • 第 53-57 行:表 [article] 到表 [categorie] 的外键约束

建议读者运行测试 [InitDB] 和 [Main]。它们给出的结果与之前相同。然而,数据库模式存在冗余,且性能将比上一版本有所下降。 或许有必要深入探讨主从关系这一问题,以确认新配置是否会因使用两个独立关系来表示同一事物(即表 [article] 与表 [categorie] 之间的多对一关系)而引发更多冲突。

现在我们使用 JPA / Toplink 实现:

使用 Toplink 的 Eclipse 项目是使用 Hibernate 的 Eclipse 项目(第 1 版)的副本:

Java代码与之前的Hibernate项目(第1版)完全相同。环境(库 – persistence.xml – 数据库管理系统 – 配置文件夹、DDL – Ant脚本)即第2.1.15.2节中所述的环境。 Eclipse 项目 [3] 位于示例文件夹 [4] 中。我们将导入该项目。

文件<persistence.xml> [2]在声明实体处进行了一处修改:


        ...
        <!-- 持久类 -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • 第 3 行和第 4 行:两个管理的实体

使用 SGBD 和 MySQL5 执行 [InitDB] 后,结果如下:

在 [1] 中, 控制台显示,在 [2] 中,生成的两个表 [jpa05_tl],在 [3] 中生成的脚本 SQL。其内容如下:

create.sql


CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

[Main] 的执行未出现错误。

此 Eclipse 项目是通过复制前一个项目生成的。由于该项目使用 Hibernate 实现,因此需从 @Entity Categorie 的 @OneToMany 关系中移除 mappedBy 属性。


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {

    // 字段
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // 非反向关系(无mappedby)类别(one)->
    // 文章 (many)
    // 通过连接表 Categorie_Article 实现,以便从
    // 从一个类别
    // 可访问多个商品
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

随后为 MySQL5 生成的 SQL 模式如下:

create.sql


CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • 第 2 行:实现前一个非反向关系 @OneToMany 的连接表。

[InitDB] 的执行未出现错误,但 [Main] 在测试 7 时崩溃,并产生以下日志(FINEST):

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Thread(Thread[main,5,main])--客户端已获取
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--线程(Thread[main,5,main])--合并克隆并保留引用 分类[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--线程(Thread[main,5,main])--注册现有对象 Article[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--注册现有对象 Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--线程(Thread[main,5,main])--已对以下对象执行删除操作:Categorie[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--已对以下对象执行删除操作:Article[6,1,B1]
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--开始提交事务
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connection(6330655)--Thread(Thread[main,5,main])--begin transaction
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--线程(Thread[main,5,main])--执行查询 DeleteObjectQuery(Article[6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--连接(6330655)--线程(Thread[main,5,main])--DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
[TopLink Warning]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--线程(Thread[main,5,main])--本地异常堆栈: 
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa06_tl_categorie_jpa06_tl_article`, CONSTRAINT `FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID` FOREIGN KEY (`articles_ID`) REFERENCES `jpa06_tl_article` (`ID`))
Error Code: 1451
Call: DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
  • 第 3 行:B 类别的 merge
  • 第 4 行:相关条目 B1 已放入上下文
  • 第5行:B类本身亦同
  • 第6行:将remove应用于类别B
  • 第7行:将remove关联至商品B1(通过级联)
  • 第8行:Java代码请求了该事务中的commit
  • 第9行:事务启动——因此该事务显然此前尚未开始。
  • 第10行:商品B1将被表[article]上的操作DELETE删除。问题就出在这里。 关联表 [categorie_article] 引用了表 [article] 中的行 B1。 在 [article] 中删除 B1 将违反外键约束。
  • 第 13 行及之后:发生异常

结论是什么?

  • 再次,我们遇到了 Hibernate 与 Toplink 之间的可移植性问题:Hibernate 通过了此测试
  • Toplink 无法正确处理以下情况:当两个关系实际上是彼此的反向关系时,其中一个未被声明为主关系,另一个被声明为反向关系。 我们可以接受这种情况,因为这实际上代表了一个配置错误。在我们的示例中,表 [article] 与连接表 [categorie_article] 之间没有关系。 因此,当对表 [article] 进行操作时,Toplink 自然不会尝试与表 [categorie_article] 进行交互。

2.5. 示例 5:带有显式连接表的多对多关系

2.5.1. 数据库模式

  • 与 [1] 表建立关联,数据库 MySQL5

我们已经了解表 [personne]、[2] 以及 [adresse]、[3]。这些表已在第 2.3.1 节中进行过探讨。 我们采用将人员地址单独放在表 [adresse] 和 [3] 中的版本。在表 [personne] 中,人员与其地址之间的关联通过外键约束来体现。

一个人会从事某些活动。这些活动存储在表 [activite] 和 [4] 中。一个人可以从事多项活动,而一项活动也可以由多人参与。 因此,[personne] 表与 [activite] 表之间存在多对多关系。该关系通过连接表 [personne_activite] [5] 实现。

2.5.2. 代表数据库的 @Entity 对象

上述表将由以下 @Entity 表示:

  • @Entity Personne 将表示表 [personne]
  • @Entity Adresse 将表示表 [adresse]
  • @Entity Activite 将表示表 [activite]
  • @Entity PersonneActivite 代表表 [personne_activite]

这些实体之间的关系如下:

  • 实体 Personne 与实体 Adresse 之间存在一对一关系:一个人 p 有一个地址 a。 持有外键的实体 Personne 将具有主关系,实体 Adresse 则具有反向关系。
  • 实体Personne与Activite之间存在多对多关系:一个人可以从事多种活动,而一种活动可以由多人参与。 这种关系可以通过在两个实体中分别添加 @ManyToMany 注解来直接实现,其中一个实体被声明为另一个实体的反向关系。该方案将在后续探讨。在此,我们通过两个一对多关系来实现这种多对多关系:
    • 一个将实体 Personne 与实体 PersonneActivite 关联的“一对多”关系: 表 [personne] 中的一个行(One)被表 [personne_activite] 中的多个行(Many)引用。 持有外键的表 [personne_activite] 将持有主关系 @ManyToOne,而实体 Personne 将持有逆关系 @OneToMany
    • 一种将实体 Activite 与实体 PersonneActivite 关联起来的“一对多”关系: 表 [activite] 中的一个(One)行被表 [personne_activite] 中的多个(Many)行引用。 持有外键的表 [personne_activite] 将持有主关系 @ManyToOne,而实体 Activite 将持有反向关系 @OneToMany

@Entity Personne 如下所示:


@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // 主关系 Personne (one) -> Adresse (one)
    // 由外键“人员”(adresse_id) -> “地址”实现
    // 级联插入 人员 -> 插入 地址
    // 人员更新 -> 地址更新
    // 人员删除的级联操作 -> 地址删除
    // 一个人员必须拥有一个地址(可空=false)
    // 1 个地址仅属于 1 个人员(唯一=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // 关系:人员 (one) -> PersonneActivite (many)
    // 现有关系的逆向映射 PersonneActivite(多)-> 人员(单)
    // 级联删除 Personne -> 删除 PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();

    // 构造器

该 @Entity 已知。我们仅对它与其他实体之间的关系进行说明:

  • 第 30-39 行:与 @Entity Adresse 建立的一对一关系 @OneToOne, 该关系通过外键 [adresse_id](第 38 行)实现,即表 [personne] 指向表 [adresse]。
  • 第 41-45 行:一个与 @Entity PersonneActivite 建立的一对多关系 @OneToMany。 一个实体(One)由连接表 [personne_activite] 中的多行(Many)引用,该连接表由 @Entity PersonneActivite 表示。 这些 PersonneActivite 对象将被放入 Set<PersonneActivite> 类型中,其中 PersonneActivite 是我们将随后定义的类型。
  • 第 44 行:此处定义的一对多关系,是 @Entity PersonneActivitepersonne 字段上定义的主关系(关键字 mappedBy)的逆关系。 这里设置了“Personne -> Activite”的删除级联:删除一个 p 实体将导致集合 p.activites 中所有 PersonneActivite 类型的持久化元素被删除。

@Entity Adresse 定义如下:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // 字段
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • 第 28-29 行: @OneToOne 关系是 @OneToOne 关系的逆关系,指向 @Entity PersonnePersonne 的第 37-38 行)。

@Entity Activite 如下所示


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {

    // 字段
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // 关系 活动 (one) -> PersonneActivite (many)
    // 现有关系 PersonneActivite (多) -> 活动 (单) 的反向关系
    // 活动 -> PersonneActivite 的删除级联
    @OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();

  • 第 6-9 行:活动的primary key
  • 第 11-13 行:活动的版本号
  • 第 15-16 行:活动名称
  • 第18-22行:@Entity Activite与@Entity PersonneActivite关联的“一对多”关系: 一个实体(One)被连接表 [personne_activite](由 @Entity PersonneActivite 表示)中的多行(Many)引用。 这些 PersonneActivite 对象将被放入 Set<PersonneActivite> 类型中
  • 第 22 行:此处定义的一对多关系,是 @Entity PersonneActivite(关键字 mappedBy)中 activite 字段上定义的主关系之反向关系。 在删除操作中存在一个从 Activite 到 PersonneActivite 的级联关系:删除将导致从集合 a.personnes 中删除类型为 PersonneActivite 的持久化元素的连接表 [personne_activite]。

@Entity PersonneActivite 如下所示:


@Entity
// 连接表
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {

    @Embeddable
    public static class Id implements Serializable {
        // 复合键的组成部分
        // 指向一个人员
        @Column(name = "PERSONNE_ID")
        private Long personneId;

        // 指向一项活动
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;

        // 构造函数
...

        // 获取器和设置器
...
        // toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }

    // Personne_Activite 类的字段
    // 复合键
    @EmbeddedId
    private Id id = new Id();

    // 主关系 PersonneActivite (多对一) -> 人员 (一对多)
    // 由外键实现:personneId (PersonneActivite (多) -> 人员 (单)
    // personneId 同时也是复合主键的组成部分
    // JPA 不应管理此外键(insertable = false, updatable = false),因为这是由应用程序自身在其构造函数中完成的
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;

    // 主关系 PersonneActivite -> 活动
    // 由外键实现:activiteId (PersonneActivite (多) -> 活动 (一)
    // activiteId 同时也是复合主键的组成部分
    // JPA 不应管理此外键(insertable = false, updatable = false),因为这是由应用程序自身在其构造函数中完成的
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;

    // 构造函数
    public PersonneActivite() {

    }

    public PersonneActivite(Personne p, Activite a) {
        // 外键由应用程序确定
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // 双向关联
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }

    // 获取器和设置器
...
    // toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

该类比之前的类更为复杂。

  • 表 [personne_activite] 包含形式为 [p,a] 的行,其中 p 是某人的主键,a 是某项活动的的主键。 每个表都必须有一个主键,[personne_activite] 也不例外。到目前为止,我们定义的主键都是由 SGBD 动态生成的。这里也可以这样做。 我们将采用另一种技术,即由应用程序自行定义表的主键值。在此,一行 [p1,a1] 表示某人 p1 正在进行活动 a1。 该表中不会出现第二条相同的记录。因此,(p,a)这对组合是作为主键的理想候选。这被称为复合主键
  • 第30-31行:复合主键。注解 @EmbeddedId(通常为 @Id)类似于应用于人员字段 Adresse@Embedded 标记。 在后一种情况下,这意味着字段 Adresse 属于一个外部类,但必须插入到与该人员相同的表中。在此处,含义相同,只是为了表明这是主键,标记变为 @EmbeddedId
  • 第 31 行:在构建对象 [PersonneActivite] 时,会同时构建一个代表主键 id 的空对象。 表示主键的类在第 7-26 行中定义,作为 [PersonneActivite] 类内部的公共静态类。其必须为公共静态类是由 Hibernate 强制要求的。 如果将 public static 替换为 private,,则会引发异常,且在相关的错误消息中可以看到,Hibernate 曾尝试执行 new PersonneActivite$Id 语句。 因此,类 Id 必须同时是静态的且为 public 的。
  • 第 6 行:主键的 Id 类被声明为 @Embeddable。回顾第 31 行,主键 id 已被声明为 @EmbeddedId。因此,对应的类必须带有 @Embeddable 注解。
  • 我们提到,表 [personne_activite] 的主键由 (p,a) 这对组合构成,其中 p 是某人的主键,a 是某项活动的的主键。 复合主键的两个元素 (p,a) 分别位于第 11 行(personneId)和第 15 行(activiteId)。 与这两个字段关联的列名称分别为:PERSONNE_ID(对应人员),ACTIVITE_ID(对应活动)。
  • 第 31 行:主键已通过其两个列(PERSONNE_ID、ACTIVITE_ID)定义。表 [personne_activite] 中没有其他列。 接下来只需定义当前所描述的 @Entity PersonneActivite 与关系型模式中其他 @Entity 之间的关系。这些关系体现了表 [personne_activite] 与其他表之间的外键约束。
  • 第 33-39 行:定义了表 [personne_activite] 与表 [personne] 之间的外键
  • 第 37 行:该关系类型为 @ManyToOne:[personne] 表中的一行(One)被 [personne_activite] 表中的多行(Many)引用。
  • 第38行:为外键列命名。此处采用与外键“personne”组件(第10行)相同的名称。属性 insertable=falseupdatable=false 的作用是阻止 Hibernate 管理该外键。因为该外键实际上是由应用程序计算得出的主键的组成部分,Hibernate 不应介入。
  • 第41-47行:定义了表[personne_activite]对表[activite]的外键。说明与前文所述相同。
  • 第 54-63 行:基于人员 p 和活动 a 构造 PersonneActivite 对象。 需要回顾的是,在创建 PersonneActivite 对象时,第 31 行中的主键 id 指向了一个空的 Id 对象。 第56-57行分别给对象Id的各个字段(personneId、activiteId)赋予了值。 这些值分别是作为构造函数参数传递的个人 p 和活动 a 的主键。因此,主键 id(第 31 行)现在有了值。
  • 第 59 行:第 39 行中的字段 personne 被赋值为 p
  • 第 60 行:第 47 行中的字段 activite 接收值 a
  • 现在已创建并初始化了一个 [PersonneActivite] 对象。 更新 @Entity Personne(第 61 行)和 Activite(第 62 行)与刚刚创建的 @Entity PersonneActivite 之间的反向关联。

至此,数据库实体的描述已完成。我们正处于一种复杂但不幸的是很常见的情况中。我们将看到,JPA 层还有另一种可能的配置,它隐藏了部分复杂性:连接表变得隐式,由 JPA 层构建和管理。 我们在此选择了虽然最为复杂但能支持关系模式演进的方案。该方案允许向连接表添加列,而连接表并非显式 @Entity 的配置则无法实现这一点。[ref1] 推荐了我们正在探讨的这一解决方案。 正是 [ref1] 中提供的信息,促成了本解决方案的制定。

2.5.3. Eclipse / Hibernate 项目

此处使用的 JPA 实现来自 Hibernate。测试的 Eclipse 项目如下:

 

Image

在 [1] 中是 Eclipse 项目,在 [2] 中是 Java 代码。该项目位于 [3] 文件中,该文件位于示例文件夹 [4] 内。我们将导入该项目。

2.5.4. 生成数据库的DDL

按照第 2.1.7 节的说明,针对 SGBD 和 MySQL5 生成的 DDL 如下:


alter table jpa07_hb_personne 
        drop 
        foreign key FKB5C817D45FE379D0;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B06CD852024;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B0668C7A284;

    drop table if exists jpa07_hb_activite;

    drop table if exists jpa07_hb_adresse;

    drop table if exists jpa07_hb_personne;

    drop table if exists jpa07_hb_personne_activite;

    create table jpa07_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa07_hb_personne 
        add index FKB5C817D45FE379D0 (adresse_id), 
        add constraint FKB5C817D45FE379D0 
        foreign key (adresse_id) 
        references jpa07_hb_adresse (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B06CD852024 (ACTIVITE_ID), 
        add constraint FKD3E49B06CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa07_hb_activite (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B0668C7A284 (PERSONNE_ID), 
        add constraint FKD3E49B0668C7A284 
        foreign key (PERSONNE_ID) 
        references jpa07_hb_personne (id);
  • 第 21-26 行:表 [activite]
  • 第 28-39 行:表 [adresse]
  • 第 41-51 行:表 [personne]
  • 第 53-57 行:连接表 [personne_activite]。请注意复合主键(第 56 行)
  • 第 59-63 行:表 [personne] 指向表 [adresse] 的外键
  • 第 65-69 行:表 [personne_activite] 到表 [activite] 的外键
  • 第 71-75 行:表 [personne_activite] 到表 [personne] 的外键

2.5.5. InitDB

[InitDB] 的代码如下:


package tests;

...
public class InitDB {

    // 常量
    private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";

    private final static String TABLE_PERSONNE = "jpa07_hb_personne";

    private final static String TABLE_ACTIVITE = "jpa07_hb_activite";

    private final static String TABLE_ADRESSE = "jpa07_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // 持久化上下文
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        // 从 EntityManagerFactory 获取 EntityManager
        // 前一个
        em = emf.createEntityManager();
        // 交易开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 查询
        Query sql1;
        // 从表 PERSONNE_ACTIVITE 中删除元素
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // 删除表 PERSONNE 中的元素
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // 删除表 ACTIVITE 中的元素
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // 删除表 ADRESSE 中的元素
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        // 创建活动
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // 活动持久化
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // 创建人员
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // 创建地址
        Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
        Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
        Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
        // 人员与地址的关联
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // 人员及关联地址的持久化
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // 未关联人员的地址 a4 的持久化
        em.persist(adr4);
        // 显示人员
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 地址显示
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        // 人员与活动之间的关联
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // 人员 <--> 活动关联的持久化
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // 人员显示
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // 地址显示
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[personnes/activites]");
        for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
            System.out.println(pa);
        }
        // 交易结束
        tx.commit();
        // 结束 EntityManager
        em.close();
        // 结束 EntityManagerFactory
        emf.close();
        // 日志
        System.out.println("terminé...");

    }
}
  • 第 27-38 行:清空表 [personne_activite]、[personne]、[adresse] 和 [activite]。 请注意,必须从包含外键的表开始处理。
  • 第 40-45 行:创建三个活动 act1act2 act3
  • 第 47-49 行:将它们放入持久化上下文中。
  • 第 51-53 行:创建三个人员 p1、p2 和 p3。
  • 第 55-58 行:创建四个地址,编号为 adr1adr4
  • 第60-65行:将地址adri与人员pi关联。由于“人员 <-> 地址”关系是双向的,因此每次都需要执行两项操作。
  • 第67-69行:将编号为p1至p3的人员纳入持久化上下文。 由于“人员 -> 地址”的层级关系,地址 adr1adr3 也将被纳入持久化上下文。
  • 第 71 行:未与任何人关联的第 4 个地址 adr4 被显式放入持久化上下文中。
  • 第 73-85 行:向持久化上下文查询 [Personne]、[Adresse] 和 [Activite] 类型的实体列表。 我们知道这些查询将触发上下文与数据库的同步:创建的实体将被插入数据库并获得主键。理解这一点对于后续内容至关重要。
  • 第 87-90 行:创建了 4 个“人员 <-> 活动”关联。其名称表明了哪个人与哪项活动相关联。可能还记得,实体 PersonneActivite 的主键是由人员主键和活动主键组成的复合键。 正因为实体 Personne Activite 在之前的同步过程中已获取了主键,所以才能执行此操作。
  • 第 92-95 行:这 4 个关联被放入持久化上下文中。
  • 第87-86行:向持久化上下文查询,以获取类型为[Personne]、[Adresse]、[Activite]和[PersonneActivite]的实体列表。 我们知道这些查询将触发上下文与数据库的同步:创建的 PersonneActivite 实体将被插入数据库。

执行 [InitDB] 并结合 MySQL5 时,控制台显示如下:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
terminé...

令人惊讶的是,第15-16行中人员p1和p2的版本号为1,而第24-26行中的三项活动也是如此。让我们试着理解一下。

第2-4行中,人员的版本号为0;第11-13行中,活动的版本号为0。上述显示发生在建立“人员 <-> 活动”关系之前。 Java代码第87-90行,在人员p1和p2与活动act1、act2act3 之间建立了关系。这些关系是通过 @Entity PersonneActivite 的构造函数实现的(参见第 2.5.2 节)。阅读该构造函数的代码可知,当人员 p 与活动 a 相关联时:

  • 活动 a 被添加到集合 p.activites
  • 人员 p 被添加到集合 a.personnes

因此,当编写 new PersonneActivite(p,a) 时,人员 p 和活动 a 将在内存中发生变更。 在 [InitDB] 的第 97-113 行中,持久化上下文与数据库同步,JPA / Hibernate 发现持久化对象 p1p2act1act2act3 已被修改。 这些修改必须在数据库中进行。实际上,这些修改已记录在连接表 [personne_activite] 中,但 JPA / Hibernate 仍会递增每个被修改的持久化元素的版本号。

在 SQL Explorer 视图中,结果如下:

  • [2]:表[jpa07_hb_*]
  • [3]:人员表
  • [4]:地址表。
  • [5]:活动表
  • [6]:人员 <-> 活动关联表

2.5.6. Main

类 [Main] 串联了若干测试,我们将逐一审查,但测试 1 除外,该测试复用了 [InitDB] 的代码来初始化数据库。

2.5.6.1. Test2

该测试如下:


// 删除 人员 p1
    public static void test2() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除 p1 上的依赖关系:对 Hibernate 并非必需,但
        // 对 TopLink 不可或缺
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // 删除 p1 中的“person”
        em.remove(p1);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • 第 4 行:使用 test1 的持久化上下文,其中人员 p1 是该上下文中的一个对象。
  • 第 13 行:删除人员 p1。由于属性:
    • cascadeType.ALL,人员 p1 的地址将被删除
    • cascadeType.REMOVE 关联 PersonneActivite,人员 p1 的活动将被删除。
  • 第 10-11 行:删除其他实体对即将被删除的实体 p1 的依赖关系(第 13 行)。活动 act1 和 act2 由实体 p1 执行。这些关联是由实体 PersonneActivite 的构建器创建的,其代码如下:

    public PersonneActivite(Personne p, Activite a) {
        // 外键由应用程序设定
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // 双向关联
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

第9行,活动a在其集合personnes中接收了一个类型为PersonneActivite的附加元素。 该元素的类型为 (p,a),用于表明人员 p 从事活动 a。 在 [Main] 的 test1 中,因此创建了两个链接 (p1,act1) 和 (p1,act2)。 test2 的第 10 行和第 11 行删除了这些依赖关系。需要注意的是,Hibernate 在未删除这些依赖关系的情况下仍可在 p1 实体上正常运行,但 Toplink 则无法运行。

  • 第17-20行:显示所有表

结果如下:

main : ----------- test1
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
  • test1(第3行)中存在的联系人p1,在test2(第22-23行)执行完毕后已不复存在
  • test1(第 11 行)中出现的 p1 人员的地址 adr1,在 test2 处理完成后已不复存在 (第29-31行)
  • p1 人员在 test1 中存在的活动 (p1,act1) (第 16 行) 和 (p1,act2) (第18行)在test1中存在,但在test2(第33-34行)执行完毕后已不再存在

2.5.6.2. Test3

该测试如下:


// 删除活动 act1
    public static void test3() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除对 act1 的依赖:Hibernate 不需要,但
        // 对 TopLink 不可或缺
        p2.getActivites().remove(p2act1);
        // 删除活动 act1
        em.remove(act1);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • 第 4 行:使用 test2 的持久化上下文
  • 第 12 行:删除活动 act1。原因是属性:
    • cascadeType.REMOVE 上的属性,PersonneActivite 表中 (p, act1) 行将被删除。
  • 第 10 行:在将 act1 移出持久化上下文之前,需先删除其他实体对该持久化对象可能存在的依赖关系。 在前一次测试中删除人员 p1 后,仅剩人员 p2 正在执行活动 act1
  • 第13-16行:显示所有表

结果如下:

main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,3],p2,act3]
  • test2 中,活动 act1 存在(第 6 行)。而在 test3 中,该活动已不存在(第 21-22 行)
  • test2 中,链接 p2,act1) 存在(第 14 行)。而在 test3 中,该链接已不存在(第 28 行)

2.5.6.3. Test4

该测试如下:


// 检索某人的活动
    public static void test4() {
        // 持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 检索到人员 p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // 扫描其活动
        for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
            System.out.println(pa);
        }
        // 通过p2的反向关系进行操作
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // 扫描其活动
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        // 交易结束
        tx.commit();
    }
  • 测试 4 显示了 p2 人员的活动。
  • 第 4 行:从一个全新的、空的上下文开始
  • 第12-14行:通过查询JPQL,显示用户p2参与的活动名称。
    • 已建立连接 Activite (a) / PersonneActivite (pa)(连接 a.personnes
    • 在该连接的行中(a,pa),显示人员 p2 的活动名称(a.nom)(pa.personne.nom='p2')。
  • 第16-21行:操作与前文相同,但借助了人员p2的关联OneToMany p2.activites。 查询 JPQL 将由 JPA 生成。由此可见反向关系 OneToMany 的优势:它避免了生成查询 JPQL。

结果如下:

1
2
3
4
5
main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation inverse) :
act3

2.5.6.4. Test5

该测试如下:


// 检索正在执行特定活动的用户
    public static void test5() {
        // 持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // 查询 p2 的活动
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // 通过 act3 的反向关系进行操作
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (PersonneActivite pa : act3.getPersonnes()) {
            System.out.println(pa.getPersonne().getNom());
        }
        // 事务结束
        tx.commit();
    }
  • 测试6显示了进行act3活动的人员。操作方法与测试6类似。我们留给读者自行推断这两个代码之间的关联。

结果如下:

1
2
3
4
5
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2

测试4和5旨在再次证明,反向关系绝非必需,且始终可以被查询JPQL所替代。

现在我们使用 JPA / Toplink 实现:

使用 Toplink 的 Eclipse 项目是使用 Hibernate 的 Eclipse 项目的副本:

Java代码与之前的Hibernate项目基本相同,仅存在一些细节差异,我们将在下文中说明。环境(库 – persistence.xml – 数据库管理系统 – 配置文件夹、DDL – Ant脚本)与第2.1.15.2节中研究的环境一致。 Eclipse 项目位于示例文件夹 [4] 中,文件名为 [3]。我们将导入该项目。

文件<persistence.xml> [2]在声明实体处进行了一处修改:


        <!-- 持久化类 -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • 第 2-5 行:管理的四个实体

使用 SGBD 和 MySQL5 执行 [InitDB] 后,结果如下:

在 [1] 中, 控制台显示;在 [2] 中,生成的表 [jpa07_tl];在 [3] 中,生成的脚本 SQL。其内容如下:

create.sql


CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

[InitDB] 和 [Main] 的执行均未出现错误。

2.6. 示例 6:带有隐式连接表的多对多关系

我们沿用示例 4,但现在使用由 JPA 层自身生成的隐式连接表进行处理。

2.6.1. 数据库模式

  • 在 [1] 中,数据库 MySQL5 - 在 [2] 中:表 [personne] – 在 [3] 中: 关联表 [adresse] – 转换为 [4]: 活动表 [activite] – 转换为 [5]:连接表 [personne_activite],用于关联人员与活动。

2.6.2. 代表数据库的 @Entity 对象

上述表将由以下 @Entity 表示:

  • @Entity Personne 将表示表 [personne]
  • @Entity Adresse 将表示表 [adresse]
  • @Entity Activite 将表示表 [activite]
  • 表 [personne_activite] 不再由 @Entity 表示

这些实体之间的关系如下:

  • 实体 Personne 与实体 Adresse 之间存在一对一关系:一个人 p 有一个地址 a。 持有外键的实体 Personne 将作为主关系,实体 Adresse 作为逆关系。
  • 多对多关系连接了实体 Personne Activite:一个人有多种活动,一种活动由多人参与。 该关系将通过在两个实体中分别添加 @ManyToMany 注解来实现,其中一个实体被声明为另一个实体的反向实体。

@Entity Personne 定义如下:


@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // SQL Server TopLink:@GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // 主关系 人员 (one) -> 地址 (one)
    // 通过外键实现:人员(adresse_id) -> 地址
    // 级联插入 人员 -> 插入 地址
    // 级联更新 人员 -> 更新 地址
    // 人员删除的级联操作 -> 地址删除
    // 一个人员必须拥有一个地址(可空=false)
    // 1个地址仅属于1个人(唯一=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // 通过关联表 personne_activite 建立“人员”(多对多)与“活动”(多对多)的关系
    // personne_activite(PERSONNE_ID) 是“人员(id)”的外键
    // personne_activite(ACTIVITE_ID) 是活动(id)上的外键
    // 级联=CascadeType.PERSIST:某人的持久性将导致其活动的持久性
    @ManyToMany(cascade={CascadeType.PERSIST})
    @JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
    private Set<Activite> activites = new HashSet<Activite>();

    // 构建器
    public Personne() {
    }

我们仅对第46至48行的@ManyToMany关系进行说明,该关系将@Entity Personne与@Entity Activite关联起来:

  • 第48行:一个人拥有活动。activites字段将表示这些活动。在之前的版本中,集合activites中元素的类型是PersonneActivite。在此处,则是Activite。 因此,现在可以直接访问某人的活动,而在之前的版本中,必须通过中间实体 PersonneActivite 进行访问。
  • 第46行:将我们正在研究的@Entity Personne与第48行集合activites中的@Entity Activite关联的关系属于多对多关系 (ManyToMany):
    • 一个人(One)从事多种活动(Many)
    • 一项活动(One)由多人(Many)参与
    • 最终,@Entity Personne Activite 通过关系 ManyToMany 相连。与关系 OneToOne 一样,该关系中实体具有对称性。 我们可以自由选择哪个 @Entity 实体将拥有主关系,哪个拥有反向关系。在此,我们决定让 @Entity Personne 拥有主关系。
    • 正如我们在前一个示例中所见,关系 @ManyToMany 需要一个连接表。 虽然之前我们曾使用 @Entity 定义过该表,但此处的关联表是通过第 47 行中的 @JoinTable 注解定义的。
      • name 属性为该表命名。
      • 连接表由其连接的各表上的外键组成。 此处有两个外键:一个位于表 [personne] 上,另一个位于表 [activite] 上。这些外键列由属性 joinColumns inverseJoinColumns 定义。
      • 属性 joinColumns 的注解 @JoinColumn 定义了 @Entity 表(即持有主关系 @ManyToMany 的表,此处为表 [personne])上的外键。 该外键列将命名为 PERSONNE_ID。
      • 属性 inverseJoinColumns 的注解 @JoinColumn 定义了 @ManyToMany(此处为表 [activite])所持有反向关系的 @Entity 表中的外键。 该外键列将命名为 ACTIVITE_ID。

@Entity Adresse 如下所示:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // 字段
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • 第 28-29 行: @OneToOne 关系是 @OneToOne 关系的逆关系,指向 @Entity PersonnePersonne 的第 37-38 行)。

@Entity Activite 如下所示


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {

    // 字段
    @Id()
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // toplink sqlserver : @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // 反向关系 活动 -> 人员
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • 第 20-21 行:将 @Entity Activite 与 @Entity Personne 关联的多对多关系。该关系已在 @Entity Personne 中定义。 因此,此处仅需说明该关系是@ManyToMany关系的逆关系mappedBy),该关系存在于@实体 Personne 的“activites”字段上。
  • 需要注意的是,反向关系始终是可选的。在此,我们利用它来获取正在进行当前活动的人员。通过集合 Set<Personne> personnes 即可获取这些人员。 @Entity Activite 的依赖关系 Personne 的加载模式未指定。我们在前面的示例中也没有指定。默认情况下,该模式为 fetch=FetchType.LAZY

至此,数据库实体的描述已全部完成。与将连接表 [personne_activite] 显式定义为独立表的情况相比,这种方式更为简单。 这种更简单的解决方案随着时间的推移可能会带来一些弊端:它不允许在连接表中添加列。然而,为了满足新的需求,这可能变得必要,例如在表 [personne_activite] 中添加一列,用于记录人员报名参加活动的日期。

2.6.3. Eclipse / Hibernate 项目

此处使用的 JPA 实现基于 Hibernate。测试的 Eclipse 项目如下:

在 [1] 中是 Eclipse 项目,在 [2] 中是 Java 代码。该项目位于 [4] 示例文件夹中的 [3] 文件内。我们将导入该项目。

2.6.4. 生成数据库的DDL

按照第 2.1.7 节的说明,针对 SGBD 和 MySQL5 生成的 DDL 如下:


alter table jpa08_hb_personne 
        drop 
        foreign key FKA44B1E555FE379D0;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A5CD852024;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A568C7A284;

    drop table if exists jpa08_hb_activite;

    drop table if exists jpa08_hb_adresse;

    drop table if exists jpa08_hb_personne;

    drop table if exists jpa08_hb_personne_activite;

    create table jpa08_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa08_hb_personne 
        add index FKA44B1E555FE379D0 (adresse_id), 
        add constraint FKA44B1E555FE379D0 
        foreign key (adresse_id) 
        references jpa08_hb_adresse (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A5CD852024 (ACTIVITE_ID), 
        add constraint FK5A6A55A5CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa08_hb_activite (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A568C7A284 (PERSONNE_ID), 
        add constraint FK5A6A55A568C7A284 
        foreign key (PERSONNE_ID) 
        references jpa08_hb_personne (id);

该 DDL 与使用显式连接表生成的结果类似,且与先前展示的模式一致:

2.6.5. InitDB

对于与前一版本完全相同且结果一致的类 [InitDB],我们将不作过多讨论。我们仅关注以下代码,该代码展示了 PersonneActivite 之间的连接:


        // 人员/活动显示
        System.out.println("[personnes/activites]");
        Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
        while (iterator.hasNext()) {
            Object[] row = (Object[]) iterator.next();
            System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
  • 第 3 行:执行连接操作的 JPQL 命令。select 的结果返回了实体 Personne Activite 的标识符,它们通过连接表相互关联。 select返回的列表由多行组成,每行包含两个Long类型的对象。为了遍历该列表,第3行请求列表中的一个Iterator对象。
  • 第4-7行:利用前面的Iterator类型对象遍历列表。
    • 第 5 行:列表中的每个元素都是一个数组,其中包含由 select 生成的结果行
    • 第6行:通过进行相应的类型转换,获取select当前结果行的元素。

[InitDB] 的结果如下:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
terminé...

2.6.6. Main

类 [Main] 串联了若干测试,我们将逐一审查其中部分测试。

2.6.6.1. Test3

该测试如下:


// 删除活动 act1
    public static void test3() {
        // 持久化上下文
        EntityManager em = getEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 从 p2 中删除活动 act1
        p2.getActivites().remove(act1);
        // 从持久化上下文中移除活动 act1
        em.remove(act1);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • 第 11 行:将活动 act1 从持久化上下文中移除
  • 第 9 行:活动 act1 属于该上下文中唯一剩余人员(即 p2)的活动。 第 9 行将活动 act1 从人员 p2 的活动列表中移除。我们这样做是为了保持持久化上下文的一致性,因为后续操作仍将使用该上下文。

结果如下:

main : ----------- test1
[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
main : ----------- test2
[personnes]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,1]
[2,3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,3]
  • act1 活动(在 test2 中位于第 26 行)已从 test3 的活动列表中移除(第 40-41 行)
  • 人员 p2 test2 中拥有活动 act1(第 33 行)。在 test3 结束时,该人员已不再拥有该活动(第 47 行)

2.6.6.2. Test6

该测试如下:


// 修改某人的活动
    public static void test6() {
        // 持久化上下文
        EntityManager em = getNewEntityManager();
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 检索人员 p2
        p2 = em.find(Personne.class, p2.getId());
        // 检索活动 act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 现在只执行活动 act2
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        // 事务结束
        tx.commit();
        // 显示新表
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • 第 4 行:使用一个新的、空的持久化上下文
  • 第 9 行:将 p2 对象从数据库导入持久化上下文
  • 第 11 行:将活动 act2 从数据库导入持久化上下文
  • 第13行:将人员p2act3)的活动从数据库导入到上下文(fetchType.LAZY)中。 是调用 [getActivites] 触发了此加载操作。 删除 p2 的活动。这并非真正的活动删除(remove),而是对人员 p2 状态的修改。该人员不再从事任何活动。
  • 第14行:为人员p2添加活动act2。最终,人员p2的所有新活动集合为{act2}。
  • 第16行:事务结束。同步机制将遍历上下文中的对象(p2、act2、act3),并发现p2的状态已发生变化。将执行SQL命令,将此变更同步到数据库。
  • 第 18-20 行:显示所有表

结果如下:

main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation principale) :
act3
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2
main : ----------- test6
[personnes]
P[2,2,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes/activites]
[2,2]
  • 在测试4结束时,人员p2正在执行活动act3(第3行)。
  • 在测试6结束时(第19行),人员p2不再执行活动act3(第3行),而是执行活动act2

我们现在使用 JPA / Toplink 实现:

使用 Toplink 的 Eclipse 项目是使用 Hibernate 的 Eclipse 项目的副本:

文件 <persistence.xml> [2] 仅在实体声明部分进行了修改:


        <!--  提供程序 -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- 持久化类 -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
...
  • 第 4-6 行:管理的实体

使用 [InitDB] 配合 SGBD 和 MySQL5 执行后,结果如下:

在 [1] 中, 控制台显示,在 [2] 中,生成的表 [jpa07_tl],在 [3] 中,生成的脚本 SQL。其内容如下:

create.sql


CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

[InitDB] 和 [Main] 的执行均未出现错误。

2.6.8. Eclipse / Hibernate 2 项目

我们通过复制前一个项目来构建一个新的 Eclipse 项目:

在 [1] 中是 Eclipse 项目,在 [2] 中是 Java 代码。该项目位于 [4] 示例文件夹中的 [3] 文件中。我们将导入该项目。

我们将Personne与Activité之间的关联修改如下:

Person


    // 通过连接表建立“人员”(多对多)与“活动”(多对多)的关系 personne_activite
    // personne_activite(PERSONNE_ID) 是 Personne(id) 的外键
    // personne_activite(ACTIVITE_ID) 是活动(id)上的外键
    // 活动不再具有级联关系
    // @ManyToMany(级联={CascadeType.PERSIST})
    @ManyToMany()
    @JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
  • 第 6 行:主关系 @ManyToMany 不再具有“人员 -> 活动”的持久性级联(参见旧版本第 5 行)

活动


    // 不再与“Personne”存在反向关系
    // @ManyToMany(mappedBy = "活动")
// private Set<Personne> personnes = new HashSet<Personne>();
  • 第 2-3 行:反向关系 @ManyToMany 活动 -> 人员 已被删除

我们旨在说明被删除的属性(级联和反向关系)并非必不可少。此新配置带来的首个变化体现在 [InitDB] 中:


        // 人员 <--> 活动 关联
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // 活动的持久化
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // 人员的持久化
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // 以及未与人员关联的地址 a4
em.persist(adr4);
  • 第7-9行:我们必须在持久化上下文中显式添加act1act3的活动。当“人员”持久化级联 -> 活动时,第11-13行会同时持久化人员p1p3及其活动act1至act3

第二个变化可见于 [Main]:


    // 检索从事特定活动的个人
    public static void test5() {
        // 持久化上下文
        EntityManager em = getNewEntityManager();
        // 事务开始
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        // 查询 p2 的活动
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // 事务结束
        tx.commit();
}
  • 第 9-12 行:查询 JPQL 获取从事活动 act3 的人员
  • 在之前的版本中,通过现已删除的反向关系“活动 -> 人员”也能获得相同的结果:

        // 通过 act3 的反向关系进行
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (Personne p : act3.getPersonnes()) {
            System.out.println(p.getNom());
}

我们通过复制前一个 Eclipse / Toplink 项目来构建一个新的 Eclipse 项目:

在 [1] 中是 Eclipse 项目,在 [2] 中是 Java 代码。该项目位于 [3] 文件夹中的示例文件夹 [4] 内。我们将导入该项目。

Java代码与Hibernate版本的代码完全相同。

2.7. 示例 7:使用命名查询

我们以最后一个示例结束这篇从第 2 段开始的关于 JPA 实体的长篇介绍,该示例展示了如何在配置文件中使用外部化的 JPQL 查询。此示例源自以下来源:

[ref2]:Mark Fisher 撰写的《在 Spring 2.0 中入门 JPA》,网址为

[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/]。

2.7.1. 示例数据库

示例数据库如下:

  • 在 [1] 中:包含餐厅名称和地址的列表
  • 在 [2] 中:餐厅地址表,仅包含街道门牌号和街道名称。restaurantadresse 之间存在一对一关系:每家餐厅仅有一个地址。
  • [3]:菜品表,包含菜品名称及一个布尔值(true/false),用于标识该菜品是否为素食
  • [4]:餐厅与菜品的关联表:一家餐厅提供多种菜品,而同一道菜品可能由多家餐厅提供。restaurant 表与 plat 表之间存在多对多关系。

2.7.2. 代表数据库的 @Entity 对象

上述表将由以下 @Entity 表示:

  • @Entity Restaurant 将表示表 [restaurant]
  • @Entity Adresse 将表示表 [adresse]
  • @Entity Plat 将表示表 [plat]

这些实体之间的关系如下:

  • 实体 Restaurant 与实体 Adresse 之间存在一对一关系:一家餐厅 r 有一个地址 a。 持有外键的实体 Restaurant 将作为主实体。实体 Adresse 没有反向关系。
  • 多对多关系连接了实体 Restaurant Plat:一家餐厅提供多道菜肴,而同一道菜肴可能由多家餐厅提供。 该关系将通过实体 Restaurant 中的 @ManyToMany 注解来实现。实体 Plat 没有反向关系。

@Entity Restaurant 定义如下:


package entites;

...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique = true, length = 30, nullable = false)
    private String nom;

    @OneToOne(cascade = CascadeType.ALL)
    private Adresse adresse;

    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
    private Set<Plat> plats = new HashSet<Plat>();

    // 构造函数
    public Restaurant() {

    }

    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }

    // 获取器和设置器
...

    // toString
    public String toString() {
        String signature = "R[" + getNom() + "," + getAdresse();
        for (Plat e : getPlats()) {
            signature += "," + e;
        }
        return signature + "]";
    }
}
  • 第 17 行:实体 Restaurant 与实体 Adresse 之间的一对关系。针对某家餐厅的所有持久化操作都会级联到其地址上。
  • 第20行:将@Entity Restaurant与第22行集合plats中的@Entity Plat关联的关系为多对多 (ManyToMany):
    • 一家餐厅(One)拥有多道菜肴(Many)
    • 一道菜(One)可能由多家餐厅(Many)提供
    • 最终,@Entity Restaurant Plat 通过 ManyToMany 关系相连。 我们决定让 @Entity Restaurant 作为主实体,而 @Entity Plat 不建立反向关系。
    • 关系 @ManyToMany 需要一个连接表。该连接表通过第 47 行中的注释 @JoinTable 进行定义。
      • name 属性为该表命名。
      • 连接表由其连接的各表上的外键组成。 此处有两个外键:一个位于表 [restaurant] 上,另一个位于表 [plat] 上。这些外键列由属性 joinColumns inverseJoinColumns 定义。
      • 属性 joinColumns 定义了 @Entity 表(即持有主关系 @ManyToMany 的表)上的外键,此处为表 [restaurant]。 此处缺少 joinColumns 属性。 在此情况下,JPA具有默认值:[table]_[clé_primaire_de_table],即[jpa10_hb_restaurant_id]。
      • 属性 inverseJoinColumns 的注解 @JoinColumn 定义了外键,指向持有反向关系 @ManyToMany 的 @Entity 表,此处即表 [plat]。 该外键列将命名为 plat_id

@Entity Adresse 如下所示:


package entites;

...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  @Column(name = "NUMERO_RUE")
  private int numeroRue;
  
  @Column(name = "NOM_RUE", length=30, nullable=false)
  private String nomRue;
  
  // getter 和 setter
 ...
 
  // 构造函数
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • @Entity 实体 Adresse 是一个与其他实体没有直接关联的实体。只能通过实体 Restaurant 来持久化它。
  • 一个地址由街道名称(第16行)和门牌号(第13行)定义。

@Entity Plat 定义如下


package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique=true, length=50, nullable=false)
    private String nom;

    private boolean vegetarien;

    // 构造函数
    public Plat() {

    }

    public Plat(String name, boolean vegetarian) {
...
    }

    // getter 和 setter
...

    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }

}
  • @Entity Plat 是一个与其他实体没有直接关系的实体。只能通过实体 Restaurant 来持久化它。
  • 一道菜肴由名称(第 12 行)和素食或非素食类型(第 14 行)定义。

2.7.3. Eclipse / Hibernate 项目

此处使用的 JPA 实现来自 Hibernate。测试的 Eclipse 项目如下:

在 [1] 中是 Eclipse 项目,在 [2] 中是 Java 代码,而在 JPA 中是配置文件。 值得注意的是,存在一个此前从未见过的文件 [orm.xml]。该项目位于示例文件夹 [4] 中的 [3] 内。我们将将其导入。

2.7.4. 生成数据库的 DDL

按照第 2.1.7 节的说明,针对 SGBD 和 MySQL5 生成的 DDL 如下:


alter table jpa10_hb_restaurant 
        drop 
        foreign key FK3E8E4F5D5FE379D0;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D11F0F78A4;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D1AFAC3E44;

    drop table if exists jpa10_hb_adresse;

    drop table if exists jpa10_hb_plat;

    drop table if exists jpa10_hb_restaurant;

    drop table if exists jpa10_hb_restaurant_plat;

    create table jpa10_hb_adresse (
        id bigint not null auto_increment,
        NUMERO_RUE integer,
        NOM_RUE varchar(30) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_plat (
        id bigint not null auto_increment,
        nom varchar(50) not null unique,
        vegetarien bit not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant (
        id bigint not null auto_increment,
        nom varchar(30) not null unique,
        adresse_id bigint,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant_plat (
        jpa10_hb_restaurant_id bigint not null,
        plat_id bigint not null,
        primary key (jpa10_hb_restaurant_id, plat_id)
    ) ENGINE=InnoDB;

    alter table jpa10_hb_restaurant 
        add index FK3E8E4F5D5FE379D0 (adresse_id), 
        add constraint FK3E8E4F5D5FE379D0 
        foreign key (adresse_id) 
        references jpa10_hb_adresse (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D11F0F78A4 (plat_id), 
        add constraint FK1D2D06D11F0F78A4 
        foreign key (plat_id) 
        references jpa10_hb_plat (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id), 
        add constraint FK1D2D06D1AFAC3E44 
        foreign key (jpa10_hb_restaurant_id) 
        references jpa10_hb_restaurant (id);
  • 第 21-26 行:表 [adresse]
  • 第 28-33 行:表 [plat]
  • 第 35-40 行:表 [restaurant]
  • 第 42-46 行:连接表 [restaurant_plat]。请注意复合主键(第 45 行)
  • 第 48-52 行:表 [restaurant] 指向表 [adresse] 的外键
  • 第 54-58 行:表 [restaurant_plat] 到表 [plat] 的外键
  • 第 60-64 行:表 [restaurant_plat] 到表 [restaurant] 的外键

该 DDL 对应已介绍的模式:

在 SQL Explorer 视图中,数据库结构如下所示:

  • 在 [1] 中:数据库的 4 张表
  • 在 [2] 中:地址
  • 在 [3] 中:菜品
  • 在 [4] 中:餐厅。[adresse_id] 引用了 [2] 中的地址。
  • 在 [5] 中:连接表 [restaurant,plat]。 [jpa10_hb_restaurant_id] 引用了 [4] 中的餐厅,而 [plat_id] 引用了 [3] 中的菜品。 因此,[1,1] 表示“Burger Barn”餐厅供应菜品“CheeseBurger”。

为了获取上述数据,已执行 Eclipse 项目中的程序 [QueryDB]。

2.7.5. 使用 Hibernate 控制台执行 JPQL 查询

我们创建一个与前述 Eclipse 项目关联的 Hibernate 控制台。我们将遵循此前已两次介绍的方法,特别是第 2.1.12 节中的步骤。

  • 在 [1] 和 [2] 中:Hibernate 控制台的配置
  • 在 [3] 中:一个查询 JPQL,以及在 [4] 中显示的结果。
  • 在 [5] 中:等效的 SQL 命令

现在我们展示一系列查询 JPQL。欢迎读者亲自运行这些查询,并发现 Hibernate 为执行它们生成的命令 SQL。

获取所有餐厅及其菜品:

获取至少提供一道素食的餐厅:

获取仅提供素食的餐厅名称:

获取供应汉堡的餐厅:

2.7.6. QueryDB

现在我们关注Eclipse项目中的[QueryDB]程序,该程序:

  • 向数据库
  • 并向其发出若干请求 JPQL。这些请求被记录在 Eclipse 项目的 [META-INF/orm.xml] 文件中:

文件 [orm.xml] 可用于配置 JPA 层,以替代 Java 注解。这为 JPA 层的配置带来了灵活性。 无需重新编译 Java 代码即可对其进行修改。可以同时使用两种方法:Java 注解和 [orm.xml] 文件。JPA 的配置首先通过 Java 注解完成,随后通过 [orm.xml] 文件进行。 因此,若需修改通过 Java 注解创建的配置且无需重新编译,只需将该配置写入 [orm.xml] 文件即可。该文件将具有最终决定权。

在本例中,文件 [orm.xml] 用于存储 JPQL 中的查询文本。其内容如下:


<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
    <description>Restaurants</description>
    <named-query name="supprimer le contenu de la table restaurant">
        <query>delete from Restaurant</query>
    </named-query>
    <named-query name="supprimer le contenu de la table plat">
        <query>delete from Plat</query>
    </named-query>
    <named-query name="obtenir tous les restaurants">
        <query>select r from Restaurant r order by r.nom asc</query>
    </named-query>
    <named-query name="obtenir toutes les adresses">
        <query>select a from Adresse a order by a.nomRue asc</query>
    </named-query>
    <named-query name="obtenir tous les plats">
        <query>select p from Plat p order by p.nom asc</query>
    </named-query>
    <named-query name="obtenir tous les restaurants avec leurs plats">
        <query>select r.nom,p.nom from Restaurant r join r.plats p</query>
    </named-query>
    <named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
        <query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
    </named-query>
    <named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
        <query>
            select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
            p1.vegetarien=false)
        </query>
    </named-query>
    <named-query name="obtenir les restaurants d'une certaine rue">
        <query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
    </named-query>
    <named-query name="obtenir les restaurants qui servent des burgers">
        <query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
    </named-query>
    <named-query name="obtenir les plats du restaurant untel">
        <query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
    </named-query>
</entity-mappings>
  • 文件 [orm.xml] 的根节点是 <entity-mappings>(第 2 行)。
  • 第 5-7 行:命名的 JPQL 查询由 < 标签包裹,其名称为 name= "... ">文本</namedquery>
    • 该标签的 name 属性即为查询名称。
    • 该标签的内容 texte 是查询的文本。

QueryDB 将执行上述查询。其代码如下:


package tests;

...
public class QueryDB {

    // 持久化上下文
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = emf.createEntityManager();

    public static void main(String[] args) {
        // 开始事务
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 删除表中的元素[restaurant]
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // 删除表中的元素 [plat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        // 创建 Address 对象
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // 对象创建 入口
        Plat ent1 = new Plat("Hamburger", false);
        Plat ent2 = new Plat("Cheeseburger", false);
        Plat ent3 = new Plat("Tofu Stir Fry", true);
        Plat ent4 = new Plat("Vegetable Soup", true);
        // 创建 Restaurant 对象
        Restaurant restaurant1 = new Restaurant();
        restaurant1.setNom("Burger Barn");
        restaurant1.setAdresse(adr1);
        restaurant1.getPlats().add(ent1);
        restaurant1.getPlats().add(ent2);
        Restaurant restaurant2 = new Restaurant();
        restaurant2.setNom("Veggie Village");
        restaurant2.setAdresse(adr2);
        restaurant2.getPlats().add(ent3);
        restaurant2.getPlats().add(ent4);
        Restaurant restaurant3 = new Restaurant();
        restaurant3.setNom("Dover Diner");
        restaurant3.setAdresse(adr3);
        restaurant3.getPlats().add(ent1);
        restaurant3.getPlats().add(ent2);
        restaurant3.getPlats().add(ent4);
        // 餐厅对象(及其他级联对象)的持久化
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        // 事务结束
        tx.commit();
        // 数据库转储
        dumpDataBase();
        // 结束 EntityManager
        em.close();
        // 结束 EntityManagerFactory
        emf.close();
    }

    // 显示数据库内容
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // 测试2
        log("données de la base");
        // 开始交易
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // 餐厅显示
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // 地址显示
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // 菜品显示
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // 餐厅与菜品之间的链接显示
        log("[restaurants/plats]");
        Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
        }
        log("[Liste des restaurants avec au moins un plat végétarien]");
        for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
            System.out.println(r);
        }
        // 查询
        log("[Liste des restaurants avec seulement des plats végétariens]");
        for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
            System.out.println(r);
        }
        // 查询
        log("[Liste des restaurants dans Dover Street]");
        for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
            System.out.println(r);
        }
        // 查询
        log("[Liste des restaurants ayant un plat de type burger]");
        record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
        }
        // 查询
        log("[Plats de Veggie Village]");
        for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
            System.out.println(r);
        }
        // 交易结束
        tx.commit();
    }

    // 日志
    private static void log(String message) {
        System.out.println(" -----------" + message);
    }

}

执行 [QueryDB] 的结果如下:

-----------données de la base
 -----------[restaurants]
R[Burger Barn,A[10,Main Street],E[Cheeseburger,false],E[Hamburger,false]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
 -----------[adresses]
A[123,Dover Street]
A[10,Main Street]
A[20,Main Street]
 -----------[plats]
E[Cheeseburger,false]
E[Hamburger,false]
E[Tofu Stir Fry,true]
E[Vegetable Soup,true]
 -----------[restaurants/plats]
[Burger Barn,Cheeseburger]
[Burger Barn,Hamburger]
[Dover Diner,Cheeseburger]
[Dover Diner,Hamburger]
[Dover Diner,Vegetable Soup]
[Veggie Village,Tofu Stir Fry]
[Veggie Village,Vegetable Soup]
 -----------[Liste des restaurants avec au moins un plat végétarien]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants avec seulement des plats végétariens]
Veggie Village
 -----------[Liste des restaurants dans Dover Street]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants ayant un plat de type burger]
[Burger Barn,10,Main Street,Cheeseburger]
[Burger Barn,10,Main Street,Hamburger]
[Dover Diner,123,Dover Street,Cheeseburger]
[Dover Diner,123,Dover Street,Hamburger]
 -----------[Plats de Veggie Village]
Tofu Stir Fry
Vegetable Soup

我们将代码与结果之间的关联留给读者自行探索。为此,建议您在 Hibernate 控制台中执行查询 JPQL,并查看与其对应的代码 SQL。

感兴趣的读者可在本教程提供的可下载示例中找到使用 Toplink 实现的上述项目:

基于 Toplink 的 Eclipse 项目是基于 Hibernate 的 Eclipse 项目的副本:

文件 <persistence.xml> [2] 声明了受管理的实体:


        <!--  提供程序 -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- 持久化类 -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>

...
  • 第 4-6 行:受管实体

存储在 [orm.xml] 中的 JPQL 查询由 Toplink 正确执行。 为此,在之前的项目中,我们特意避免使用 HQL(Hibernate 查询语言)查询,因为它实际上是 JPQL 的超集,且其中某些语法不被 JPQL 所支持。

2.8. 结论

至此,我们结束了对 JPA 实体的探讨。虽然耗时较长,但仍有(对高级开发者而言)重要的内容尚未涉及。再次建议阅读参考书籍,例如本教程所使用的:

[ref1]:《Java Persistence with Hibernate》,作者:Christian Bauer 和 Gavin King,Manning 出版社