Datastore

chrislewis edited this page Jul 27, 2011 · 11 revisions
Clone this wiki locally

Highchair datastore provides a simple mapping layer and type-safe query DSL for persisting and querying scala objects to the google datastore. The datastore has limitations that differentiate its semantics from SQL databases. This makes leveraging a java library intended for SQL databases awkward - and java libraries are already too awkward. Highchair embraces the limitations of the datastore in an idiomatic scala API.

Mapping Case Classes

Persistent objects are defined as case classes which mixin the Entity trait.

import highchair.datastore.Entity
case class Person(
  key:        Option[Key],
  firstName:  String,
  middleName: Option[String],
  lastName:   String,
  age:        Int
) extends Entity[Person]

Note: Highchair currently requires mapped classes to receive an Option[Key] as the first constructor argument.

For query logic, mix the Kind trait into a dedicated class. The companion object is a good choice.

import highchair.datastore.Kind
object Person extends Kind[Person] {
  val firstName   = property[String]("firstName")
  val middleName  = property[Option[String]]("middleName")
  val lastName    = property[String]("lastName")
  val age         = property[Int]("age")
  val * = firstName ~ middleName ~ lastName ~ age
}

Note that the upcoming 0.0.5 release simplifies the previous mapping to:

object Person extends Kind[Person] {
  val firstName   = property[String]
  val middleName  = property[Option[String]]
  val lastName    = property[String]
  val age         = property[Int]
}

Type-Safe Queries

Queries are constructed with a type-safe DSL, rewritten in 0.0.4 to provide a feel intentionally similar to rogue. With the Person class we just mapped, we can build queries like so:

Person where (_.name is "Chris") and (_.age > 20)

And with additional constraints:

Person where (_.name is "Chris")
  and (_.middleName is Some("Aaron"))
  and (_.age > 20)
  orderDesc (_.age)

These expressions build immutable Query which can be safely reused across requests.

val fnChris = Person where (_.name is "Chris")
val over20 = fnChris and (_.age > 20)

Executing Queries

Query objects are executed by fetch methods. These methods receive an implicit DatastoreService parameter, which enables a kind of ad-hoc, call-site dependency injection. Highchair provides connection facilities in the Connection object:

import highchair.datastore.Connection.default
val chris = Person where (_.name is "Chris") fetch()
// chris is a collection of Person instances

If you want a single result, the fetchOne method will yield an Option[Person].

Query Limits, Offsets, and Counts

The number of results can be constrained by a limit. If you want the first 10:

Person where (_.name is "Chris") fetch (limit = 10)

Offsets determine the starting point by skipping over `n results in the result set. If you want to skip the first 1000:

Person where (_.name is "Chris") fetch (offset = 1000)

Explicitly naming the parameters is nicer to read, but if you want both, it's optional:

Person where (_.name is "Chris") fetch (10, 1000)

The datastore doesn't provide an explicit means of counting results, but it does provide efficient "keys-only" queries. Such queries retrieve only the Key objects of matched entities, and not their properties. Highchair supports these queries and, among other things, you can use them to efficiently count matching records:

Person where (_.name is "Chris") fetchKeys() size

The fetchKeys method executes the Query as a keys-only query.