Google Code предлагается на следующих языках: English – Español – 日本語 – 한국어 – Português – Pусский – 中文(简体) – 中文(繁體)
Хранилище данных App Engine поддерживает транзакции. Транзакция представляет собой одну или несколько операций, которые либо полностью выполняются, либо полностью не выполняются. Приложение может выполнить несколько операций и вычислений в единой транзакции.
Транзакция представляет собой одну или несколько операций в хранилище данных, которые либо полностью выполняются, либо полностью не выполняются. Если транзакция выполняется успешно, то к хранилищу данных применяются результаты всех ее действий. Если во время выполнения транзакции произошел сбой, не применяется ни один.
Каждая операция записи в хранилище данных является атомарной. Создание, обновление или удаление объекта либо выполняется, либо нет. Операция может не быть выполнена из-за многочисленных конфликтов, возникающих вследствие одновременных попыток изменения одного и того же объекта несколькими пользователями. Она также может не быть выполнена, если приложение достигло назначенной квоты. Кроме того, в хранилище данных может возникнуть внутренняя ошибка. Во всех этих случаях результаты выполнения операции не применяются и API хранилища данных вызывает исключение.
Рассмотрим пример увеличения значения поля counter в объекте под названием ClubMembers (класс не показан) с помощью API транзакций для 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();
}
}
Каждый объект входит в группу объектов, т.е. в набор из одного или нескольких объектов, которыми можно управлять в одной транзакции. Отношения групп объектов указывают App Engine, что необходимо хранить несколько объектов в одной части распределенной сети. Транзакция задает операции в хранилище данных для группы объектов. Все операции применяются ко всей группе или не применяются вообще, если выполнение транзакции не удалось.
Когда приложение создает объект, оно может указать, что другой объект является родителем нового объекта. Указание родителя для нового объекта включает новый объект в группу объектов, в которую входит родительский объект.
Объект без родителя является корневым объектом. Объект, который является родителем другого объекта, также может иметь родителя. Последовательность родительских объектов от данного объекта до корневого составляет путь для объекта, участники которого являются родителями объекта. Родитель объекта определяется во время создания объекта и не может быть изменен впоследствии.
Все объекты с данным корневым объектом в качестве родителя находятся в одной группе объектов. Все объекты в группе хранятся в одном узле хранилища данных. Одна транзакция может изменить несколько объектов в одной группе или добавить новые объекты в группу, включив родителя нового объекта в группу.
Реализация JDO-интерфейса в App Engine позволяет представляет зависимые отношения "один к одному" или "один ко многим" с помощью групп объектов. Это позволяет выполнять изменение объекта и всех его потомков в одной транзакции. См. статью Отношения.
Для других ситуаций можно создать объект с явно заданным родителем группы объектов, указав в поле первичного ключа объекта полный ключ, содержащий ключ родителя. Поле первичного ключа объекта должно содержать либо экземпляр класса Key, либо ключ, закодированный в виде строки (а не просто название ключа типа Long или String).
При использовании строковых идентификаторов, назначаемых приложением можно создать объект с родителем группы объектов, указав в его ключевом поле полное значение ключа, содержащее ключ родителя. Чтобы создать значение ключа с помощью родителя группы объектов, воспользуйтесь классом KeyFactory.Builder следующим образом:
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);
Получить доступ к ключу родителя группы объектов отдельно от ключа объекта можно с помощью поля:
// ...
@Persistent
@Extension(vendorName="datanucleus", key="gae.parent-pk", value="true")
private Key customerKey;
Чтобы создать объект с созданным системой числовым идентификатором и родителем группы объектов, необходимо использовать поле ключа родителя группы объектов (такое как customerKey выше). Задайте ключ родителя в качестве значения ключевого поля родителя, а значение ключевого поля объекта оставьте нулевым. При сохранении объекта хранилище данных запишет в ключевое поле полный ключ, содержащий родителя группы объектов.
Если в классе имеется поле родителя группы объектов, в запросах по полю родителя можно использовать фильтр равенства. (Фильтры неравенства для родительских ключей не поддерживаются.)
Хранилище данных налагает ряд ограничений на действия, которые можно выполнить в одной транзакции.
Все операции в хранилище данных, включенные в транзакцию, должны работать с объектами из одной группы. Это относится к получению объектов по ключу, обновлению и удалению объектов. Обратите внимание, что все корневые объекты входят в разные группы, поэтому одна транзакция не может создать или выполнить какие-либо иные действия более чем с одним корневым объектом.
Приложение не может выполнять запросы во время транзакции. Однако во время транзакции приложение может получать объекты из хранилища данных с помощью ключей и гарантированно получит объект, согласованный с остальными действиями транзакции. Ключи можно подготовить перед выполнением транзакции или создать внутри транзакции с помощью названий ключей или идентификаторов.
Приложение не может создать или обновить объект более одного раза в одной транзакции.
JDO выполняет все действия от вызова метода tx.begin() до вызова метода tx.commit() в одной транзакции. Если какое-либо из действий не удается выполнить из-за того, что запрошенная группа объектов используется другим процессом, JDO вызывает исключение JDODataStoreException или JDOException вследствие исключения java.util.ConcurrentModificationException.
В системе с оптимистической конкуренцией приложение нередко пытается выполнить транзакцию по несколько раз. JDO выполняет транзакцию лишь единожды; при необходимости ее должно повторять приложение. Например:
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;
}
}
}
Попытка внести обновления в несколько групп объектов за одну транзакцию вызовет исключение JDOFatalUserException. Обратите внимание, что каждый объект, у которого нет родителя группы объектов, находится в собственной группе, так что создать несколько объектов без родителей в одной транзакции нельзя.
Попытка многократного обновления одного и того же объекта в одной транзакции (как в случае повторяемых вызовов метода makePersistent()) вызовет исключение JDOFatalUserException. Вместо этого следует изменить постоянно хранящиеся объекты в транзакции, а затем применить изменения с помощью метода commit().
В следующем примере показан один из вариантов применения транзакций: обновление объекта путем присвоения его свойству нового значения.
Key k = KeyFactory.createKey("Employee", "k12345");
Employee e = pm.getObjectById(Employee.class, k);
e.counter += 1;
pm.makePersistent(e);
Для выполнения этого действия необходима транзакция, поскольку в противном случае другой пользователь может обновить это же значение после того, как данная операция получит объект, но до того, как она успеет сохранить его измененную версию. Если не использовать транзакцию, запрос пользователя использует значение свойства counter прежде, чем другой пользователь выполнит обновление, и при сохранении новое значение будет перезаписано. При использовании транзакции приложение узнает об обновлении, выполняемом другим пользователем. Если объект обновляется во время транзакции, то транзакция завершается и вызывается исключение. Приложение может повторно выполнить транзакцию, используя новые данные.
Другое распространенное применение транзакций – обновление объекта с помощью именованного ключа либо создание объекта, если он не существует:
// 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();
}
}
Как и ранее, транзакция необходима для корректной обработки случая, когда другой пользователь пытается создать или обновить объект с тем же строковым идентификатором. Если объект не существует и два пользователя попытаются создать его, то при отсутствии транзакции второй пользователь перезапишет объект, созданный первым, даже не зная об этом. При наличии транзакции атомарное действие второго пользователя завершится ошибкой, а когда приложение повторит попытку, оно получит новый объект и выполнит его обновление.
Совет. Транзакцию следует выполнять как можно быстрее, чтобы уменьшить вероятность того, что используемые ей объекты будут изменены и транзакцию придется выполнять повторно. Подготовьте как можно больше данных вне транзакции, а затем проведите ее, чтобы выполнить операции с хранилищем данных, зависящие от согласованного состояния. Приложение должно подготовить ключи для используемых в транзакции объектов, а затем получить объекты внутри транзакции.
Рекомендуемая конфигурация JDO устанавливает для свойства datanucleus.appengine.autoCreateDatastoreTxns значение true. Это свойство доступно только в App Engine. Оно сообщает реализации JDO, что необходимо связать транзакции хранилища данных с JDO-транзакциями, выполняемыми кодом приложения. Вероятно, такая возможность заинтересует вас при создании приложения с нуля. Однако если у вас уже есть JDO-приложение, которое нужно перенести на App Engine, можно воспользоваться альтернативной конфигурацией хранения данных, которая задает данному свойству значение 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>
Чтобы понять, чем это может быть полезно, вспомните о том, что внутри транзакции можно работать только с объектами, принадлежащими к одной и той же группе. Приложения, создаваемые для традиционных баз данных, обычно предполагают доступность глобальных транзакций, дающих возможность обновлять внутри транзакции любой набор записей. Поскольку хранилище данных App Engine не поддерживает глобальные транзакции, выполнение кода, подразумевающего, что они доступны, приведет к вызову исключений. Вместо того, чтобы просматривать весь свой код (который может быть велик) и удалять все, что относится к управлению транзакциями, вы можете просто отключить транзакции в хранилище данных. Конечно, это не решает проблему, связанную с предположениями кода об атомарности изменения нескольких записей, но позволяет вашему приложению работать, а вам – изменять код, выполняющий транзакции, постепенно и по необходимости, а не весь сразу.