Developers Club geek daily blog

1 year, 1 month ago
In the previous articles ("Google Cloud Endpoints on Java: Manual. p.1", "Google Cloud Endpoints on Java: Manual. the p. 2 (Frontend)", "Google Cloud Endpoints on Java: Manual. the p. 3") we investigated creation of API on Google Cloud Endpoints and a frontend to it on AngularJS.

However the guide to creation of API would be incomplete without work with the database.

In this article we will consider Objectify framework for work with the App Engine Datastore database which is built in GAE.

App Engine Datastore


App Engine Datastore represents not relational NoSQL-database (schemaless NoSQL datastore) of type "storage a key value" (Key-value database).

Key

The key is a unique identifier of "object" (in App Engine datastore it is called "Entity") in the database.

The key consists of three components:

Kind (type): which corresponds to an object type in the database (by means of Objectify we model kind in the form of the class Java, i.e. conditionally telling kind in our case means object class placed in the database)

Identifier (identifier): a unique identifier of object which can be or line (String) and in this case it is called name, or number (Long) in this case it is called Id. I.e. type identifier "01234" — it is name, and a type 01234 — it is Id. The identifier has to be unique among objects of one type, objects of different type can have the identical identifier, i.e. we can have object like "line" with the 01 identifier, and object like "column" with the 01 identifier. For newly created object in the database the identifier if it is not set explicitly, is generated automatically.

Parent (object group): objects in base can integrates in "object groups", for this purpose in parent it is specified or a key of "parent" object, or that is null (by default) for the objects which are not included in groups.

Object (Entity)

The object (Entity) in the database has properties (properties) which may contain values (Value type), their compliance to data types of Java (Java types)) is provided in the table:
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 To 1500 bytes

values more than 1500 bytes are thrown out by an exception IllegalArgumentException
Text string (long) com.google.appengine.api.datastore.Text None To 1 megabyte

It is not indexed
Byte string (short) com.google.appengine.api.datastore.ShortBlob Byte order To 1500 bytes

Values big 1500 bytes throw out an exception IllegalArgumentException
Byte string (long) com.google.appengine.api.datastore.Blob None To 1 megabyte

It is not indexed
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...)
To 1500 bytes

Values big 1500 bytes throw out an exception IllegalArgumentException
Blobstore key com.google.appengine.api.blobstore.BlobKey Byte order
Embedded entity com.google.appengine.api.datastore.EmbeddedEntity None it is not indexed
Null null None

Operations with the database

Objectify makes three basic operations:

save (): to save object in the database

delete (): to delete object from the database

load (): to load object or the list (List) of objects from the database.

Transaction (Transactions) and object group (Entity Groups)

To integrate objects in roditelsky group the object shall not exist in base, it is enough specify an object key. Removal of "parent object" does not lead to removal "child", they will continue to refer to its key.

By means of this mechanism objects in the database can be organized in the form of hierarchical structures.
The relations "parent object" — "child object" can be set (parent–child relationship) as between objects of one type (for example, the great-grandfather-> the grandfather-> the father-> I-> the son) and objects of different type (for example, for object like "car" objects like "wheel", "engine" can be child objects)

At the same time each "child" object can have only one "parent" object. And, as the key of parent object is part of a key of object, we cannot add or move away him after the object is created — we do not change a key. Therefore it is necessary to approach use of "a parent key" with care.

As a rule a framework of one transaction we can get data access only from one object group (but there is a method to involve several groups in one transaction)
When any object in group for group changes the time mark (timestamp) changes. The time mark to be given for the whole group, and is updated when any object in group changes.

When we make transaction, each object group which is affected by transaction is noted as involved (enlisted) in this transaction. When transaction is transferred (committed), all time marks of the groups involved in transaction are checked. If any of time marks changed (as other transaction changed object(s) in group at this time) that all transaction is cancelled and ConcurrentModificationException exception is thrown out. For more details see github.com/objectify/objectify/wiki/Concepts#optimistic-concurrency
Objectify processes such exceptions and repeats transaction. Therefore transaction have to be idempotentna (idempotent), i.e. we have to have an opportunity to repeat transaction any number of times and to receive the same result.

In more detail about transaction in Objectify, see: github.com/objectify/objectify/wiki/Transactions

Connection of Objectify in the project


For use of a framework we to be necessary to add to the objectify.jar and guava.jar project.
Objectify is in Maven repository, it is enough to us to add to pom.xml:
  <dependencies>
    <dependency>
      <groupId>com.googlecode.objectify</groupId>
      <artifactId>objectify</artifactId>
      <version>5.1.9</version>
    </dependency>
  </dependencies>

— objectify.jar and guava.jar will be added to the project.
Objectify uses the filter which should be registered in 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>


Let's create the class UserData which will model object (Entity) in the database:
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;
    }
}



Further we should create a class in which we will register the classes created for the description of objects in the database and which will contain a method the issuing service object of Objectify (Objectify service object) which methods we will use for interaction with the database. Let's call it 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();
    }
}


Now we will create API (we will call the UserDataAPI.java file):
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
    }
}



Now to the address { project ID } .appspot.com / _ ah/api/explorer we can test API by means of the web interface adding, deleting and loading objects from the database.
Work with the database in Google App Engine/Google Cloud Endpoints on Java: Objectify framework

In the developer's console to the address console.developers.google.com/datastore/entities/query, having selected the corresponding project, we get access in the web interface allowing to work with the database, including to create, delete, sort objects:
Work with the database in Google App Engine/Google Cloud Endpoints on Java: Objectify framework

Links:


Objectify wiki

Objectify JavaDoc

Java Datastore API

Storing Data in Datastore (Google Tutorial)

Short representation of a framework from his creator of Jeff Schnitzer (@jeffschnitzer) on Google I/O 2011: youtu.be/imiquTOLl64? t=3m40s

This article is a translation of the original post at habrahabr.ru/post/274239/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus