В предыдущих статьях (
Google Cloud Endpoints на Java: Руководство. ч. 1
Google Cloud Endpoints на Java: Руководство. ч. 2 (Frontend)
Google Cloud Endpoints на Java: Руководство. ч. 3 )
мы разбирали создание API на Google Cloud Endpoints и фронтенда к нему на AngularJS.
Однако руководство по созданию API было бы неполным без работы с базой данных.
В этой статье мы рассмотрим фреймворк Objectify для работы с встроенной в GAE базой данных App Engine Datastore.
App Engine Datastore
App Engine Datastore представляет собой нереляционную NoSQL-базу данных (schemaless NoSQL datastore) типа «хранилище ключ-значение» (Key-value database).
Ключ
Ключ является уникальным идентификатором «объекта» (в App Engine datastore это называется «Entity») в базе данных.
Ключ состоит из трех составляющих:
Kind (тип): который соответствует типу объекта в базе данных (с помощью Objectify мы моделируем kind в виде класса Java, т.е. условно говоря в нашем случае kind означает класс объекта размещенного в базе данных)
Identifier (идентификатор): уникальный идентификатор объекта, который может быть либо строкой (String), и в этом случае он называется name, либо числом (Long) в этом случае он называется Id. Т.е. идентификатор вида "01234"
— это name, а вида 01234
— это Id. Идентификатор должен быть уникальным среди объектов одного типа, объекты разного типа могут иметь одинаковый идентификатор, т.е. мы можем иметь объект типа «строка» с идентификатором «01», и объект типа «колонка» с идентификатором «01». Для вновь создаваемого объекта в базе данных идентификатор, если он не задан явным образом, генерируется автоматически.
Parent(группа объектов): объекты в базе могут объединяется в «группы объектов», для этого в parent указывается либо ключ «родительского» объекта, либо таковым является null (по умолчанию) для объектов не включенных в группы.
Объект (Entity)
Объект (Entity) в базе данных имеет свойства (properties) которые могут содержать значения (Value type), их соответствие типам данных Java (Java types)) приведено в таблице:
Value type | Java type(s) | Sort order | Notes |
---|---|---|---|
Integer | short int long java.lang.Short java.lang.Integer java.lang.Long |
Numeric | |
Floating-point number | float double java.lang.Float java.lang.Double |
Numeric | 64-bit double precision, IEEE 754 |
Boolean | boolean java.lang.Boolean |
false or true |
|
Text string (short) | java.lang.String |
Unicode | До 1500 bytes
значения больше 1500 bytes выбрасывает исключение |
Text string (long) | com.google.appengine.api.datastore.Text |
None | До 1 megabyte
Не индексируется |
Byte string (short) | com.google.appengine.api.datastore.ShortBlob |
Byte order | До 1500 bytes
Значения большие 1500 bytes выбрасывают исключение |
Byte string (long) | com.google.appengine.api.datastore.Blob |
None | До 1 megabyte
Не индексируется |
Date and time | java.util.Date |
Chronological | |
Geographical point | com.google.appengine.api.datastore.GeoPt |
By latitude, then longitude |
|
Postal address | com.google.appengine.api.datastore.PostalAddress |
Unicode | |
Telephone number | com.google.appengine.api.datastore.PhoneNumber |
Unicode | |
Email address | com.google.appengine.api.datastore.Email |
Unicode | |
Google Accounts user | com.google.appengine.api.users.User |
Email address in Unicode order |
|
Instant messaging handle | com.google.appengine.api.datastore.IMHandle |
Unicode | |
Link | com.google.appengine.api.datastore.Link |
Unicode | |
Category | com.google.appengine.api.datastore.Category |
Unicode | |
Rating | com.google.appengine.api.datastore.Rating |
Numeric | |
Datastore key | com.google.appengine.api.datastore.Key or the referenced object (as a child) |
By path elements (kind, identifier, kind, identifier…) |
До 1500 bytes
Значения большие 1500 bytes выбрасывают исключение |
Blobstore key | com.google.appengine.api.blobstore.BlobKey |
Byte order | |
Embedded entity | com.google.appengine.api.datastore.EmbeddedEntity |
None | не индексируется |
Null | null |
None |
Операции с базой данных
Objectify производит три базовых операции:
save(): сохранить объект в базе данных
delete(): удалить объект из базы данных
load(): загрузить объект или список (List) объектов из базы данных.
Трансакции (Transactions) и группы объектов (Entity Groups)
Для того чтобы объединить объекты в группу «родительский» объект не обязательно должен существовать в базе, достаточно указать ключ объекта. Удаление «родительского объекта» не приводит к удалению «дочерних», они продолжат ссылаться на его ключ.
С помощью этого механизма объекты в базе данных можно организовывать в виде иерархических структур.
Отношения «родительский объект» — «дочерний объект» (parent–child relationship) могут быть установлены как между объектами одного типа (например, прадед -> дед -> отец -> я -> сын) так и объектами разного типа (например, для объекта типа «автомобиль» дочерними объектами могут быть объекты типа «колесо», «двигатель»)
При этом у каждого «дочернего» объекта может быть только один «родительский» объект. И, поскольку ключ родительского объекта является частью ключа объекта, мы не можем добавлять или убирать его после того как объект создан — ключ не изменяем. Поэтому к использованию «родительского ключа» надо подходить с осторожностью.
Как правило рамках одной трансакции мы можем получить доступ к данным только из одной группы объектов (но существует способ задействовать в одной трансакции несколько групп)
Когда изменяется любой объект в группе для группы меняется отметка времени (timestamp). Отметка времени ставиться для целой группы, и обновляется когда изменяется любой объект в группе.
Когда мы производим трансакцию, то каждая группа объектов которую затрагивает трансакция отмечается как задействованная (enlisted) в данной трансакции. Когда трансакция передана (committed), проверяются все отметки времени групп, задействованных в трансакции. Если любая из отметок времени изменилась (поскольку другая трансакция в это время изменила объект(ы) в группе) то вся трансакция отменяется и выбрасывается исключение ConcurrentModificationException. Подробнее см. github.com/objectify/objectify/wiki/Concepts#optimistic-concurrency
Objectify обрабатывает такого рода исключения и повторяет трансакцию. Поэтому трансакции должны быть идемпотентны (idempotent), т.е. мы должны иметь возможность повторить трансакцию любое количество раз и получить тот же самый результат.
Подробнее о трансакциях в Objectify, см.: github.com/objectify/objectify/wiki/Transactions
Подключение Objectify в проект
Для использования фреймворка нам понадобиться добавить в проект objectify.jar и guava.jar.
Objectify есть в репозитории Maven, нам достаточно добавить в pom.xml:
<dependencies> <dependency> <groupId>com.googlecode.objectify</groupId> <artifactId>objectify</artifactId> <version>5.1.9</version> </dependency> </dependencies>
— objectify.jar и guava.jar будут добавлены в проект.
Objectify использует фильтр который надо прописать в WEB-INF/web.xml:
<filter> <filter-name>ObjectifyFilter</filter-name> <filter-class>com.googlecode.objectify.ObjectifyFilter</filter-class> </filter> <filter-mapping> <filter-name>ObjectifyFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Создадим класс UserData, который будет моделировать объект (Entity) в базе данных:
package com.appspot.hello_habrahabr_api; import com.googlecode.objectify.annotation.Entity; import com.googlecode.objectify.annotation.Id; import com.googlecode.objectify.annotation.Index; import com.googlecode.objectify.annotation.Cache; import java.io.Serializable; @Entity // indicates that this is an Entity @Cache // Annotate your entity classes with @Cache to make them cacheable. // The cache is shared by all running instances of your application // and can both improve the speed and reduce the cost of your application. // Memcache requests are free and typically complete in a couple milliseconds. // Datastore requests are metered and typically complete in tens of milliseconds. public class UserData implements Serializable { @Id // indicates that the userId is to be used in the Entity's key // @Id field can be of type Long, long, or String // Entities must have have at least one field annotated with @Id String userId; @Index // this field will be indexed in database private String createdBy; // email @Index private String firstName; @Index private String lastName; private UserData() { } // There must be a no-arg constructor // (or no constructors - Java creates a default no-arg constructor). // The no-arg constructor can have any protection level (private, public, etc). public UserData(String createdBy, String firstName, String lastName) { this.userId = firstName + lastName; this.createdBy = createdBy; this.firstName = firstName; this.lastName = lastName; } /* Getters and setters */ // You need getters and setters to have a serializable class if you need to send it from backend to frontend, // to avoid exception: // java.io.IOException: com.google.appengine.repackaged.org.codehaus.jackson.map.JsonMappingException: No serializer found for class ... // public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getCreatedBy() { return createdBy; } public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
Далее нам следует создать класс в котором зарегистрируем классы созданные для описания объектов в базе данных, и который будет содержать метод выдающий сервисный объект Objectify (Objectify service object), методы которого мы будет использовать для взаимодействия с базой данных. Назовем его OfyService:
package com.appspot.hello_habrahabr_api; import com.googlecode.objectify.Objectify; import com.googlecode.objectify.ObjectifyFactory; import com.googlecode.objectify.ObjectifyService; /** * Custom Objectify Service that this application should use. */ public class OfyService { // This static block ensure the entity registration. static { factory().register(UserData.class); } // Use this static method for getting the Objectify service factory. public static ObjectifyFactory factory() { return ObjectifyService.factory(); } /** * Use this static method for getting the Objectify service object in order * to make sure the above static block is executed before using Objectify. * * @return Objectify service object. */ @SuppressWarnings("unused") public static Objectify ofy() { return ObjectifyService.ofy(); } }
Теперь создадим API (назовем файл UserDataAPI.java):
package com.appspot.hello_habrahabr_api; import com.google.api.server.spi.config.Api; import com.google.api.server.spi.config.ApiMethod; import com.google.api.server.spi.config.ApiMethod.HttpMethod; import com.google.api.server.spi.config.Named; import com.google.api.server.spi.response.NotFoundException; import com.google.api.server.spi.response.UnauthorizedException; import com.google.appengine.api.users.User; import com.googlecode.objectify.Key; import com.googlecode.objectify.Objectify; import java.io.Serializable; import java.util.List; import java.util.logging.Logger; /** * explore this API on: * hello-habrahabr-api.appspot.com/_ah/api/explorer * {project ID}.appspot.com/_ah/api/explorer */ @Api( name = "userDataAPI", // The api name must match '[a-z]+[A-Za-z0-9]*' version = "v1", scopes = {Constants.EMAIL_SCOPE}, clientIds = {Constants.WEB_CLIENT_ID, Constants.API_EXPLORER_CLIENT_ID}, description = "UserData API using OAuth2") public class UserDataAPI { private static final Logger LOG = Logger.getLogger(UserDataAPI.class.getName()); // Primitives and enums are not allowed as return type in @ApiMethod // So we create inner class (which should be a JavaBean) to serve as wrapper for String private class MessageToUser implements Serializable { private String message; public MessageToUser() { } public MessageToUser(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } @ApiMethod( name = "createUser", path = "createUser", httpMethod = HttpMethod.POST) @SuppressWarnings("unused") public MessageToUser createUser(final User gUser, @Named("firstName") final String firstName, @Named("lastName") final String lastName // instead of @Named arguments, we could also use // another JavaBean for modelling data received from frontend ) throws UnauthorizedException { if (gUser == null) { LOG.warning("User not logged in"); throw new UnauthorizedException("Authorization required"); } Objectify ofy = OfyService.ofy(); UserData user = new UserData(gUser.getEmail(), firstName, lastName); ofy.save().entity(user).now(); return new MessageToUser("user created: " + firstName + " " + lastName); } @ApiMethod( name = "deleteUser", path = "deleteUser", httpMethod = HttpMethod.DELETE) @SuppressWarnings("unused") public MessageToUser deleteUser(final User gUser, @Named("firstName") final String firstName, @Named("lastName") final String lastName ) throws UnauthorizedException { if (gUser == null) { LOG.warning("User not logged in"); throw new UnauthorizedException("Authorization required"); } Objectify ofy = OfyService.ofy(); String userId = firstName + lastName; Key<UserData> userDataKey = Key.create(UserData.class, userId); ofy.delete().key(userDataKey); return new MessageToUser("User deleted: " + firstName + " " + lastName); } @ApiMethod( name = "findUsersByLastName", path = "findUsersByLastName", httpMethod = HttpMethod.GET) @SuppressWarnings("unused") public List<UserData> findUsers(final User gUser, @Named("query") final String query ) throws UnauthorizedException, NotFoundException { if (gUser == null) { LOG.warning("User not logged in"); throw new UnauthorizedException("Authorization required"); } Objectify ofy = OfyService.ofy(); List<UserData> result = ofy.load().type(UserData.class).filter("lastName ==", query).list(); // for queries see: // https://github.com/objectify/objectify/wiki/Queries#executing-queries if (result.isEmpty()) { throw new NotFoundException("no results found"); } return result; // we need to return a serializable object } }
Теперь по адресу {project ID}.appspot.com/_ah/api/explorer мы можем с помощью веб-интерфейса протестировать API добавляя, удаляя и загружая объекты из базы данных.
В консоли разработчика по адресу console.developers.google.com/datastore/entities/query, выбрав соответствующий проект, мы получаем доступ в веб-интерфейсу позволяющему работать с базой данных, в том числе создавать, удалять, сортировать объекты:
Ссылки:
Storing Data in Datastore (Google Tutorial)
Краткое представление фреймворка от его создателя Jeff Schnitzer (@jeffschnitzer) на Google I/O 2011:
ссылка на оригинал статьи http://habrahabr.ru/post/274239/
Добавить комментарий