Содержание


Платформа Google App Engine для Java-приложений

Часть 3. Хранение объектов и связи между ними

Сохранение Java-объектов в хранилище данных платформы Google App Engine

Comments

Серия контента:

Этот контент является частью # из серии # статей: Платформа Google App Engine для Java-приложений

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Платформа Google App Engine для Java-приложений

Следите за выходом новых статей этой серии.

Одной из целей платформы App Engine для Java является избавление разработчиков от необходимости создания слоя хранения данных для масштабируемых Web-приложений. Была ли эта цель достигнута? Эта статья завершает мое введение в платформу Google App Engine описанием ее инфраструктуры хранения данных, которая базируется на API JDO (Java Data Object) и JPA (Java Persistence API). Идея этой инфраструктуры изначально выглядела многообещающе, однако она не лишена серьезных недостатков, которые будут объяснены ниже. Прочитав эту статью, вы узнаете о принципах работы инфраструктуры, существующих проблемах, а также о вариантах хранения данных, предоставляемых платформой облачных вычислений от Google Java-разработчикам.

В процессе чтения статьи и выполнения сопутствующих примеров не следует забывать, что платформы App Engine для Java в настоящее время находятся в стадии предварительного релиза. Таким образом, даже если инфраструктура хранения Java-объектов на данный момент не отвечает вашим требованиям или надеждам, она может и должна измениться в будущем. Мое личное ощущение от использования App Engine для создания масштабируемых Java-приложений, интенсивно работающих с данными, заключается в том, что эта задача не для робких или консервативных разработчиков. Вы скорее почувствуете себя брошенным в глубокую реку — спасателей поблизости нет, поэтому выплывет ваш проект или пойдет ко дну — зависит целиком и полностью от вас.

Обратите внимание, что примеры к этой статье базируются на приложении для управления контактами, которое было создано в предыдущем выпуске серии. Оно должно быть собрано и готово к запуску, чтобы вы могли выполнять примеры, приведенные ниже.

Основные принципы инфраструктуры хранения данных и ее дырявые абстракции

Аналогично изначальной версии App Engine, Googe App Engine для Java использует внутреннюю инфраструктуру Google под названием BigTree для обслуживания масштабируемых Java-приложений, а именно: для распределения данных, репликации и балансирования нагрузки. Большая часть этих действий выполняется прозрачно для Java-разработчиков, которым предоставляется набор стандартных API Java. Интерфейсы самого хранилища основаны на JDO и JPA, которые, в свою очередь, базируются на открытом проекте  DataNucleus. Кроме того, App Engine для Java предоставляет специальный интерфейс, представляющий собой низкоуровневый адаптер для непосредственного обращения к хранилищу данных App Engine. Этот интерфейс основывается на реализации BigTable от Google (более подробная информация о BigTable приведена в первой статье серии).

Инфраструктура хранения Java-данных в App Engine для Java не столь очевидна в использовании, как аналогичная инфраструктура в оригинальной версии App Engine. Абстракции на базе JDO и JPA оказываются несколько "дырявыми" (leaky), поскольку BigTable не является реляционной системой управления базами данных. Например, App Engine для Java не позволяет выполнять запросы с операторами соединения. Вы можете описывать отношения в JPA и JDO, но они могут отражать только явные связи в базе данных. Кроме того, в рамках одной атомарной транзакции могут сохраняться только объекты, принадлежащие одной группе сущностей. В соответствии с принятыми соглашениями, связи принадлежат той же группе сущностей, что и их родители. В то же время связи, не имеющие родителей, находятся в разных группах.

Пересмотр нормализации данных

Работа с масштабируемым хранилищем данных App Engine требует от вас пересмотра убеждений о преимуществах нормализации. Разумеется, если вы достаточно долго занимаетесь разработкой реальных систем, то вам уже приходилось жертвовать нормализацией в угоду производительности. Однако при использовании хранилища App Engine данные приходится денормализовывать постоянно. Денормализация больше не является чем-то предосудительным, более того, это средство проектирования, которое вы будете использовать во многих ситуациях при создании приложений для App Engine.

Один из основных недостатков дырявого слоя абстрагирования инфраструктуры хранения данных App Engine проявляется при попытке переноса приложения, написанного для реляционной СУБД, на эту платформу. Хранилище App Engine не проектировалось в качестве полной замены РСУБД, поэтому действия, поддерживаемые платформой, не так просто использовать в приложении, изначально созданном на основе реляционной базы данных. Еще менее вероятно, что вам удастся взять существующую схему базы данных и транслировать ее в модель данных App Engine. Таким образом, если вы решите переносить ранее созданное корпоративное Java-приложение на платформу App Engine, тщательно продумывайте каждый свой шаг. Google App Engine – это платформа для изначально проектировавшихся для нее приложений, хотя она поддерживает интерфейсы JDO и JPA для перевода этих приложений в более традиционную, пусть и денормализованную, форму.

Сложности со связями

Другим слабым местом предварительной версии платформы App Engine для Java является работа со связями между сохраняемыми объектами. Для создания связей необходимо использовать расширения JDO, специфичные для App Engine. Ключи генерируются на основе артефактов BigTable, например, "первичные ключи" включают ключи типа "родитель-дети", которые являются частью всех дочерних объектов, поэтому вам приходится работать с нереляционной базой данных. Сохранение данных также имеет недостатки, одним из которых является необходимость использования нестандартного класса Key. Во-первых, неясно, что делать с этим классом при переносе вашей модели данных на РСУБД. Во-вторых, этот класс не может транслироваться в JavaScript при помощи GWT, поэтому использующие его объекты модели данных не могут являться частью GWT-слоя приложения.

Разумеется, на момент написания этой статьи доступна только предварительная версия Google App Engine для Java, которая еще не готова для промышленных приложений. Это становится совершенно очевидно при чтении документации по работе со связями JDO, которая имеет множество пробелов и незавершенных примеров.

Пакет разработчика приложения для платформы App Engine для Java поставляется вместе с набором демонстрационных примеров, многие из которых используют JDO (и ни один не использует JPA). При этом нет ни одного примера, включающего работу с простой связью (даже среди тех, которые называются jdoexamples). Вместо этого иллюстрируется сохранение единичных объектов в хранилище данных. Форум по Google App Engine для Java переполнен вопросами об использовании связей с редко встречающимися ответами. По-видимому, некоторые разработчики сумели заставить связи работать, но это потребовало от них существенных усилий.

Общее мнение таково, что управлять связями в App Engine для Java необходимо без существенной поддержки со стороны JDO и JPA. Вам следует использовать BigTable – технологию, которая успешно зарекомендовала себя в качестве основы масштабируемых приложений. Работа с BigTable избавляет вас от необходимости работы с сырыми API, но при этом приходится иметь дело с более низкоуровневыми интерфейсами.

Работа с JDO в App Engine для Java

Несмотря на все трудности переноса традиционных Java-приложений на App Engine, а также сложности со связями, существуют ситуации, при которых имеет смысл использовать инфраструктуру хранения данных Google App Engine. Мы завершим эту статью рассмотрением примера, который должен дать вам представление о том, как работает слой хранения данных этой платформы. Мы начнем с приложения для управления контактами, созданного в предыдущей статье, однако на этот раз объекты Contact будут помещаться в хранилище данных App Engine.

В предыдущей статье мы создали простой GWT-интерфейс, позволяющий выполнять операции CRUD над объектами типа Contact. Интерфейс доступа к данным приведен в листинге 1.

Листинг 1. Простой интерфейс ContactDAO
package gaej.example.contact.server;

import java.util.List;

import gaej.example.contact.client.Contact;

public interface ContactDAO {
	void addContact(Contact contact);
	void removeContact(Contact contact);
	void updateContact(Contact contact);
	List<Contact> listContacts();
}

Далее мы создали фиктивную реализацию этого интерфейса, которая хранила данные в оперативной памяти (листинг 2).

Листинг 2. Фиктивная реализация DAO в классе ContactDAOMock
package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class ContactDAOMock implements ContactDAO {

	Map<String, Contact> map = new LinkedHashMap<String, Contact>();
	
	{
		map.put("rhightower@mammatus.com", new Contact("Rick Hightower", 
                                 "rhightower@mammatus.com", "520-555-1212"));
		map.put("scott@mammatus.com", new Contact("Scott Fauerbach", 
                                 "scott@mammatus.com", "520-555-1213"));
		map.put("bob@mammatus.com", new Contact("Bob Dean", 
                                 "bob@mammatus.com", "520-555-1214"));

	}
	
	public void addContact(Contact contact) {
		String email = contact.getEmail();
		map.put(email, contact);
	}

	public List<Contact> listContacts() {
		return Collections.unmodifiableList(new ArrayList<Contact>(map.values()));
	}

	public void removeContact(Contact contact) {
		map.remove(contact.getEmail());
	}

	public void updateContact(Contact contact) {		
		map.put(contact.getEmail(), contact);
	}

}

Далее мы рассмотрим, что произойдет, если заменить эту фиктивную реализацию классом, который будет взаимодействовать с хранилищем данных Google App Engine. Мы будем использовать JDO для сохранения экземпляров класса Contact. Поскольку мы использовали модуль App Engine для Eclipse, наше приложение уже включает все библиотеки, необходимые для использования JDO. Оно также включает файл jdoconfig.xml, поэтому для работы с JDO остается только добавить аннотации к классу Contact.

В листинге 3 показана расширенная реализация ContactDAO, использующая JDO для сохранения, выборки, обновления и удаления объектов.

Листинг 3. Реализация интерфейса ContactDAO на основе JDO
package gaej.example.contact.server;

import gaej.example.contact.client.Contact;

import java.util.List;

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

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

	public static PersistenceManagerFactory getPersistenceManagerFactory() {
		return pmfInstance;
	}

	public void addContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		try {
			pm.makePersistent(contact);
		} finally {
			pm.close();
		}
	}

	@SuppressWarnings("unchecked")
	public List<Contact> listContacts() {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		String query = "select from " + Contact.class.getName();
		return (List<Contact>) pm.newQuery(query).execute();
	}

	public void removeContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		try {
			pm.currentTransaction().begin();

			// У нас нет ссылки на удаляемый контакт,
			// поэтому необходимо найти его в хранилище
			contact = pm.getObjectById(Contact.class, contact.getId());
			pm.deletePersistent(contact);

			pm.currentTransaction().commit();
		} catch (Exception ex) {
			pm.currentTransaction().rollback();
			throw new RuntimeException(ex);
		} finally {
			pm.close();
		}
	}

	public void updateContact(Contact contact) {
		PersistenceManager pm = getPersistenceManagerFactory()
				.getPersistenceManager();
		String name = contact.getName();
		String phone = contact.getPhone();
		String email = contact.getEmail();

		try {
			pm.currentTransaction().begin();
			// У нас нет ссылки на обновляемый контакт,
			// поэтому необходимо найти его в хранилище
			contact = pm.getObjectById(Contact.class, contact.getId());
			contact.setName(name);
			contact.setPhone(phone);
			contact.setEmail(email);
			pm.makePersistent(contact);
			pm.currentTransaction().commit();
		} catch (Exception ex) {
			pm.currentTransaction().rollback();
			throw new RuntimeException(ex);
		} finally {
			pm.close();
		}
	}

}

Реализация методов

Далее рассмотрим реализацию каждого из методов, показанных в листинге 3. Вы увидите, что, несмотря на измененные имена методов, их предназначение вам знакомо.

Во-первых, мы создали статическую фабрику PersistenceManagerFactory для обращения к объектам PersistenceManager. Если у вас есть опыт работы с JPA, то PersistenceManager может напомнить вам EntityManager или Hibernate Session, если вы ранее использовали Hibernate. По сути, PersistenceManager является основным интерфейсом для системы хранения данных в JDP. Он представляет собой сессию работы с базой данных. Метод getPersistenceManagerFactory() возвращает статически инициализированную фабрику PersistenceManagerFactory, как показано в листинге 4.

Листинг 4. Метод getPersistenceManagerFactory() возвращает экземпляр PersistenceManagerFactory
private static final PersistenceManagerFactory pmfInstance = JDOHelper
		.getPersistenceManagerFactory("transactions-optional");

public static PersistenceManagerFactory getPersistenceManagerFactory() {
	return pmfInstance;
}

Метод addContact() добавляет новую контактную запись в хранилище. Для этого он сначала создает экземпляр PersistenceManager, а затем вызывает его метод makePersistence(), который преобразует экземпляр Contact, содержащий значения, заполненные пользователями через интерфейс GWT, в объект постоянного хранения (persistent object). Код метода показан в листинге 5.

Листинг 5. Метод addContact()
public void addContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	try {
		pm.makePersistent(contact);
	} finally {
		pm.close();
	}
}

Обратите внимание, что в листинге 5 объект persistenceManager находится внутри блока finally, что гарантирует высвобождение связанных с ним ресурсов.

Метод listContact(), показанный в листинге 6, создает запрос при помощи persistenceManager, который соответствует источнику данных. Затем он вызывает метод execute(), который возвращает список контактов из хранилища.

Листинг 6. Метод listContact()
@SuppressWarnings("unchecked")
public List<Contact> listContacts() {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	String query = "select from " + Contact.class.getName();
	return (List<Contact>) pm.newQuery(query).execute();
}

Метод removeContact() проверяет наличие контактной записи с указанным идентификатором перед попыткой ее удаления (листинг 7). Эта проверка необходима, поскольку удаляемый экземпляр Contact был получен от слоя интерфейса, который ничего не знает о слое хранения данных. Для удаления контактной записи надо сначала получить ассоциированный с ней экземпляр Contact, находящийся в кэше PersistenceManager.

Листинг 7. Метод removeContact()
public void removeContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	try {
		pm.currentTransaction().begin();

		// У нас нет ссылки на удаляемый контакт,
		// поэтому необходимо найти его в хранилище
		contact = pm.getObjectById(Contact.class, contact.getId());
		pm.deletePersistent(contact);

		pm.currentTransaction().commit();
	} catch (Exception ex) {
		pm.currentTransaction().rollback();
		throw new RuntimeException(ex);
	} finally {
		pm.close();
	}
}

Метод updateContact(), показанный в листинге 8, похож на removeContact() тем, что он так же проверяет наличие экземпляра Contact в хранилище. Выполнив проверку, метод копирует все свойства контакта, полученного от слоя GWT, в экземпляр Contact, полученный от PersistenceManager. Объекты, запрошенные у менеджера хранилища, проверяются им на предмет изменений. В случае обнаружения изменений объект PersistenceManager сохраняет их в базе данных и подтверждает транзакцию.

Листинг 8. Метод updateContact()
public void updateContact(Contact contact) {
	PersistenceManager pm = getPersistenceManagerFactory()
			.getPersistenceManager();
	String name = contact.getName();
	String phone = contact.getPhone();
	String email = contact.getEmail();

	try {
		pm.currentTransaction().begin();
		// У нас нет ссылки на редактируемый контакт,
		// поэтому необходимо найти его в хранилище
		contact = pm.getObjectById(Contact.class, contact.getId());
		contact.setName(name);
		contact.setPhone(phone);
		contact.setEmail(email);
		pm.makePersistent(contact);
		pm.currentTransaction().commit();
	} catch (Exception ex) {
		pm.currentTransaction().rollback();
		throw new RuntimeException(ex);
	} finally {
		pm.close();
	}
}

Аннотирование сохраняемых классов

Для того чтобы экземпляры Contact были сохраняемыми объектами, необходимо отметить этот класс аннотацией @PersistenceCapable. Кроме того, следует добавить аннотации ко всем его сохраняемым свойствам, как показано в листинге 9.

Листинг 9. Аннотирование класса Contact
package gaej.example.contact.client;

import java.io.Serializable;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Contact implements Serializable {

	private static final long serialVersionUID = 1L;
	@PrimaryKey
	@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
	private Long id;
	@Persistent
	private String name;
	@Persistent
	private String email;
	@Persistent
	private String phone;

	public Contact() {

	}

	public Contact(String name, String email, String phone) {
		super();
		this.name = name;
		this.email = email;
		this.phone = phone;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

}

Благодаря преимуществам объектно-ориентированного программирования и проектирования на основе интерфейсов, нам достаточно просто заменить изначальный класс ContactDAOMock на только что созданный ContactJdoDAO. Это изменение будет совершенно прозрачным для GWT-интерфейса приложения.

Единственное, что необходимо изменить в результате смены класса для доступа к данным — это способ инициализации DAO в сервисе (листинг 10).

Листинг 10. Сервлет RemoteServiceServlet
public class ContactServiceImpl extends RemoteServiceServlet implements ContactService {
	private static final long serialVersionUID = 1L;
	//private ContactDAO contactDAO = new ContactDAOMock();
	private ContactDAO contactDAO = new ContactJdoDAO();
...

Заключение

В этой серии из трех статей вы познакомились с платформой Google App Engine для Java, в частности, с ее инфраструктурой для хранения данных, которая играет одну из ключевых ролей для масштабируемых корпоративных приложений. В целом данная инфраструктура оставила разочаровывающее впечатление, хотя важно помнить, что платформа пока находится на стадии предварительного релиза. Java-приложения для App Engine оказываются привязанными к низкоуровневым элементам инфраструктуры, несмотря на использование JDO или JPA. Кроме того, платформа содержит недостаточно документации о своей системе хранения данных, а из поставляемых с ней примеров невозможно понять, как следует работать с сохраняемыми связями.

Даже если бы поддержка JDO и JPA была полностью завершена, представляется маловероятным, что вы сможете создать Java-приложение для App Engine и с легкостью перенести его в окружение на основе РСУБД. Как минимум, для этого вам придется написать изрядный объем кода.

Я искренне надеюсь, что инфраструктура хранения данных будет усовершенствована со временем. В настоящее время, если вам действительно необходимо работать с App Engine, то, скорее всего, следует игнорировать стандартные API и использовать низкоуровневые интерфейсы хранилища. Подводя итоги, можно сказать, что работать с платформой App Engine вполне возможно. Однако если вы ранее использовали JDO или JPA, то далее вам придется учиться на собственных ошибках, во-первых, из-за дырявых абстракций, описанных в начале этой статьи, а во-вторых, из-за тех функций инфраструктуры, которые либо не работают должным образом, либо плохо документированы.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: Google App Engine for Java: Part 3: Persistence and relationships (Ричард Хайтауэр, developerWorks, агуст 2009 г.). (EN)
  • Прочитайте статью Платформа Google App Engine для Java-приложений, часть 1: Знакомство (Ричард Хайтауэр, developerWorks, август 2009 г.) и узнайте о том, как быстро создавать простые но масштабируемые приложения при помощи средств Eclipse для платформы App Engine. (EN)
  • Ознакомьтесь со статьей Платформа Google App Engine для Java-приложений, часть 2: Создание шедевра (Ричард Хайтауэр, developerWorks, август 2009 г.), в которой содержится краткое руководство по созданию собственного менеджера контактов, приоткрывающего возможности платформы Google App Engine. Вам понадобится это приложение для выполнения примеров к настоящей статье. (EN)
  • Ознакомьтесь со статьей Использование облачных вычислений. Часть 1: возможности облачных вычислений для приложений (Марк О'Нил, Mark O'Neill, developerWorks, апрель 2009 г.). В трех статьях этой серии описываются платформы облачных вычислений от трех крупнейших производителей: Amazon, Google, Microsoft® и SalesForce.com. (EN)
  • Обратите внимание на видео Google IO, в котором инженер Макс Росс (Max Ross) из Google рассказывает о взамодействии стандартов JDO и JPA с хранилищем данных Google App Engine. (EN)
  • Прочитайте руководство по JDO проекта Apache DB и узнайте об использовании ObjectRelationalBridge и API JDO для решения простых прикладных задач. (EN)
  • Ознакомьтесь с научными публикациями Google, чтобы узнать, что такое BigTable. (EN)
  • Посетите сайт открытого проекта DataNucleus, который включает ряд программных продуктов для управления данными при помощи Java-технологий (этот проект ранее назывался JPOX - Java Persistent Objects). (EN)
  • Загрузите подключаемые модули App Engine для Eclipse. (EN)
  • Получите учетную запись App Engine для развертывания ваших приложений на инфраструктуре Google App Engine. (EN)
  • Узнайте больше о Google App Engine на официальном сайте платформы. (EN)
  • Более подробную информацию об использовании возможностей App Engine можно почерпнуть из Java-документации платформы. (EN)
  • Узнайте о том, какие стандартные API и инфраструктуры Java совместимы с App Engine, на странице Будет ли это работать в App Engine для Java? (EN)

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Мобильные приложения, Open source
ArticleID=696684
ArticleTitle=Платформа Google App Engine для Java-приложений: Часть 3. Хранение объектов и связи между ними
publish-date=07012011