- PVSM.RU - https://www.pvsm.ru -

Работа с базой данных в Google App Engine-Google Cloud Endpoints на Java: фреймворк Objectify

В предыдущих статьях (
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

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 выбрасывает исключение IllegalArgumentException

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 выбрасывают исключение IllegalArgumentException

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 выбрасывают исключение IllegalArgumentException

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) объектов из базы данных.

Трансакции (Transactions) и группы объектов (Entity Groups)

Для того чтобы объединить объекты в группу «родительский» объект не обязательно должен существовать в базе, достаточно указать ключ объекта. Удаление «родительского объекта» не приводит к удалению «дочерних», они продолжат ссылаться на его ключ.

С помощью этого механизма объекты в базе данных можно организовывать в виде иерархических структур.
Отношения «родительский объект» — «дочерний объект» (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 в проект

Для использования фреймворка нам понадобиться добавить в проект 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 добавляя, удаляя и загружая объекты из базы данных.
Работа с базой данных в Google App Engine-Google Cloud Endpoints на Java: фреймворк Objectify - 1

В консоли разработчика по адресу console.developers.google.com/datastore/entities/query [30], выбрав соответствующий проект, мы получаем доступ в веб-интерфейсу позволяющему работать с базой данных, в том числе создавать, удалять, сортировать объекты:
Работа с базой данных в Google App Engine-Google Cloud Endpoints на Java: фреймворк Objectify - 2

Ссылки:

Objectify wiki [31]

Objectify JavaDoc [32]

Java Datastore API [7]

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/