Como definir classes de dados com JDO

Você pode usar a JDO para armazenar objetos de dados simples Java (às vezes referidos como "POJOs" ou "Plain Old Java Objects") no armazenamento de dados. Cada objeto que é definido como permanente com o PersistenceManager se torna uma entidade no armazenamento de dados. Anotações podem ser usadas para dizer à JDO como armazenar e recriar instâncias de suas classes de dados.

Observação: versões anteriores do JDO usam arquivos XML .jdo em vez de anotações em Java. Eles ainda funcionam com JDO 2.3. Nesta documentação, discutiremos apenas o uso de anotações Java com classes de dados.

Anotações de classe e campo

Cada objeto salvo pela JDO torna-se uma entidade no armazenamento de dados do Google App Engine. O tipo da entidade é derivado do nome simples da classe (classes internas usam o caminho $ sem o nome do pacote). Cada campo permanente da classe representa uma propriedade da entidade, com o nome da propriedade igual ao nome do campo (com letras maiúsculas e minúsculas preservadas).

Para declarar que uma classe Java pode ser armazenada e recuperada do armazenamento de dados com a JDO, atribua uma anotação @PersistenceCapable a ela. Por exemplo:

import javax.jdo.annotations.PersistenceCapable;

@PersistenceCapable
public class Employee {
    // ...
}

Para colocar os campos da classe de dados no armazenamento de dados, declare-os como campos permanentes. Para declarar um campo como permanente, atribua a ele a anotação @Persistent:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Para declarar um campo como não persistente (ele não fica armazenado no armazenamento de dados e não é restaurado quando o objeto é recuperado), dê a ele uma anotação @NotPersistent.

Dica: a JDO especifica que os campos de determinados tipos serão permanentes por padrão se nem a anotação @Persistent nem @NotPersistent forem especificadas. Os campos de todos os outros tipos não são permanentes por padrão. Consulte a documentação do DataNucleus para uma descrição completa desse comportamento. Como nem todos os tipos de valor do núcleo do armazenamento de dados do App Engine são permanentes por padrão de acordo com a especificação JDO, recomendamos anotar campos explicitamente como @Persistent ou @NotPersistent para maior clareza.

O tipo de um campo pode ser qualquer um destes descritos em detalhes abaixo.

  • um dos tipos principais compatíveis com o armazenamento de dados
  • uma coleção (como uma java.util.List<...>) ou uma matriz de valores de um tipo principal do armazenamento de dados
  • uma instância ou coleção de instâncias de uma classe @PersistenceCapable
  • uma instância ou um conjunto de instâncias de uma classe Serializable
  • uma classe incorporada, armazenada como propriedades na entidade

Uma classe de dados precisa ter apenas um campo dedicado ao armazenamento da chave principal da entidade do armazenamento de dados correspondente. Você pode escolher entre quatro tipos diferentes de campos de chave. Cada um deles usa um tipo de valor e anotações diferentes. Consulte Como criar dados: chaves para mais informações. O tipo mais flexível de campo de chave é um objeto Key preenchido automaticamente pela JDO com um valor exclusivo em todas as outras instâncias da classe quando o objeto é salvo no armazenamento de dados pela primeira vez. As chaves principais do tipo Key exigem uma anotação @PrimaryKey e uma anotação @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY):

Dica: torne todos os seus campos permanentes private ou protected (ou protegidos por pacotes) e forneça acesso público apenas por meio de métodos de acesso. O acesso direto a um campo permanente de outra classe pode ignorar o aprimoramento de classe JDO. Como alternativa, você pode tornar outras classes @PersistenceAware. Consulte a documentação do DataNucleus para mais informações.

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

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PrimaryKey;

// ...
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

Veja um exemplo de classe de dados:

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

import java.util.Date;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

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

    @Persistent
    private String firstName;

    @Persistent
    private String lastName;

    @Persistent
    private Date hireDate;

    public Employee(String firstName, String lastName, Date hireDate) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.hireDate = hireDate;
    }

    // Accessors for the fields. JDO doesn't use these, but your application does.

    public Key getKey() {
        return key;
    }

    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public Date getHireDate() {
        return hireDate;
    }
    public void setHireDate(Date hireDate) {
        this.hireDate = hireDate;
    }
}

Principais tipos de valor

Para representar uma propriedade contendo um único valor de um tipo principal, declare o campo do tipo Java e use a anotação @Persistent:

import java.util.Date;
import javax.jdo.annotations.Persistent;

// ...
    @Persistent
    private Date hireDate;

Objetos serializáveis

Um valor de campo pode conter uma instância de uma classe Serializable, armazenando o valor serializado da instância em um único valor de propriedade do tipo Blob. Para instruir a JDO a serializar o valor, o campo usa a anotação @Persistent(serialized=true). Os valores Blob não são indexados e não podem ser usados em filtros de consulta ou ordens de classificação.

Veja um exemplo de uma classe Serializable simples que representa um arquivo, incluindo o conteúdo do arquivo, um nome de arquivo e um tipo MIME. Esta não é uma classe de dados JDO. Por isso, não há anotações de persistência.

import java.io.Serializable;

public class DownloadableFile implements Serializable {
    private byte[] content;
    private String filename;
    private String mimeType;

    // ... accessors ...
}

Para armazenar uma instância de uma classe Serializable como um valor Blob em uma propriedade, declare um campo em que o tipo é a classe e use a anotação @Persistent(serialized = "true"):

import javax.jdo.annotations.Persistent;
import DownloadableFile;

// ...
    @Persistent(serialized = "true")
    private DownloadableFile file;

Objetos filhos e Relacionamentos

Um valor de campo que é uma instância de uma classe @PersistenceCapable cria um relacionamento proprietário de um para um entre dois objetos. Um campo que é um conjunto dessas referências cria um relacionamento proprietário de um-para-vários.

Importante: relacionamentos proprietários têm implicações para transações, grupos de entidades e exclusões em cascata. Consulte Transações e Relacionamentos para mais informações.

Este é um exemplo simples de um relacionamento proprietário de um-para-um entre um objeto Employee e um objeto 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;

    @Persistent
    private String city;

    @Persistent
    private String stateOrProvince;

    @Persistent
    private String zipCode;

    // ... accessors ...
}

Employee.java

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

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

    @Persistent
    private ContactInfo myContactInfo;

    // ... accessors ...
}

Neste exemplo, se o aplicativo cria uma instância Employee, preenche o campo myContactInfo com a nova instância ContactInfo e salva a instância Employee com pm.makePersistent(...), o armazenamento de dados cria duas entidades. Uma delas tem o tipo "ContactInfo", que representa a instância ContactInfo. A outra é do tipo "Employee". A chave da entidade ContactInfo tem a chave da entidade Employee como pai do grupo de entidades.

Classes incorporadas

Classes incorporadas permitem que você modele um valor de campo usando uma classe sem criar uma nova entidade do armazenamento de dados e sem formar um relacionamento. Os campos do valor de objeto são armazenados diretamente na entidade do armazenamento de dados do objeto que contém.

Qualquer classe de dados @PersistenceCapable pode ser usada como objeto incorporado em outra classe de dados. Os campos @Persistent da classe estão incorporados no objeto. Se você atribuir à classe a ser incorporada a anotação @EmbeddedOnly, ela somente poderá ser usada como classe incorporada. A classe incorporada não precisa de um campo de chave principal, porque não é armazenada como uma entidade separada.

Veja um exemplo de classe incorporada. O exemplo transforma a classe incorporada em uma classe interna da classe de dados que a utiliza. Isso é útil, mas não é necessário para tornar a classe incorporável.

import javax.jdo.annotations.Embedded;
import javax.jdo.annotations.EmbeddedOnly;
// ... imports ...

@PersistenceCapable
public class EmployeeContacts {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    Key key;
    @PersistenceCapable
    @EmbeddedOnly
    public static class ContactInfo {
        @Persistent
        private String streetAddress;

        @Persistent
        private String city;

        @Persistent
        private String stateOrProvince;

        @Persistent
        private String zipCode;

        // ... accessors ...
    }

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;
}

Os campos da classe incorporada são armazenados como propriedades na entidade, usando o nome de cada campo e o nome da propriedade correspondente. Se tiver mais de um campo no objeto em que o tipo é uma classe incorporada, renomeie os campos de uma delas para que não entre em conflito com a outra. Você especifica novos nomes de campo usando argumentos para a anotação @Embedded. Por exemplo:

    @Persistent
    @Embedded
    private ContactInfo homeContactInfo;

    @Persistent
    @Embedded(members = {
        @Persistent(name="streetAddress", columns=@Column(name="workStreetAddress")),
        @Persistent(name="city", columns=@Column(name="workCity")),
        @Persistent(name="stateOrProvince", columns=@Column(name="workStateOrProvince")),
        @Persistent(name="zipCode", columns=@Column(name="workZipCode")),
    })
    private ContactInfo workContactInfo;

De modo semelhante, os campos no objeto não podem usar nomes que entram em conflito com campos de classes incorporadas, a menos que os campos incorporados sejam renomeados.

Como as propriedades permanentes da classe incorporada são armazenadas na mesma entidade que os outros campos, você pode usar campos permanentes da classe incorporada em filtros de consulta e ordens de classificação na JDOQL. Você pode fazer referência ao campo incorporado usando o nome do campo externo, um ponto (.) e o nome do campo incorporado. Isso funciona se os nomes de propriedades dos campos incorporados foram ou não alterados usando anotações @Column.

    select from EmployeeContacts where workContactInfo.zipCode == "98105"

Coleções

Uma propriedade do armazenamento de dados pode ter mais de um valor. Na JDO, isso é representado por um único campo com um tipo de coleção, onde a coleção é de um dos tipos de valor principal ou uma classe Serializable. Há suporte para os seguintes tipos de conjunto:

  • java.util.ArrayList<...>
  • java.util.HashSet<...>
  • java.util.LinkedHashSet<...>
  • java.util.LinkedList<...>
  • java.util.List<...>
  • java.util.Map<...>
  • java.util.Set<...>
  • java.util.SortedSet<...>
  • java.util.Stack<...>
  • java.util.TreeSet<...>
  • java.util.Vector<...>

Se um campo é declarado como uma "List", objetos retornados pelo armazenamento de dados têm um valor "ArrayList". Se um campo é declarado como um "Set", o armazenamento de dados retorna um "HashSet". Se um campo é declarado como um "SortedSet", o armazenamento de dados retorna um "TreeSet".

Por exemplo, um campo de tipo List<String> é armazenado como zero ou mais valores de string para a propriedade, um para cada valor na List.

import java.util.List;
// ... imports ...

// ...
    @Persistent
    List<String> favoriteFoods;

Uma coleção de objetos filho (das classes @PersistenceCapable) cria várias entidades com um relacionamento de um para muitos. Consulte Relacionamentos.

Propriedades do armazenamento de dados com mais de um valor têm um comportamento especial para filtros de consulta e ordens de classificação. Consulte a página Consultas do Datastore para mais informações.

Campos de objeto e propriedades de entidade

O armazenamento de dados do Google App Engine faz distinção entre uma entidade sem uma propriedade atribuída e uma entidade com um valor null para uma propriedade. A JDO não oferece suporte a essa distinção: cada campo de um objeto tem um valor, possivelmente null. Se um campo com um tipo de valor anulável (algo diferente de um tipo integrado como int ou boolean) for definido como null, quando o objeto é salvo, a entidade resultante terá a propriedade definida com um valor nulo.

Se uma entidade do armazenamento de dados for carregada em um objeto e não tiver uma propriedade para um dos campos do objeto e o tipo do campo for um tipo de valor único anulável, o campo será definido como null. Quando o objeto for salvo novamente no armazenamento de dados, a propriedade null ficará definida no armazenamento de dados como o valor nulo. Se o campo não for de um tipo de valor anulável, carregar uma entidade sem a propriedade correspondente gerará uma exceção. Isso não acontecerá se a entidade for criada a partir da mesma classe JDO usada para recriar a instância, mas pode acontecer se a classe JDO mudar ou se a entidade foi criada usando a API de nível inferior em vez de JDO.

Se o tipo de um campo for uma coleção de um tipo de dados principal ou uma classe Serializable e não houver valores para a propriedade na entidade, a coleção vazia será representada no armazenamento de dados por meio da configuração da propriedade como um valor nulo único. Se o tipo do campo for um tipo de matriz, será atribuída uma matriz de zero elementos. Se o objeto for carregado e não houver um valor para a propriedade, um conjunto vazio do tipo adequado será atribuído ao campo. Internamente, o armazenamento de dados sabe a diferença entre um conjunto vazio e um conjunto que contém um valor nulo.

Se a entidade tiver uma propriedade sem um campo correspondente no objeto, ela não poderá ser acessada a partir do objeto. Se o objeto for salvo novamente no armazenamento de dados, a propriedade adicional será excluída.

Se uma entidade tiver uma propriedade em que o valor é de um tipo diferente do campo correspondente no objeto, a JDO tentará lançar o valor para o tipo de campo. Se o valor não puder ser lançado para o tipo de campo, a JDO gerará uma ClassCastException. No caso de números (números inteiros longos e pontos flutuantes de dupla precisão), o valor será convertido, não lançado. Se o valor de propriedade numérica for maior que o tipo de campo, ocorrerá um estouro da conversão sem gerar uma exceção.

Você pode declarar uma propriedade não indexada adicionando a linha

    @Extension(vendorName="datanucleus", key="gae.unindexed", value="true")

acima da propriedade na definição da classe. Consulte a seção Propriedades não indexadas dos documentos principais para ver informações adicionais sobre o que significa uma propriedade não indexada.

Herança

Criar classes de dados que utilizam herança é natural e a JDO oferece suporte a essa ação. Antes de falar sobre como a herança da JDO funciona no App Engine, recomendamos que você leia a documentação do DataNucleus sobre esse assunto e depois volte. Concluído? OK. A herança JDO no Google App Engine funciona como descrito na documentação do DataNucleus, com algumas restrições adicionais. Discutiremos essas restrições e, em seguida, daremos alguns exemplos concretos.

A estratégia de herança "new-table" permite que você divida os dados de um único objeto em várias "tabelas". Porém, como o armazenamento de dados do App Engine não é compatível com junções, a operação em um objeto de dados com essa estratégia de herança requer uma chamada de procedimento remoto para cada nível de herança. Isso pode ser bastante ineficiente. Portanto, a estratégia de herança "new-table" não é compatível com as classes de dados que não estejam na raiz de suas hierarquias de herança.

Já a estratégia de herança "superclass-table" permite que você armazene os dados de um objeto de dados na "tabela" de sua superclasse. Ainda que não haja ineficiências inerentes nessa estratégia, atualmente não há suporte para ela. Podemos rever isso em versões futuras.

Por outro lado, as estratégias "subclass-table" e "complete-table" funcionam conforme descrito na documentação do DataNucleus. Você também pode usar "new-table" para qualquer objeto de dados que esteja na raiz da hierarquia de herança. Vejamos um exemplo:

Worker.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 Worker {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    @Persistent
    private String department;
}

Employee.java

// ... imports ...

@PersistenceCapable
public class Employee extends Worker {
    @Persistent
    private int salary;
}

Intern.java

import java.util.Date;
// ... imports ...

@PersistenceCapable
public class Intern extends Worker {
    @Persistent
    private Date internshipEndDate;
}

No exemplo, adicionamos uma anotação @Inheritance à declaração de classe Worker com o atributo strategy> definido como InheritanceStrategy.SUBCLASS_TABLE. Isso instrui a JDO a armazenar todos os campos persistentes de Worker nas entidades do armazenamento de dados das subclasses. A entidade do armazenamento de dados criada como resultado da chamada de makePersistent() com uma instância Employee tem duas propriedades chamadas "department" e "salary". A entidade do armazenamento de dados criada como o resultado da chamada makePersistent() com uma instância Intern terá duas propriedades chamadas “department” e “internshipEndDate”. O armazenamento de dados não contém entidades do tipo "Worker".

Agora, vamos deixar as coisas um pouco mais interessantes. Suponha que, além de ter Employee e Intern, também queiramos uma especialização de Employee que descreva os funcionários que deixaram a empresa:

FormerEmployee.java

import java.util.Date;
// ... imports ...

@PersistenceCapable
@Inheritance(customStrategy = "complete-table")
public class FormerEmployee extends Employee {
    @Persistent
    private Date lastDay;
}

Neste exemplo, adicionamos uma anotação @Inheritance à declaração de classe FormerEmployee com seu atributo custom-strategy> definido como "complete-table". Ela faz com que a JDO armazene todos os campos permanentes de FormerEmployee e respectivas superclasses nas entidades de armazenamento de dados correspondentes às instâncias de FormerEmployee. A entidade de armazenamento de dados criada como resultado da chamada a makePersistent() com uma instância de FormerEmployee terá três propriedades, denominadas "department", "salary" e "lastDay". Nenhuma entidade do tipo "Employee" corresponde a um FormerEmployee. No entanto, se você chamar makePersistent() com um objeto que tenha o tipo de ambiente de execução Employee, crie uma entidade do tipo “Employee”.

Misturar relacionamentos com herança funciona desde que os tipos declarados de seus campos de relacionamento correspondam aos tipos de tempo de execução dos objetos que está atribuindo a esses campos. Consulte a seção Relacionamentos polimórficos para mais informações.