透過 JDO 定義資料類別

您可以使用 JDO 在資料儲存庫中儲存一般 Java 資料物件 (亦稱為「Plain Old Java Object」,簡寫為「POJO」)。每個透過 PersistenceManager 設為永久性的物件都是資料儲存庫中的實體,您需要使用註解讓 JDO 瞭解如何儲存和重新建立資料類別的執行個體。

注意:早期版本的 JDO 使用 .jdo XML 檔案,而非 Java 註解,XML 檔案仍適用於 JDO 2.3。本文件僅介紹如何併用 Java 註解與資料類別。

類別和欄位註解

JDO 所儲存的每個物件都會成為 App Engine 資料儲存庫中的實體,而實體的種類衍生自類別的簡單名稱 (內部類別使用不含套件名稱的 $ 路徑)。類別的每個永久性欄位代表實體的屬性,而屬性名稱即為欄位名稱 (大小寫相同)。

如要宣告 Java 類別能夠透過 JDO 在資料儲存庫中儲存和擷取,請為該類別提供 @PersistenceCapable 註解。例如:

import javax.jdo.annotations.PersistenceCapable;

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

您必須先將資料類別的欄位宣告為永久性欄位,才可將這些欄位儲存在資料儲存庫中。如要將欄位宣告為永久性,請提供 @Persistent 註解:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

若要將欄位宣告為非永久性 (亦即不會儲存在資料儲存庫中,且擷取時物件不會還原),您需要為其提供 @NotPersistent 註解。

提示:如果 @Persistent@NotPersistent 註解皆未指定,且所有其他類型的欄位皆非預設為永久性,那麼 JDO 預設會將特定類型欄位指定為永久性欄位。請參閱 DataNucleus 說明文件瞭解此行為的完整說明。根據 JDO 的規格,並非所有 App Engine 資料儲存庫的核心值類型都預設為永久性的,故建議您明確地將欄位註解為 @Persistent@NotPersistent 以清楚辨識。

欄位類型可以是下列任一種,詳細說明如下:

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

資料類別必須含有一個 (且是唯一一個) 專屬欄位,專門儲存對應資料儲存庫實體主要鍵。您可以從 4 種不同種類的鍵欄位中選擇,每一種鍵欄位都使用不同的值類型和註解。(詳情請參閱建立資料:鍵)。最具彈性的鍵欄位類型是 Key 物件,當您第一次將這類物件儲存至資料儲存庫時,JDO 會自動為這類物件填入一個不重複的值,該物件類別的其他所有執行個體均不會使用該值。Key 類型的主鍵需要 @PrimaryKey 註解和 @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY) 註解:

提示:將所有永久性欄位設定為 privateprotected (或受套件保護),並且僅透過存取者方法提供公開存取權。直接從其他類別存取永久性欄位可能會略過 JDO 類別強化。或者,您可以建立其他類別 @PersistenceAware。詳情請參閱 DataNucleus 說明文件

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

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PrimaryKey;

// ...
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

資料類別的範例如下:

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

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String firstName;

    @Persistent
    private String lastName;

    @Persistent
    private Date hireDate;

    public Employee(String firstName, String lastName, Date hireDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.hireDate = hireDate;
    }

    // Accessors for the fields. JDO 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;
    }
}

核心值類型

如要表示包含單一核心類型值的屬性,請宣告 Java 類型的欄位,並使用 @Persistent 註解:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Serializable 物件

欄位值可包含 Serializable 類別的執行個體,將執行個體的序列化值儲存在 Blob 類型的單一屬性值中。該欄位使用註解 @Persistent(serialized=true),告知 JDO 序列化該值。由於未建立 Blob 值的索引,因此無法將 Blob 值用於查詢篩選器或排序順序。

以下這個簡單的 Serializable 類別範例代表一個檔案,其中包含檔案內容、檔案名稱和 MIME 類型。不過,這個例子不是 JDO 資料類別,因此並沒有永久性註解。

import java.io.Serializable;

public class DownloadableFile implements Serializable {
    private byte[] content;
    private String filename;
    private String mimeType;

    // ... accessors ...
}

如要將 Serializable 類別的例項做為 Blob 值儲存在屬性中,請宣告類型為 Serializable 類別的欄位,並使用 @Persistent(serialized = "true") 註解:

import javax.jdo.annotations.Persistent;
import DownloadableFile;

// ...
    @Persistent(serialized = "true")
    private DownloadableFile file;

子項物件和關聯性

@PersistenceCapable 類別執行個體的欄位值,會在兩個物件間建立一對一的從屬關聯性。而這類參照集合中的欄位則會建立一對多的從屬關聯性。

重要事項:從屬關聯性隱含交易、實體群組和階層式刪除。詳情請參閱「交易」和「關聯性」。

下方的的簡單範例說明 Employee 物件和 ContactInfo 物件之間的一對一從屬關聯性:

ContactInfo.java

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

@PersistenceCapable
public class ContactInfo {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String streetAddress;

    @Persistent
    private String city;

    @Persistent
    private String stateOrProvince;

    @Persistent
    private String zipCode;

    // ... accessors ...
}

Employee.java

import ContactInfo;
// ... imports ...

@PersistenceCapable
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private ContactInfo myContactInfo;

    // ... accessors ...
}

在此範例中,如果應用程式建立 Employee 例項,使用新的 ContactInfo 例項填入其 myContactInfo 欄位,並使用 pm.makePersistent(...) 保存 Employee 例項,則資料儲存庫會建立兩個實體。一個屬於 "ContactInfo" 種類,表示 ContactInfo 例項。另一個屬於 "Employee" 種類。ContactInfo 實體的鍵具有 Employee 實體的鍵,做為其實體群組父項。

內嵌類別

內嵌類別可讓您使用類別建立欄位值的模型,而不需建立新的資料儲存庫實體和設定關聯性。物件值的欄位會直接儲存在內含物件的資料儲存庫實體中。

任何 @PersistenceCapable 資料類別都可以用作另一個資料類別的內嵌物件。類別的 @Persistent 欄位被內嵌於物件中。若您讓類別內嵌 @EmbeddedOnly 註解,則該類別只能用作內嵌類別。內嵌類別不需要主要鍵欄位,因為內嵌類別並不會儲存為個別實體。

以下是內嵌類別的範例,在這個範例中,針對使用此內嵌類別的資料類別,內嵌類別已設為其內部類別;這個做法相當實用,但並非設定內嵌類別的必要做法。

import javax.jdo.annotations.Embedded;
import javax.jdo.annotations.EmbeddedOnly;
// ... imports ...

@PersistenceCapable
public class EmployeeContacts {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    Key key;
    @PersistenceCapable
    @EmbeddedOnly
    public static class ContactInfo {
        @Persistent
        private String streetAddress;

        @Persistent
        private String city;

        @Persistent
        private String stateOrProvince;

        @Persistent
        private String zipCode;

        // ... accessors ...
    }

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;
}

內嵌類別的欄位會儲存為實體的屬性,並採用每個欄位的名稱和對應屬性的名稱。如果您的物件類型為內嵌類別,且擁有一個以上的欄位,您必須重新命名欄位名稱,以免彼此衝突。您可以使用 @Embedded 註解的參數指定新的欄位名稱。例如:

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;

    @Persistent
    @Embedded(members = {
        @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")),
        @Persistent(name="city", columns=@Column(name="workCity")),
        @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")),
        @Persistent(name="zipCode", columns=@Column(name="workZipCode")),
    })
    private ContactInfo workContactInfo;

同樣地,物件的欄位也不能使用會與內嵌類別的欄位衝突的名稱,除非內嵌欄位已重新命名。

由於內嵌類別的永久性屬性與其他欄位儲存在相同的實體中,您可在 JDOQL 查詢篩選器和排序順序中使用內嵌類別的永久性欄位。您可以使用外部欄位名稱、點 (.) 和內嵌欄位名稱來參照內嵌欄位,無論是否使用 @Column 註解變更了內嵌欄位的屬性名稱,此方法都有效。

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

集合

資料儲存庫屬性可擁有一個以上的值。在 JDO 中是由 Collection 類型的單一欄位表示,其中集合是核心值類型之一或是 Serializable 類別。支援的 Collection 類型如下:

  • java.util.ArrayList<...>
  • java.util.HashSet<...>
  • java.util.LinkedHashSet<...>
  • java.util.LinkedList<...>
  • java.util.List<...>
  • java.util.Map<...>
  • java.util.Set<...>
  • java.util.SortedSet<...>
  • java.util.Stack<...>
  • java.util.TreeSet<...>
  • java.util.Vector<...>

如果欄位宣告為 List,則資料儲存庫會傳回帶有 ArrayList 值的物件。如果欄位宣告為 Set,則資料儲存庫會傳回 HashSet。如果欄位宣告為 SortedSet,則資料儲存庫會傳回 TreeSet。

例如,List<String> 類型的欄位儲存為屬性的零或多個字串值,List 中每個值各有一個。

import java.util.List;
// ... imports ...

// ...
    @Persistent
    List<String> favoriteFoods;

子物件的集合 (@PersistenceCapable 類別) 建立一對多關係的多個實體。請參閱「關係」。

如果資料儲存庫屬性含有多個值,則這類屬性會有特殊的查詢篩選器和排序順序行為。詳情請參閱資料儲存庫查詢頁面。

物件欄位和實體屬性

App Engine 資料儲存庫會區分未指定屬性的實體,以及屬性為 null 值的實體。JDO 不支援這種區別:物件的每個欄位都有一個值,可能為 null。如果將可為空值類型的欄位 (intboolean 等內建類型以外的欄位) 設為 null,則儲存物件時,生成的實體將使用空值為屬性。

若將資料儲存區實體載入至物件,該物件的某個欄位沒有屬性,同時該欄位的類型是可為空值的單一值類型,則該欄位將設為 null。當物件存回資料儲存庫時,null 屬性在資料儲存區中設定為空值。如果欄位是不可設為空值的值類型,則載入沒有對應屬性的實體時會擲回例外狀況。如果是從重新建立執行個體時使用的同一個 JDO 類別建立實體,將不會發生上述情況,但如果 JDO 類別經過變更,或是改用低階 API 取代 JDO 建立實體,即會發生上述情況。

如果欄位的類型是核心資料類型的集合或 Serializable 類別,而且實體上沒有屬性值,則屬性會設為單一空值,代表資料儲存庫中的空集合。如果欄位類型是陣列類型,則會將 0 元素的陣列指派給此欄位。如果載入的物件沒有屬性值,則會將適當類型的空集合指派給欄位。從內部而言,資料儲存庫可區別空集合和含有空值的集合之間的差異。

如果實體的某個屬性在物件中沒有對應的欄位,則無法從物件存取該屬性。當物件存回資料儲存庫時,將會刪除多餘的屬性。

如果實體的某個屬性值類型與物件中對應欄位的類型不同,JDO 會嘗試將該值轉化為欄位類型;如果無法將該值轉化為欄位類型,JDO 會擲回 ClassCastException。如果值是數字 (長整數和雙精度浮點數),JDO 會嘗試轉換而非轉化。如果數字屬性值大於欄位類型,則轉換會發生溢位,但不會擲回例外狀況。

您可以新增下列行,以宣告未編入索引的屬性:

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

此行應在類別定義中的屬性上方。請參閱主要文件的未編入索引的屬性部分,以瞭解屬性為未編入索引時所代表的意義等資訊。

繼承

建立資料類別時運用繼承特性是相當合理的做法,而且 JDO 支援此做法。在我們討論 JDO 繼承在 App Engine 中的運作方式之前,建議您先閱讀 DataNucleus 說明文件中有關這個主題的內容,然後再回來討論。已經看完了?好的。JDO 繼承在 App Engine 中的運作原理和 DataNucleus 文件說明的原理一樣,但有一些額外的限制。我們將說明這些限制並提供一些具體的範例。

「new-table」繼承策略可讓您跨多個「資料表」分割單一資料物件的資料,但由於 App Engine 資料儲存庫不支援彙整,因此使用此繼承策略對資料物件執行作業時,需要針對每個繼承層級執行遠端程序呼叫。這項作業的效率可能非常低,因此不屬於同一資料類別繼承階層的根目錄資料類別並不支援「new-table」繼承策略。

其次,「superclass-table」繼承策略允許您將資料物件的資料儲存在其父類別的「資料表」中。雖然這項策略並無任何繼承效率問題,但我們目前暫不支援這項策略,我們會在未來發行的版本中討論這個項目。

現在有一個好消息,「subclass-table」和「complete-table」策略的運作方式與在 DataNucleus 文件中的說明相同,您也可以將「new-table」用於其繼承階層根目錄下的任何資料物件。以下面這段程式碼為例:

Worker.java

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.Inheritance;
import javax.jdo.annotations.InheritanceStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable
@Inheritance(strategy = InheritanceStrategy.SUBCLASS_TABLE)
public abstract class Worker {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String department;
}

Employee.java

// ... imports ...

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

Intern.java

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

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

在這個範例中,我們已將 @Inheritance 註解新增至 Worker 類別宣告,並將該註解的 strategy> 屬性設為 InheritanceStrategy.SUBCLASS_TABLE。這可讓 JDO 將 Worker 的所有持續性欄位都儲存在其子類別的資料儲存庫實體中。因使用 Employee 例項呼叫 makePersistent() 而建立的資料儲存庫實體,具有名為「department」和「salary」的兩個屬性。因使用 Intern 例項呼叫 makePersistent() 而建立的資料儲存庫實體,將具有名為「department」與「internshipEndDate」的兩個屬性。資料儲存區不包含任何「Worker」種類的實體。

現在,我們把這個範例變得更有趣一點。假設除了擁有 EmployeeIntern 以外,我們還想要擁有一個特殊的 Employee,用來描述已經離職的員工:

FormerEmployee.java

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

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

在這個範例中,我們已將 @Inheritance 註解新增至 FormerEmployee 類別宣告,並將其 custom-strategy> 屬性設為「complete-table」。這可讓 JDO 將 FormerEmployee 的所有持續性欄位及其父類別儲存在與 FormerEmployee 執行個體對應的資料儲存庫實體中。因使用 FormerEmployee 執行個體呼叫 makePersistent() 而建立的資料儲存庫實體,將擁有名為「department」、「salary」和「lastDay」的三個屬性。沒有任何「Employee」種類的實體對應於 FormerEmployee。然而,若使用執行階段類型為 Employee 的物件調用 makePersistent(),則可以建立「Employee」種類的實體。

只要關聯性欄位的宣告類型與指派給這些欄位的物件的執行階段類型相符,即可運用關聯性和繼承的結合。詳情請參閱「多型態關係」一節。