Relações de entidades no JDO

Pode modelar relações entre objetos persistentes através de campos dos tipos de objetos. Uma relação entre objetos persistentes pode ser descrita como owned, em que um dos objetos não pode existir sem o outro, ou unowned, em que ambos os objetos podem existir independentemente da respetiva relação entre si. A implementação do App Engine da interface JDO pode modelar relações um-para-um e relações um-para-muitos, tanto unidirecionais como bidirecionais, com e sem proprietário.

As relações não pertencentes não são suportadas na versão 1.0 do plug-in DataNucleus para o App Engine, mas pode gerir estas relações diretamente armazenando chaves do Datastore em campos. O App Engine cria automaticamente entidades relacionadas em grupos de entidades para suportar a atualização de objetos relacionados em conjunto, mas é da responsabilidade da app saber quando usar transações da base de dados.

A versão 2.x do plugin DataNucleus para o App Engine suporta relações não pertencentes com uma sintaxe natural. A secção Relações não pertencentes indica como criar relações não pertencentes em cada versão do plug-in. Para atualizar para a versão 2.x do plug-in DataNucleus para o App Engine, consulte o artigo Migrar para a versão 2.x do plug-in DataNucleus para o App Engine.

Relações um-para-um pertencentes

Cria uma relação de propriedade unidirecional um-para-um entre dois objetos persistentes através de um campo cujo tipo é a classe da classe relacionada.

O exemplo seguinte define uma classe de dados ContactInfo e uma classe de dados Employee, com uma relação um para um de Employee para 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;
    }

    // ...
}

Os objetos persistentes são representados como duas entidades distintas no repositório de dados, com dois tipos diferentes. A relação é representada através de uma relação de grupo de entidades: a chave da criança usa a chave do principal como o principal do grupo de entidades. Quando a app acede ao objeto filho através do campo do objeto principal, a implementação do JDO executa uma consulta principal do grupo de entidades para obter o filho.

A classe secundária tem de ter um campo de chave cujo tipo possa conter as informações da chave principal: uma chave ou um valor de chave codificado como uma string. Consulte o artigo Criar dados: chaves para ver informações sobre os tipos de campos de chaves.

Cria uma relação bidirecional individual usando campos em ambas as classes, com uma anotação no campo da classe secundária para declarar que os campos representam uma relação bidirecional. O campo da classe secundária tem de ter uma anotação @Persistent com o argumento mappedBy = "...", em que o valor é o nome do campo na classe principal. Se o campo num objeto for preenchido, o campo de referência correspondente no outro objeto é preenchido automaticamente.

ContactInfo.java

import Employee;

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

Os objetos secundários são carregados a partir do arquivo de dados quando são acedidos pela primeira vez. Se não aceder ao objeto secundário num objeto principal, a entidade do objeto secundário nunca é carregada. Se quiser carregar o filho, pode "tocá-lo" antes de fechar o PersistenceManager (por exemplo, chamando getContactInfo() no exemplo acima) ou adicionar explicitamente o campo filho ao grupo de obtenção predefinido para que seja obtido e carregado com o pai:

Employee.java

import ContactInfo;

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

Relações um-para-muitos de propriedade

Para criar uma relação um-para-muitos entre objetos de uma classe e vários objetos de outra, use uma coleção da classe relacionada:

Employee.java

import java.util.List;

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

Uma relação bidirecional de um para muitos é semelhante a uma relação de um para um, com um campo na classe principal a usar a anotação @Persistent(mappedBy = "..."), em que o valor é o nome do campo na classe secundária:

Employee.java

import java.util.List;

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

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

Os tipos de recolha indicados em Definir classes de dados: recolhas são suportados para relações um-para-muitos. No entanto, as matrizes não são suportadas para relações um-para-muitos.

O App Engine não suporta consultas de junção: não pode consultar uma entidade principal usando um atributo de uma entidade secundária. (Pode consultar uma propriedade de uma classe incorporada porque as classes incorporadas armazenam propriedades na entidade principal. Consulte o artigo Definir classes de dados: classes incorporadas.)

Como as coleções ordenadas mantêm a respetiva ordem

As coleções ordenadas, como List<...>, preservam a ordem dos objetos quando o objeto principal é guardado. O JDO requer que as bases de dados preservem esta ordem armazenando a posição de cada objeto como uma propriedade do objeto. O App Engine armazena isto como uma propriedade da entidade correspondente, usando um nome de propriedade igual ao nome do campo principal seguido de _INTEGER_IDX. As propriedades de posição são ineficientes. Se um elemento for adicionado, removido ou movido na coleção, todas as entidades subsequentes ao local modificado na coleção têm de ser atualizadas. Este processo pode ser lento e propenso a erros se não for realizado numa transação.

Se não precisar de preservar uma ordem arbitrária numa coleção, mas precisar de usar um tipo de coleção ordenada, pode especificar uma ordenação com base nas propriedades dos elementos através de uma anotação, uma extensão ao JDO fornecida pelo 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 ArrayList<ContactInfo>();

A anotação @Order (através da extensão list-ordering) especifica a ordem pretendida dos elementos da coleção como uma cláusula de ordenação JDOQL. A ordenação usa valores de propriedades dos elementos. Tal como acontece com as consultas, todos os elementos de uma coleção têm de ter valores para as propriedades usadas na cláusula de ordenação.

O acesso a uma coleção executa uma consulta. Se a cláusula de ordenação de um campo usar mais do que uma ordem de ordenação, a consulta requer um índice do Datastore. Consulte a página Índices do Datastore para ver mais informações.

Para maior eficiência, use sempre uma cláusula de ordenação explícita para relações de um para muitos de tipos de coleções ordenadas, se possível.

Relações não pertencentes

Além das relações de propriedade, a API JDO também oferece uma funcionalidade para gerir relações sem propriedade. Esta funcionalidade funciona de forma diferente consoante a versão do plug-in DataNucleus para o App Engine que estiver a usar:

  • A versão 1 do plug-in DataNucleus não implementa relações não pertencentes através de uma sintaxe natural, mas pode continuar a gerir estas relações através de valores Key em vez de instâncias (ou coleções de instâncias) dos objetos do modelo. Pode considerar o armazenamento de objetos Key como a modelagem de uma "chave externa" arbitrária entre dois objetos. O arquivo de dados não garante a integridade referencial com estas referências de chaves, mas a utilização de chaves facilita muito a modelagem (e, em seguida, a obtenção) de qualquer relação entre dois objetos.

    No entanto, se optar por esta solução, tem de garantir que as chaves são do tipo adequado. O JDO e o compilador não verificam os tipos Key por si.
  • Versão 2.x do plug-in DataNucleus implementa relações não pertencentes através de uma sintaxe natural.

Sugestão: em alguns casos, pode ser necessário modelar uma relação de propriedade como se não fosse de propriedade. Isto deve-se ao facto de todos os objetos envolvidos numa relação de propriedade serem colocados automaticamente no mesmo grupo de entidades e um grupo de entidades só poder suportar entre 1 e 10 escritas por segundo. Por exemplo, se um objeto principal estiver a receber 0,75 gravações por segundo e um objeto secundário estiver a receber 0, 75 gravações por segundo, pode fazer sentido modelar esta relação como não pertencente para que o principal e o secundário residam nos seus próprios grupos de entidades independentes.

Relações individuais não pertencentes

Suponhamos que quer modelar pessoas e comida, em que uma pessoa só pode ter uma comida favorita, mas uma comida favorita não pertence à pessoa porque pode ser a comida favorita de qualquer número de pessoas. Esta secção mostra como o fazer.

No JDO 2.3

Neste exemplo, atribuímos Person a um membro do tipo Key, em que Key é o identificador exclusivo de um objeto Food. Se uma instância de Person e a instância de Food referida por Person.favoriteFood não estiverem no mesmo grupo de entidades, não pode atualizar a pessoa e a comida favorita dessa pessoa numa única transação, a menos que a configuração do JDO esteja definida para ativar transações entre grupos (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;

    // ...
}

No JDO 3.0

Neste exemplo, em vez de dar Person uma chave que representa a comida favorita, criamos um membro privado do tipo 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;

    // ...
}

Relações um-para-muitos não pertencentes

Suponhamos que queremos permitir que uma pessoa tenha vários alimentos favoritos. Mais uma vez, a comida favorita não pertence à pessoa, porque pode ser a comida favorita de qualquer número de pessoas.

No JDO 2.3

Neste exemplo, em vez de dar a Person um membro do tipo Set<Food> para representar os alimentos favoritos da pessoa, damos a Person um membro do tipo Set<Key>, em que o conjunto contém os identificadores únicos de objetos Food. Tenha em atenção que, se uma instância de Person e uma instância de Food contidas em Person.favoriteFoods não estiverem no mesmo grupo de entidades, tem de definir a configuração do JDO para ativar as transações entre grupos (XG) se quiser atualizá-las na mesma transação.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Key> favoriteFoods;

    // ...
}

No JDO 3.0

Neste exemplo, atribuímos a Person um membro do tipo Set<Food> em que o conjunto representa as comidas favoritas da pessoa.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relações muitos-para-muitos

Podemos modelar uma relação de muitos-para-muitos mantendo coleções de chaves em ambos os lados da relação. Vamos ajustar o nosso exemplo para permitir que Food acompanhe as pessoas que o consideram um favorito:

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;

Neste exemplo, o Person mantém um conjunto de valores que identificam exclusivamente os objetos Food que são favoritos, e o Food mantém um conjunto de valores que identificam exclusivamente os objetos Person que o consideram um favorito.KeyKey

Quando modela uma relação muitos-para-muitos usando valores Key, tenha em atenção que é da responsabilidade da app manter ambos os lados da relação:

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());
}

Se uma instância de Person e uma instância de Food contidas em Person.favoriteFoods não estiverem no mesmo grupo de entidades e quiser atualizá-las numa única transação, tem de definir a configuração JDO para ativar transações entre grupos (XG).

Relações, grupos de entidades e transações

Quando a sua aplicação guarda um objeto com relações de propriedade no datastore, todos os outros objetos que podem ser alcançados através de relações e que precisam de ser guardados (são novos ou foram modificados desde a última vez que foram carregados) são guardados automaticamente. Isto tem implicações importantes para as transações e os grupos de entidades.

Considere o seguinte exemplo que usa uma relação unidirecional entre as classes Employee e ContactInfo acima:

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

    pm.makePersistent(e);

Quando o novo objeto Employee é guardado através do método pm.makePersistent(), o novo objeto ContactInfo relacionado é guardado automaticamente. Uma vez que ambos os objetos são novos, o App Engine cria duas novas entidades no mesmo grupo de entidades, usando a entidade Employee como principal da entidade ContactInfo. Da mesma forma, se o objeto Employee já tiver sido guardado e o objeto ContactInfo relacionado for novo, o App Engine cria a entidade ContactInfo usando a entidade Employee existente como principal.

No entanto, repare que a chamada para pm.makePersistent() neste exemplo não usa uma transação. Sem uma transação explícita, ambas as entidades são criadas através de ações atómicas separadas. Neste caso, é possível que a criação da entidade Employee seja bem-sucedida, mas a criação da entidade ContactInfo falhe. Para garantir que ambas as entidades são criadas com êxito ou que nenhuma é criada, tem de usar uma transação:

    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();
        }
    }

Se ambos os objetos foram guardados antes de a relação ser estabelecida, o App Engine não pode "mover" a entidade ContactInfo existente para o grupo de entidades da entidade Employee, porque os grupos de entidades só podem ser atribuídos quando as entidades são criadas. O App Engine pode estabelecer a relação com uma referência, mas as entidades relacionadas não vão estar no mesmo grupo. Neste caso, as duas entidades podem ser atualizadas ou eliminadas na mesma transação se definir a configuração JDO para ativar transações entre grupos (XG). Se não usar transações XG, a tentativa de atualizar ou eliminar entidades de grupos diferentes na mesma transação vai gerar uma JDOFatalUserException.

Se guardar um objeto principal cujos objetos secundários foram modificados, as alterações aos objetos secundários são guardadas. É recomendável permitir que os objetos principais mantenham a persistência de todos os objetos secundários relacionados desta forma e usar transações ao guardar alterações.

Crianças dependentes e eliminações em cascata

Uma relação de propriedade pode ser "dependente", o que significa que o secundário não pode existir sem o principal. Se uma relação for dependente e um objeto principal for eliminado, todos os objetos secundários também são eliminados. A quebra de uma relação de propriedade e dependência através da atribuição de um novo valor ao campo dependente no elemento principal também elimina o elemento secundário antigo. Pode declarar uma relação individual pertencente como dependente adicionando dependent="true" à anotação Persistent do campo no objeto principal que faz referência ao secundário:

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

Pode declarar que uma relação de propriedade de um para muitos é dependente adicionando uma anotação @Element(dependent = "true") ao campo no objeto principal que faz referência à coleção secundária:

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

Tal como acontece com a criação e a atualização de objetos, se precisar que cada eliminação numa eliminação em cascata ocorra numa única ação atómica, tem de realizar a eliminação numa transação.

Nota: a implementação do JDO faz o trabalho de eliminar objetos secundários dependentes, não o arquivo de dados. Se eliminar uma entidade principal através da API de baixo nível ou da Google Cloud consola, os objetos secundários relacionados não são eliminados.

Relações polimórficas

Embora a especificação JDO inclua suporte para relações polimórficas, as relações polimórficas ainda não são suportadas na implementação DO do App Engine. Esta é uma limitação que esperamos remover em lançamentos futuros do produto. Se precisar de fazer referência a vários tipos de objetos através de uma classe base comum, recomendamos a mesma estratégia usada para implementar relações não pertencentes: armazene uma referência de chave. Por exemplo, se tiver uma classe base Recipe com especializações Appetizer, Entree> e Dessert, e quiser modelar o favorito Recipe de um Chef, pode modelá-lo da seguinte forma:

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;
}

Infelizmente, se instanciar um Entree e o atribuir a Chef.favoriteRecipe, recebe um UnsupportedOperationException quando tenta persistir o objeto Chef. Isto deve-se ao facto de o tipo de tempo de execução do objeto, Entree, não corresponder ao tipo declarado do campo de relação, Recipe. A solução alternativa consiste em alterar o tipo de Chef.favoriteRecipe de Recipe para Key:

Chef.java

// ... imports ...

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

    @Persistent
    private Key favoriteRecipe;
}

Uma vez que Chef.favoriteRecipe já não é um campo de relação, pode referir-se a um objeto de qualquer tipo. A desvantagem é que, tal como acontece com uma relação não pertencente, tem de gerir esta relação manualmente.