Transact a Schema

Previously: you learned how to connect to a database. This tutorial expects that you have a running REPL where you have already connected to the database "hello".

One of the great things about Datomic is that everything is data, including the definition of the schema for your database. Because it is just data, it can be created and manipulated just like any other data you put into the database. To add new schema, you issue a transaction that carries the new schema definition.

For this tutorial, we'll create a basic model for movies: a movie can have a title, a release year, and a genre. Title and genre are represented as strings, while release year is an integer. In a reletional database, each of those (title, release year, and genre) would be considered "fields" in a "table". In Datomic, each of those is an attribute, which can be associated with an entity. In order to start adding movie data to our database, we'll have to tell it about those three attributes. And, since schema is just data, you describe your custom attributes using Datomic's built-in attributes.

Every attribute definition has three required attributes:

  • :db/ident which specifies a unique name for your attribute
  • :db/valueType which specifies the type of data that can be stored in the attribute
  • :db/cardinality which specifies whether the attribute stores a single value, or a collection of values

You should also supply the optional :db/doc attribute, which stores a docstring of the attribute which you can query for later.

Attribute definitions are defined as maps of data. Our first attribute, "title", would be defined as:

{:db/ident :movie/title
 :db/valueType :db.type/string
 :db/cardinality :db.cardinality/one
 :db/doc "The title of the movie"}

(Note that :db.type/string and :db.cardinality/one are examples of built-in attributes in Datomic, used for the purpose of defining attributes. You can learn more about them in the reference guide.)

Similarly, "genre" would be:

{:db/ident :movie/genre
 :db/valueType :db.type/string
 :db/cardinality :db.cardinality/one
 :db/doc "The genre of the movie"}

Since "year" will be stored as a number, we'll have to change the :db.type for it:

{:db/ident :movie/release-year
 :db/valueType :db.type/long
 :db/cardinality :db.cardinality/one
 :db/doc "The year the movie was released in theaters"}

You could choose to send each of these attributes to the database in individual transactions, but for efficiency, we'll batch them up into a single transaction. To do that, you combine the three maps into a single vector of maps. Let's bind our schema vector to a var called movie-schema:

user=> (def movie-schema [{:db/ident :movie/title
                           :db/valueType :db.type/string
                           :db/cardinality :db.cardinality/one
                           :db/doc "The title of the movie"}

                          {:db/ident :movie/genre
                           :db/valueType :db.type/string
                           :db/cardinality :db.cardinality/one
                           :db/doc "The genre of the movie"}

                          {:db/ident :movie/release-year
                           :db/valueType :db.type/long
                           :db/cardinality :db.cardinality/one
                           :db/doc "The year the movie was released in theaters"}])

Now that we have our attribute definitions ready to go, we have to issue a transaction via the Client library, using the "transact" method. Transact takes two parameters:

  • an active connection
  • a map of data. In this case, our map will only have one key (:tx-data) which will contain our vector of attribute definitions.

Note

Remember the note about asynchrony in the Connect to a Database section and remember to use <!! around your call to transact.

Go ahead and transact the schema:

user=> (<!! (client/transact conn {:tx-data movie-schema}))

You should get back a response like the following:

{:db-before {:database-id "58a47389-f1ab-4d81-85b6-715cecde9bac", 
             :t 63, 
             :next-t 1000, 
             :history false}, 
 :db-after {:database-id "58a47389-f1ab-4d81-85b6-715cecde9bac", 
            :t 1000, 
            :next-t 1001, 
            :history false}, 
 :tx-data [ #datom[13194139534312 50 #inst "2017-02-15T15:28:31.174-00:00" 13194139534312 true] 
            #datom[63 10 :movie/title 13194139534312 true] 
            #datom[63 40 23 13194139534312 true] 
            #datom[63 41 35 13194139534312 true] 
            #datom[63 62 "The title of the movie" 13194139534312 true] 
            #datom[64 10 :movie/genre 13194139534312 true] 
            #datom[64 40 23 13194139534312 true] 
            #datom[64 41 35 13194139534312 true] 
            #datom[64 62 "The genre of the movie" 13194139534312 true] 
            #datom[65 10 :movie/release-year 13194139534312 true] 
            #datom[65 40 22 13194139534312 true] 
            #datom[65 41 35 13194139534312 true] 
            #datom[65 62 "The year the movie was released in theaters" 13194139534312 true] 
            #datom[0 13 65 13194139534312 true] 
            #datom[0 13 64 13194139534312 true] 
            #datom[0 13 63 13194139534312 true]], 
 :tempids {-9223301668109598144 63, -9223301668109598143 64, -9223301668109598142 65}}

The presence of the :db-before and :db-after values shows you that new information was transmitted and persisted in your database. You can learn more about the details of this return value by reading the details of transact in the reference guide.

Next, we'll start adding movies into the database.