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 tiposKey
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 ListcontactInfos;
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.