Issuing App Engine datastore queries with the Low-Level API
Last time, I wrote an introduction to using the low-level API for creating entities, setting keys, and getting keys by value.
Basic queries and sorts
These are useful when we know the keys, but its often very useful to be able to query entities by their properties. Consider the Person entities we created for the last example, Alice and Bob:
Entity alice = new Entity("Alice", "Person"); alice.setProperty("gender", "female"); alice.setProperty("age", 20); Entity bob = new Entity(“Person”, “Bob”); bob.setProperty("gender", "male"); bob.setProperty("age", "23"); DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); datastore.put(alice); datastore.put(bob);
Let’s create a query to find the first 10 Persons that are female and sort them by age ascending. How would we write this?
Query findFemalesQuery = new Query("Person"); findFemalesQuery.addFilter("gender", FilterOperator.EQUAL, "female"); findFemalesQuery.addSort("age", SortDirection.ASCENDING); datastore.prepare(findFemalesQuery).asList(FetchOptions.Builder.withLimit(10));
Here are the steps we took:
- Created a Query object, specifying the Query kind
- Added a QueryFilter. Note that this is typesafe. We specify the enum representing the FilterOperator we want to use
- Added a QuerySort. Again, like the QueryFilter, we select the property to sort on as well as an enum representing either an ascending order or descending order.
- We prepare the query. On this result we return it as either an Iterator or as a List of Entities. On this method we can either execute the default query, or we can pass a set of options. In the example above, we use FetchOptions.Builder to set the only option we care about: the limit. We only want 10, so we call withLimit() and pass it 10.
The query interface works well because it’s typesafe where the datastore is typesafe, and not so when the datastore is not – you won’t get errors at runtime because you misspelled “WHERE”, for instance, but you have to be careful not to misspell the properties you are looking for. The flexibility of this interface means that no longer are we constrained by the “every object must have the same bag of properties” frame of thinking. Furthermore, because we don’t need to know the property names apriori (we can use getProperties() and return a Map), we can iterate through this and figure out the keys/value pairs at runtime. This leads to some very powerful abstractions.
Doing a keys only query
It sometimes makes sense for us to only retrieve the keys in a given query. It’s actually incredibly easy, so as long as we know what to expect:
Query findFemalesQuery = new Query("Person"); findFemalesQuery.addFilter("gender", FilterOperator.EQUAL, "female"); findFemalesQuery.addSort("age", SortDirection.ASCENDING); findFemalesQuery.setKeysOnly(); List<Entity> results = datastore.prepare(findFemalesQuery).asList( FetchOptions.Builder.withLimit(10));
The only code that’s different in creating the Query object is that we call setKeysOnly(). This still returns a List of entity objects with only the Kind and Key populated. If we wrote a test for this, it would look like this:
Entity alice = results.get(0); assertEquals("Return Key for Entity", KeyFactory.createKey("Person", "Alice"), alice.getKey()); assertNull("Should not return female property", alice.getProperty("gender")); assertEquals("Returns Entities with no properties", 0, alice.getProperties().size());
Only the Kind and Key are populated in these Entity objects. Even though the API looks similar, under the hood, the behavior is completely different. Recall how queries work underneath the hood:
- Traverse an index and retrieve keys
- Using those keys, fetch the entities from the datastore
The time to do a query depends on the index traversal time as well as the number of entities to retrieve. In a keys only query, this is what happens:
- Traverse an index and retrieve keys
We completely eliminate step 2 from the process. If all we want is Key information or are counting entities (and the count can be done using only indexes), this is the approach we would take.
Let’s pretend Alice and Bob have child entities:
Entity madHatter = new Entity("Friend", "Mad Hatter", alice.getKey()); Entity doormouse = new Entity("Friend", "Doormouse", alice.getKey()); Entity chesireCat = new Entity("Friend", "Chesire Cat", alice.getKey()); Entity redQueen = new Entity("Friend", "Red Queen", bob.getKey()); datastore.put(madHatter); datastore.put(doormouse); datastore.put(chesireCat); datastore.put(redQueen);
Alice now has Friends Mad Hatter, Doormouse and the Chesire Cat as child entities, while Bob has on the Red Queen. How do we find all friends of Alice or Bob? Like so:
Query friendsOfAliceQuery = new Query("Friend"); friendsOfAliceQuery.setAncestor(alice.getKey()); List<Entity> results = datastore.prepare(friendsOfAliceQuery).asList(FetchOptions.Builder.withDefaults()); Query friendsOfBobQuery = new Query("Friend"); friendsOfBobQuery.setAncestor(bob.getKey()); results = datastore.prepare(friendsOfBobQuery).asList(FetchOptions.Builder.withDefaults());
What’s great about these queries is that the datastore knows exactly where to start. Because keys embed parent Key information – Mad Hatter, Doormouse and the Chesire Cat all have “Alice” as a prefix in their key (this is also why you cannot change an entity’s entity group after creation), we know that we just need to start the query from Alice’s Key and just traverse entities with a Key greater than Alice. It’s also a great way of organizing data. Just be aware that too many transactions on a single entity group will destroy your throughput, so design for as small entity groups as possible.
Hopefully this blog post explains a few more features of the low-level API. Understanding the low-level API is an important step in understanding the datastore, and understanding the datastore is a critical step for learning how to build efficient, optimized applications for App Engine.