搭配使用 JPA 與 App Engine

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

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

1.x 版的外掛程式包含在 App Engine Java SDK 中,其實作了 JPA 1.0 版,且這項實作以 DataNucleus Access Platform 1.1 版為基礎。

注意:此頁面提供的操作說明適用於 JPA 1 版,這個版本的 JPA 使用 App Engine 適用的 1.x 版 DataNucleus 外掛程式。您也可以使用 DataNucleus 2.x 版外掛程式,以便使用 JPA 2.0。2.x 外掛程式提供一些新的 API 與功能;但是,升級與 1.x 版並非完全回溯相容。如果您使用 JPA 2.0 重新建構應用程式,則必須更新及重新測試程式碼。如要進一步瞭解新版本,請參閱「搭配使用 JPA 2.0 與 App Engine」。

設定 JPA

如要使用 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/user/orm/ 目錄中找到它們。

請將 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.store.appengine.jpa.DatastorePersistenceProvider</provider>
        <properties>
            <property name="datanucleus.NontransactionalRead" value="true"/>
            <property name="datanucleus.NontransactionalWrite" value="true"/>
            <property name="datanucleus.ConnectionURL" value="appengine"/>
        </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 執行個體。舉例來說,下列 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.store.appengine.jpa.DatastorePersistenceProvider</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.store.appengine.jpa.DatastorePersistenceProvider</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" />
        </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 1.0 功能

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

  • 多對多從屬關聯性和非從屬關聯性。您可以使用明確金鑰值來部署非從屬關聯性,不過 API 不會強制執行類型檢查。
  • 「彙整」查詢。您對父項種類執行查詢時,不得在篩選器中使用子項實體的欄位。不過,您可以使用金鑰直接在查詢中測試父項的關係欄位。
  • 匯總查詢 (「group by」、「having」、「sum」、「avg」、「max」和「min」)
  • 多型態查詢。您無法透過類別查詢取得子類別的執行個體。每個類別都是由資料儲存庫中的個別實體種類代表。