My favorites | Português | Sign in

Transações

O armazenamento de dados do Google App Engine suporta transações. Uma transação é uma operação ou conjunto de operações que tem total sucesso ou falha completamente. Um aplicativo pode executar diversas operações e cálculos em uma única transação.

Utilização de transações

Uma transação é uma operação ou conjunto de operações do armazenamento de dados que tem sucesso total ou falha completamente. Se a transação tiver sucesso, todos os efeitos pretendidos são aplicados ao armazenamento de dados. Se a transação falhar, nenhum dos efeitos é aplicado.

Cada operação de gravação no armazenamento de dados é atômica. Uma tentativa de criar, atualizar ou excluir uma entidade pode ser realizada ou não. Uma operação pode falhar devido a uma alta taxa de contenção, com usuários demais tentando modificar uma entidade ao mesmo tempo. Ou pode falhar devido ao aplicativo ter atingido a quota limite. Ou pode haver um erro interno do armazenamento de dados. Em todos os casos, os efeitos da operação não são aplicados e a API de armazenamento de dados emite uma exceção.

Veja abaixo um exemplo de incrementação de um campo chamado counter em um objeto ClubMembers (classe não mostrada( usando a API de transação JDO:

import javax.jdo.Transaction;

import ClubMembers;   // not shown

// ...
        // PersistenceManager pm = ...;

        Transaction tx = pm.currentTransaction();

        try {
            tx.begin();
    
            ClubMembers members = pm.getObjectById(ClubMembers.class, "k12345");
            members.incrementCounterBy(1);
            pm.makePersistent(members);
    
            tx.commit();
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }

Grupos de entidades

Cada entidade pertence a um grupo de entidade, um conjunto de uma ou mais entidades que podem ser manipuladas em uma única transação. Os relacionamentos de grupo de entidades informam ao Google App Engine que este deve armazenar diversas entidades na mesma parte da rede distribuída. Uma transação configura operações de armazenamento de dados para um grupo de entidades e todas as operações são aplicadas como um grupo ou não são aplicadas, se a transação falhar.

Ao criar uma entidade, o aplicativo pode atribuir outra entidade como pai da nova entidade. Atribuir um pai a uma nova entidade a coloca no mesmo grupo que a entidade pai.

Uma entidade sem pai é uma entidade raiz. Uma entidade que é pai de outra também pode ter um pai. Uma cadeia de entidades pai desde uma entidade até a raiz é o caminho da entidade, e os membros do caminho são os ancestrais da entidade. O pai de uma entidade é definido quando a entidade é criada e não pode ser alterado posteriormente.

Todas as entidades com uma dada entidade raiz como ancestral estão no mesmo grupo de entidades. Todas as entidades de um grupo são armazenadas no mesmo nó do armazenamento de dados. Uma única transação pode modificar diversas entidades de um único grupo ou adicionar novas entidades ao grupo, tornando o pai da nova entidade uma entidade existente no grupo.

Como criar entidades com grupos de entidades

A implementação da interface JDO do Google App Engine representa relacionamentos proprietários de um-para-um ou de um-para-vários usando grupos de entidades. Isso permite alterar um objeto e os seus objetos filho para que ocorram na mesma transação. Consulte Relacionamentos.

Em outras situações, é possível criar uma entidade com um pai explícito do grupo de entidades ao definir o campo da chave principal do objeto para a chave completa, incluindo a chave do pai. O campo da chave principal do objeto deve ser uma instância Key ou uma string de chave codificada (e não um nome de chave Long ou String simples).

Ao usar IDs de string atribuídos pelo aplicativo, você pode criar um objeto com um pai do grupo de entidades ao definir o seu campo de chave para o valor da chave completa, o que inclui a chave do pai. Para criar um valor de chave usando um pai do grupo de entidades, use a classe KeyFactory.Builder, como mostrado abaixo:

import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class AccountInfo {
    @PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;

    public void setKey(Key key) {
        this.key = key;
    }
}

// ...
        KeyFactory.Builder keyBuilder = new KeyFactory.Builder(Customer.class.getSimpleName(), "custid985135");
        keyBuilder.addChild(AccountInfo.class.getSimpleName(), "acctidX142516");
        Key key = keyBuilder.getKey();

        AccountInfo acct = new AccountInfo();
        acct.setKey(key);
        pm.makePersistent(acct);

É possível acessar a chave do pai do grupo de entidades separadamente da chave do objeto usando um campo, como mostrado abaixo:

// ...
    @Persistent
    @Extension(vendorName="datanucleus", key="gae.parent-pk", value="true")
    private Key customerKey;

Para criar um objeto com um ID numérico gerado pelo sistema e um pai do grupo de entidades, use um campo de chave do pai do grupo de entidades (como o customerKey, acima). Atribua a chave do pai ao campo da chave do pai e deixe o campo da chave do objeto definido como nulo. Depois de salvar o objeto, o armazenamento de dados preenche o campo da chave com a chave completa, incluindo o pai do grupo de entidades.

Se uma classe tiver um campo do pai do grupo de entidades, é possível usar um filtro de igualdade em uma consulta no campo do pai (as chaves do pai não suportam filtros de desigualdade).

O que pode ser feito em uma transação

O armazenamento de dados impõe diversas restrições sobre o que pode ser feito dentro de uma única transação.

Todas as operações de armazenamento de dados de uma transação devem ser realizadas em entidades do mesmo grupo. Isso inclui a recuperação das entidades por chave, a atualização das entidades e a exclusão das entidades. Cada entidade raiz pertence a um grupo de entidades separado. Assim, uma única transação não pode criar ou operar com mais de uma entidade raiz.

Um aplicativo não pode executar uma consulta durante uma transação. No entanto, um aplicativo pode recuperar entidades do armazenamento de dados usando chaves durante uma transação e ter a garantia de que a entidade obtida é consistente com o resto da transação. Você pode preparar as chaves antes da transação ou pode construir chaves dentro da transação com os nomes ou IDs da chave.

Um aplicativo não pode criar ou atualizar uma entidade mais de uma vez em uma única transação.

A JDO realiza todas as ações entre a chamada para tx.begin() e a chamada para tx.commit() em uma única transação. Se alguma ação falhar devido ao uso do grupo de entidades solicitado por outro processo, a JDO gera uma JDODataStoreException ou uma JDOException, causada por uma java.util.ConcurrentModificationException.

Em um sistema com concorrência otimista, é normal que um aplicativo tente realizar a transação várias vezes antes de desistir. A JDO realiza a transação apenas uma vez. O aplicativo deve repetir a transação, se desejado. Por exemplo:

        for (int i = 0; i < NUM_RETRIES; i++) {
            pm.currentTransaction().begin();

            ClubMembers members = pm.getObjectById(ClubMembers.class, "k12345");
            members.incrementCounterBy(1);

            try {
                pm.currentTransaction().commit();
                break;

            } catch (JDOCanRetryException ex) {
                if (i == (NUM_RETRIES - 1)) { 
                    throw ex;
                }
            }
        }

A tentativa de atualizar mais de um grupo de entidades na mesma transação gerará uma JDOFatalUserException. Observe que cada objeto que não tem um pai do grupo de entidades fica no seu próprio grupo de entidades. Portanto, você não pode criar várias entidades sem pai em uma única transação.

A tentativa de atualizar a mesma entidade várias vezes em uma única transação (como chamadas makePersistent() repetidas) gerará uma JDOFatalUserException. Em vez disso, apenas modifique os objetos persistentes na transação e permita a chamada para commit() para aplicar as alterações.

Usos para as transações

Este exemplo demonstra um uso das transações: a atualização de uma entidade com um novo valor de propriedade relativo ao seu valor atual.

        Key k = KeyFactory.createKey("Employee", "k12345");
        Employee e = pm.getObjectById(Employee.class, k);
        e.counter += 1;
        pm.makePersistent(e);

Isso exige uma transação, pois o valor pode ser atualizado por outro usuário após o código obter o objeto, mas antes de salvar o objeto modificado. Sem uma transação, a solicitação do usuário usará o valor de counter anterior à atualização do outro usuário e a gravação substituirá o novo valor. Com uma transação, o aplicativo recebe as informações sobre a atualização do outro usuário. Se a entidade for atualizada durante a transação, ocorrerá uma falha da transação com uma exceção. O aplicativo pode repetir a transação para usar os novos dados.

Outro uso comum para as transações é atualizar uma entidade com uma chave nomeada ou criá-la, caso ainda não exista:

        // PersistenceManager pm = ...;

        Transaction tx = pm.currentTransaction();

        String id = "jj_industrial";
        String companyName = "J.J. Industrial";
       
        try {
            tx.begin();

            Key k = KeyFactory.createKey("SalesAccount", id);
            SalesAccount account;
            try {
                account = pm.getObjectById(Employee.class, k);
            } catch (JDOObjectNotFoundException e) {
                account = new SalesAccount();
                account.setId(id);
            }

            account.setCompanyName(companyName);
            pm.makePersistent(account);

            tx.commit();

        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }

Assim como antes, é necessária uma transação para manipular o caso no qual outro usuário tenta criar ou atualizar uma entidade com o mesmo ID de string. Sem uma transação, se a entidade não existir e dois usuários tentarem criá-la, o segundo substituirá o primeiro sem saber que isso aconteceu. Com uma transação, a segunda tentativa falhará atomicamente e o aplicativo poderá fazer uma nova tentativa para obter a nova entidade e atualizá-la.

Dica: Uma transação deve ocorrer o mais rápido possível para reduzir a possibilidade de que as entidades usadas pela transação sejam alteradas, exigindo novas tentativas da transação. Procure preparar os dados fora da transação o máximo possível e, em seguida, execute a transação para realizar as operações com o armazenamento de dados que dependam de um estado consistente. O aplicativo deve preparar as chaves para os objetos usados dentro da transação e obter as entidades dentro da transação.

Como desativar transações e transferir aplicativos JDO existentes

A configuração JDO recomendada define uma propriedade chamada datanucleus.appengine.autoCreateDatastoreTxns como true. Essa é uma propriedade específica do Google App Engine que instrui a implementação JDO a associar transações de armazenamento de dados com transações JDO gerenciadas no código do aplicativo. Se você estiver construindo um novo aplicativo do zero, provavelmente é isso o que você deseja. No entanto, se você tiver um aplicativo existente com base em JDO que deseja executar no Google App Engine, convém usar uma configuração persistente 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 porque isso pode ser útil, lembre-se de que você pode operar apenas em objetos que pertençam ao mesmo grupo de entidades dentro de uma transação. Os aplicativos construídos usando bancos de dados tradicionais normalmente assumem a disponibilidade das transações globais, que permitem a atualização de qualquer conjunto de registros dentro de uma transação. Como o armazenamento de dados do Google App Engine não suporta transações globais, o código que assume a disponibilidade das transações globais produzirá exceções. Em vez de analisar a sua base de códigos (possivelmente grande) e remover todo o seu código de gerenciamento de transação, você pode simplesmente desativar as transações do armazenamento de dados. É claro que isso não resolve em nada as suposições feitas pelo seu código com relação à atomicidade das modificações de diversos registros, mas permite que você faça o seu aplicativo funcionar para que possa se concentrar na recriação do seu código transacional, pouco a pouco e conforme o necessário, em vez de fazê-lo de uma vez.