trydatomic

Chapters

Modeling Data

So far we have queried data that was already in the database. Now let's see how it got there. To understand Datomic's data model, we will compare it with a relational database, using a small slice of our Pokemon data.

The normalised way in SQL needs two tables. A Pokemon has one or two types, so the types can't live in a single column of the pokemon table. They go in a second table that points back to it.

Pokemon Table

idnumbernamehp
1001Bulbasaur45
2004Charmander39
3025Pikachu35
4092Gastly30
5093Haunter45
6094Gengar60

Pokemon Types Table

pokemon_idtype
1Grass
1Poison
2Fire
3Electric
4Ghost
4Poison
5Ghost
5Poison
6Ghost
6Poison

To get the names of the Ghost type Pokemon, we can run a query like:

SELECT p.name FROM pokemon p JOIN pokemon_types t ON p.id = t.pokemon_id WHERE t.type = 'Ghost' -- Gastly, Haunter, Gengar

Let's rewrite this database using Datomic.

We won't need a second table for this. Datomic has a Universal Schema. Think of it as one big table with five columns that stores everything in our Database. For now, let's focus on three of the five columns, we'll meet the other two in Time travel.

  1. Entity identifies the "thing" we are referring to
  2. Attribute associates an Attribute with the Entity
  3. Value defines the Value of the Attribute associated with the Entity

We can model the information from the SQL database using these three columns, with one Attribute for each column we had before, plus one for the type.

With this structure, the data for Gastly and Pikachu could look like this:

EntityAttributeValue
1000:pokemon/number092
1000:pokemon/nameGastly
1000:stat/hp30
1000:pokemon/typeGhost
1000:pokemon/typePoison
,,,
1002:pokemon/number025
1002:pokemon/namePikachu
1002:stat/hp35
1002:pokemon/typeElectric

Notice that Gastly has two rows for :pokemon/type: the same Entity and the same Attribute, with a different Value. There is no second table to join. The ids above are just for illustration, Datomic assigns them for you.

Let's implement it.

To install Attributes we need to define at least three things for each of them:

  • :db/ident an identifier, like :pokemon/name
  • :db/valueType a type for the values of this attribute, like :db.type/string or others
  • :db/cardinality whether this attribute accepts one or many values

A Pokemon has one name but can have several types, so :pokemon/type is :db.cardinality/many. That is what replaces the second table. Two of our attributes could be defined and installed like this:

(d/transact conn {:tx-data [{:db/ident :pokemon/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one} {:db/ident :pokemon/type :db/valueType :db.type/string :db/cardinality :db.cardinality/many}]})

The other attributes (:pokemon/number, :stat/hp and the rest) are installed the same way. The Cardinality many chapter shows what this means for queries.

Now that our attributes are installed, let's add our data. We don't choose entity ids, we just say what we know about each Pokemon:

(d/transact conn {:tx-data [{:pokemon/number "001" :pokemon/name "Bulbasaur" :pokemon/type ["Grass" "Poison"] :stat/hp 45} ,,,]})

Finally, we can query our Datomic database to find the names of the Ghost type Pokemon. Click "Run" below and compare the result with the SQL query.

Query
[:find ?name :where [?e :pokemon/type "Ghost"] [?e :pokemon/name ?name]]

As in Anatomy of a query, the shared variable ?e ties the two patterns to the same Pokemon. It does the job of the JOIN ... ON in the SQL above, without a second table.

TRY

Edit the query above to also return each Pokemon's number. Copy the last pattern, change its attribute to :pokemon/number and its variable to ?number, and add ?number to :find.

Bonus: no NULLs

In SQL, a column that only some rows use is filled with NULL for the rest. In Datomic there is nothing to fill: if we don't know a fact, it isn't stored. Only a few Pokemon have a :pokemon/category, so only they show up here:

Query
[:find ?name ?category :where [?e :pokemon/name ?name] [?e :pokemon/category ?category]]
You can now
  • Compare a relational schema to Datomic's entity/attribute/value model.
  • Define an attribute with :db/ident, :db/valueType, and :db/cardinality.
  • Explain why Datomic has no NULLs: an attribute the entity lacks is simply not stored.