Como usar o JDO 3.0 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 em Java do App Engine inclui a versão 2.x do plug-in DataNucleus para App Engine. Esse plug-in corresponde à versão 3.0 da DataNucleus Access Platform, que permite usar o App Engine Datastore por meio da JDO 3.0.

Consulte a documentação da Access Platform 3.0 para mais informações sobre o JDO. Em especial, confira Mapeamento do JDO e API JDO (todos em inglês).

Aviso: o plug-in do DataNucleus 2.x para o App Engine usa o DataNucleus v3.x. Esse novo plug-in não é totalmente retrocompatível com o anterior (1.x). Se você atualizar para a nova versão, não se esqueça de atualizar e testar seu aplicativo.

Criar ferramentas compatíveis com JDO 2.x e 3.0

É possível usar o Maven para usar as versões 2.x ou 3.0 do plug-in DataNucleus para App Engine:

  • Para usuários do Maven: você pode aprimorar as classes com as seguintes configurações no seu arquivo pom.xml:
    <plugin>
        <groupId>org.datanucleus</groupId>
        <artifactId>maven-datanucleus-plugin</artifactId>
        <version>3.2.0-m1</version>
        <configuration>
            <api>JDO</api>
            <props>${basedir}/datanucleus.properties</props>
            <verbose>true</verbose>
            <enhancerName>ASM</enhancerName>
        </configuration>
        <executions>
            <execution>
                <phase>process-classes</phase>
                <goals>
                    <goal>enhance</goal>
                </goals>
            </execution>
        </executions>
        <dependencies>
            <dependency>
                <groupId>org.datanucleus</groupId>
                <artifactId>datanucleus-core</artifactId>
                <version>3.1.3</version>
            </dependency>
        </dependencies>
    </plugin>

Como migrar para a versão 2.x do plug-in DataNucleus

Nesta seção, explicamos como fazer upgrade do app para que ele use a versão 2.x do plug-in DataNucleus para App Engine, que corresponde ao DataNucleus Access Platform 3.0 e JDO 3.0. Este plug-in não tem retrocompatibilidade total com o plug-in 1.x e está sujeito a mudanças. Se você fizer o upgrade, certifique-se de atualizar e testar o código de seu aplicativo.

Novos comportamentos padrão

A versão 2.x do plug-in DataNucleus do App Engine tem alguns padrões diferentes da versão anterior:

  • Chamadas não transacionais para PersistenceManager.makePersistent() e PersistenceManager.deletePersistent() agora são executadas atomicamente. Essas funções foram executadas anteriormente na próxima transação ou em PersistenceManager.close().
  • Agora não há mais uma exceção na alocação duplicada PersistenceManagerFactory (PMF). Em vez disso, se você tiver a propriedade de persistência datanucleus.singletonPMFForName definida como true, ela retornará o PMF singleton atualmente alocado para esse nome.
  • Agora, os relacionamentos sem proprietário são compatíveis. Veja Relacionamentos sem proprietário.

Alterações nos arquivos de configuração

Para atualizar seu aplicativo para a versão 2.x do plug-in DataNucleus do App Engine, você precisa alterar as definições de configuração no build.xml e no jdoconfig.xml.

Atenção! depois de atualizar sua configuração, você precisa testar seu código de aplicativo para garantir a compatibilidade com versões anteriores. Se estiver configurando um novo aplicativo e quiser usar a versão mais recente do plug-in, consulte Como configurar a JDO 3.0.

  1. A propriedade PersistenceManagerFactoryClass foi alterada. Alterar esta linha em jdoconfig.xml:

    <property name="javax.jdo.PersistenceManagerFactoryClass"
              value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>

    para:
    <property name="javax.jdo.PersistenceManagerFactoryClass"
              value="org.datanucleus.api.jdo.JDOPersistenceManagerFactory"/>

Em build.xml

O destino copyjars precisa ser alterado para acomodar o DataNucleus 2.x:

  1. O destino copyjars foi alterado. Atualize esta seção:
    <target name="copyjars"
        description="Copies the App Engine JARs to the WAR.">
      <mkdir dir="war/WEB-INF/lib" />
      <copy
          todir="war/WEB-INF/lib"
          flatten="true">
        <fileset dir="${sdk.dir}/lib/user">
          <include name="**/*.jar" />
        </fileset>
      </copy>
    </target>
    para:
    <target name="copyjars"
        description="Copies the App Engine JARs to the WAR.">
      <mkdir dir="war/WEB-INF/lib" />
      <copy
          todir="war/WEB-INF/lib"
          flatten="true">
        <fileset dir="${sdk.dir}/lib/user">
            <include name="**/appengine-api-1.0-sdk*.jar" />
        </fileset>
        <fileset dir="${sdk.dir}/lib/opt/user">
          <include name="appengine-api-labs/v1/*.jar" />
          <include name="jsr107/v1/*.jar" />
          <include name="datanucleus/v2/*.jar" />
        </fileset>
      </copy>
    </target>
  2. O destino datanucleusenhance foi alterado. Atualize esta seção:
    <target name="datanucleusenhance" depends="compile"
        description="Performs enhancement on compiled data classes.">
      <enhance_war war="war" />
    </target>
    para:
    <target name="datanucleusenhance" depends="compile"
        description="Performs enhancement on compiled data classes.">
        <enhance_war war="war">
                <args>
                <arg value="-enhancerVersion"/>
                <arg value="v2"/>
            </args>
        </enhance_war>
    </target>

Como configurar a JDO 3.0

Para usar a JDO para acessar o armazenamento de dados, um aplicativo do Google App Engine precisa disto:

  • Os JARs para JDO e o plug-in DataNucleus precisam estar no diretório war/WEB-INF/lib/ do aplicativo.
  • 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. Eles podem ser encontrados no diretório appengine-java-sdk/lib/opt/user/datanucleus/v2/.

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.api.jdo.JDOPersistenceManagerFactory"/>
        <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.singletonPMFForName" 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, no entanto, que essas configurações se aplicam somente a consultas de ancestral dentro de um determinado grupo de entidades. As consultas não ancestrais são sempre consistentes, independentemente da política de leitura prevalecente.

<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.api.jdo.JDOPersistenceManagerFactory"/>
        <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.api.jdo.JDOPersistenceManagerFactory"/>
        <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" />
        <property name="datanucleus.singletonPMFForName" value="true" />
    </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 deve reutilizar uma única instância. Uma maneira fácil de gerenciar a instância de PersistenceManagerFactory é criar uma classe wrapper 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 3.0

A implementação do Google App Engine não oferece suporte aos seguintes recursos da interface JDO:

  • Relacionamentos próprios 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.

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.api.jdo.JDOPersistenceManagerFactory"/>
        <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.