Google Datastorage is built on BigTable which is addresses highly scalability and geographically dispersed locations. In constrast to RDBMS'es, Google Datastorage has other principles one must take into account when persisting objects in the cloud. Google provides DatastoreService, but it has more machine-friendly than human-friendly query interface1.
Fortunately, there is at least three frameworks providing higher abstraction for using the DatastoreService. Objectify, Twig and SimpleDS address the complexity of DatastoreService by providing a human-friendly query interface for applications objects' persistence.
Selecting between those three took me a few hours as I was browsing through their discussion groups and evaluating the quality of Wiki. Especially, effort the developer Jeff Schnitzer has put on speaking in favor2 of Objectify and defending its design principles against other candidates made me an impression that the project is really worth trying :)
Let's get started with Objectify
The following tutorial is written for developers who have no previous experience with Google App Engine. If you feel you're more experienced, just leap the steps you feel not suitable for your purposes.
This stupid app demonstrates a simple one-to-many relationship between a car and its owner, and how to manage their persistence with Objectify.
Relational domain model
1. Install Eclipse and Google Plugin for Eclipse
2. Create new GAE project
Name your project and package as you wish but deselect Use Google Web Toolkit and then click Finish
3. Download Objectify 2.0.2 and copy the jar into lib-path and add it also to build path
4. You can test run your application and get the "hello world" printed on browser screen
Everything seems to be ok, so let's proceed with some real code
5. Let's create an entity class Person which stores the car owner instance
import javax.persistence.Id;
import com.googlecode.objectify.annotation.Entity;
@Entity
public class Person {
@Id private Long id;
private String name;
/* setters and getters omitted for brevity */
}
Notes:
- @Entity is not obligatory for Objectify but good for distinguishing entities easily
- @Id is the only required parameter and can be Long, long or String. I prefer Long, because id is auto generated if entity does not exist
6. Create Car entity class
import javax.persistence.Id;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Parent;
@Entity
public class Car {
@Id public Long id;
private String license;
@Parent public Key<Person> owner;
private Car() {}
public Car(Key<Person> owner) {
this();
this.owner = owner;
}
/* setters and getters omitted for brevity */
}
Notes:
- @Parent informs that the entity is in many-one relationship with Person (unlike in JDO/JPA, only children entities annotate their parent entities). Parent must be a type of Objectify Key
data type. - Car constructor must be created with Key<person> argument to set the owner
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/plain");
resp.getWriter().println("Hello, world");
}
...with...
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("text/html");
String html = "<form>Owner id (blank for new)<input name="id" type="text"> "+
"Car license <input name="license" type="text"><input type="submit"></form>";
resp.getWriter().println(html);
if( req.getParameter("license") == null ) {
return;
}
if( req.getParameter("license").length() == 0 ) {
resp.getWriter().println("License not set");
return;
}
}
Notes:
- You should have basic form with car id and license fields when you restart the application and browse it
- Owner id's are auto-generated by Objectify so you can not give distinctive name by yourself
@SuppressWarnings("serial")
public class ObjectifyCarsServlet extends HttpServlet {
static {
ObjectifyService.register(Person.class);
ObjectifyService.register(Car.class);
}
public void doGet(HttpServletRequest req, HttpServletResponse resp)
...
Notes:
- For further reading about registering entities, go to Objectify Best Practices
Objectify ofy = ObjectifyService.begin();
Person owner = new Person();
if( req.getParameter("id") != null && req.getParameter("id").length() > 0 ) {
owner = ofy.find(Person.class, Long.parseLong(req.getParameter("id")) );
}
else
ofy.put(owner);
Notes:
- Create Objectify object, ofy, for persistence manager
- Then, we either find an existing owner or create a new one
Key<Person> ownerKey = new Key<Person>(Person.class, owner.getId());
Car car = new Car(ownerKey);
car.setLicense(req.getParameter("license"));
ofy.put(car);
Notes:
- Declare new ownerKey data-storage key for the owner object
- Create a car instance for the owner
- Persist the car instance with Objectify.put()
Query<Car> q = ofy.query(Car.class).ancestor(ownerKey);
resp.getWriter().println("<pre>Cars for owner id "+owner.getId()+":");
for( Car c : q )
resp.getWriter().println(c.getLicense());
Notes:
- ofy.query(Car.class) searches all Car.class entities from the database
- ancestor(ownerKey) omits all Car entities which don't have the requested owner from the result set (it took me quite a while to find out that Objectify Wiki lacks an example of using Query
.ancestor() method) - Query
is a collection type so it can be iterated through and strip down to single entities