Relacionamentos de entidades no JDO

Você pode modelar relacionamentos entre objetos persistentes usando campos dos tipos de objeto. Um relacionamento entre objetos persistentes pode ser descrito como proprietário, em que um dos objetos não pode existir sem o outro, ou não proprietário, em que os dois objetos podem existir independentemente do relacionamento entre si. A implementação do App Engine da interface JDO pode modelar relacionamentos de um para um proprietários e não proprietários, bem como relacionamentos de um para muitos, de modo unidirecional e bidirecional.

Os relacionamentos não proprietários não são compatíveis com a versão 1.0 do plug-in DataNucleus para App Engine, mas você mesmo pode gerenciá-los armazenando as chaves do armazenamento de dados nos campos diretamente. O App Engine cria entidades relacionadas em grupos de entidades automaticamente para aceitar a atualização conjunta de objetos relacionados, mas é responsabilidade do aplicativo saber quando usar transações de armazenamento de dados.

A versão 2.x do plug-in DataNucleus para App Engine é compatível com relacionamentos não proprietários com uma sintaxe natural. A seção Relacionamentos não proprietários mostra como criar relacionamentos não proprietários em cada versão do plug-in. Para fazer upgrade para a versão 2.x do plug-in DataNucleus para App Engine, consulte Como migrar para a versão 2.x do plug-in DataNucleus para App Engine.

Relacionamentos proprietários de um para um

É possível criar um relacionamento proprietário unidirecional de um para um entre dois objetos persistentes usando um campo que tem como tipo a classe da classe relacionada.

O exemplo a seguir define uma classe de dados ContactInfo e uma classe de dados Employee, com um relacionamento de 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 armazenamento de dados, com dois tipos diferentes. A representação do relacionamento usa um relacionamento de grupo de entidades: a chave do filho usa a chave do pai como o pai do respectivo grupo de entidades. Quando o aplicativo acessa o objeto filho usando o campo do objeto pai, a implementação JDO realiza uma consulta ao pai do grupo de entidades para conseguir o filho.

A classe filho precisa ter um campo de chave do tipo que contenha informações da chave pai: uma chave ou um valor de chave codificado como uma string. Consulte Criar dados: chaves para informações sobre tipos de campo de chave.

Para criar um relacionamento bidirecional de um para um, use os campos das duas classes e inclua uma anotação no campo da classe filho para declarar que os campos representam um relacionamento bidirecional. O campo da classe filho precisa ter uma anotação @Persistent com o argumento mappedBy = "...", em que o valor é o nome do campo na classe pai. Se o campo estiver preenchido em um objeto, o campo de referência correspondente no outro objeto será preenchido automaticamente.

ContactInfo.java

import Employee;

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

Objetos filhos são carregados a partir do armazenamento de dados no primeiro acesso. Se você não acessar o objeto filho em um objeto pai, a entidade do objeto filho nunca será carregada. Para carregar o filho, "toque-o" antes de fechar o PersistenceManager (por exemplo, ao chamar getContactInfo() no exemplo acima), ou então adicione explicitamente o campo filho ao grupo de busca padrão para ser recuperado e carregado com o pai:

Employee.java

import ContactInfo;

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

Relacionamentos proprietários de um para muitos

Para criar um relacionamento de 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;

Um relacionamento bidirecional de um para muitos é semelhante ao de um para um, com um campo na classe pai usando a anotação @Persistent(mappedBy = "..."), em que o valor é o nome do campo na classe filho:

Employee.java

import java.util.List;

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

ContactInfo.java

import Employee;

// ...
    @Persistent
    private Employee employee;

Os tipos de coleção listados em Como definir classes de dados: coleções são compatíveis com relacionamentos de um para muitos. No entanto, as matrizes não são compatíveis com esses relacionamentos.

O App Engine não aceita consultas de junção: não é possível consultar uma entidade pai usando um atributo de uma entidade filho. É possível consultar a propriedade de uma classe incorporada, já que as classes incorporadas armazenam propriedades na entidade pai. Consulte Definir classes de dados: classes incorporadas.)

Como coleções ordenadas mantêm sua ordem

Coleções ordenadas, como List<...>, preservam a ordem dos objetos quando o objeto pai é salvo. A JDO exige que os bancos de dados preservem essa ordem armazenando a posição de cada objeto como uma propriedade do objeto. O Google App Engine armazena essa propriedade como uma propriedade da entidade correspondente, usando um nome de propriedade igual ao do campo do pai seguido por _INTEGER_IDX. As propriedades de posicionamento não são eficientes. Quando um elemento é adicionado, removido ou movido na coleção, todas as entidades posteriores ao local modificado da coleção precisam ser atualizadas. Quando realizada fora de uma transação, essa ação tende a ser lenta e sujeita a erros.

Se não for necessário preservar uma ordem arbitrária em uma coleção, mas for preciso usar um tipo ordenado de coleção, é possível especificar uma ordenação com base nas propriedades dos elementos usando uma anotação, uma extensão da 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 (usando a extensão list-ordering) especifica a ordem desejada dos elementos da coleção como uma cláusula de ordenação JDOQL. A ordenação usa valores de propriedades dos elementos. Assim como acontece com consultas, todos os elementos de uma coleção precisam ter valores para as propriedades usadas na cláusula de ordenação.

O acesso a uma coleção realiza uma consulta. Se a cláusula de ordenação de um campo usa mais de uma ordem de classificação, a consulta exige um índice de armazenamento de dados. Consulte a página Índices de armazenamento de dados para mais informações.

Para fins de eficiência, sempre que possível, use uma cláusula de ordenação explícita para relacionamentos de um para muitos de tipos de coleção ordenados.

Relacionamentos não proprietários

Além dos relacionamentos proprietários, a JDO API também fornece um recurso para o gerenciamento dos relacionamentos não proprietários. Esse recurso varia conforme a versão do plug-in DataNucleus para App Engine que você está usando:

  • A versão 1 do plug-in DataNucleus não implementa relacionamentos não proprietários usando uma sintaxe natural, mas ainda é possível gerenciar esses relacionamentos usando valores Key no lugar de instâncias (ou coleções de instâncias) dos objetos de modelo. O armazenamento de objetos Key pode ser considerado como a modelagem de uma "chave estrangeira" arbitrária entre dois objetos. O armazenamento de dados não garante a integridade referencial dessas referências de Key, mas o uso de Key facilita a modelagem (e subsequente busca) de qualquer relacionamento entre dois objetos.

    No entanto, ao seguir essa orientação, você precisa garantir que as chaves sejam do tipo apropriado. A JDO e o compilador não verificam os tipos Key para você.
  • A versão 2.x do plug-in DataNucleus implementa relacionamentos não proprietários usando uma sintaxe natural.

Dica: em alguns casos, pode ser necessário modelar um relacionamento proprietário como se fosse não proprietário. Isso se deve ao fato de que todos os objetos envolvidos em um relacionamento proprietário são colocados automaticamente no mesmo grupo de entidades, e um grupo de entidades tem capacidade de apenas uma a dez gravações por segundo. Por exemplo, se um objeto pai estiver recebendo 0,75 gravações por segundo e um objeto filho estiver recebendo 0,75 gravações por segundo, faz sentido modelar esse relacionamento como não proprietário para que tanto o pai quanto o filho residam nos próprios grupos de entidades independentes.

Relacionamentos não proprietários de um para um

Imagine que queremos modelar pessoa e comida, em que uma pessoa só pode ter uma comida favorita, mas uma comida favorita não pertence à pessoa porque pode ser a favorita de várias outras. Nesta seção, mostramos como fazer isso.

Na JDO 2.3

Neste exemplo, fornecemos a Person 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 mencionadas por Person.favoriteFood não estiverem no mesmo grupo de entidades, não será possível atualizar a pessoa e a comida favorita dela em uma transação única, a menos que a configuração de JDO esteja definida como 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;

    // ...
}

Na JDO 3.0

Neste exemplo, em vez de dar a Person uma chave que representa a comida favorita dela, 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;

    // ...
}

Relacionamentos não proprietários de um para muitos

Agora, imagine que queremos deixar uma pessoa ter várias comidas favoritas. Novamente, uma comida favorita não pertence à pessoa, porque pode ser a comida favorita de várias outras.

Na JDO 2.3

Nesse exemplo, em vez de dar a Person um membro do tipo Set<Food> para representar as comidas favoritas da pessoa, demos a Person um membro de tipo Set<Key>, em que o conjunto contém os identificadores exclusivos de objetos Food. Se uma instância de Person e uma instância de Food contidas em Person.favoriteFoods não estiverem no mesmo grupo de entidades, será necessário definir sua configuração de JDO como ativar transações entre grupos (XG) se você 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;

    // ...
}

Na JDO 3.0

Neste exemplo, damos a Person um membro do tipo Set<Food>, em que o conjunto representa as comidas favoritas de Person.

Person.java

// ... imports ...

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

    @Persistent
    private Set<Food> favoriteFoods;

    // ...
}

Relacionamentos de muitos para muitos

Podemos modelar um relacionamento de muitos para muitos mantendo coleções de chaves nos dois lados do relacionamento. Vamos ajustar o nosso exemplo para deixar Food rastrear as pessoas que a consideram a sua comida favorita:

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;

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

Ao modelar um relacionamento de muitos para muitos usando valores Key, saiba que é responsabilidade do aplicativo manter os dois lados do relacionamento:

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 você quiser atualizá-las em uma única transação, defina sua configuração de JDO como ativar transações entre grupos (XG).

Relacionamentos, grupos de entidades e transações

Quando um objeto com relacionamentos proprietários é salvo pelo aplicativo no armazenamento de dados, todos os outros objetos acessíveis por meio dos relacionamentos e que precisam ser salvos, por serem novos ou terem sido modificados desde o último carregamento, serão salvos automaticamente. Isso gera implicações importantes para as transações e os grupos de entidades.

Considere o exemplo a seguir usando um relacionamento 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 é salvo usando o método pm.makePersistent(), o novo objeto ContactInfo relacionado é salvo automaticamente. Como os dois objetos são novos, o App Engine cria duas entidades novas no mesmo grupo de entidades, usando a entidade Employee como pai da entidade ContactInfo. Da mesma maneira, se o objeto Employee já tiver sido salvo e o objeto ContactInfo relacionado for novo, o App Engine vai criar a entidade ContactInfo usando a entidade Employee existente como pai.

Observe, no entanto, que a chamada para pm.makePersistent() nesse exemplo não usa uma transação. Sem uma transação explícita, as duas entidades são criadas por meio de ações atômicas separadas. Nesse caso, é possível que a entidade Employee seja criada, mas que a criação da entidade ContactInfo falhe. Para garantir que as duas entidades sejam criadas ou que nenhuma entidade seja criada, use 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 salvos antes do relacionamento ser estabelecido, o App Engine não pode "mover" a entidade ContactInfo existente para o grupo de entidades Employee, porque os grupos de entidades só podem ser atribuídos quando as entidades são criadas. O App Engine pode estabelecer o relacionamento com uma referência, mas as entidades relacionadas não estarão no mesmo grupo. Nesse caso, as duas entidades poderão ser atualizadas ou excluídas na mesma transação se você definir a configuração de JDO como ativar transações entre grupos (XG). Se você não usa transações XG, a tentativa de atualizar ou excluir entidades de diferentes grupos na mesma transação gerará uma JDOFatalUserException.

Salvar um objeto pai com objetos filhos que foram modificados salvará as alterações nos objetos filhos. É uma boa ideia permitir que objetos pai mantenham a persistência de todos os objetos filhos relacionados dessa maneira e usar transações ao salvar alterações.

Filhos dependentes e exclusões em cascata

Um relacionamento proprietário pode ser "dependente", o que significa que o filho não pode existir sem o pai. Se um relacionamento for dependente e um objeto pai for excluído, todos os objetos filhos também serão excluídos. Interromper um relacionamento proprietário dependente atribuindo um valor novo ao campo dependente no pai também exclui o filho antigo. É possível declarar que um relacionamento proprietário de um para um é dependente adicionando dependent="true"à anotaçãoPersistent do campo no objeto pai que se refere ao filho:

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

Você pode declarar que um relacionamento proprietário de um para muitos é dependente adicionando uma anotação @Element(dependent = "true") ao campo no objeto pai que se refere ao conjunto filho:

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

Assim como acontece ao criar e atualizar objetos, se for necessário que cada exclusão em uma exclusão em cascata ocorra em uma única ação atômica, realize a exclusão em uma transação.

Observação: a implementação JDO faz o trabalho de exclusão dos objetos filhos dependentes, não do armazenamento de dados. Se você excluir uma entidade pai usando a API de nível inferior ou o console do Google Cloud, os objetos filho relacionados não serão excluídos.

Relacionamentos polimórficos

A especificação do JDO inclui compatibilidade com relacionamentos polimórficos. No entanto, eles ainda não são compatíveis com a implementação do JDO no App Engine. Esperamos remover essa limitação em versões futuras do produto. Se precisar fazer referência a vários tipos de objeto por meio de uma classe base comum, recomendamos a mesma estratégia usada para implementar relacionamentos não proprietários: armazenar uma referência a Key. Por exemplo, se você tem uma classe base Recipe com especializações Appetizer, Entree e Dessert e pretende modelar a Recipe favorita de um Chef, você pode fazê-lo da seguinte maneira:

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 você instanciar uma Entree e atribuí-la a Chef.favoriteRecipe, você receberá uma UnsupportedOperationException quando tentar persistir o objeto Chef. Isso ocorre porque o tipo de ambiente de execução do objeto, Entree, não corresponde ao tipo declarado do campo de relacionamento, Recipe. A solução é 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 não é mais um campo de relacionamento, ele pode fazer referência a um objeto de qualquer tipo. O lado negativo é que, como em um relacionamento não proprietário, é necessário realizar o gerenciamento manualmente.