搭配 App Engine 來使用 JPA

Java Persistence API (JPA) 是一種可以在 Java 中存取資料庫的標準介面,可讓您自動對應 Java 類別與資料庫表格。有一種開放原始碼外掛程式可讓您利用 Datastore 來使用 JPA,而本頁就是提供如何開始使用該外掛程式的相關資訊。

警告:我們認為,大多數的開發人員在使用低階的 Datastore API,或是專為 Datastore 開發的某種開放原始碼 API (例如 Objectify) 時,會有比較好的體驗。JPA 是專為搭配傳統關聯資料庫來使用所設計的,因此無法明確指出 Datastore 與關聯資料庫不同的部分,例如實體群組與祖系查詢。這可能會導致難以瞭解及修正的小問題發生。

App Engine Java SDK 包含 Datastore 適用的 DataNucleus 外掛程式 2.x 版。這個外掛程式相當於 DataNucleus Access Platform 3.0 版,而後者可讓您透過 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 適用的 DataNucleus 外掛程式 2.x 版或 3.0 版:

  • 針對 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>

遷移至 DataNucleus 外掛程式 2.x 版

本節操作說明將介紹如何升級您的應用程式,以便使用 App Engine 適用的 2.x 版 DataNucleus 外掛程式,該外掛程式與 DataNucleus Access Platform 3.0 和 JPA 2.0 相對應。2.x 外掛程式版本與 1.x 版本無法完全回溯相容,並且可能會在無預警的情況下進行變更。如果您要執行升級作業,請務必更新及測試應用程式程式碼。

新的預設行為

App Engine DataNucleus 外掛程式 2.x 版中有些預設值與先前 1.x 版的不同:

  • JPA「持續性提供者」現在是 org.datanucleus.api.jpa.PersistenceProviderImpl
  • 「層級 2 快取」預設為啟用。如要取得先前的預設行為,請將持續性屬性 datanucleus.cache.level2.type 設定為「none」 (或者是,在 classpath 中加入 datanucleus-cache 外掛程式,並將持續性屬性 datanucleus.cache.level2.type 設定為「javax.cache」,即可使用 Memcache 進行 L2 快取)。
  • 資料儲存庫 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 中

copyjars 目標必須變更,才能容納 DataNucleus 2.x:

  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

如要使用 JPA 存取資料儲存庫,App Engine 應用程式必須符合以下要求:

  • JPA 和資料儲存庫 JAR 必須位於應用程式的 war/WEB-INF/lib/ 目錄中。
  • 名為 persistence.xml 的設定檔必須位於應用程式的 war/WEB-INF/classes/META-INF/ 目錄中,且該檔案中的設定必須要叫 JPA 使用 App Engine 資料儲存庫。
  • 專案的建構程序必須對已編譯的資料類別執行後編譯「強化」步驟,才能建立這些資料類別與 JPA 實作之間的關聯。

複製 JAR

App Engine Java SDK 包括 JPA 和資料儲存庫 JAR,您可以在 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 檔案

JPA 介面需要名為 persistence.xml 的設定檔,且該設定檔必須位於應用程式的 war/WEB-INF/classes/META-INF/ 目錄中。您可以直接在該目錄中建立這個檔案,也可透過建構程序將這個檔案從來源目錄複製到該目錄中。

建立含有以下內容的檔案:

<?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" />

您可以分別為讀取作業和寫入作業設定資料儲存庫呼叫期限。針對讀取作業,請使用 JPA 標準屬性 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" />

您可以在相同的 persistence.xml 檔案中使用多個 <persistence-unit> 元素,且分別有不同的 name 屬性,以便在相同的應用程式中使用有不同設定的 EntityManager 執行個體。舉例來說,下列 檔案會建立兩組設定,一個名為 "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

「classpath」 必須包含 appengine-java-sdk/lib/tools/ 目錄中的 JAR datanucleus-core-*.jardatanucleus-jpa-*datanucleus-enhancer-*.jarasm-*.jargeronimo-jpa-*.jar (其中 * 是每個 JAR 的適當版本號碼),以及所有的資料類別。

如要進一步瞭解 DataNucleus 位元組程式碼強化工具,請參閱 DataNucleus 說明文件

取得 EntityManager 執行個體

應用程式必須利用 EntityManager 類別的執行個體來與 JPA 互動。如要取得此執行個體,請針對 EntityManagerFactory 類別的執行個體執行實例化作業並呼叫方法。Factory 會使用 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 的單例執行個體。

應用程式會使用 Factory 執行個體,為每個會存取資料儲存庫的要求建立一個 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 資料儲存庫中的實體,而實體的種類衍生自類別的簡單名稱 (不含套件名稱)。類別的每個持續性欄位都代表實體的某個屬性,且屬性名稱即為欄位名稱 (大小寫相同)。

如要宣告 Java 類別能夠利用 JPA 儲存在資料儲存庫中,以及從資料儲存庫中擷取,請為該類別提供 @Entity 註解。例如:

import javax.persistence.Entity;

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

將要儲存在資料儲存庫中的資料類別欄位,必須屬於預設會持續的類型,或是屬於已明確宣告為持續性的類型。如需詳細說明 JPA 預設持續性行為的圖表,請瀏覽 DataNucleus 網站。如要將某個欄位明確宣告為持續性,請提供 @Basic 註解:

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

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

// ...
    @Basic
    private ShortBlob data;

欄位可以屬於下列任何一種類型:

  • 資料儲存庫支援的其中一種核心類型
  • 核心資料儲存庫類型之值的集合 (例如 java.util.List<...>)
  • @Entity 類別的執行個體或執行個體集合
  • 儲存為實體屬性的嵌入類別

資料類別必須具備一個公開或受保護的預設建構函式,以及一個專門儲存對應資料儲存庫實體主鍵的欄位。您可以從四種不同種類的金鑰欄位中選擇,每一種金鑰欄位使用不同的值類型和註解。(詳情請參閱建立資料:金鑰。) 最簡單的金鑰欄位類型是長整數值,當您第一次將這類物件儲存至資料儲存庫時,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 說明文件中與此主題有關的內容,然後再往下閱讀。已經看完了?好的。JPA 繼承在 App Engine 中的運作原理和 DataNucleus 文件中說明的原理一樣,但還包括一些額外的限制。我們將說明這些限制並提供一些具體範例。

「JOINED」繼承策略可讓您跨多個「資料表」分割單一資料物件的資料,但由於 App Engine 資料儲存庫不支援彙整,因此使用此繼承策略對資料物件執行操作時,需要針對每個繼承層級執行遠端程序呼叫。這項操作的效率可能非常低,因此資料類別不支援「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;
}

在這個範例中,我們為 Worker 類別宣告新增了 @MappedSuperclass 註解,這會指示 JPA 將 Worker 的所有持續性欄位,都儲存在其子類別的資料儲存庫實體中。因利用 Employee 執行個體呼叫 persist() 而建立的資料儲存庫實體,將會有名為「department」及「salary」的兩個屬性。因利用 Intern 執行個體呼叫 persist() 而建立的資料儲存庫實體,將會有名為「department」及「inernshipEndDate」的兩個屬性。資料儲存庫中不會有任何屬於「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;
}

在這個範例中,我們已經將 @Inheritance 註解新增至 FormerEmployee 類別宣告,並將該註解的 strategy 屬性設定為 InheritanceType.TABLE_PER_CLASS。這會讓 JPA 將 FormerEmployee 的所有持續性欄位及其父類別儲存在與 FormerEmployee 執行個體相對應的資料儲存庫實體中。因利用 FormerEmployee 執行個體呼叫 persist() 而建立的資料儲存庫實體,將會有名為「department」、「salary」及「salary」的三個屬性。絕對不會出現與 FormerEmployee 相對應的「Employee」種類實體,但如果您使用執行階段類型為 Employee 的物件來呼叫 persist(),就會建立「Employee」種類的實體。

只要關係欄位的宣告類型與指派給這些欄位之物件的執行階段類型相符,關係與繼承的結合就能夠運作。詳情請參閱多型態關係一節。儘管其中提供的是 JDO 範例,不過該章節中說明的概念和限制均適用於 JPA。

不支援的 JPA 功能

App Engine 實作不支援以下的 JPA 介面功能:

  • 多對多從屬關係。
  • 「彙整」查詢。當您對父項種類執行查詢時,不可在篩選器中使用子項實體的欄位。提醒您,您可以使用金鑰直接在查詢中測試父項的關係欄位。
  • 匯總查詢 (「group by」、「having」、「sum」、「avg」、「max」、「min」)
  • 多型態查詢。您無法透過查詢類別來取得子類別的執行個體。每個類別都是由資料儲存庫中個別的實體種類來代表。
本頁內容對您是否有任何幫助?請提供意見:

傳送您對下列選項的寶貴意見...

這個網頁
Java 8 適用的 App Engine 標準環境