Google App Engine for Java: CRUD Operations with JDO and Spring MVC
In a prior post, I provided and introduction to GAE for Java including getting your local development environment setup and uploading your application to Google’s servers.
For this post, I will outline how to do simple CRUD operations using GAE for Java and the latest version of Spring MVC (which is 3.0.0.M3). Combining GAE for Java and the Spring MVC framework makes for a powerful way of making your application both flexible and scalable.
Datastore Options with Google App Engine for Java
The basis for just about any web application generally includes a data store for storing information. Instead of a relational database like MySQL or Oracle, Google uses something called BigTable. For an explanation about how BigTable differs from a traditional relational database, you should check out "The Softer Side of Schemas" video from the Google I/O 2009 conference. With respect to GAE for java, you have essentially three different ways to interact with BigTable:
- JPA - Java Peristence API
- JDO - Java Data Objects
- Low level API - provided by Google for doing stuff that you can't otherwise do with JPA or JDO. Note that both JPA and JDO use this low-level API under the covers.
Development with Google App Engine for Java
To make development with GAE for Java, the folks at Google provide a couple of different ways to compile, package, and deploy your application. For the two mainstream Java IDE’s, IntelliJ IDEA and Eclipse, there are plug-ins available. If you don’t have one of these IDE’s or prefer to work from the command line, there’s Ant scripts available. My IDE of choice is IntelliJ IDEA, and the plug-in available is actually actively developed by JetBrains, makers of IDEA.
Getting Started with the View
Below is a simple web form for collecting information on API’s:
<%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
<%@include file="_inc/tags.jsp" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title><spring:message code="app.title"/></title>
<link rel="stylesheet" href="<c:url value="/_css/style.css"/>" type="text/css" media="screen"/>
</head>
<body>
<jsp:include page="/WEB-INF/view/_inc/menu.jsp"/>
<h2><spring:message code="api.add"/></h2>
<c:url var="saveUrl" value='/api/save' context='/dispatcher'/>
<form:form action="${saveUrl}" commandName="api" method="post">
<form:hidden path="key"/>
<div>Name: <form:input path="name"/></div>
<div>Description: <form:textarea path="description" rows="3" cols="40"/></div>
<div>Url: <form:input path="url"/></div>
<div>Company: <form:input path="company"/></div>
<div>Created: <fmt:formatDate value="${api.created}" type="both" dateStyle="full"/></div>
<div>Last Updated: <fmt:formatDate value="${api.lastUpdated}" type="both" dateStyle="full"/></div>
<div>Data formats:
<%--@elvariable id="availableDataFormats" type="java.lang.Array"--%>
<form:checkboxes path="dataFormats" items="${availableDataFormats}"/>
</div>
<div>Protocols: <%--@elvariable id="availableProtocols" type="java.lang.Array"--%>
<form:checkboxes path="protocols" items="${availableProtocols}"/></div>
<input type="submit" value="save"/> <a href="<c:url value="/apis" context="/dispatcher"/>">cancel</a>
</form:form>
</body>
</html>
Note the isELIgnored="false" attribute in the page directive. I found that this is essential when using JSTL expressions with GAE. Otherwise, you’ll get a nasty exception.
A Simple POJO with JDO Annotations
package com.digitalsanctum.angrygerbils.domain;
import com.google.appengine.api.datastore.KeyFactory;
import org.apache.commons.lang.builder.ToStringBuilder;
import javax.jdo.annotations.*;
import java.util.Date;
import java.util.List;
/**
* Created by IntelliJ IDEA.
*
* @author shane
* @since Jun 24, 2009 8:43:10 AM
*/
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Api {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private com.google.appengine.api.datastore.Key key;
@Persistent
private String name;
@Persistent
private String description;
@Persistent
private String company;
@Persistent
private String url;
@Persistent
private Date created;
@Persistent
private Date lastUpdated;
@Persistent(defaultFetchGroup = "true")
private List<String> dataFormats;
@Persistent(defaultFetchGroup = "true")
private List<String> protocols;
public List<String> getDataFormats() {
return dataFormats;
}
public void setDataFormats(List<String> dataFormats) {
this.dataFormats = dataFormats;
}
public List<String> getProtocols() {
return protocols;
}
public void setProtocols(List<String> protocols) {
this.protocols = protocols;
}
public com.google.appengine.api.datastore.Key getKey() {
return key;
}
public String getKeyAsString() {
if (key == null) return null;
return KeyFactory.keyToString(key);
}
public void setKey(com.google.appengine.api.datastore.Key key) {
this.key = key;
}
public boolean isNew() {
return getKey() == null;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Date getCreated() {
return created;
}
public void setCreated(Date created) {
this.created = created;
}
public Date getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated(Date lastUpdated) {
this.lastUpdated = lastUpdated;
}
@Override
public String toString() {
return new ToStringBuilder(this).
append("key", getKey()).
append("name", name).
append("company", company).
append("created", getCreated()).
append("lastUpdated", getLastUpdated()).
append("url", url).
append("dataFormats", getDataFormats()).
toString();
}
}
Spring 3 MVC Controller Using Annotations
Below is the ApiController.java class with an ApiService injected via Spring. It uses the nice REST url mappings with variable substitution introduced to the framework as of version 3:
package com.digitalsanctum.angrygerbils.web.controller;
import com.digitalsanctum.angrygerbils.domain.Api;
import com.digitalsanctum.angrygerbils.domain.DataFormat;
import com.digitalsanctum.angrygerbils.domain.Protocol;
import com.digitalsanctum.angrygerbils.service.ApiService;
import com.digitalsanctum.angrygerbils.web.editor.GoogleDatastoreKeyEditor;
import com.google.appengine.api.datastore.KeyFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.propertyeditors.CustomCollectionEditor;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* Created by IntelliJ IDEA.
*
* @author shane
* @since Jun 24, 2009 8:44:03 AM
*/
@Controller
public class ApiController extends BaseController {
@Autowired
private ApiService apiService;
@ModelAttribute("availableDataFormats")
public DataFormat[] populateDataFormats() {
return DataFormat.values();
}
@ModelAttribute("availableProtocols")
public Protocol[] populateProtocols() {
return Protocol.values();
}
@RequestMapping(value = "/apis", method = RequestMethod.GET)
public String apisHandler(Model model) {
model.addAttribute("api", new Api());
model.addAttribute("apis", apiService.getAll());
return "apis";
}
@RequestMapping(value = "/api/add", method = RequestMethod.GET)
public String apiAddHandler(Model model) {
model.addAttribute("api", new Api());
return "apiForm";
}
@RequestMapping(value = "/api/delete/{keyAsString}", method = RequestMethod.GET)
public String apiDeleteHandler(@PathVariable String keyAsString) {
apiService.deleteApi(KeyFactory.stringToKey(keyAsString));
return "redirect:/dispatcher/apis";
}
@RequestMapping(value = "/api/edit/{keyAsString}", method = RequestMethod.GET)
public String apiEditHandler(@PathVariable String keyAsString,
Model model) {
model.addAttribute("api", apiService.getApi(KeyFactory.stringToKey(keyAsString)));
return "apiForm";
}
@RequestMapping(value = "/api/save", method = RequestMethod.POST)
public String apiSaveHandler(@ModelAttribute Api api) {
apiService.saveApi(api);
return "redirect:/dispatcher/apis";
}
@RequestMapping(value = "/api/search", method = RequestMethod.POST)
public String apiSearchHandler(Model model,
@ModelAttribute Api api) {
Collection<Api> apis = apiService.search(api.getName());
model.addAttribute("apis", apis);
return "apis";
}
@InitBinder
protected void initBinder(HttpServletRequest request,
ServletRequestDataBinder binder) throws Exception {
binder.registerCustomEditor(String.class,
new StringTrimmerEditor(false)
);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class,
new CustomDateEditor(dateFormat, false)
);
binder.registerCustomEditor(com.google.appengine.api.datastore.Key.class,
new GoogleDatastoreKeyEditor()
);
binder.registerCustomEditor(List.class, new CustomCollectionEditor(List.class));
}
}
Below is a custom class I introduced called GoogleDatastoreKeyEditor. This helps with being able to bind complex objects from a web form. Here I use some static methods KeyFactory.stringToKey(text) and KeyFactory.keyToString(value) to easily go back and forth between string and object representations of a Key.
package com.digitalsanctum.angrygerbils.web.editor;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import org.apache.log4j.Logger;
import java.beans.PropertyEditorSupport;
/**
* Created by IntelliJ IDEA.
*
* @author shane
* @since Jul 1, 2009 9:19:33 PM
*/
public class GoogleDatastoreKeyEditor extends PropertyEditorSupport {
private static final Logger log = Logger.getLogger(GoogleDatastoreKeyEditor.class);
@Override
public void setAsText(String text) {
if (text == null || text.length() == 0) {
setValue(null);
} else {
Key key = null;
try {
key = KeyFactory.stringToKey(text);
} catch (Exception e) {
log.error("Cannot parse key: " + text, e);
}
setValue(key);
}
}
@Override
public String getAsText() {
Key value = (Key) getValue();
return (value != null ? KeyFactory.keyToString(value) : "");
}
}
Getting Interesting: ApiServiceImpl.java
Here’s where all the actual work is done by providing methods that take care of the CRUD operations:
package com.digitalsanctum.angrygerbils.service;
import com.digitalsanctum.angrygerbils.domain.Api;
import com.google.appengine.api.datastore.Key;
import org.apache.log4j.Logger;
import org.springframework.orm.jdo.support.JdoDaoSupport;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* Created by IntelliJ IDEA.
*
* @author shane
* @since Jun 24, 2009 8:43:35 AM
*/
public class ApiServiceImpl extends JdoDaoSupport implements ApiService {
private static final Logger log = Logger.getLogger(ApiServiceImpl.class);
public Api getApi(Key key) {
PersistenceManager pm = getPersistenceManager();
Api api = null;
try {
api = pm.getObjectById(Api.class, key);
} catch (JDOObjectNotFoundException e) {
log.error("Api not found", e);
} finally {
releasePersistenceManager(pm);
}
log.info(api);
return api;
}
public Collection<Api> search(String queryString) {
PersistenceManager pm = getPersistenceManager();
Query query = pm.newQuery(Api.class);
query.setOrdering("name");
query.setFilter("name >= id && name < id2");
query.declareParameters("String id, String id2");
Collection<Api> apis = pm.detachCopyAll((Collection<Api>) query.execute(queryString, queryString + "\ufffd"));
return apis;
}
public List<Api> getAll() {
PersistenceManager pm = getPersistenceManager();
String query = "select from " + Api.class.getName() + " order by name";
List<Api> apis = (List<Api>) pm.newQuery(query).execute();
int apiSize = apis.size();
if (log.isInfoEnabled()) {
log.info("returning " + apiSize + " apis");
for (Api api : apis) {
log.info(api);
}
}
return apis;
}
public void saveApi(Api api) {
if (api.isNew()) {
insertApi(api);
} else {
updateApi(api);
}
}
public void deleteApi(Key key) {
PersistenceManager pm = getPersistenceManager();
try {
Api api = pm.getObjectById(Api.class, key);
pm.deletePersistent(api);
} catch (Exception e) {
log.error("Error deleting api");
} finally {
releasePersistenceManager(pm);
}
}
private void updateApi(Api api) {
PersistenceManager pm = getPersistenceManager();
try {
Api orig = pm.getObjectById(Api.class, api.getKey());
orig.setName(api.getName());
orig.setDescription(api.getDescription());
orig.setLastUpdated(new Date());
orig.setUrl(api.getUrl());
orig.setCompany(api.getCompany());
orig.setDataFormats(api.getDataFormats());
orig.setProtocols(api.getProtocols());
} catch (Exception e) {
log.error("Error updating api", e);
} finally {
releasePersistenceManager(pm);
}
}
private void insertApi(Api api) {
PersistenceManager pm = getPersistenceManager();
try {
Date now = new Date();
api.setCreated(now);
api.setLastUpdated(now);
pm.makePersistent(api);
} catch (Exception e) {
log.error("Error inserting api", e);
} finally {
releasePersistenceManager(pm);
}
}
}
So that’s a whirlwind tour of how to do some simple CRUD options with GAE for Java and Spring MVC. I’m in the process of figuring out to do things like many-to-many relationships and indexes. Look for that in my next post.
blog comments powered by Disqus