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, 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 armazenamento é feito pelo App Engine como uma propriedade da entidade correspondente, usando um nome de propriedade igual ao do campo do pai, seguido de _INTEGER_IDX. As propriedades da posição são ineficientes. 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:

  • Os relacionamentos não proprietários não são implementados com a Version 1 do plug-in DataNucleus por meio de uma sintaxe natural. No entanto, ainda é possível gerenciar esses relacionamentos usando valores Key, em vez de instâncias ou coleções de instâncias dos objetos do 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 de Key por 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, Person recebe um membro do tipo Key, em que a Key é o identificador exclusivo de um objeto Food. Caso uma instância de Person e a instância de Food referida por Person.favoriteFood não estejam no mesmo grupo de entidades, não será possível atualizar a pessoa e a comida favorita dela em uma única transação, a menos que a configuração do JDO esteja configurada como ativar transações entre grupos (XG, na sigla em inglês).

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 representando sua comida favorita, criamos um membro particular 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

Neste exemplo, em vez de dar a Person um membro do tipo Set<Food> para representar as comidas favoritas da pessoa, damos a Person um membro do tipo Set<Key>, em que o conjunto contém os identificadores exclusivos de objetos Food. Observe que, quando uma instância de Person e uma instância de Food contidas em Person.favoriteFoods não estão no mesmo grupo de entidades, é preciso definir a configuração do JDO como ativar transações entre grupos (XG, na sigla em inglês) para 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 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;

Neste exemplo, Person mantém um conjunto de valores Key que identificam de forma exclusiva os objetos Food que são favoritos, e Food mantém um conjunto de valores Key que identificam de forma exclusiva os objetos Person que a consideram favorita.

Ao modelar um relacionamento de muitos para muitos usando valores Key, esteja ciente de que é responsabilidade do aplicativo manter ambos os 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());
}

Caso uma instância de Person e uma instância de Food contidas em Person.favoriteFoods não estejam no mesmo grupo de entidades, para atualizá-las em uma única transação será preciso definir a configuração do JDO como ativar transações entre grupos (XG, na sigla em inglês).

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

Observe, no entanto, que a chamada a pm.makePersistent(), neste 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();
        }
    }

Caso os dois objetos tenham sido salvos antes do relacionamento ser estabelecido, o App Engine não conseguirá "mover" a entidade ContactInfo atual para o grupo de entidades da entidade Employee, porque só é possível atribuir grupos de entidades quando elas 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 da JDO como ativar transações entre grupos (XG, na sigla em inglês). 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. Para declarar um relacionamento proprietário de um para um como dependente, adicione dependent="true" à anotação Persistent do campo no objeto pai que faz referência ao filho:

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

Para declarar um relacionamento proprietário de um para muitos como dependente, adicione uma anotação @Element(dependent = "true") ao campo no objeto pai que faz referência à coleção do 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 do 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 baixo ou o Console do Google Cloud Platform, os objetos filhos 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 de chave. Por exemplo, caso você tenha uma classe base de Recipe com especializações de Appetizer, Entree e Dessert e queira modelar a Recipe favorita de um Chef, será possível modelá-la 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, receberá uma UnsupportedOperationException ao tentar continuar com o objeto Chef. Isso ocorre porque o tipo de tempo de execução do objeto, Entree, não corresponde ao tipo declarado do campo de relacionamento, Recipe. A solução alternativa é mudar 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;
}

Por não ser mais um campo de relacionamento, Chef.favoriteRecipe 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.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…

Ambiente padrão do App Engine para Java 8