Definir classes de dados com JDO

Pode usar o JDO para armazenar objetos de dados Java simples (por vezes, denominados "Plain Old Java Objects" ou "POJOs") no arquivo de dados. Cada objeto que é tornado persistente com o PersistenceManager torna-se uma entidade no arquivo de dados. Usa anotações para indicar ao JDO como armazenar e recriar instâncias das suas classes de dados.

Nota: as versões anteriores do JDO usam ficheiros XML .jdo em vez de anotações Java. Estes continuam a funcionar com o JDO 2.3. Esta documentação apenas aborda a utilização de anotações Java com classes de dados.

Anotações de classe e campo

Cada objeto guardado pelo JDO torna-se uma entidade no arquivo de dados do App Engine. O tipo da entidade é derivado do nome simples da classe (as classes internas usam o caminho $ sem o nome do pacote). Cada campo persistente da classe representa uma propriedade da entidade, com o nome da propriedade igual ao nome do campo (com a capitalização preservada).

Para declarar uma classe Java como capaz de ser armazenada e obtida a partir do arquivo de dados com JDO, atribua à classe uma anotação @PersistenceCapable. Por exemplo:

import javax.jdo.annotations.PersistenceCapable;

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

Os campos da classe de dados que vão ser armazenados no arquivo de dados têm de ser declarados como campos persistentes. Para declarar um campo como persistente, atribua-lhe uma anotação @Persistent:

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

// ...
    @Persistent
    private Date hireDate;

Para declarar um campo como não persistente (não é armazenado no arquivo de dados e não é restaurado quando o objeto é obtido), atribua-lhe uma anotação @NotPersistent.

Sugestão: o JDO especifica que os campos de determinados tipos são persistentes por predefinição se não forem especificadas as anotações @Persistent nem @NotPersistent, e os campos de todos os outros tipos não são persistentes por predefinição. Consulte a documentação do DataNucleus para ver uma descrição completa deste comportamento. Uma vez que nem todos os tipos de valores principais do App Engine datastore são persistentes por predefinição de acordo com a especificação JDO, recomendamos que anote explicitamente os campos como @Persistent ou @NotPersistent para o esclarecer.

O tipo de um campo pode ser qualquer um dos seguintes. Estas são descritas detalhadamente abaixo.

  • um dos tipos principais suportados pelo arquivo de dados
  • Uma coleção (como um java.util.List<...>) ou uma matriz de valores de um tipo de arquivo de dados principal
  • Uma instância ou uma coleção de instâncias de uma classe @PersistenceCapable
  • Uma instância ou uma coleção de instâncias de uma classe serializável
  • Uma classe incorporada, armazenada como propriedades na entidade

Uma classe de dados tem de ter um e apenas um campo dedicado ao armazenamento da chave principal da entidade de armazenamento de dados correspondente. Pode escolher entre quatro tipos diferentes de campos de chaves, cada um com um tipo de valor e anotações diferentes. (Consulte o artigo Criar dados: chaves para mais informações.) O tipo de campo de chave mais flexível é um objeto Key que é preenchido automaticamente pelo JDO com um valor exclusivo em todas as outras instâncias da classe quando o objeto é guardado no armazenamento de dados pela primeira vez. As chaves primárias do tipo Key requerem uma anotação @PrimaryKey e uma anotação @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY):

Sugestão: torne todos os campos persistentes private ou protected (ou protegidos por pacotes) e forneça acesso público apenas através de métodos de acesso. O acesso direto a um campo persistente de outra classe pode ignorar a melhoria da classe JDO. Em alternativa, pode tornar outras aulas @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;

Segue-se um exemplo de uma 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;
    }
}

Tipos de valores essenciais

Para representar uma propriedade que contenha um único valor de um tipo principal, declare um 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 num único valor de propriedade do tipo Blob. Para indicar ao JDO que deve 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 nem em ordens de ordenação.

Segue-se um exemplo de uma classe Serializable simples que representa um ficheiro, incluindo o conteúdo do ficheiro, um nome de ficheiro e um tipo MIME. Esta não é uma classe de dados JDO, pelo que não existem 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 numa propriedade, declare um campo cujo tipo seja a classe e use a anotação @Persistent(serialized = "true"):

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

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

Objetos e relações secundárias

Um valor de campo que é uma instância de uma classe @PersistenceCapable cria uma relação individual pertencente entre dois objetos. Um campo que é uma coleção de referências deste tipo cria uma relação de propriedade de um para muitos.

Importante: as relações de propriedade têm implicações para as transações, os grupos de entidades e as eliminações em cascata. Consulte os artigos Transações e Relações para mais informações.

Segue-se um exemplo simples de uma relação individual pertencente 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 a app criar uma instância Employee, preencher o respetivo campo myContactInfo com uma nova instância ContactInfo e, em seguida, guardar a instância Employee com pm.makePersistent(...), o arquivo de dados cria duas entidades. Um é do tipo "ContactInfo", que representa a instância ContactInfo. O outro é do tipo "Employee". A chave da entidade ContactInfo tem a chave da entidade Employee como o respetivo principal do grupo de entidades.

Turmas incorporadas

As classes incorporadas permitem-lhe modelar um valor de campo através de uma classe sem criar uma nova entidade de arquivo de dados e formar uma relação. Os campos do valor do objeto são armazenados diretamente na entidade de armazenamento de dados para o objeto que o contém.

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

Segue-se um exemplo de uma classe incorporada. Este exemplo torna a classe incorporada uma classe interna da classe de dados que a usa. Isto é útil, mas não é necessário para tornar uma 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 de uma classe incorporada são armazenados como propriedades na entidade, usando o nome de cada campo e o nome da propriedade correspondente. Se tiver mais do que um campo no objeto cujo tipo é uma classe incorporada, tem de mudar o nome dos campos de um para que não entrem em conflito com outro. Especifica novos nomes de campos através de 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;

Da mesma forma, os campos no objeto não podem usar nomes que entrem em conflito com os campos de classes incorporadas, a menos que os campos incorporados sejam renomeados.

Uma vez que as propriedades persistentes da classe incorporada são armazenadas na mesma entidade que os outros campos, pode usar campos persistentes da classe incorporada em filtros de consultas JDOQL e ordens de ordenação. Pode consultar o campo incorporado através do nome do campo exterior, um ponto (.) e o nome do campo incorporado. Isto funciona independentemente de os nomes das propriedades dos campos incorporados terem sido alterados ou não através de anotações @Column.

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

Coleções

Uma propriedade da base de dados pode ter mais do que um valor. No JDO, isto é representado por um único campo com um tipo de coleção, em que a coleção é de um dos tipos de valores principais ou uma classe serializável. São suportados os seguintes tipos de coleções:

  • 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 for declarado como uma lista, os objetos devolvidos pelo arquivo de dados têm um valor ArrayList. Se um campo for declarado como um conjunto, o arquivo de dados devolve um HashSet. Se um campo for declarado como SortedSet, o arquivo de dados devolve um TreeSet.

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

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

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

Uma coleção de objetos secundários (de classes @PersistenceCapable) cria várias entidades com uma relação um-para-muitos. Consulte Relações.

As propriedades do Datastore com mais de um valor têm um comportamento especial para filtros de consulta e ordens de ordenação. Consulte a página Consultas do Datastore para obter mais informações.

Campos de objetos e propriedades de entidades

O arquivo de dados do App Engine distingue entre uma entidade sem uma determinada propriedade e uma entidade com um valor null para uma propriedade. O JDO não suporta esta distinção: todos os campos de um objeto têm um valor, possivelmente null. Se um campo com um tipo de valor anulável (algo diferente de um tipo incorporado, como int ou boolean) estiver definido como null, quando o objeto é guardado, a entidade resultante tem a propriedade definida com um valor nulo.

Se uma entidade do arquivo de dados for carregada num 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 é definido como null. Quando o objeto é novamente guardado no arquivo de dados, a propriedade null é definida no arquivo de dados como o valor nulo. Se o campo não for de um tipo de valor anulável, o carregamento de uma entidade sem a propriedade correspondente gera uma exceção. Isto não acontece se a entidade tiver sido 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 tiver sido criada através da API de baixo nível em vez do JDO.

Se o tipo de um campo for uma coleção de um tipo de dados principal ou uma classe serializável e não existirem valores para a propriedade na entidade, a coleção vazia é representada no arquivo de dados definindo a propriedade como um único valor nulo. Se o tipo do campo for um tipo de matriz, é-lhe atribuída uma matriz de 0 elementos. Se o objeto for carregado e não existir nenhum valor para a propriedade, é atribuída ao campo uma coleção vazia do tipo adequado. Internamente, o arquivo de dados sabe a diferença entre uma coleção vazia e uma coleção que contém um valor nulo.

Se a entidade tiver uma propriedade sem um campo correspondente no objeto, essa propriedade fica inacessível a partir do objeto. Se o objeto for guardado novamente no repositório de dados, a propriedade adicional é eliminada.

Se uma entidade tiver uma propriedade cujo valor seja de um tipo diferente do campo correspondente no objeto, o JDO tenta converter o valor para o tipo de campo. Se o valor não puder ser convertido para o tipo de campo, o JDO lança uma ClassCastException. No caso de números (inteiros longos e flutuantes de largura dupla), o valor é convertido e não convertido. Se o valor da propriedade numérica for maior do que o tipo de campo, a conversão transborda sem gerar uma exceção.

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 secção Propriedades não indexadas dos documentos principais para ver informações adicionais sobre o que significa uma propriedade não estar indexada.

Herança

A criação de classes de dados que usam a herança é algo natural e o JDO suporta-o. Antes de falarmos sobre como a herança JDO funciona no App Engine, recomendamos que leia a documentação do DataNucleus sobre este assunto e, em seguida, volte. Concluído? OK. A herança JDO no App Engine funciona conforme descrito na documentação do DataNucleus, com algumas restrições adicionais. Vamos abordar estas restrições e, em seguida, apresentar alguns exemplos concretos.

A estratégia de herança "new-table" permite dividir os dados de um único objeto de dados em várias "tabelas", mas, uma vez que o datastore do App Engine não suporta junções, a operação num objeto de dados com esta estratégia de herança requer uma chamada de procedimento remoto para cada nível de herança. Isto é potencialmente muito ineficiente, pelo que a estratégia de herança "new-table" não é suportada em classes de dados que não estejam na raiz das respetivas hierarquias de herança.

Em segundo lugar, a estratégia de herança "tabela de superclasse" permite-lhe armazenar os dados de um objeto de dados na "tabela" da respetiva superclasse. Embora não existam ineficiências inerentes nesta estratégia, esta não é suportada atualmente. Podemos rever esta situação em versões futuras.

Agora, as boas notícias: as estratégias "subclass-table" e "complete-table" funcionam conforme descrito na documentação do DataNucleus, e também pode usar "new-table" para qualquer objeto de dados que esteja na raiz da respetiva 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;
}

Neste exemplo, adicionámos uma anotação @Inheritance à declaração de classe com o respetivo atributo strategy> definido como InheritanceStrategy.SUBCLASS_TABLE.Worker Isto indica ao JDO para armazenar todos os campos persistentes do Worker nas entidades de armazenamento de dados das respetivas subclasses. A entidade da base de dados criada como resultado da chamada makePersistent() com uma instância Employee tem duas propriedades denominadas "department" e "salary". A entidade da base de dados criada como resultado da chamada makePersistent() com uma instância Intern terá duas propriedades denominadas "department" e "internshipEndDate". O arquivo de dados não contém entidades do tipo "Worker".

Agora, vamos tornar as coisas um pouco mais interessantes. Suponhamos que, além de ter Employee e Intern, também queremos uma especialização de Employee que descreva os funcionários que saíram da empresa:

FormerEmployee.java

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

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

Neste exemplo, adicionámos uma anotação @Inheritance à declaração da classe FormerEmployee com o respetivo atributo custom-strategy> definido como "complete-table". Isto indica ao JDO para armazenar todos os campos persistentes de FormerEmployee e das respetivas superclasses em entidades de armazenamento de dados correspondentes a instâncias de FormerEmployee. A entidade da base de dados criada como resultado da chamada makePersistent() com uma instância FormerEmployee terá três propriedades denominadas "department", "salary" e "lastDay". Nenhuma entidade do tipo "Employee" corresponde a um FormerEmployee. No entanto, se chamar makePersistent() com um objeto cujo tipo de tempo de execução seja Employee, cria uma entidade do tipo "Employee".

A combinação de relações com a herança funciona desde que os tipos declarados dos campos de relação correspondam aos tipos de tempo de execução dos objetos que está a atribuir a esses campos. Consulte a secção sobre Relações polimórficas para mais informações.