JDO 中的實體關聯性

您可以使用物件類型欄位,為不同的持續性物件建立關係模型。持續性物件之間的關係可分為「從屬」與「非從屬」:具有從屬關係的物件必須同時存在;具有非從屬關係的物件則可單獨存在。JDO 介面的 App Engine 實作可建立關係模型,其中種類包括:一對一從屬與非從屬關係、一對多從屬與非從屬關係;關係方向可分為單向與雙向。

App Engine 適用的 DataNucleus 1.0 版外掛程式不支援非從屬關係,如要自行管理這些關係,您可以將資料儲存庫金鑰直接儲存在欄位中。App Engine 會自動在實體群組中建立相關實體,藉以支援同時更新相關物件,但應用程式必須知道何時要使用資料儲存庫交易。

App Engine 適用的 DataNucleus 2.x 版外掛程式支援使用自然語法的非從屬關係。非從屬關係一節將說明如何在每個外掛程式版本中建立非從屬關係。如要升級至 App Engine 適用的 DataNucleus 2.x 版外掛程式,請參閱「遷移至 App Engine 適用的 DataNucleus 2.x 版外掛程式」。

一對一從屬關係

您可以使用類型為相關類別的欄位,在兩個持續性物件之間建立單向一對一的從屬關係。

下列範例定義了 ContactInfo 與 Employee 的資料類別,其中 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;

    // ...
}

Employee.java

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

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

    @Persistent
    private ContactInfo contactInfo;

    ContactInfo getContactInfo() {
        return contactInfo;
    }
    void setContactInfo(ContactInfo contactInfo) {
        this.contactInfo = contactInfo;
    }

    // ...
}

在資料儲存庫中,持續性物件以屬於兩個不同種類的兩個不同實體來表示,關係則以實體群組關係來表示:子項金鑰會使用父項金鑰做為實體群組父項。當應用程式使用父項物件欄位存取子項物件時,JDO 實作即會執行實體群組父項查詢來取得子項。

子項類別必須具備能包含父項金鑰資訊的金鑰欄位,父項金鑰資訊可以是金鑰或編碼成字串的金鑰值。如要瞭解金鑰欄位類型,請參閱建立資料:金鑰一節。

您可以同時使用兩個類別的欄位來建立雙向一對一關係,並在子項類別欄位中加上註解,藉以宣告這些欄位代表雙向關係。子項類別欄位必須有 @Persistent 註解與引數 mappedBy = "...",其中的值為父項類別的欄位名稱。如果其中一個物件欄位有填入值,則系統也會在另一物件的對應參照欄位中自動填入值。

ContactInfo.java

import Employee;

// ...
    @Persistent(mappedBy = "contactInfo")
    private Employee employee;

第一次存取子項物件時,系統會從資料儲存庫載入子項物件。如果您未存取父項物件中的子項物件,則系統永遠不會載入子項物件的實體。如要載入子項,您可以在關閉 PersistenceManager「之前」「聯繫」子項 (例如,在上述範例中呼叫 getContactInfo()),或將子項欄位明確新增至預設的擷取群組,讓系統一起擷取、載入子項與父項:

Employee.java

import ContactInfo;

// ...
    @Persistent(defaultFetchGroup = "true")
    private ContactInfo contactInfo;

一對多從屬關係

如要在某個類別的物件和另一個類別的多個物件之間建立一對多關係,您可以使用相關類別的集合:

Employee.java

import java.util.List;

// ...
    @Persistent
    private List<ContactInfo> contactInfoSets;

雙向一對多關係與一對一關係類似,兩者在父項類別中都有使用註解 @Persistent(mappedBy = "...") 的欄位,其中的值為子項類別的欄位名稱:

Employee.java

import java.util.List;

// ...
    @Persistent(mappedBy = "employee")
    private List<ContactInfo> contactInfoSets;

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

定義資料類別:集合一文中列出的集合類型支援一對多關係,但是陣列「不」支援一對多關係。

App Engine 不支援彙整查詢:您無法使用子項實體的屬性來查詢父項實體。(您可以查詢嵌入類別的屬性,因為嵌入類別會儲存父項實體的屬性。請參閱定義資料類別:嵌入類別一節。)

排序集合維持順序的方式

排序集合,如 List<...>,會在父項物件儲存時保留物件的順序。JDO 要求資料庫將每個物件的位置儲存為物件屬性,藉以保留物件順序。App Engine 會將此順序儲存為對應實體的屬性,使用的屬性名稱與父項欄位名稱相同 (同樣後接 _INTEGER_IDX)。位置屬性效率不彰。當您在集合中新增、移除或移動元素後,必須一併更新位在集合中經過修改的位置之後的所有實體。更新的速度可能十分緩慢,如果作業未透過交易執行,也容易出錯。

如果您不需要保留集合中的任意順序,但需要使用排序集合類型,則您可以使用註解,即 DataNucleus 提供的 JDO 擴充功能,根據元素屬性來指定排序:

import java.util.List;
import javax.jdo.annotations.Extension;
import javax.jdo.annotations.Order;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    @Order(extensions = @Extension(vendorName="datanucleus",key="list-ordering", value="state asc, city asc"))
    private List<ContactInfo> contactInfoSets = new ArrayList<ContactInfo>();

@Order 註解 (使用 list-ordering 擴充功能) 會將您需要的集合元素順序指定為 JDOQL 排序子句。排序會使用元素的屬性值。與查詢相同,集合的所有元素均必須含有排序子句所使用的屬性值。

存取集合時會執行查詢。如果欄位的排序子句使用多個排序順序,則查詢需要 Datastore 索引;詳情請參閱 Datastore 索引頁面。

如要提高效率,我們建議您在排序集合類型的一對多關係中,盡量使用明確的排序子句。

非從屬關係

除了從屬關係之外,JDO API 也提供非從屬關係的管理功能。這個功能的運作方式會隨您使用的外掛程式版本而有所不同,其中外掛程式為 App Engine 適用的 DataNucleus:

  • 版本 1 的 DataNucleus 外掛程式無法使用自然語法來實作非從屬關係,但仍可以使用 Key 值來代替模型物件中的例項 (或例項集合),藉以管理這些關係。您可以在兩個物件之間建立任意「外鍵」模型時儲存金鑰物件。資料儲存庫不保證這些金鑰參照能夠提供完整參考,但金鑰的使用可讓您輕鬆地在兩個物件之間建立 (然後擷取) 任何的關係模型。

    然而,在您採取此做法前,應先確保這些金鑰均屬於適當的類型。JDO 和編譯器不會為您檢查 Key 類型。
  • 版本 2.x 的 DataNucleus 外掛程式可使用自然語法來實作非從屬關係。

提示:在某些情況下,您可能必須為從屬關係建立非從屬模型。這是因為所有涉及從屬關係的物件都會自動放在相同的實體群組中,但一個實體群組僅支援每秒 1 至 10 次寫入。因此,假設父項物件每秒接收 0.75 次寫入,而子項物件也是每秒接收 0.75 次寫入,那麼或許我們可以為這個關係建立非從屬模型,讓父項物件和子項物件各自存放在獨立的實體群組中。

一對一非從屬關係

假設我們想在個人和食物之間建立關聯性模型,其中每個人只能選擇一種喜愛的食物,但某種食物不專屬於某人,因為可能有好幾個人喜歡同種食物。本節說明如何建立此種關聯性模型。

在 JDO 2.3 中

在本範例中,我們將 Key 類型的成員提供給 Person,其中 KeyFood 物件的唯一 ID。如果 Person 的例項和 Person.favoriteFood 所參照的 Food 例項位於相同的實體群組中,則您無法在單一交易中更新使用者和該使用者喜愛的食物,除非 JDO 設定設為啟用跨群組 (XG) 交易

Person.java

// ... imports ...

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

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

在 JDO 3.0 中

在此範例中,我們不再將代表喜歡食物的金鑰指派給 Person,而是建立一個 Food 類型的不公開成員:

Person.java

// ... imports ...

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

    @Persistent
    @Unowned
    private Food favoriteFood;

    // ...
}

Food.java

import Person;
// ... imports ...

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

    // ...
}

一對多非從屬關係

現在,假設我們想讓個人選擇多種喜愛的食物。同樣地,每種受人喜愛的食物不需專屬於某人,因為可能有好幾個人喜歡同種食物:

在 JDO 2.3 中

在此範例中,我們不再將代表個人喜愛食物的 Set<Food> 類型成員指派給 Person,而是將 Set<Key> 類型成員提供給 Person,其中的集合含有 Food 物件的唯一 ID。請注意,如果 Person.favoriteFoods 所含的 Person 例項和 Food 例項不在相同的實體群組中,而您想要在單一交易中更新這些例項,則您必須將 JDO 設定設為啟用跨群組 (XG) 交易

Person.java

// ... imports ...

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

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

在 JDO 3.0 中

在此範例中,我們將 Set<Food> 類型的成員提供給 Person,其中的集合代表個人喜歡的食物。

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

多對多關係

我們可以在關係的兩端來維護金鑰集合,藉以建立多對多關係模型。讓我們調整一下範例,讓 Food 追蹤將此食物視為最愛的個人:

Person.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> favoriteFoods;

Food.java

import java.util.Set;
import com.google.appengine.api.datastore.Key;

// ...
    @Persistent
    private Set<Key> foodFans;

在此範例中,Person 會維護一組 Key 值,這些值可識別做為喜愛食物的 Food 物件,而 Food 則會維護一組 Key 值,這些值則識別喜愛該食物的 Person 物件。

使用 Key 值建立多對多關聯性模型時,請注意應用程式必須自行維護此關係的兩端。

Album.java

// ...
public void addFavoriteFood(Food food) {
    favoriteFoods.add(food.getKey());
    food.getFoodFans().add(getKey());
}

public void removeFavoriteFood(Food food) {
    favoriteFoods.remove(food.getKey());
    food.getFoodFans().remove(getKey());
}

如果 Person.favoriteFoods 所含的 Person 例項和 Food 例項不在同一個實體群組中,而您想要在單一交易中更新這些例項,則您必須將 JDO 設定設為啟用跨群組 (XG) 交易

關係、實體群組和交易

當應用程式將含有從屬關係的物件儲存至資料儲存庫時,所有可透過關係找到且需要被儲存的其他物件 (新建立的物件或在前次載入後曾經修改的物件) 將會被自動儲存。這項特性對交易和實體群組而言關係重大。

請使用上述 EmployeeContactInfo 類別之間的單向關係來思考下列範例:

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    pm.makePersistent(e);

使用 pm.makePersistent() 方法儲存新的 Employee 物件時,系統會自動儲存新的相關 ContactInfo 物件。因為這兩個都是新物件,所以 App Engine 會在相同的實體群組中建立兩個新的實體,並以 Employee 實體做為 ContactInfo 實體的父項。同樣地,如果 Employee 物件已經儲存,而相關的 ContactInfo 物件是新建物件,則 App Engine 會使用現有的 Employee 實體做為父項來建立 ContactInfo 實體。

但請注意,在本例中,對 pm.makePersistent() 的呼叫並未使用交易。沒有明確的交易時,這兩個實體會透過個別的單一性動作來建立。在這種情況下,Employee 實體的建立可能會成功,但是 ContactInfo 實體的建立會失敗。為了確保能成功建立兩個實體,或是不建立兩個實體,您必須使用交易:

    Employee e = new Employee();
    ContactInfo ci = new ContactInfo();
    e.setContactInfo(ci);

    try {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        pm.makePersistent(e);
        tx.commit();
    } finally {
        if (tx.isActive()) {
            tx.rollback();
        }
    }

如果在建立關係「之前」就儲存了這兩個物件,則 App Engine 就無法將現有的 ContactInfo 實體「移動」至 Employee 實體的實體群組,因為實體群組只能在實體建立時指派。App Engine 可以透過參照來建立關係,但是相關的實體不會位於同一個群組中。在此情況下,如果將 JDO 設定設為啟用跨群組 (XG) 交易,就能在單一交易中更新或刪除這兩個實體。如果您沒有使用 XG 交易,則在嘗試以單一交易更新或刪除不同群組中實體時,系統將會擲出 JDOFatalUserException

儲存父項物件時,如果其子項物件經過修改,系統將一併儲存子項物件的變更。我們建議您讓父項物件維護所有相關子項物件的持續性,然後在儲存變更時使用交易。

相依子項和串聯刪除

從屬關係可以是「相依」的,意即如果沒有父項,子項就無法存在。如果物件之間具有相依關係,當父項物件遭到刪除時,所有的子項物件也會一併刪除。如果透過指派新值給父項的相依欄位來破解相依的從屬關係,這種做法也會同時刪除舊的子項。您可以在父項物件參照子項的欄位中,新增 dependent="true"Persistent 註解,將一對一從屬關係宣告為相依關係:

// ...
    @Persistent(dependent = "true")
    private ContactInfo contactInfo;

您可以在父項物件參照子項集合的欄位中新增 @Element(dependent = "true") 註解,將一對多從屬關係宣告為相依關係:

import javax.jdo.annotations.Element;
// ...
    @Persistent
    @Element(dependent = "true")
    private List contactInfos;

如同建立和更新物件,如果您需要在單一性動作中觸發串聯刪除中的每項刪除作業,您必須在交易中執行刪除。

注意:相依子項物件的刪除作業由 JDO 實作執行,而「不是」由資料儲存庫執行。如果您使用低階 API 或 Google Cloud 主控台刪除父項實體,系統不會一併刪除相關的子項物件。

多型態關係

即使 JDO 規格支援多型態關係,但 App Engine DO 實作還無法支援多型態關係。我們希望在未來推出的版本中去除這項限制。如果您需要透過常用基礎類別來參照多個物件類型,我們建議您採用與實作非從屬關係時使用的相同策略:儲存金鑰參照。例如,如果您的 Recipe 基礎類別含有 AppetizerEntreeDessert 規格,且您想要建立 Chef 喜歡的 Recipe 模型,則您可以依照下列方式建立模型:

Recipe.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 Recipe {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private int prepTime;
}

Appetizer.java

// ... imports ...

@PersistenceCapable
public class Appetizer extends Recipe {
// ... appetizer-specific fields
}

Entree.java

// ... imports ...

@PersistenceCapable
public class Entree extends Recipe {
// ... entree-specific fields
}

Dessert.java

// ... imports ...

@PersistenceCapable
public class Dessert extends Recipe {
// ... dessert-specific fields
}

Chef.java

// ... imports ...

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

    @Persistent(dependent = "true")
    private Recipe favoriteRecipe;
}

但如果您將 Entree 實例化並指派給 Chef.favoriteRecipe,則在您嘗試建立 Chef 物件的持續性時,將會收到 UnsupportedOperationException。這是因為物件的執行階段類型 Entree 不符合關係欄位的宣告類型 Recipe。解決方法是將 Chef.favoriteRecipe 的類型從 Recipe 變更為 Key

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

因為 Chef.favoriteRecipe 不再是關係欄位,所以可以參照任何類型的物件。但這個做法有缺點,如同所有的非從屬關係,您必須手動管理這項關係。