Избранное | Русский | Войти

Отношения

Отношения между постоянно хранимыми объектами можно моделировать с помощью полей объектных типов. Между постоянно хранимыми объектами возможны зависимые отношения, при которых объекты не могут существовать друг без друга, и независимые, при которых объекты могут существовать вне зависимости от отношений друг с другом. Реализация JDO-интерфейса в App Engine позволяет моделировать зависимые отношения "один к одному" и зависимые отношения "один ко многим", оба типа отношений могут быть как однонаправленными, так и двунаправленными. Синтаксис пока не поддерживает независимые отношения естественным образом, но вы можете построить их самостоятельно, сохраняя ключи хранилища данных непосредственно в полях. В группах объектов App Engine автоматически создает объекты, состоящие в отношениях, что позволяет реализовать совместное обновление таких объектов, однако само приложение должно знать, когда следует использовать транзакции хранилища данных.

Зависимые отношения "один к одному"

Однонаправленные зависимые отношения "один к одному" между двумя постоянно хранимыми объектами создаются с помощью поля, типом которого является класс зависимого класса.

В следующем примере определяются классы данных ContactInfo и Employee с отношением "один к одному".

ContactInfo.java

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

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

    @Persistent
    private String streetAddress;

    // ...
}

Employee.java

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

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Employee {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private ContactInfo contactInfo;

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

    // ...
}

Постоянно хранимые объекты представляются в виде двух отдельных объектов в хранилище данных, с двумя различными типами. Отношение представляется с помощью отношения группы объектов: ключ потомка использует ключ родителя в качестве своего родителя группы объектов. Когда приложение обращается к дочернему объекту с помощью поля родительского объекта, JDO-реализация выполняет запрос к родителю группы объектов, чтобы получить потомка.

У дочернего класса должно быть поле ключа, тип которого позволяет содержать сведения о ключе родителя: либо Key, либо значение Key, закодированное в виде строки. Сведения о типах поля ключа приведены в статье Создание данных: ключи.

Двунаправленные отношения "один к одному" создаются с помощью полей обоих классов, а также аннотации в поле дочернего класса, в которой объявляется, что эти поля представляют двунаправленное отношение. В поле дочернего класса должна быть аннотация @Persistent с аргументом mappedBy = "...", где значением является название поля родительского класса. При заполнении поля в одном из объектов соответствующее поле-ссылка в другом объекте заполняется автоматически.

ContactInfo.java

import Employee;

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

Дочерние объекты загружаются из хранилища данных при первом обращении к ним. Если не обращаться к потомку родительского объекта, он никогда не будет загружен. (Интерфейс хранилища данных не поддерживает "упредительную" загрузку дочерних объектов. Хранилище данных не поддерживает объединенные запросы, поэтому реализация упредительной загрузки не будет способствовать сокращению количества вызовов хранилища данных приложением.)

Зависимые отношения "один ко многим"

Для создания отношения "один ко многим" между объектом одного класса и множеством объектов другого используется коллекция зависимого класса:

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. Позиционные свойства неэффективны. При добавлении, удалении или перемещении элемента коллекции необходимо обновлять все следующие за ним объекты. Это может занимать много времени и приводить к ошибкам в случае, если не используется транзакция.

Если нет необходимости поддерживать произвольный порядок в коллекции, но нужно использовать упорядоченный тип коллекции, можно указать упорядочивание по свойствам элементов с помощью аннотации, расширения JDO, предоставляемого DataNucleus:

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 List<ContactInfo>();

Аннотация @Order (использующая расширение list-ordering) указывает нужный порядок элементов коллекции в виде оператора упорядочивания JDOQL. При упорядочивании используются значения свойств элементов. Как и в случае с запросами, свойства всех элементов коллекции, используемых в операторе упорядочивания, должны иметь значения.

При обращении к коллекции выполняется запрос. Если оператор упорядочивания поля использует несколько порядков сортировки, для запроса требуется индекс хранилища данных. Подробнее об индексах рассказано в статье Запросы и индексы.

С целью обеспечения эффективности для отношений "один ко многим" коллекций упорядоченного типа по возможности следует использовать явный оператор упорядочивания.

Независимые отношения

Помимо зависимых отношений, API JDO также предоставляет возможность управления независимыми отношениями. В JDO-реализации App Engine эта возможность не представлена, но поводов для беспокойства нет: такими отношениями можно управлять с помощью значений Key вместо экземпляров (или коллекций экземпляров) объектов модели. Сохранение объектов Key можно представить как моделирование произвольного "внешнего ключа" между двумя объектами. Хранилище данных не гарантирует ссылочной целостности таких ссылок Key, но использование объектов Key позволяет очень просто моделировать (а затем получать) любые отношения между двумя объектами. Однако при выборе этого способа нужно помнить о нескольких вещах. Во-первых, приложение должно проверять, имеют ли объекты Key соответствующий тип – JDO и компилятор не выполняют каких-либо проверок типа. Во-вторых, все объекты должны принадлежать к одной и той же группе объектов для обеспечения возможности выполнения атомарного обновления объектов, участвующих в отношении.

Совет. В некоторых случаях может быть необходимо смоделировать зависимое отношение так, как если бы оно было независимым. Это связано с тем, что все объекты, участвующие в зависимом отношении автоматически размещаются в одной группе объектов, а группа объектов поддерживает всего от одной до десяти операций записи в секунду. Поэтому если, например, необходимо выполнять 0,75 операций записи в секунду и для родительского, и для дочернего объектов, может иметь смысл смоделировать их отношение как независимое, чтобы предок и потомок могли находиться в собственных независимых группах объектов.

Независимые отношения "один к одному"

Предположим, нам нужно смоделировать отношения между объектами, описывающими человека и блюдо. При этом у человека может быть только одно любимое блюдо, но блюдо не может принадлежать только одному человеку, так как может быть любимым для любого количества людей:

Person.java

// ... imports ...

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private Key favoriteFood;

    // ...
}

Food.java

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

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Food {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    // ...
}

В этом примере вместо добавления к объекту Person участника типа Food, представляющего любимое блюдо данного человека, лучше добавить к объекту Person участника типа Key, где Key является уникальным идентификатором объекта Food. Обратите внимание, что если экземпляр Person и экземпляр Food, на который ссылается Person.favoriteFood, не находятся в одной группе объектов, обновить данные о человеке и его любимом блюде в одной транзакции невозможно.

Независимые отношения "один ко многим"

Теперь предположим, что у человека может быть несколько любимых блюд. Блюдо опять не может принадлежать только одному человеку, так как может быть любимым для любого количества людей:

Person.java

// ... imports ...

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Person {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Long id;

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

В этом примере вместо добавления к объекту Person участника типа Set<Food>, представляющего любимые блюда данного человека, лучше добавить к объекту Person участника типа Set<Key>, который представляет собой набор уникальных идентификаторов объектов Food. Обратите внимание, что если экземпляр Person и экземпляр Food, содержащийся в Person.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 и экземпляр Food, содержащийся в Person.favoriteFoods, не находятся в одной группе объектов, обновить данные о человеке и его любимых блюдах в одной транзакции невозможно. Если расположить объекты в одной группе невозможно, приложение должно учитывать возможность того, что блюда будут обновляться без соответствующего отражения обновлений в наборе любимых блюд людей, и наоборот, обновление любителей определенного блюда не будет отражено в перечне любителей в объекте этого блюда.

Отношения, группы объектов и транзакции

При сохранении объекта с зависимым отношением в хранилище данных все остальные объекты, которые связаны с ним посредством отношений и требуют сохранения (новые объекты или объекты, измененные с момента последней загрузки) сохраняются автоматически. Это имеет важные последствия для транзакций и групп объектов.

Рассмотрим следующий пример, в котором используется однонаправленное отношение между описанными выше классами Employee и ContactInfo:

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

    pm.makePersistent(e);

При сохранении нового объекта Employee с помощью метода pm.makePersistent() зависимый новый объект ContactInfo сохраняется автоматически. Поскольку оба объекта являются новыми, App Engine создает два новых объекта в одной группе, используя объект Employee как родителя объекта ContactInfo. Аналогичным образом, если объект Employee уже сохранялся, а связанный с ним объект ContactInfo является новым, App Engine создает объект ContactInfo, используя существующий объект Employee в качестве его родителя.

Однако следует обратить внимание, что вызов метода 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 сможет установить отношение с помощью ссылки, но связанные этим отношением объекты не будут находиться в одной группе. В этом случае два объекта невозможно будет обновить или удалить в одной транзакции. Попытка обновления или удаления объектов из разных групп в одной транзакции вызовет исключение JDOFatalUserException.

При сохранении родительского объекта, чьи потомки были изменены, изменения будут сохранены и для дочерних объектов. Полезно позволять родительским объектам таким образом поддерживать постоянство хранения всех связанных с ними дочерних объектов и использовать транзакции при сохранении изменений.

Зависимые потомки и каскадные удаления

JDO-реализация в App Engine обеспечивает выполнение "зависимости" всех зависимых отношений. При удалении родительского объекта все его потомки также удаляются. При разрушении зависимого отношения путем присвоения зависимому полю родительского объекта нового значения прежний дочерний объект удаляется.

Как и при создании и обновлении объектов, для выполнения каждого удаления из каскада в одном атомарном действии необходимо выполнять удаление в транзакции.

Примечание. Удалением зависимых дочерних объектов занимается JDO-реализация, а не хранилище данных. При удалении родительского объекта с помощью низкоуровневого API или Консоли администрирования связанные с ним дочерние объекты не удаляются.