A Scala API for Datomic
Here is a very simple sample to get started with Datomisca.
You can find this sample in Datomic Github Samples Getting-Started
You can add that in your build.sbt
or Build.scala
depending on your choice.
resolvers += Resolver.bintrayRepo("dwhjames", "maven")
// to get Datomic free (for pro, you must put in your own repo or local)
resolvers += "clojars" at "https://clojars.org/repo"
The latest release is 0.7-RC1
libraryDependencies ++= Seq(
"com.github.dwhjames" %% "datomisca" % "0.7-RC1",
"com.datomic" % "datomic-free" % "0.9.4724"
)
The following imports should be sufficient to get you started.
import scala.concurrent.ExecutionContext.Implicits.global
import datomisca._
To use Datomisca, you need an implicit connection to Datomic in your scope.
// Datomic URI definition
val uri = "datomic:mem://datomisca-getting-started"
// Datomic Connection as an implicit in scope
implicit val conn = Datomic.connect(uri)
Datomic’s public API is threadsafe, and there is no need to pool the Datomic connection. Datomic will return the same instance of Connection for a given URI, no matter how many times you ask. And Datomic will cache that single instance even if you don’t. (Stuart Halloway 2013-06-25)
We start from scratch so let’s first create a DB.
Datomic.createDatabase(uri)
This method returns a boolean. If true, then a fresh database was created, or else a database already existed for the given URI.
Datomisca allows to define your Schema in a programmatic way.
Here you create your:
Attributes and enumerated entites are gathered in a schema representing your entity.
Let’s create four attributes to represent a Person
:
name: SchemaType.string, Cardinality.one
home: SchemaType.string, Cardinality.one
birth: SchemaType.instant, Cardinality.one
hobbies: SchemaType.ref, Cardinality.many
object PersonSchema {
// Namespaces definition to be reused in Schema
object ns {
val person = new Namespace("person") {
val hobby = Namespace("person.hobby")
}
}
// Attributes
val name = Attribute(
ns.person / "name",
SchemaType.string,
Cardinality.one).withDoc("Person's name")
val home = Attribute(
ns.person / "home",
SchemaType.string,
Cardinality.one).withDoc("Person's hometown")
val birth = Attribute(
ns.person / "birth",
SchemaType.instant,
Cardinality.one).withDoc("Person's birth date")
val hobbies = Attribute(
ns.person / "hobbies",
SchemaType.ref,
Cardinality.many).withDoc("Person's hobbies")
// hobby enumerated values
val movies = AddIdent(ns.person.hobby / "movies")
val music = AddIdent(ns.person.hobby / "music")
val reading = AddIdent(ns.person.hobby / "reading")
val sports = AddIdent(ns.person.hobby / "sports")
val travel = AddIdent(ns.person.hobby / "travel")
// Schema
val txData: Seq[TxData] = Seq(
name, home, birth, characters, // attributes
movies, music, reading, sports, travel // ident entities
)
}
Namespace
is just a helper making our code clearer when creating keywords.Attribute
and AddIdent
are helpers for creating Datomic schema data.PersonSchema
and ns
objects are our idiom for gathering schema information, but feel free to organize your schemas as you see fit.Now we have a schema, let’s transact it into our database. This is our first operation using the transactor and as you may know, Datomisca manages transactor’s communication in an asynchronous and non-blocking way based on Scala 2.10 Execution Context.
To ask the transaction to perform some operations, we use the following method:
Datomic.transact(txData: TraversableOnce[TxData])(implicit conn: Connection, ec: ExecutionContext): Future[TxReport]
As you can see, it accepts a collection of transaction data and returns a Future[TxReport]
.
If you are unfamilar with Scala Future, then consult this overview.
So let’s transact our schema into Datomic:
Datomic.transact(PersonSchema.txData) flatMap { tx =>
...
// do something
...
}
We use
flatMap
because we expect to perform other asynchronous operations upon the completion of the transaction.
The following code will construct the transaction data for a person called John, whose hometown is Brooklyn, was born on Jan, 1 1980, and likes travelling and watching movies.
// John temporary ID
val johnId = DId(Partition.USER)
// John person entity
val john: TxData = (
SchemaEntity.newBuilder
+= (PersonSchema.name -> "John")
+= (PersonSchema.home -> "Brooklyn, NY")
+= (PersonSchema.birth -> new java.util.Date(80, 0, 1))
++= (PersonSchema.hobbies -> Set(PersonSchema.movies, PersonSchema.travel))
) withId johnId
The transaction data john
is equivalent to the following Clojure map.
(let [johnId (d/tempid :db.part/user)]
{:db/id (d/tempid :db.part/user)
:person/name "John"
:person/home "Brooklyn, NY"
:person/birth (java.util.Date 80 0 1)
:person/hobbies [:person.hobby/movies :person.hobby/travel]})
In Datomisca, the DId
type is one of the ways of constructing entity
ids, and here we are constructing a temporary entity id in the user partition.
The SchemaEntity
builder follows Scala’s Builder
for collections. This is
an idiom for incrementally building collections. To build up transaction data
for a new entity, we use attribute–value pairs, rather than keyword–value
pairs. This provides a level of type-safety, as the attribute stores the
schema type and the cardinality along with the keyword ident. The value of the
pair is statically checked against the attribute’s type and cardinality.
Transacting regular data and schema data is no different.
// creates an entity
Datomic.transact(john) map { tx =>
val realJohnId = tx.resolve(johnId)
...
// Do something else
}
tx.resolve(johnId)
is used to retrieve the real Id after insertion from temporary Id.val Seq(realId1, realId2, realId3) = tx.resolve(id1, id2, id3)
So now that we have an entity in our DB, let’s try to query for it.
In Datomisca, you write your queries in Datalog exactly in the same way as Clojure. Leveraging Scala’s 2.10 macros, Datomisca validates the syntax of your query at compile-time and also deduces the number of input/output parameters (more features are also in the roadmap).
Let’s write a “find person by name” query:
val queryFindByName = Query("""
[:find ?e ?home
:in $ ?name
:where
[?e :person/name ?name]
[?e :person/home ?home]]
""")
$
and ?age
) and returning two ouput parameters (?e
and ?home
)$
identifies the database as an input data source?name
is our “by name” input parameterDatomisca’s query macro also supports string interpolation, which means that the query can be written as follows.
val queryFindByName = Query(s"""
[:find ?e ?home
:in $$ ?name
:where
[?e ${PersonSchema.name} ?name]
[?e ${PersonSchema.home} ?home]]
""")
Remember to watch out for escaping the datasource $
as $$
. The toString
method is called on the values of expressions that are interpolated. The
string representation of attributes is their keyword, which is why we can
rewrite the query this way. The query treats expressions of type String
specially, by double quoting them, so,
val name = "John"
val queryFindByName = Query(s"""
[:find ?e ?home
:in $$
:where
[?e ${PersonSchema.name} $name]
[?e ${PersonSchema.home} ?home]]
""")
will result in a query with a clause
[?e :person/name "John"]
Queries are executed using the Datomic.q
method, with your query and the appropriate input parameters.
val results = Datomic.q(queryFindByName, conn.database, "John")
Datomic.q
expects a query and the right number of input parameters according to your query (here two)conn.database
is the ‘current’ database value available from the connection conn
.The query results are bound to the name results
.
According to the input query, the compiler has inferred that there should be two output parameters.
Thus, results
is a Iterable[(Any, Any)]
.
results.headOption map {
case (eid: Long, home: String) =>
...
// do something
}
Note that results is a Iterable[(Any, Any)]
and not Iterable[(Long, String)]
as you might hope. Why? Because with the info provided in the query,
it’s impossible to infer those types directly. In the future, we hope to
extend the power of the query macro to provide type-safety for output
parameters using schema information. Therefore, for now, you must type match with
a case
.
With the previous query, we retrieved eid
, which is an entity id, and now we
can get the entity from the database and inspect it.
val entity: Entity = conn.database.entity(eid)
As before, conn.database
retrieves the currently available value of the
database, and the entity
method looks up the entity map for a given
identifier.
The Entity
and
RichEntity
apis provide various ways of interact with entities. The apply
method
on the implicit RichEntity
allows us to use attributes rather than
keywords to retrieve values, in a similar fashion to how we constructed
transaction data above.
val johnName: String = entity(PersonSchema.name)
val johnHome: String = entity(PersonSchema.home)
val johnBirth: java.util.Date = entity(PersonSchema.birth)
The attributes possess the type information, so Datomisca computes the correct return type.
Datomisca is able to do this for all primitives, of cardinality one or many, but
it can’t do this for reference attributes as Datomic will return values of type
Entity
in most cases, but Keyword
if the referenced entity has an ident
attribute, which is the case here:
val johnHobbies = entity.read[Set[Keyword]](PersonSchema.hobbies)
The read
method allows us to do a type-safe cast.
Read the more detailed guides and the API docs for more details about what was covered here.