В части 1 нашего знакомства с SimpleDB для моделирования CRUD-приложения, обрабатывающего данные о соревнованиях и их участниках, мы использовали API, разработанный компанией Amazon. Помимо непривычного для большинства Java-разработчиков решения Amazon использовать только строковые данные, сам по себе Amazon API, скорее всего, не вызвал у вас особенного восторга. В конце концов, современные API для работы с реляционными базами данных достаточно стандартизированы и продуманы, а самое главное, хорошо изучены.
Большинство современных реляционных инфраструктур включают в себя реализацию Java Persistence API. Благодаря этой особенности моделирование доменных объектов для Java-приложений практически любого типа в случае целого ряда реляционных СУБД превращается в легко решаемую и хорошо изученную задачу. Вполне понятно нежелание разработчиков использовать новые подходы и средства доменного моделирования, когда в их распоряжении уже имеются хорошо знакомые и работающие инструменты. В случае SimpleDB ничего нового изучать и не придется.
Во второй части моего обзора SimpleDB я расскажу вам, как с помощью рефакторинга можно обеспечить совместимость нашего приложения для хранения данных о соревнованиях со спецификацией JPA. Затем мы перенесем это приложение на SimpleJPA и рассмотрим несколько полезных особенностей этой инновационной открытой платформы, которые помогают быстрее адаптироваться к принципам NoSQL, упрощают процесс доменного моделирования и организацию облачных хранилищ.
Hibernate и JPA: краткий экскурс в историю
Большинство современных разработчиков Java используют для обеспечения долговременного хранения объектов данных Hibernate (и Spring). Hibernate не только занимает ведущее место в списке инфраструктур с открытым кодом - он практически произвел переворот в сфере ORM. До появления Hibernate разработчики Java утопали в болоте объектных компонентов EJB; нам приходилось либо разрабатывать свое собственное ORM, либо покупать готовые решения у таких вендоров, как IBM®. С появлением Hibernate все эти проблемы и затраты ушли в прошлое, уступив место POJO-платформе моделирования, которую современные разработчики уже воспринимают как данность.
Java Persistence API (JPA) появился в ответ на популярность инновационного решения Hibernate по использованию POJO (Plain old Java objects) для моделирования данных. В настоящее время JPA поддерживается в версии EJB 3.0 и в Google App Engine. Собственно, и сам Hibernate является реализацией JPA (в том случае, если вы используете Hibernate EntityManager).
Учитывая, насколько Java-разработчики привыкли использовать POJO при моделировании приложений для работы с данными, можно предположить, что аналогичная функциональность реализована и в SimpleDB. В конце концов, это же своего рода база данных, не правда ли?
Моделирование данных с помощью объектов
Для использования SimpleJPA нам придется пересмотреть наше определение объектов класса Racer и Runner с точки зрения спецификации JPA. К счастью, основы JPA достаточно просты: вы добавляете к POJO-объектам аннотации, а реализация EntityManager позаботится обо всем остальном – и не нужно никакого XML.
Две основные аннотации, используемые JPA, - это @Entity и @Id, которые соответственно определяют POJO как постоянно хранимый объект и задают его идентификационный ключ. Кроме того, для переноса нашего приложения на JPA нам потребуются еще две аннотации, используемые для управления взаимосвязями между объектами: @OneToMany и @ManyToOne.
В первой части данной статьи мы научились сохранять соревнования и их участников. При этом я не использовал объекты для представления этих сущностей, мы просто воспользовались для сохранения их свойств средствами Amazon API. Если бы мне потребовалось смоделировать взаимосвязи между соревнованиями и их участниками, я мог бы сделать это с помощью кода, приведенного в листинге 1:
Листинг 1. Простой объект Race
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
//методы-установщики и методы-получатели здесь пропущены
}
|
В листинге 1 я описал объект Race, обладающий четырьмя свойствами, последнее из которых – коллекция участников соревнований. После этого я могу создать объект Runner (см. листинг 2), в котором будет храниться имя участника (упростим задачу), его номер социального страхования и соревнование, в котором участвует данный спортсмен.
Листинг 2. Простой объект Runner, связанный с объектом Race
public class Runner {
private String name;
private String ssn;
private Race race;
//методы-установщики и методы-получатели здесь пропущены
}
|
Как показано в листингах 1 и 2, я логически определил модель связи между бегунами и соревнованиями как много-к-одному. В реальной модели следовало бы создать связи типа много-ко-многим (поскольку один и тот же спортсмен, как правило, участвует в нескольких соревнованиях), но мы пока не будем усложнять задачу. Кроме того, я опустил конструкторы, методы-установщики и методы-получатели. Мы включим их в код чуть позже.
Адаптация созданных выше объектов к реализации SimpleJPA не потребует значительных усилий. Сначала нужно указать, что объекты должны храниться постоянно. Для этого я добавлю к каждому из объектов аннотацию @Entity. Кроме того, следует определить взаимосвязи между объектами с помощью аннотации @OneToMany для объекта Race и @ManyToOne для объекта Runner.
Аннотация @Entity применяется на уровне класса, в то время как аннотации отношений используются на уровне методов-получателей. Все вышесказанное продемонстрировано в листингах 3 и 4:
Листинг 3. Объект Race с JPA-аннотацией
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//остальные методы-установщики и методы-получатели здесь пропущены
}
|
В листинге 3 метод getRunners имеет аннотацию @OneToMany. Дополнительно указывается, что связь определяется свойством race объекта Runner.
Аналогичным образом аннотируется метод getRace объекта Runner (см. листинг 4):
Листинг 4. Объект Runnerс JPA-аннотацией
@Entity
public class Runner {
private String name;
private String ssn;
private Race race;
@ManyToOne
public Race getRace() {
return race;
}
//other setters and getters left out...
}
|
Большинство хранилищ (как реляционных, так и нереляционных) требуют какого-либо способа задания уникальности данных. Таким образом, если мне нужно хранить эти два объекта в базе данных постоянно, я должен как минимум добавить каждому из них уникальный идентификатор. Листинг 5 добавляет доменному объекту Race свойство id типа BigInteger. Точно такую же операцию я применю к объекту Runner.
Листинг5. Добавление ID к объекту Race
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
private BigInteger id;
@Id
public BigInteger getId() {
return id;
}
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//остальные методы-установщики и методы-получатели здесь пропущены
}
|
Аннотация @Id в листинге 5 не дает никакой информации об управлении ID. Программа предполагает, что все манипуляции над этим свойством я буду проводить самостоятельно, вручную, без использования EntityManager.
До сих пор все мои операции никак не были связаны с SimpleDB. Объекты Race и Runner с JPA-аннотациями могут постоянно храниться в произвольной базе данных, поддержка которой включена в реализацию JPA. Список подобных баз включает в себя Oracle, DB2, MySQL и, как вы, вероятно, уже догадались, SimpleDB.
SimpleJPA – это открытая реализация JPA для Amazon SimpleDB. Несмотря на то, что SimpleJPA не поддерживает полностью спецификацию JPA (например, в JPA-запросах нельзя использовать join), объем поддерживаемой функциональности достаточно велик и заслуживает отдельного исследования.
Одно из основных преимуществ SimpleJPA заключается в том, что он позволяет с легкостью обойти особенности лексикографического поиска, с которыми мы столкнулись в первой части этой статьи. SimpleJPA конвертирует числовые типы в строки и добавляет, если требуется, необходимое число ведущих нулей. В большинстве случаев это значит, что вам не потребуется дополнительных преобразований доменной модели для работы со строковыми типами (есть лишь одно исключение, которое мы рассмотрим чуть позже).
Поскольку SimpleJPA - это реализация JPA, то при работе с SimpleJPA вы можете использовать JPA-совместимые домены. Единственное требование при этом – использовать строковые идентификаторы объектов. Это означает, что в вашей модели свойство id должно иметь тип java.lang.String. Чтобы упростить задачу, SimpleJPA включает базовый класс IdedTimestampedBase, который используется для задания ID доменных объектов, а также для создания и обновления атрибутов времени (по сути, SimpleDB создает уникальный идентификатор объекта).
Перенос приложений на SimpleJPA
Для обеспечения совместимости классов Race и Runner с SimpleJPA я могу либо расширить весьма удобный базовый класс, либо изменить тип идентификатора классов Race и Runner с BigInteger на String. Я предпочитаю воспользоваться первым вариантом (см. листинг 6):
Листинг 6. Преобразование класса Race в расширение базового класса SimpleJPA IdedTimestampedBase
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//остальные методы-установщики и методы-получатели здесь пропущены
}
|
Я не привожу здесь аналогичное преобразование класса Runner - вы вполне можете выполнить его самостоятельно. Все, что вам нужно сделать, - это расширить базовый класс IdedTimestampedBase и убрать объявление свойства id класса Runner.
Изменение типа идентификаторов классов Race и Runner – это первый шаг в цепочке преобразований, необходимых для обеспечения совместимости нашего приложения с SimpleJPA. Следующий шаг – преобразование базовых типов данных, таких как double, int и float, к типам Integer и BigDecimal.
Начнем со свойства distance объектов класса Race. По моему мнению, тип BigDecimal более удобен в использовании, чем тип Double (по крайней мере в текущей реализации SimpleJPA), так что я изменю тип distance на BigDecimal (см. листинг 7):
Листинг 7. Изменение типа distance на BigDecimal
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private BigDecimal distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//остальные методы-установщики и методы-получатели здесь пропущены
}
|
Теперь оба класса Runner и Race могут использоваться для постоянного хранения объектов в реализации SimpleJPA.
Использование SimpleJPA для работы с SimpleDB
Работа с доменными объектами в хранилище SimpleDB с помощью SimpleJPA мало чем отличается от работы с обычной реляционной базой данных с помощью реализации JPA. Если у вас есть опыт разработки приложений средствами JPA, то вряд ли вас ожидают какие-нибудь сюрпризы. Единственная операция, которая может оказаться незнакомой, - это конфигурирование SimpleJPA EntityManagerFactoryImpl (для выполнения этого шага необходимо указать данные вашей учетной записи в Amazon Web Services и префиксное имя домена в SimpleDB; в качестве альтернативного решения можно использовать файл свойств, содержащий данные учетной записи в пути поиска классов classpath).
Указание префиксного имени при создании экземпляра SimpleJPA EntityManagerFactoryImpl определяет домен SimpleDB, имя которого начинается с вашего префикса, за которым через черточку указываются имена доменных объектов. Так, например, если я в качестве префиксного имени указал "b50", то при создании элемента Race в SimpleDB имя соответствующего доменного объекта будет "b50-Race".
После создания экземпляра SimpleDB EntityManagerFactoryImpl все остальные операции осуществляются посредством функций интерфейса. Вам потребуется экземпляр EntityManager, который можно создать с помощью EntityManagerFactoryImpl (см. листинг 8):
Листинг 8. Создание экземпляра EntityManager
Map<String,String> props = new HashMap<String,String>();
props.put("accessKey","...");
props.put("secretKey","..");
EntityManagerFactoryImpl factory =
new EntityManagerFactoryImpl("b50", props);
EntityManager em = factory.createEntityManager();
|
После того как вы создали экземпляр EntityManager, вы можете свободно манипулировать доменными объектами. Например, я могу создать объект класса Race следующим образом:
Листинг 9. Создание объекта Race
Race race = new Race();
race.setName("Charlottesville Marathon");
race.setLocation("Charlottesville, VA");
race.setDistance(new BigDecimal(26.2));
em.persist(race);
|
В листинге 9 SimpleJPA обрабатывает HTTP-запрос на создание в облаке объекта класса Race. SimpleJPA также позволяет мне извлечь элемент race с помощью JPA-запроса (см. листинг 10). Еще раз хочу напомнить, что мы не можем использовать операцию объединения в подобных запросах; тем не менее мы можем проводить поиск по числовым значениям.
Листинг 10. Поиск соревнований по длине дистанции
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race race : races){
System.out.println(race);
}
|
Одним из основных достоинств SimpleJPA является скрытый механизм преобразования числовых значений в строковые. Если вы включите в код распечатку запросов, формируемых SimpleJPA, вы увидите ваш запрос в том виде, в котором он передается базе данных SimpleDB. Окончательный вид запроса приведен в листинге 11 – обратите внимание на преобразование числового значения длины дистанции.
Листинг 11. Магическое преобразование числового параметра средствами SimpleJPA
amazonQuery: Domain=b50-Race, query=select * from `b50-Race` where `distance` = '0922337203685477583419999999999999928946' |
Не правда ли, очень удобно иметь в своем распоряжении средства для автоматического преобразования числа в строку с необходимым выравниванием?
Несмотря на то, что SimpleDB не поддерживает запросы с кросс-доменным объединением, вы тем не менее можете использовать взаимосвязи между объектами разных доменов. Например, вы можете хранить ключевой индекс связанного объекта в другом объекте, а потом извлекать нужный вам связанный объект по значению ключа (мы обсуждали такой подход в части 1. Аналогичный подход реализован и в SimpleJPA. Ранее я продемонстрировал вам, как можно связать классы Runners и Race посредством JPA-аннотаций. Таким образом, я могу создать элемент класса Runner, добавить в него указание на существующий элемент race, а потом сохранить объект Runner, как показано в листинге 12:
Листинг 12. Определение взаимосвязи объектов в SimpleJPA
Runner runner = new Runner();
runner.setName("Mark Smith");
runner.setSsn("555-55-5555");
runner.setRace(race);
race.addRunner(runner);
em.persist(runner);
em.persist(race);
//Сохраняем измененный объект race, к которому теперь «привязан» участник runner
|
Обратите внимание, в листинге 12 я сохраняю измененный объект класса Race, поскольку я добавил к нему объект класса Runner. Кроме того, я добавил к классу Race новый метод addRunner, который позволяет добавлять новый объект Runner в список участников соревнования.
Если я теперь выполню поиск соревнований по длине дистанции, в результат выполнения запроса будет включен список участников соревнований (см. листинг 13):
Листинг 13. Еще одно преимущество использования взаимосвязей
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
System.out.println(race);
List<Runner> runners = race.getRunners();
for(Runner rnr : runners){
System.out.println(rnr);
}
}
|
Использование экземпляра EntityManager позволяет мне удалять объекты с помощью метода remove, как показано в листинге 14:
Листинг 14. Удаление объекта класса Race
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
em.remove(race);
}
|
При удалении объекта Race, как показано в листинг 14, связанные с ним объекты Runner не удаляются (я, конечно, могу изменить это поведение с помощью JPA-аннотации EntityListeners, т.е. я могу подключиться к процессу удаления и удалить соответствующие объекты Runner).
В ходе нашего ускоренного тура по SimpleDB я рассказал вам, как оперировать объектами в нереляционном хранилище с помощью API семейства веб-сервисов Amazon и интерфейса SimpleJPA. Simple JPA представляет собой реализацию подмножества Java Persistence API и позволяет без особых проблем обеспечить постоянное хранение объектов в SimpleDB. Как вы уже убедились, одно из основных преимуществ использования SimpleJPA состоит в том, что он поддерживает автоматическое преобразование объектов базовых типов в строковые объекты, которыми оперирует SimpleDB. Кроме того, SimpleJPA учитывает отсутствие объединений в SimpleDB, что сильно облегчает процесс моделирования взаимосвязей данных. Расширенные listener-интерфейсы SimpleJPA обеспечивают реализацию правил логической целостности данных, - еще одного свойства, которое является привычным и ожидаемым для реляционного мира.
Основной итог нашего знакомства с SimpleJPA таков: этот интерфейс позволяет быстро и без особых проблем получить доступ к широким возможности масштабирования по минимальной цене. Благодаря SimpleJPA вы сможете эффективно использовать ваши знания и опыт, накопленные в течение нескольких лет работы с инфраструктурами типа Hibernate, для работы в нереляционной среде облачного хранилища.
Научиться
- Оригинал статьи (EN).
- В серии статей Вторая волна разработки Java-приложений рассматриваются технологии и инструменты, меняющие принципы создания приложений на Java, в том числе Gaelyk(декабрь 2009 г.), Google App Engine (август 2009 г.) и CouchDB (ноябрь 2009 г.).
- Прочитайте статью Шаблоны NoSQL (Рикки Хо, Ricky Ho, Pragmatic Programming Techniques, ноябрь 2009 г.), в которой приводится обзор и перечень СУБД типа NoSQL, а также подробное рассмотрение их общих архитектурных принципов.
- Прочитав статью Облачные вычисления на платформе Amazon Web Services. Часть 5: обработка данных в облаке при помощи SimpleDB (Прабхакар Чаганти, Prabhakar Chaganti, developerWorks, февраль 2009 г.), вы узнаете о технологии SimpleDB от Amazon, а также о некоторых возможностях boto – открытой библиотеки для взаимодействия с CouchDB, написанной на Python.
- Статья Облачные вычисления на платформе Amazon Web Services. Часть 1: введение (Прабхакар Чаганти, Prabhakar Chaganti, developerWorks, июль 2009 г.) познакомит вас с альтернативными методами разработки и построения высоконадежных масштабируемых приложений с помощью веб-сервисов Amazon.
- В статье Google App Engine для Java. Часть 3: долговременное хранение в взаимосвязи (Ричард Хайтауэр, Richard Hightower, developerWorks, август 2009 г.) Рик Хайтауэр обсуждает недостатки Java-инфрастурктуры Google App Engine для долговременного хранения данных и рассматривает возможные обходные решения существующих проблем.
Получить продукты и технологии
-
SimpleJPA: реализация Java Persistence API (JPA) для Amazon SimpleDB. Другими словами, это ORM-инфраструктура для создания «облачной» базы данных.

Эндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.