Polymorphism in Objectify

I'm currently learning how to use Objectify with Google AppEngine (GAE).  I was tripped-up while trying to use the polymorphic capabilities of Objectify.  With the patient help of Jeff on the objectify-appengine group, I realized that I was confusing Objectify's polymorphic object hierarchy with my code's inheritance tree.

I put together a demo to illustrate the difference.  Consider an object hierarchy where Vehicle is extended by Car and Motorcycle:

Vehicle.java:

@Entity
public class Vehicle extends Foundation {
    @Id
    private String licensePlate;

    @Index
    int numWheels;

    @Index
    int numCylinders;

    public String getLicensePlate() {
        return licensePlate;
    }

    public void setLicensePlate(String licensePlate) {
        this.licensePlate = licensePlate;
    }
}


Here are the meanings of the annotations on the Vehicle class:

  • @Entity - Makes the Vehicle class the root of a Objectify polymorphic hierarchy.  An application could have many roots that are unrelated to each other.  e.g. Vehicle, Fruit, Planet, etc.
  • @Id - This tells Objectify that the licensePlate field will be used as the unique identifier for this Vehicle; it will make up the Id part of the underlying datastore Key.
  • @Index - This will cause the numWheels and numCylinders fields to be indexed in the datastore.  This is required if you want to perform a query for Vehicles with a certain number of wheels or cylinders.

Car.java:


@EntitySubclass
public class Car extends Vehicle {

    @Index
    private int numDoors;

    public Car() {
        numWheels = 4;
        numCylinders = 4;
        numDoors = 4;
    }
}


Motorcycle.java:


@EntitySubclass
public class Motorcycle extends Vehicle {

    public Motorcycle() {
        numWheels = 2;
        numCylinders = 4;
    }
}


Here are the meanings of the annotations on the Car and Motorcycle classes:

  • @EntitySubclass - Makes the Car and Motorcycle classes descendants of Vehicle.
  • @Index - This will cause the numDoors field to be indexed in the datastore.  This will allow you to query for Vehicles with a certain number of doors.

You may have noticed that the Vehicle class extends Foundation.  Here's a look at...

Foundation.java:


public class Foundation {
    /**
     * @return A JSON representation of this entity.
     */
    public String toJSON() {
        return new JSONObject(this).toString();
    }
}

This is where I got tripped-up when I started coding with Objectify.  Foundation is the root of my inheritance tree (excluding Object, of course), but it is NOT the root of my Objectify hierarchy.  I originally had the @Entity annotation on Foundation, which does not make sense because Foundation will be extended by all manner of unrelated classes.  (It simply holds a method so that any entity can represent itself as a JSON string).  The @Entity annotation belongs on the root of each of your polymorphic hierarchies; Vehicle in this case.


Examples of Polymorphic Queries


Now that we've got an object hierarchy, lets play around with some queries that illustrate how polymorphism works in Objectify.  Here are some code snippets from a JUnit test I wrote:

Setup the Test:


    @Before
    public void setup() {
        helper.setUp();

        // Create a Car
        Car car = new Car();
        car.setLicensePlate("IMA-CAR");
        OfyService.ofy().save().entity(car).now();

        // Create a Motorcycle
        Motorcycle motorcycle = new Motorcycle();
        motorcycle.setLicensePlate("IMA-BIKE");
        OfyService.ofy().save().entity(motorcycle).now();
    }

Get a four-wheeled vehicle:


    @Test
    public void getFourWheeledVehicle() {
        Vehicle vehicle = OfyService.ofy().load().type(Vehicle.class).filter("numWheels", 4).first().get();

        assertTrue(vehicle instanceof Car);
        assertEquals(vehicle.getLicensePlate(), "IMA-CAR");

        logger.info(vehicle.toJSON());
    }

Notice that even though we queried for a Vehicle, we got a Car because it was the only record in the datastore with 4 wheels. (and because Car is an @EntitySubclass of Vehicle)

Lookup a Car by its license plate:


    @Test
    public void getCar() {
        Car car = OfyService.ofy().load().type(Car.class).id("IMA-CAR").get();

        assertTrue(car instanceof Car);
        assertEquals(car.getLicensePlate(), "IMA-CAR");

        logger.info(car.toJSON());
    }

Remember how the licensePlate field had the @Id annotation? That's why we can use the car's license plate to do a unique lookup by Id.

Query for all four-cylindered vehicles:


    @Test
    public void getFourCylinderVehicles() {
        Query query = OfyService.ofy().load().type(Vehicle.class).filter("numCylinders", 4);

        // View the output and you'll see that both a Car and a Motorcycle came back in the Query
        for(Vehicle vehicle : query) {
            logger.info(vehicle.toJSON());
        }
    }

Since we're lucky enough to have a 4-cylinder motorcycle, this query will return both Car and Motorcycle instances.

Query for all four-doored vehicles:


    @Test
    public void getFourDoorVehicles() {
        Query query = OfyService.ofy().load().type(Vehicle.class).filter("numDoors", 4);

        // Since numDoors is only defined on Car, that's all we should get back in this query
        for(Vehicle vehicle : query) {
            assertTrue(vehicle instanceof Car);
        }
    }

Even though numDoors was defined on Car (not Vehicle), a query on the Vehicle class will return Car instances with 4 doors.

... And now for the curveball:


    @Test
    public void getFourDoorCars() {
        Query query = OfyService.ofy().load().type(Car.class).filter("numDoors", 4);

        for(Car car : query) {
            // You won't see this line logged, because the query won't return any results.
            logger.info(car.toJSON());
        }
    }

This query won't return any records!  This is because of the way Objectify is working under the covers.  The data is stored in the datastore as a Vehicle, with a discriminator field identifying the subclass as a Car.  By default that discriminator is not indexed, and therefore this query yields no results.  If you need to query using a subclass, you can use the annotation @EntitySubclass(index=true)on those subclasses that need to be queried directly.  If you find yourself doing this often, you may want to look at your object hierarchy and see if it needs to be reworked.

Get this from Github

I put this demo on Github in case anyone wants to download it and play around with polymorphic queries in Objectify.

Comments

Post a Comment

Popular posts from this blog

A Simple Java Http Client

Wearing the Golden Handcuffs