- PVSM.RU - https://www.pvsm.ru -
В предыдущих статьях (
Google Cloud Endpoints на Java: Руководство. ч. 1 [1]
Google Cloud Endpoints на Java: Руководство. ч. 2 (Frontend) [2]
Google Cloud Endpoints на Java: Руководство. ч. 3 [3] )
мы разбирали создание API на Google Cloud Endpoints [4] и фронтенда к нему на AngularJS [5].
Однако руководство по созданию API было бы неполным без работы с базой данных.
В этой статье мы рассмотрим фреймворк Objectify [6] для работы с встроенной в GAE базой данных App Engine Datastore [7].
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) в базе данных имеет свойства (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 [8] |
None | До 1 megabyte
Не индексируется |
Byte string (short) | com.google.appengine.api.datastore.ShortBlob [9] |
Byte order | До 1500 bytes
Значения большие 1500 bytes выбрасывают исключение |
Byte string (long) | com.google.appengine.api.datastore.Blob [10] |
None | До 1 megabyte
Не индексируется |
Date and time | java.util.Date |
Chronological | |
Geographical point | com.google.appengine.api.datastore.GeoPt [11] |
By latitude, then longitude |
|
Postal address | com.google.appengine.api.datastore.PostalAddress [12] |
Unicode | |
Telephone number | com.google.appengine.api.datastore.PhoneNumber [13] |
Unicode | |
Email address | com.google.appengine.api.datastore.Email [14] |
Unicode | |
Google Accounts user | com.google.appengine.api.users.User [15] |
Email address in Unicode order |
|
Instant messaging handle | com.google.appengine.api.datastore.IMHandle [16] |
Unicode | |
Link | com.google.appengine.api.datastore.Link [17] |
Unicode | |
Category | com.google.appengine.api.datastore.Category [18] |
Unicode | |
Rating | com.google.appengine.api.datastore.Rating [19] |
Numeric | |
Datastore key | com.google.appengine.api.datastore.Key [20]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 [21] |
Byte order | |
Embedded entity [22] | com.google.appengine.api.datastore.EmbeddedEntity [23] |
None | не индексируется |
Null | null |
None |
Objectify производит три базовых операции:
save(): сохранить объект в базе данных
delete(): удалить объект из базы данных
load(): загрузить объект или список (List) объектов из базы данных.
Для того чтобы объединить объекты в группу «родительский» объект не обязательно должен существовать в базе, достаточно указать ключ объекта. Удаление «родительского объекта» не приводит к удалению «дочерних», они продолжат ссылаться на его ключ.
С помощью этого механизма объекты в базе данных можно организовывать в виде иерархических структур.
Отношения «родительский объект» — «дочерний объект» (parent–child relationship) могут быть установлены как между объектами одного типа (например, прадед -> дед -> отец -> я -> сын) так и объектами разного типа (например, для объекта типа «автомобиль» дочерними объектами могут быть объекты типа «колесо», «двигатель»)
При этом у каждого «дочернего» объекта может быть только один «родительский» объект. И, поскольку ключ родительского объекта является частью ключа объекта, мы не можем добавлять или убирать его после того как объект создан — ключ не изменяем. Поэтому к использованию «родительского ключа» надо подходить с осторожностью.
Как правило рамках одной трансакции [24] мы можем получить доступ к данным только из одной группы объектов (но существует способ задействовать в одной трансакции несколько групп)
Когда изменяется любой объект в группе для группы меняется отметка времени (timestamp). Отметка времени ставиться для целой группы, и обновляется когда изменяется любой объект в группе.
Когда мы производим трансакцию, то каждая группа объектов которую затрагивает трансакция отмечается как задействованная (enlisted) в данной трансакции. Когда трансакция передана (committed), проверяются все отметки времени групп, задействованных в трансакции. Если любая из отметок времени изменилась (поскольку другая трансакция в это время изменила объект(ы) в группе) то вся трансакция отменяется и выбрасывается исключение ConcurrentModificationException. Подробнее см. github.com/objectify/objectify/wiki/Concepts#optimistic-concurrency [25]
Objectify обрабатывает такого рода исключения и повторяет трансакцию. Поэтому трансакции должны быть идемпотентны [26] (idempotent), т.е. мы должны иметь возможность повторить трансакцию любое количество раз и получить тот же самый результат.
Подробнее о трансакциях в Objectify, см.: github.com/objectify/objectify/wiki/Transactions [27]
Для использования фреймворка нам понадобиться добавить в проект objectify.jar и guava.jar [28].
Objectify есть в репозитории Maven [29], нам достаточно добавить в 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 [30], выбрав соответствующий проект, мы получаем доступ в веб-интерфейсу позволяющему работать с базой данных, в том числе создавать, удалять, сортировать объекты:
Objectify wiki [31]
Objectify JavaDoc [32]
Storing Data in Datastore [33] (Google Tutorial)
Краткое представление фреймворка от его создателя Jeff Schnitzer [34] (@jeffschnitzer [35]) на Google I/O 2011: youtu.be/imiquTOLl64?t=3m40s [36]
Автор: ageyev
Источник [37]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/107764
Ссылки в тексте:
[1] Google Cloud Endpoints на Java: Руководство. ч. 1: http://habrahabr.ru/post/268863/
[2] Google Cloud Endpoints на Java: Руководство. ч. 2 (Frontend): http://habrahabr.ru/post/270459/
[3] Google Cloud Endpoints на Java: Руководство. ч. 3 : http://habrahabr.ru/post/271385/
[4] Google Cloud Endpoints: https://cloud.google.com/appengine/docs/java/endpoints/
[5] AngularJS: https://angularjs.org/
[6] Objectify: https://github.com/objectify/objectify
[7] App Engine Datastore: https://cloud.google.com/appengine/docs/java/datastore/
[8] com.google.appengine.api.datastore.Text
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Text
[9] com.google.appengine.api.datastore.ShortBlob
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/ShortBlob
[10] com.google.appengine.api.datastore.Blob
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Blob
[11] com.google.appengine.api.datastore.GeoPt
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/GeoPt
[12] com.google.appengine.api.datastore.PostalAddress
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/PostalAddress
[13] com.google.appengine.api.datastore.PhoneNumber
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/PhoneNumber
[14] com.google.appengine.api.datastore.Email
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Email
[15] com.google.appengine.api.users.User
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/users/User
[16] com.google.appengine.api.datastore.IMHandle
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/IMHandle
[17] com.google.appengine.api.datastore.Link
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Link
[18] com.google.appengine.api.datastore.Category
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Category
[19] com.google.appengine.api.datastore.Rating
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Rating
[20] com.google.appengine.api.datastore.Key
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/datastore/Key
[21] com.google.appengine.api.blobstore.BlobKey
: https://cloud.google.com/appengine/docs/java/javadoc/com/google/appengine/api/blobstore/BlobKey
[22] Embedded entity: https://cloud.google.com#Java_Embedded_entities
[23] com.google.appengine.api.datastore.EmbeddedEntity
: https://cloud.google.com/appengine/docs/java/datastore/entities#Java_Embedded_entities
[24] трансакции: https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B0%D0%BD%D0%B7%D0%B0%D0%BA%D1%86%D0%B8%D1%8F_%28%D0%B8%D0%BD%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82%D0%B8%D0%BA%D0%B0%29
[25] github.com/objectify/objectify/wiki/Concepts#optimistic-concurrency: https://github.com/objectify/objectify/wiki/Concepts#optimistic-concurrency
[26] идемпотентны: https://ru.wikipedia.org/wiki/%D0%98%D0%B4%D0%B5%D0%BC%D0%BF%D0%BE%D1%82%D0%B5%D0%BD%D1%82%D0%BD%D0%BE%D1%81%D1%82%D1%8C#.D0.92_.D0.B8.D0.BD.D1.84.D0.BE.D1.80.D0.BC.D0.B0.D1.82.D0.B8.D0.BA.D0.B5
[27] github.com/objectify/objectify/wiki/Transactions: https://github.com/objectify/objectify/wiki/Transactions
[28] guava.jar: https://github.com/google/guava/wiki
[29] репозитории Maven: http://search.maven.org/#artifactdetails|com.googlecode.objectify|
[30] console.developers.google.com/datastore/entities/query: https://console.developers.google.com/datastore/entities/query
[31] Objectify wiki: https://github.com/objectify/objectify/wiki
[32] Objectify JavaDoc: http://www.javadoc.io/doc/com.googlecode.objectify/objectify
[33] Storing Data in Datastore: https://cloud.google.com/appengine/docs/java/gettingstarted/usingdatastore
[34] Jeff Schnitzer: https://github.com/stickfigure
[35] @jeffschnitzer: https://twitter.com/jeffschnitzer
[36] youtu.be/imiquTOLl64?t=3m40s: https://youtu.be/imiquTOLl64?t=3m40s
[37] Источник: http://habrahabr.ru/post/274239/
Нажмите здесь для печати.