Lazy Ids

Unsurprisingly, a LazyId is a referernce to something by its Id. It has two parts – a key, and how to look the item up:

LazyId(id).of(lookupMethod)

Why is it split into two calls like that? It lets us take better advantage of Scala’s implicits, if we have an implicit lookup method in scope. For instance, we could write:

LazyId(id).of[User]

And if there is an implicit lookup method for Users (and that kind of key) in scope, then it will be used.

Equality for Lazy IDs

Two ``LazyId``s are equal if they have the same key and the same lookup method. This typically means that:

LazyId(1).of[User] != LazyId(1).of[Course]

LazyIds are lazy Refs that memoise their result

A LazyId[User] is a subtype of Ref[User]. If we call map, flatMap, or many of the other functions on the LazyId, it will call the lookup method. This uses a lazy val, so that once the item has been looked up, it will be remembered.

For instance, if your lookup method is asynchronous, and returns a Future (wrapped as a RefFuture), then that future will be memoised in the lazy val.

If you don’t want the memoised result (for instance you have made an update that has changed the value in the database), you can always use either lazyId.copy, which will produce a duplicate lazy id with the same key and lookup method but with the lazy val not memoised yet. Or you could use RefById, which is like LazyId but without the lazy val (ie, no memoisation of the result).

Look up methods return another Ref

A lookup method for a single item:

trait LookUpOne[T, K] {
  def lookUpOne[KK <: K](r:RefById[T]):Ref[T]
}

It takes the key, and can return any kind of Ref. So, it could work asynchronously, returning a RefFuture[T]. Or it could work synchronously, returning RefItself[T], RefFailed, or RefNone.

As the lookups for different types of item can be independent of one another, it is simple to write applications where different items live in different databases.

Getting the key

LazyId[T] is also an ImmediateId[T]. This means that the ID for this Ref, if there is one, is immediately available:

val id = lazyId.getId

If you just use string IDs everywhere (and your data uses the trait HasStringId) you won’t need to worry about this next bit, but we also have a mechanism to “canonicalise” ids.

Should LazyId("1").of[User], LazyId(1).of[User] and LazyId(1L).of[User] all refer to the same item?

Or, if you’re using something like MongoDB, what about converting strings to an ObjectId?

And if we have the item itself (a RefItself), then how do we extract the id from it? Is it in id, or (as in MongoDB objects) in _id, or somewhere else?

LazyId.getId takes an implicit parameter of type GetsId[T, KK]. This is an object that knows how to get the ID of that kind of item, and knows how to convert ids that might be in the wrong type into a “canonical” form.

Getting the key of a Future lookup

Suppose we want the ID of a reference, but we don’t have a LazyId, we’ve got a database query:

val rUser:Ref[User] = UserDAO.byName("Algernon Moncrieff")

And suppose our database is asynchronous, and gives us a Future (wrapped as a RefFuture).

Clearly for this reference, we can’t get the ID immediately – we’d need to wait for the database to have finished running the query, ie for the Future to complete.

So instead we would use the refId method that is defined for all ``Ref``s:

val rId = rUser.refId

This will give us a Ref[K] where K is the type of ID this item has.

If we want to wait (block) and get that as an Option, we can:

val blockingId = rId.fetch.toOption

but we can also keep working asynchronously, because Ref is a monad that is happy with asynchronicity:

for {
  id <- rId
} yield ...

Generally doing the latter (using the monadic map and flatMap methods, or for notation) is better. If the ID is immediately available, it will run immediately (negligible cost); if it is a Future computation it will run when the Future is complete.

Lookup caches

Although LazyId``s memoise their results, sometimes you want to ensure that even if you have another ``LazyId to the same item, it won’t repeat the lookup.

This is what LookupCache is for. The class is available to you; it’s up to you when you use it.

Remember, two LazyId``s are equal if their IDs and their lookup methods are the same. So, a ``LookupCache keeps a simple concurrent mutable map of LazyId to looked up Ref.

A typical usage might be:

for {
  item <- cache.lookUp(lazyId)
} ...

This will return the cached Ref if it is available, or cache the LazyId (which memoises its result) if there is not already a value in the cache.

If you have an item, you can also pre-seed a cache with it:

val item = User(id=1, name="Algernon Moncrieff)
cache.remember[Int, User](item.itself)

This uses an implicit lookup method and an implicit GetsId. The reason being that we need to construct a LazyId of the canonical type, including the lookup method it would use, and then store this Ref as the cached value for it.

Lookup catalogs

LazyId(1).of[User] requires a look up method to be passed in (implicitly) at compile time.

It may be that you’d rather delay that until runtime – keep a registry or catalog of lookup methods, and have your start-up configuration register lookup methods that will be used.

This is what LookUpCatalog is for:

val catalog = new LookUpCatalog
val ref = catalog.lazyId(classOf[User], 1)

And separately:

catalog.registerLookUp(classOf[User], userLookup)

You’ll notice that when using LookUpCatalog, we pass in a Class object when we want to register a look up method, or create a LazyId that uses the catalog. This is because of type erasure in Scala (and Java).

Two LazyId``s from a ``LookUpCatalog are considered equal if they have the same ID, the same class (passed in), and come from the same catalog.