Como usar a JDO 2.3 com o App Engine

A Java Data Objects (JDO) é uma interface padrão para acessar bancos de dados em Java, fornecendo um mapeamento entre classes Java e tabelas de banco de dados. Há um plug-in de código aberto disponível para usar a JDO com o Datastore. Nesta página, fornecemos informações sobre como começar a usar esse plug-in.

Aviso: acreditamos que a maioria dos desenvolvedores terá uma experiência melhor usando a API Datastore de nível inferior ou uma das APIs de código aberto desenvolvidas especificamente para o Datastore, como Objectify (em inglês). A JDO foi projetada para uso com bancos de dados relacionais tradicionais. Portanto, ela não pode representar explicitamente alguns dos aspectos do Datastore que a tornam diferente dos bancos de dados relacionais, como grupos de entidades e consultas de ancestral. Isso pode causar problemas sutis que são difíceis de entender e corrigir.

O SDK Java do App Engine inclui uma implementação da JDO 2.3 para o App Engine Datastore. A implementação é baseada na versão 1.0 da DataNucleus Access Platform, a implementação de referência de código aberto para JDO 2.3.

Observação: as instruções nesta página referem-se à JDO versão 2.3, que usa a versão 1.0 do plug-in DataNucleus para App Engine. O App Engine agora oferece o plug-in DataNucleus 2.x, que permite executar a JDO 3.0. O novo plug-in aceita relacionamentos sem proprietário e fornece uma série de novos recursos e APIs. Esse upgrade não é totalmente compatível com a versão anterior. Se você recriar um aplicativo usando a JDO 3.0, precisará atualizar e testar novamente o código. Para mais informações sobre a nova versão, consulte JDO 3.0 (link em inglês). Para mais informações sobre upgrade, consulte Como usar a JDO 3.0 com o App Engine.

Como configurar a JDO 2.3

Para usar a JDO com o objetivo de acessar o armazenamento de dados, um aplicativo do App Engine precisa que:

  • os JARs da JDO e do plug-in DataNucleus do App Engine estejam no diretório war/WEB-INF/lib/ do app;
  • um arquivo de configuração denominado jdoconfig.xml precisa estar no diretório war/WEB-INF/classes/META-INF/ do app, com configuração que instrui a JDO a usar o armazenamento de dados do App Engine;
  • o processo de criação do projeto execute uma etapa de “aprimoramento” pós-compilação nas classes de dados compiladas para associá-las à implementação da JDO.

Como copiar os JARs

A JDO e os JARs do armazenamento de dados estão incluídos no SDK para Java do Google App Engine. Você os encontra no diretório appengine-java-sdk/lib/user/orm/.

Copie os JARs para o diretório war/WEB-INF/lib/ do seu aplicativo.

Certifique-se de que appengine-api.jar também esteja no diretório war/WEB-INF/lib/. Pode ser que você já tenha copiado esse arquivo ao criar o projeto. O plug-in DataNucleus do Google App Engine usa esse JAR para acessar o armazenamento de dados.

Como criar o arquivo jdoconfig.xml

A interface JDO precisa de um arquivo de configuração denominado jdoconfig.xml no diretório war/WEB-INF/classes/META-INF/ do aplicativo. Você pode criar esse arquivo nesse local diretamente ou copiar o arquivo de um diretório de origem durante o processo de criação.

Crie o arquivo com o seguinte conteúdo:

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>
</jdoconfig>

Como configurar a política de leitura do Datastore e o duração máxima de chamada

Conforme descrito na página Consultas do Datastore, você pode personalizar o comportamento do Datastore definindo a política de leitura (consistência forte versus eventual) e a duração máxima de chamada. Na JDO, você faz isso especificando os valores pretendidos no elemento <persistence-manager-factory> do arquivo jdoconfig.xml. Todas as chamadas feitas com uma determinada instância do PersistenceManager usarão os valores de configuração efetivos quando o gerenciador for criado por PersistenceManagerFactory. Também é possível modificar essas configurações para um único objeto Query.

Para definir a política de leitura para um PersistenceManagerFactory, inclua uma propriedade denominada datanucleus.appengine.datastoreReadConsistency. Os valores possíveis são EVENTUAL e STRONG: se não for especificado, o padrão será STRONG. Observe que essas configurações se referem somente a consultas de ancestral em um determinado grupo de entidades. As consultas entre grupos e que não são de ancestral sempre têm consistência eventual, independentemente da política de leitura que prevalece.

        <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

Você pode definir durações máximas da chamada ao armazenamento de dados separadas para leituras e gravações. Para leituras, use a propriedade padrão da JDO javax.jdo.option.DatastoreReadTimeoutMillis. Para gravações, use javax.jdo.option.DatastoreWriteTimeoutMillis. O valor é uma quantidade de tempo, em milissegundos.

        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />

Se você quiser usar transações entre grupos (XG, na sigla em inglês), adicione a seguinte propriedade:

        <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

É possível pode ter vários elementos <persistence-manager-factory> no mesmo arquivo jdoconfig.xml, usando atributos name diferentes, para usar instâncias de PersistenceManager com configurações diferentes no mesmo app. Por exemplo, o seguinte arquivo jdoconfig.xml estabelece dois conjuntos de configuração, um denominado "transactions-optional" e outro denominado "eventual-reads-short-deadlines":

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>

    <persistence-manager-factory name="eventual-reads-short-deadlines">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>

        <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />
    </persistence-manager-factory>
</jdoconfig>

Consulte Como receber uma instância do PersistenceManager abaixo para informações sobre como criar um PersistenceManager com um conjunto de configuração específico.

Como aprimorar classes de dados

A JDO usa uma etapa de "melhoria" pós-compilação no processo de criação para associar classes de dados à implementação da JDO.

Para realizar a etapa de aprimoramento em classes compiladas a partir da linha de comando, use o seguinte comando:

java -cp classpath com.google.appengine.tools.enhancer.Enhance
class-files

O classpath precisa conter o JAR appengine-tools-api.jar do diretório appengine-java-sdk/lib/, bem como todas as classes de dados.

Para mais informações sobre o otimizador de bytecode do DataNucleus, consulte a documentação do DataNucleus (em inglês).

Como receber uma instância PersistenceManager

Um aplicativo interage com a JDO usando uma instância da classe PersistenceManager. Essa instância pode ser recebida ao instanciar e chamar um método em uma instância da classe PersistenceManagerFactory. A fábrica usa a configuração da JDO para criar instâncias de PersistenceManager.

Como uma instância de PersistenceManagerFactory demora para ser inicializada, um aplicativo precisa reutilizar uma única instância. Para impor esse comportamento, uma exceção é gerada quando o aplicativo instancia mais de uma PersistenceManagerFactory (com o mesmo nome de configuração). Uma maneira fácil de gerenciar a instância de PersistenceManagerFactory é criar uma classe wrapper de singleton com uma instância estática, desta forma:

PMF.java

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
    private static final PersistenceManagerFactory pmfInstance =
        JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

Dica: "transactions-optional" se refere ao nome do conjunto de configuração no arquivo jdoconfig.xml. Caso seu aplicativo use vários conjuntos de configuração, você precisará estender esse código para chamar JDOHelper.getPersistenceManagerFactory(), conforme quiser. Seu código precisa armazenar em cache uma instância singleton de cada PersistenceManagerFactory.

O aplicativo usa a instância de fábrica para criar uma instância PersistenceManager para cada solicitação que acessa o armazenamento de dados.

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

import PMF;

// ...
    PersistenceManager pm = PMF.get().getPersistenceManager();

O PersistenceManager é usado para armazenar, atualizar e excluir objetos de dados e para executar consultas do armazenamento de dados.

Ao terminar de usar a instância PersistenceManager, é necessário chamar o método close() dela. É um erro usar a instância do PersistenceManager depois de chamar o método close() correspondente.

    try {
        // ... do stuff with pm ...
    } finally {
        pm.close();
    }

Recursos incompatíveis da JDO 2.3

A implementação do App Engine é incompatível com os seguintes recursos da interface JDO:

  • Relacionamentos sem proprietário. Você pode implementar relacionamentos sem proprietário usando valores explícitos de Key. A sintaxe da JDO para relacionamentos sem proprietário poderá ser aceita em uma versão futura.
  • Relacionamentos com proprietário de muitos para muitos.
  • Consultas "join". Não é possível usar um campo de uma entidade filha em um filtro ao executar uma consulta sobre o tipo do pai. Observe que você pode testar o campo de relacionamento do pai diretamente em uma consulta usando uma chave.
  • Agrupamento JDOQL e outras consultas agregadas.
  • Consultas polimórficas. Não é possível realizar uma consulta de uma classe para receber instâncias de uma subclasse. Cada classe é representada por um tipo de entidade separado no armazenamento de dados.
  • IdentityType.DATASTORE para a anotação @PersistenceCapable. Somente IdentityType.APPLICATION é aceito.
  • Atualmente há um bug que impede relacionamentos de um para muitos em que o pai e o filho são da mesma classe, dificultando a modelagem de estruturas de árvore. Isso será corrigido em uma versão futura. Você pode contornar esse problema armazenando valores explícitos de Key para o pai ou para os filhos.

Como desativar transações e portar aplicativos JDO existentes

A configuração da JDO que recomendamos define uma propriedade denominada datanucleus.appengine.autoCreateDatastoreTxns como true. Esta é uma propriedade específica do Google App Engine que instrui a implementação da JDO a associar transações do armazenamento de dados a transações JDO gerenciadas no código do aplicativo. Se você está criando um novo aplicativo desde o princípio, isso é recomendável. Entretanto, se houver um aplicativo baseado em JDO que você queira executar no App Engine, talvez convenha usar uma configuração de permanência alternativa que define o valor dessa propriedade como false:

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="false"/>
    </persistence-manager-factory>
</jdoconfig>

Para entender por que isso pode ser útil, lembre-se de que só é possível operar em objetos que pertencem ao mesmo grupo de entidades dentro de uma transação. Aplicativos criados usando bancos de dados tradicionais tipicamente assumem a disponibilidade de transações globais, que permitem que você atualize qualquer conjunto de registros dentro de uma transação. O armazenamento de dados do App Engine não oferece suporte a transações globais. Por isso, o App Engine gera exceções caso seu código assuma a disponibilidade de transações globais. Em vez de repassar sua base de código (potencialmente grande) e remover todo o código de gerenciamento de transações, você pode simplesmente desativar transações do armazenamento de dados. Isso não trata dos pressupostos feitos por seu código sobre a atomicidade de modificações em vários registros. Entretanto, é possível colocar seu aplicativo em funcionamento para você se dedicar à refatoração incremental do código transacional, conforme necessário, em vez de fazer tudo ao mesmo tempo.