Wednesday, June 19, 2013

Component Entities

This post demonstrates Datomic's component entities, and highlights a new way to create components available in today's release.  You can follow along in the code via the sample project.

The code examples use Groovy, a JVM language that combines similarity to Java with concision.  If you are a Java developer new to Groovy, you may want to read this first.

Why Components?

In a database, some entities are their own identities, and others exist only as part of a larger parent entity.  In Datomic, the latter entities are called components, and are reached from the parent via an attribute whose schema includes :db/isComponent true

As a familiar example, consider orders, line items, and products.  Orders have references to line items, and those references are through a component attribute, since line items have no independent existence outside of an order.  Line items, in turn, have references to products.  References to products are not component references, because products exist regardless of whether or not they are part of any particular order.

The schema for a line item component reference looks like this:

{:db/id #db/id[:db.part/db]
 :db/ident :order/lineItems
 :db/isComponent true
 :db/valueType :db.type/ref
 :db/cardinality :db.cardinality/many
 :db.install/_attribute :db.part/db}

Notice also that line items are :db.cardinality/many, since a single order can have many of them.

Component attributes gain three special abilities in Datomic:
  • you can create components via nested maps in a transaction (new in 0.8.4020)
  • touching an entity recursively touches all its components
  • :db.fn/retractEntity recursively deletes all its components
Each of these abilities is demonstrated below.

Creating Components

To demonstrate line item components, let's create an order for some chocolate and whisky.  First, here is a query for products ?e matching a particular description ?v:

productQuery = '''[:find ?e
                   :in $ ?v
                   :where [?e :product/description ?v]]''';

Now, we can query for the products we want to order:

(chocolate, whisky) = ['Expensive Chocolate', 'Cheap Whisky'].collect {
  q(productQuery, conn.db(), it)[0][0];
}
===> [17592186045454, 17592186045455]

The statement above uses Groovy's multiple assignment to assign chocolate to the first query result, and whisky to the second.

Now that we have some products, we can create an order with some line items. As of today's release, you can do this via nested maps:

order = [['order/lineItems': [['lineItem/product': chocolate,
                               'lineItem/quantity': 1,
                               'lineItem/price': 48.00],
                              ['lineItem/product': whisky,
                               'lineItem/quantity': 2,
                               'lineItem/price': 38.00]],
          'db/id': tempid()]];

The nested maps above expand into two subentities.  Notice that you do not need to create a tempid for the nested line items -- they will be auto-assigned tempids in the same partition as the parent order.

The order above is pure data (a list of maps). This greatly facilitates development, testing, and composition.  When we are ready to put the data in the database, the transaction is as simple as:

conn.transact(order).get();

Touching Components

Now we can query to find the order we just created.  To demonstrate that query can reach anywhere within your data, we will do a multiway join to find the order via product description:

ordersByProductQuery = '''
[:find ?e
 :in $ ?productDesc
 :where [?e :order/lineItems ?item]
        [?item :lineItem/product ?prod]
        [?prod :product/description ?productDesc]]''';

The query above joins

  • from the provided productDesc input to to the product entity ?prod
  • from ?prod to the order item ?item
  • from ?item to the order ?e
and returns ?e.

We are going to immediately pass ?e to datomic's entity API, so let's take a moment to create a Groovy closure qe that automates query + get entity:


qe = { query, db, Object[] more ->
  db.entity(q(query, db, *more)[0][0])
}


Now we can find an order the includes chocolate:


order = qe(ordersByProductQuery, db, 'Expensive Chocolate');


Because the Datomic database is an immutable value in your own address space, entities can be lazily realized.  When you first look at the order, you won't see any attributes at all:


===> {:db/id 17592186045457}


The touch API will realize all the immediate attributes of the order, plus it will recursively realize any components:


order.touch();
===> {:order/lineItems #{{:lineItem/product #, 
                          :lineItem/price 38.00M, 
                          :lineItem/quantity 2, 
                          :db/id 17592186045459} 
                         {:lineItem/product #, 
                          :lineItem/price 48.00M, 
                          :lineItem/quantity 1, 
                          :db/id 17592186045458}}, 
      :db/id 17592186045457}


Notice that the line items are immediately realized, and you can see all their attributes.  However, the products are not immediately realized, since they are not components.   You can, of course, touch them yourself if you want.

Retracting Components

I am not as hungry or thirsty as I thought.  Let's retract that order, using Datomic's :db.fn/retractEntity:


conn.transact([[":db.fn/retractEntity", order[":db/id"]]]).get();


Retracting an entity will retract all its subcomponents, in this case the line items.  To see that the line items are gone, we can count all the line items in our database:


q('''[:find (count ?e)
      :where [?e :order/lineItems]]''',
  db);
===> []


References to non-components will not be retracted.  The products are all still there:


q('''[:find (count ?e)
      :where [?e :product/description]]''',
  db);
===> [[2]]

Conclusion


Components allow you to create substantial trees of data with nested maps, and then treat the entire tree as a single unit for lifecycle management (particularly retraction).  All nested items remain visible as first-class targets for query, so the shape of your data at transaction time does not dictate the shape of your queries.  This is a key value proposition of Datomic when compared to row, column, or document stores.

1 comment :

  1. > touching an entity recursively touches all its components
    Maybe this needs a comma, like: touching an entity, recursively touches all its components.

    ReplyDelete