将 JPA 与 App Engine 搭配使用

Java Persistence API (JPA) 是访问 Java 数据库的一个标准接口,为 Java 类和数据库表之间提供自动映射。有一个开源插件可将 JPA 与 Datastore 结合使用,本页面介绍了如何使用该插件。

警告:我们认为,对于大多数开发者来说,使用低层级的 Datastore API 或某种专为 Datastore 开发的开源 API(如 Objectify)会获得更好的体验。 JPA 是专为与传统关系型数据库搭配使用而设计的,所以无法明确表示使 Datastore 不同于关系型数据库的某些方面(如实体组和祖先查询)。 这可能会导致难以理解和解决的细微问题。

App Engine Java SDK 包含 Datastore 的 DataNucleus 插件的 2.x 版。此插件相当于 3.0 版的 DataNucleus Access Platform,使您能够通过 JPA 2.0 使用 App Engine Datastore。

如需详细了解 JPA,请参阅 Access Platform 3.0 文档。请特别留意 JPA 文档JPA API

警告:App Engine 的 DataNucleus 插件 2.x 版本使用 DataNucleus 3.x 版本。2.x 插件并不能向后完全兼容之前的 1.x 插件。如果升级到新版本,请务必更新并测试您的应用。

构建支持 JPA 2.x 和 3.0 的工具

您可以通过 Apache Ant 或 Maven 来使用 App Engine 的 2.x 或 3.0 版 DataNucleus 插件:

  • 对于 Ant 用户:SDK 包含执行增强步骤的 Ant 任务。在设置您的项目时,必须复制 JAR 并创建配置文件。
  • 对于 Maven 用户:您可以在 pom.xml 文件中使用以下配置来增强类:
                <plugin>
                    <groupId>org.datanucleus</groupId>
                    <artifactId>maven-datanucleus-plugin</artifactId>
                    <version>3.2.0-m1</version>
                    <configuration>
                        <api>JDO</api>
                        <props>${basedir}/datanucleus.properties</props>
                        <verbose>true</verbose>
                        <enhancerName>ASM</enhancerName>
                    </configuration>
                    <executions>
                        <execution>
                            <phase>process-classes</phase>
                            <goals>
                                <goal>enhance</goal>
                            </goals>
                        </execution>
                    </executions>
                    <dependencies>
                        <dependency>
                            <groupId>org.datanucleus</groupId>
                            <artifactId>datanucleus-api-jdo</artifactId>
                            <version>3.1.3</version>
                        </dependency>
                    </dependencies>
                </plugin>

迁移到 2.x 版 DataNucleus 插件

本部分说明了如何将应用升级为使用 App Engine 的 2.x 版 DataNucleus 插件,该插件相当于 DataNucleus Access Platform 3.0 和 JPA 2.0。2.x 版插件不能向后完全兼容 1.x 版,并且可能会在没有任何警告的前提下进行更改。如果升级,请务必更新并测试应用代码。

新的默认行为

2.x 版 App Engine DataNucleus 插件与之前的 1.x 版相比,存在一些不同的默认值:

  • JPA“持久性提供者”现在为 org.datanucleus.api.jpa.PersistenceProviderImpl
  • 默认启用 Level2 缓存。要获得之前的默认行为,请将持久性属性 datanucleus.cache.level2.type设置为 none。(或者,在类路径中包含 datanucleus-cache 插件,并将持久性属性 datanucleus.cache.level2.type 设置为 javax.cache以使用 Memcache 进行 L2 缓存。
  • Datastore IdentifierFactory 现在默认为 datanucleus2 。要获得之前的默认行为,请将持久性属性 datanucleus.identifierFactory 设置为 datanucleus1
  • 现在,以原子方式执行对 EntityManager.persist()EntityManager.merge()EntityManager.remove() 的非事务性调用。(以前,调用会在下一个事务中或 EntityManager.close() 之后执行。)
  • JPA 已启用 retainValues,这意味着加载字段的值在提交后会被保留在对象中。
  • javax.persistence.query.chunkSize 不再使用。请改用 datanucleus.query.fetchSize
  • 现在重复 EMF 分配不再发生异常。如果您将持久性属性 datanucleus.singletonEMFForName 设置为 true,则该属性将返回当前为名称分配的单例 EMF。
  • 现在支持非所属关系。
  • 现在支持 Datastore Identity。

如需新功能的完整列表,请参阅版本说明

配置文件变更

要将应用升级到 App Engine DataNucleus 插件的 2.0 版,您需要更改 build.xmlpersistence.xml 中的配置设置。如果您正在设置新应用,并希望使用最新版本的 DataNucleus 插件,请继续设置 JPA 2.0

警告!更新配置后,您需要测试应用代码以确保向后兼容性。

在 build.xml 中

要适应 DataNucleus 2.x,需要更改 copyjars 目标:

  1. copyjars 目标已更改。将此部分:
      <target name="copyjars"
          description="Copies the App Engine JARs to the WAR.">
        <mkdir dir="war/WEB-INF/lib" />
        <copy
            todir="war/WEB-INF/lib"
            flatten="true">
          <fileset dir="${sdk.dir}/lib/user">
            <include name="**/*.jar" />
          </fileset>
        </copy>
      </target>

    更新为:
      <target name="copyjars"
          description="Copies the App Engine JARs to the WAR.">
        <mkdir dir="war/WEB-INF/lib" />
        <copy
            todir="war/WEB-INF/lib"
            flatten="true">
          <fileset dir="${sdk.dir}/lib/user">
            <include name="**/appengine-api-1.0-sdk*.jar" />
          </fileset>
          <fileset dir="${sdk.dir}/lib/opt/user">
            <include name="appengine-api-labs/v1/*.jar" />
            <include name="jsr107/v1/*.jar" />
            <include name="datanucleus/v2/*.jar" />
          </fileset>
        </copy>
      </target>
  2. datanucleusenhance 目标已更改。将此部分:
      <target name="datanucleusenhance" depends="compile"
          description="Performs enhancement on compiled data classes.">
        <enhance_war war="war" />
      </target>

    更新为:
      <target name="datanucleusenhance" depends="compile"
          description="Performs enhancement on compiled data classes.">
          <enhance_war war="war">
                  <args>
                  <arg value="-enhancerVersion"/>
                  <arg value="v2"/>
              </args>
          </enhance_war>
      </target>

在 persistence.xml 中

<provider> 目标已更改。将此部分:

        <provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>

更新为:

        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>

设置 JPA 2.0

要使用 JDA 访问数据存储区,App Engine 应用需要满足以下条件:

  • JPA 和数据存储区 JAR 必须位于应用的 war/WEB-INF/lib/ 目录中。
  • 名为 persistence.xml 的配置文件必须位于应用的 war/WEB-INF/classes/META-INF/ 目录中,该文件包含告诉 JDA 使用 App Engine 数据存储区的属性。
  • 项目构建过程必须对已编译数据类执行后编译“增强”步骤,以将这些数据类与 JPA 实现相关联。

复制 JAR

JDA 和数据存储区 JAR 都包含在 App Engine Java SDK 中。您可以在 appengine-java-sdk/lib/opt/user/datanucleus/v2/ 目录中找到它们。

将 JAR 复制到您的应用的 war/WEB-INF/lib/ 目录中。

确保 appengine-api.jar 也在 war/WEB-INF/lib/ 目录中。(您可能已经在创建您的项目时复制了此文件。)App Engine DataNucleus 插件使用此 JAR 文件访问数据存储区。

创建 persistence.xml 文件

JDA 接口需要应用的 war/WEB-INF/classes/META-INF/ 目录中名为 persistence.xml 的配置文件。您可以直接在此位置中创建该文件,也可以让编译流程从源目录中复制该文件。

请使用以下内容创建该文件:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
            <property name="datanucleus.singletonEMFForName" value="true"/>
        </properties>

    </persistence-unit>

</persistence>

Datastore 读取政策和调用期限

Datastore 查询页面所述,您可以为 persistence.xml 文件中的 EntityManagerFactory 设置读取政策(强一致性与最终一致性)和数据存储区调用期限。这些设置将置于 <persistence-unit> 元素中。使用指定的 EntityManager 进行的所有调用都会使用 EntityManagerFactory 在创建该管理器时选定的配置。您还可以针对个别 Query 替换这些选项(详见下文)。

要设置读取策略,只需包括一个名为 datanucleus.appengine.datastoreReadConsistency 的属性。其可能的值是 EVENTUAL(用于最终一致性读取)和 STRONG (用于强一致性读取)。如果没有指定,默认值为 STRONG

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

您可以为读取和写入分别设置数据存储区调用期限。对于读取,请使用 JDA 标准属性 javax.persistence.query.timeout。对于写入,请使用 datanucleus.datastoreWriteTimeout。调用期限值是一个时间量,以毫秒为单位。

            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />

如果您要使用跨组 (XG) 事务,请添加以下属性:

            <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

借助不同的 name 特性,您可以在同一个 persistence.xml 文件中拥有多个 <persistence-unit> 元素,从而可在同一个应用中使用具有不同配置的 EntityManager 实例。例如,以下 persistence.xml 文件建立了两组配置,一组名为 "transactions-optional",另一组名为 "eventual-reads-short-deadlines"

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="transactions-optional">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="eventual-reads-short-deadlines">
        <provider>org.datanucleus.api.jpa.PersistenceProviderImpl</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>

            <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
            <property name="javax.persistence.query.timeout" value="5000" />
            <property name="datanucleus.datastoreWriteTimeout" value="10000" />
            <property name="datanucleus.singletonEMFForName" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

如需了解如何使用指定配置集创建 EntityManager,请参阅下面的获取 EntityManager 实例

您可以为个别 Query 对象替换读取策略和调用截止时间。要为某个 Query 替换读取策略,请调用其 setHint() 方法,如下所示:

        Query q = em.createQuery("select from " + Book.class.getName());
        q.setHint("datanucleus.appengine.datastoreReadConsistency", "EVENTUAL");

如上所述,可能的值是 "EVENTUAL""STRONG"

要替换读取超时,请调用 setHint(),如下所示:

        q.setHint("javax.persistence.query.timeout", 3000);

在通过键提取实体时,无法替换这些选项的配置。

增强数据类

JPA 的 DataNucleus 实现在编译流程中使用后编译“增强”步骤,以将数据类与 JPA 实现相关联。

您可以从命令行使用以下命令对已编译的类执行增强步骤:

java -cp classpath org.datanucleus.enhancer.DataNucleusEnhancer
class-files

类路径必须包含来自 appengine-java-sdk/lib/tools/ 目录的 JAR datanucleus-core-*.jardatanucleus-jpa-*datanucleus-enhancer-*.jarasm-*.jargeronimo-jpa-*.jar(其中 * 是每个 JAR 的相应版本号),以及您的所有数据类。

如需详细了解 DataNucleus 字节码增强器,请参阅 DataNucleus 文档

获取 EntityManager 实例

应用使用 EntityManager 类的实例与 JPA 进行交互。您可以通过在 EntityManagerFactory 类的实例上实例化并调用一个方法来获取此实例。工厂实例使用 JPA 配置(由名称 "transactions-optional" 标识)来创建 EntityManager 实例。

因为对 EntityManagerFactory 实例进行初始化需要时间,所以应尽可能地重用单个实例。实现此操作的一个简单方法是使用静态实例创建单例封装类,如下所示:

EMF.java

import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public final class EMF {
    private static final EntityManagerFactory emfInstance =
        Persistence.createEntityManagerFactory("transactions-optional");

    private EMF() {}

    public static EntityManagerFactory get() {
        return emfInstance;
    }
}

提示:"transactions-optional" 指的是 persistence.xml 文件中的配置集的名称。如果您的应用使用多个配置集,则必须根据需要扩展此代码来调用 Persistence.createEntityManagerFactory()。您的代码应该缓存每个 EntityManagerFactory 的单例实例。

该应用使用工厂实例为访问数据存储区的每个请求创建一个 EntityManager 实例。

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;

import EMF;

// ...
    EntityManager em = EMF.get().createEntityManager();

您使用 EntityManager 来存储、更新和删除数据对象,并执行数据存储区查询。

使用完 EntityManager 实例后,必须调用其 close() 方法。在调用 EntityManager 实例的 close() 方法之后使用该实例是一种错误做法。

    try {
        // ... do stuff with em ...
    } finally {
        em.close();
    }

类和字段注释

JPA 保存的每个对象都将成为 App Engine Datastore 中的一个实体。实体的种类是从类的简单名称派生而来(不含包名称)。类的每个持久字段都代表实体的一个属性,属性的名称与字段的名称相同(保留大小写)。

要声明能够使用 JDA 在数据存储区中存储和检索 Java 类,可以为该类提供一个 @Entity 批注。例如:

import javax.persistence.Entity;

@Entity
public class Employee {
    // ...
}

需要在 Datastore 中存储的数据类字段必须是默认持久的类型,或明确声明为持久的类型。 您可以在 DataNucleus 网站上找到详述 JPA 默认持久性行为的图表。要将字段显式声明为持久字段,可以为它提供一个 @Basic 批注:

import java.util.Date;
import javax.persistence.Enumerated;

import com.google.appengine.api.datastore.ShortBlob;

// ...
    @Basic
    private ShortBlob data;

字段的类型可以是以下任何一项:

  • 数据存储区支持的核心类型之一
  • 核心数据存储区类型的值的集合(如 java.util.List<...>
  • @Entity 类的一个实例或实例集合
  • 一个存储为实体属性的嵌入式类

数据类必须有一个公共的或受保护的默认构造函数,以及一个专用于储存相应 Datastore 实体主键的字段。您可以在 4 种不同类型的键字段中选择,每种字段使用不同的值类型和注释。(如需了解详情,请参阅创建数据:键。)最简单的键字段是一个长整数值,在第一次将对象保存到 Datastore 中时,JPA 使用和该类的其他所有实例不一样的值来自动填充该值。长整数键使用一个 @Id 批注和一个 @GeneratedValue(strategy = GenerationType.IDENTITY) 批注:

import com.google.appengine.api.datastore.Key;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

// ...
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

下面是一个示例数据类:

import com.google.appengine.api.datastore.Key;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String firstName;

    private String lastName;

    private Date hireDate;

    // Accessors for the fields. JPA doesn't use these, but your application
    does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

继承

JPA 支持创建使用继承的数据类。在介绍 JPA 继承在 App Engine 上的工作原理之前,建议您阅读有关此主题的 DataNucleus 文档,然后再阅读本文。阅读完了吗?好的。JDA 继承在 App Engine 上按照 DataNucleus 文档中所述方式工作,并且会受到一些额外的限制。我们将讨论这些限制,然后给出一些具体的示例。

“JOINED”继承策略允许您将单个数据对象的数据拆分到多个“表”中,但由于App Engine Datastore 不支持联接,对于使用此继承策略的数据对象进行操作需要在继承的每个层级进行远程过程调用。这可能会非常低效,所以数据类不支持“JOINED”继承策略。

其次,“SINGLE_TABLE”继承策略允许您将数据对象的数据存储在与位于继承架构根目录层的持久类相关联的单个“表”中。尽管此策略并没有内在的低效,但目前不受支持。我们可能会在将来的版本中再次审视这一策略。

现在让我们谈谈好的消息:“TABLE_PER_CLASS”和“MAPPED_SUPERCLASS”策略都按照 DataNucleus 文档中所述方式工作。让我们看一个示例:

Worker.java

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@Entity
@MappedSuperclass
public abstract class Worker {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Key key;

    private String department;
}

Employee.java

// ... imports ...

@Entity
public class Employee extends Worker {
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@Entity
public class Intern extends Worker {
    private Date internshipEndDate;
}

在这个示例中,我们将一个 @MappedSuperclass 批注添加到 Worker 类声明。这告诉 JDA 将 Worker 的所有持久字段存储在其子类的数据存储区实体中。因使用 Employee 实例调用 persist() 而创建的数据存储区实体将有两个名为“department”和“salary”的属性。因使用 Intern 实例调用 persist() 而创建的数据存储区实体将有两个名为“department” 和“inernshipEndDate”的属性。Datastore 中不会有种类为“Worker”的实体。

现在,让我们把它变得更有趣一点。假设除了拥有 EmployeeIntern 之外,我们还想有一个描述已离开公司的员工的特殊 Employee

FormerEmployee.java

import java.util.Date;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
// ... imports ...

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class FormerEmployee extends Employee {
    private Date lastDay;
}

在此例中,我们通过将 strategy 属性设置为 InheritanceType.TABLE_PER_CLASS,为 FormerEmployee 类声明添加了一个 @Inheritance 批注。这告诉 JDA 将 FormerEmployee 及其超类的所有持久字段存储在对应于 FormerEmployee 实例的数据存储区实体中。因使用 FormerEmployee 实例调用 persist() 而创建的数据存储区实体将有三个分别名为“department”、“salary”和“lastDay”的属性。永远都不会有与 FormerEmployee 相对应的“Employee”种类实体,但如果您使用运行时类型为 Employee 的对象调用 persist(),您将会创建一个“Employee”种类实体。

只要关系字段的声明类型与您将分配给这些字段的对象的运行时类型相匹配,就可以混合关系和继承。如需了解详情,请参阅有关多态关系的部分。本部分包含一些 JDO 示例,但概念和限制与 JPA 的相同。

不受支持的 JPA 2.0 功能

下面的 JDA 接口功能不受 App Engine 实现的支持:

  • 有主的多对多关系。
  • “Join”查询。在对父种类执行查询时,您不能在过滤器中使用子实体的字段。请注意,您可以使用某个键直接在查询中测试父类型的关系字段。
  • 聚合查询(group by、having、sum、avg、max、min)
  • 多态查询。您不能通过对某个类执行查询来获取子类的实例。在数据存储区中,每个类都是由一个单独的实体类型表示的。