В предыдущих статьях (
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: youtu.be/imiquTOLl64?t=3m40s
Автор: ageyev