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
| id | number | name | hp |
|---|---|---|---|
| 1 | 001 | Bulbasaur | 45 |
| 2 | 004 | Charmander | 39 |
| 3 | 025 | Pikachu | 35 |
| 4 | 092 | Gastly | 30 |
| 5 | 093 | Haunter | 45 |
| 6 | 094 | Gengar | 60 |
Pokemon Types Table
| pokemon_id | type |
|---|---|
| 1 | Grass |
| 1 | Poison |
| 2 | Fire |
| 3 | Electric |
| 4 | Ghost |
| 4 | Poison |
| 5 | Ghost |
| 5 | Poison |
| 6 | Ghost |
| 6 | Poison |
To get the names of the Ghost type Pokemon, we can run a query like:
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.
- Entity identifies the "thing" we are referring to
- Attribute associates an Attribute with the Entity
- 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:
| Entity | Attribute | Value |
|---|---|---|
| 1000 | :pokemon/number | 092 |
| 1000 | :pokemon/name | Gastly |
| 1000 | :stat/hp | 30 |
| 1000 | :pokemon/type | Ghost |
| 1000 | :pokemon/type | Poison |
| ,,, | ||
| 1002 | :pokemon/number | 025 |
| 1002 | :pokemon/name | Pikachu |
| 1002 | :stat/hp | 35 |
| 1002 | :pokemon/type | Electric |
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/identan identifier, like:pokemon/name:db/valueTypea type for the values of this attribute, like:db.type/stringor others:db/cardinalitywhether 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:
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:
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.
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.
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:
- 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.