Storing Data with Objectify and Datastore

This walkthrough describes how user supplied messages are stored in Google Cloud Datastore using Objectify.

It also shows how these messages are retrieved and displayed to the user.

In the following code walkthrough, you'll learn about the following items:

  • The Objectify dependency in your project pom.xml file.
  • The data model classes needed for the Guestbook app.
  • The entries in web.xml that route requests to SignGuestbookServlet.
  • The JSP code allowing users to post greetings to the data store, and display all stored greetings.
  • The SignGuestbookServlet code that handles the form submission and saves the messages to the datastore.

This page is part of a multi-page tutorial. To start from the beginning and see instructions for setting up, go to Creating a Guestbook.

The Objectify dependency in pom.xml

To use the Objectify library, you must declare the dependency in the final/pom.xml file, so Maven can install it, as follows:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>${guava.version}</version>
</dependency>
<dependency>
  <groupId>com.googlecode.objectify</groupId>
  <artifactId>objectify</artifactId>
  <version>${objectify.version}</version>
</dependency>

Maven installs the library the next time you run a target that needs it, such as the development server.

The data model classes

With Objectify, you create classes whose instances will represent data store entities in your code. Objectify does the work of translating these Java objects to the entities stored in Cloud Datastore .

The following code shows the Greeting class representing a message posted by the user:

package com.example.guestbook;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;

import java.lang.String;
import java.util.Date;
import java.util.List;

/**
 * The @Entity tells Objectify about our entity.  We also register it in {@link OfyHelper}
 * Our primary key @Id is set automatically by the Google Datastore for us.
 *
 * We add a @Parent to tell the object about its ancestor. We are doing this to support many
 * guestbooks.  Objectify, unlike the AppEngine library requires that you specify the fields you
 * want to index using @Index.  Only indexing the fields you need can lead to substantial gains in
 * performance -- though if not indexing your data from the start will require indexing it later.
 *
 * NOTE - all the properties are PUBLIC so that we can keep the code simple.
 **/
@Entity
public class Greeting {
  @Parent Key<Guestbook> theBook;
  @Id public Long id;

  public String author_email;
  public String author_id;
  public String content;
  @Index public Date date;

  /**
   * Simple constructor just sets the date
   **/
  public Greeting() {
    date = new Date();
  }

  /**
   * A convenience constructor
   **/
  public Greeting(String book, String content) {
    this();
    if( book != null ) {
      theBook = Key.create(Guestbook.class, book);  // Creating the Ancestor key
    } else {
      theBook = Key.create(Guestbook.class, "default");
    }
    this.content = content;
  }

  /**
   * Takes all important fields
   **/
  public Greeting(String book, String content, String id, String email) {
    this(book, content);
    author_email = email;
    author_id = id;
  }

}

The sample also uses the model class Guestbook to represent an entire guestbook:

package com.example.guestbook;

import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;

/**
 * The @Entity tells Objectify about our entity.  We also register it in
 * OfyHelper.java -- very important.
 *
 * This is never actually created, but gives a hint to Objectify about our Ancestor key.
 */
@Entity
public class Guestbook {
  @Id public String book;
}

While our sample doesn't explicitly create a Guestbook object in the datastore, the sample uses the Guestbook class as part of the datastore key for the Greeting objects. This demonstrates how you can define keys so that all of the greetings in a guestbook could be updated in a single datastore transaction.

The helper file OfyHelper.java

In order to use Objectify in a JSP, we need a helper class that registers each model class in the JSP servlet context. The following code in OfyHelper.java shows how to do this:

package com.example.guestbook;

import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyFactory;
import com.googlecode.objectify.ObjectifyService;

import javax.servlet.ServletContextListener;
import javax.servlet.ServletContextEvent;

/**
 * OfyHelper, a ServletContextListener, is setup in web.xml to run before a JSP is run.  This is
 * required to let JSP's access Ofy.
 **/
public class OfyHelper implements ServletContextListener {
  public void contextInitialized(ServletContextEvent event) {
    // This will be invoked as part of a warmup request, or the first user request if no warmup
    // request.
    ObjectifyService.register(Guestbook.class);
    ObjectifyService.register(Greeting.class);
  }

  public void contextDestroyed(ServletContextEvent event) {
    // App Engine does not currently invoke this method.
  }
}

This file refers to the Greeting and Guestbook model classes used in the sample.

Mapping the OfyHelper helper in the servlet

Objectify requires a filter to clean up any thread-local transaction contexts and pending asynchronous operations that remain at the end of a request. You add this filter in web.xml as follows:

<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>
<listener>
  <listener-class>com.example.guestbook.OfyHelper</listener-class>
</listener>

Handling user input in the JSP template

The following imports in the JSP file show the Cloud Datastore model classes and the required Objectify classes:

<%@ page import="com.example.guestbook.Greeting" %>
<%@ page import="com.example.guestbook.Guestbook" %>
<%@ page import="com.googlecode.objectify.Key" %>
<%@ page import="com.googlecode.objectify.ObjectifyService" %>

Just above the line <form action="/guestbook.jsp" method="get">, notice the following code:

<%
    // Create the correct Ancestor key
      Key<Guestbook> theBook = Key.create(Guestbook.class, guestbookName);

    // Run an ancestor query to ensure we see the most up-to-date
    // view of the Greetings belonging to the selected Guestbook.
      List<Greeting> greetings = ObjectifyService.ofy()
          .load()
          .type(Greeting.class) // We want only Greetings
          .ancestor(theBook)    // Anyone in this book
          .order("-date")       // Most recent first - date is indexed.
          .limit(5)             // Only show 5 of them.
          .list();

    if (greetings.isEmpty()) {
%>
<p>Guestbook '${fn:escapeXml(guestbookName)}' has no messages.</p>
<%
    } else {
%>
<p>Messages in Guestbook '${fn:escapeXml(guestbookName)}'.</p>
<%
      // Look at all of our greetings
        for (Greeting greeting : greetings) {
            pageContext.setAttribute("greeting_content", greeting.content);
            String author;
            if (greeting.author_email == null) {
                author = "An anonymous person";
            } else {
                author = greeting.author_email;
                String author_id = greeting.author_id;
                if (user != null && user.getUserId().equals(author_id)) {
                    author += " (You)";
                }
            }
            pageContext.setAttribute("greeting_user", author);
%>
<p><b>${fn:escapeXml(greeting_user)}</b> wrote:</p>
<blockquote>${fn:escapeXml(greeting_content)}</blockquote>
<%
        }
    }
%>

<form action="/sign" method="post">
    <div><textarea name="content" rows="3" cols="60"></textarea></div>
    <div><input type="submit" value="Post Greeting"/></div>
    <input type="hidden" name="guestbookName" value="${fn:escapeXml(guestbookName)}"/>
</form>

When the user loads the page, the template performs a datastore query for all Greeting entities that use the same Guestbook in their key. The query is sorted by date in reverse chronological order, and results are limited to the five most recent greetings.

The rest of the template displays the results as HTML, and also renders the form that the user can use to post a new greeting.

The SignGuestbookServlet.java servlet code walkthrough

The SignGuestbookServlet servlet is the HTTP POST handler for the greetings posted by users. It takes the greeting (content) from the incoming request and stores it in Cloud Datastore as a Greeting entity:

package com.example.guestbook;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;

import java.io.IOException;
import java.util.Date;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.googlecode.objectify.ObjectifyService;

/**
 * Form Handling Servlet
 * Most of the action for this sample is in webapp/guestbook.jsp, which displays the
 * {@link Greeting}'s. This servlet has one method
 * {@link #doPost(<#HttpServletRequest req#>, <#HttpServletResponse resp#>)} which takes the form
 * data and saves it.
 */
public class SignGuestbookServlet extends HttpServlet {

  // Process the http POST of the form
  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
    Greeting greeting;

    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();  // Find out who the user is.

    String guestbookName = req.getParameter("guestbookName");
    String content = req.getParameter("content");
    if (user != null) {
      greeting = new Greeting(guestbookName, content, user.getUserId(), user.getEmail());
    } else {
      greeting = new Greeting(guestbookName, content);
    }

    // Use Objectify to save the greeting and now() is used to make the call synchronously as we
    // will immediately get a new page using redirect and we want the data to be present.
    ObjectifyService.ofy().save().entity(greeting).now();

    resp.sendRedirect("/guestbook.jsp?guestbookName=" + guestbookName);
  }
}

Notice that the servlet stores properties such as the content of the post, the date the post was created, and, if the user is signed in, the user's ID and email address.

This servlet is mapped to the URLs it serves in web.xml:

<servlet>
  <servlet-name>sign</servlet-name>
  <servlet-class>com.example.guestbook.SignGuestbookServlet</servlet-class>
</servlet>

<servlet-mapping>
  <servlet-name>sign</servlet-name>
  <url-pattern>/sign</url-pattern>
</servlet-mapping>

<welcome-file-list>
    <welcome-file>guestbook.jsp</welcome-file>
</welcome-file-list>

Send feedback about...

App Engine standard environment for Java