En mi anterior post realizaba una pequeña introducción a Datomic, una base de datos inmutable bastante alejada de las típicas soluciones relacionales o documentales que tan de moda están en estos días.
Después de esa pequeña presentación de Datomic y los conceptos en que se basa, vamos a ver cómo podemos trabajar con ella, centrándonos en la parte de definir un esquema de datos e insertar información. Para ello usaré clojure porque parece el lenguaje más apropiado para tratar con Datomic. Si no estás familiarizado con el lenguaje, esta introducción rápida a la sintaxis de clojure te puede resultar útil.
OJO: No soy un experto en Datomic. En realidad, no le he dedicado más que unas horas para jugar con ella, por lo que es posible probable que parte de lo que diga no sea completamente cierto o, directamente, sea falso. Lo que pretendo con estos posts es poner en claro mis propias ideas y, si de paso, te sirven para aprender algo nuevo o despertar tu curiosidad, estupendo.
Creando una base de datos
Para poder empezar a jugar con Datomic, lo primero que necesitamos es configurar nuestro proyecto para incluir una dependencia sobre Datomic. Podemos crear un proyecto usando leiningen y modificar el fichero project.clj
para incluir la librería de Datomic:
(defproject datomic-playground "0.1.0-SNAPSHOT" :description "Proyecto para jugar con Datomic" :url "https://blog.koalite.com" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.5.1"] [com.datomic/datomic-free "0.9.4532"]])
Tras esto, deberemos referenciar el API de Datomic para usarlo desde nuestro código. Aparentemente, es idiomático usar como alias d
:
(ns datomic-playground.core [:require [datomic.api :as d]])
En el post anterior vimos que datomic tenía varios sistemas de almacenamiento, incluyendo un sistema en memoria que resulta ideal para nuestras pruebas. Podemos crear una base de datos de memoria con la función create-database
y conectarnos a ella con la función connect
:
(def uri "datomic:mem://koalite") (d/create-database uri) (def conn (d/connect uri))
Refrescando conceptos: atributos y entidades
Como explicaba en el post anterior, Datomic se basa en el concepto de atributos para almacenar la información. Un atributo define una relación (o un hecho, si prefieres verlo así) entre un Id
de una entidad y un valor determinado. Los atributos puede ser de diferentes tipos (string
, long
, referencia a otra entidad, etc.) y puede tener distinta cardinalidad (contener un único valor o varios valores del mismo tipo).
Las entidades no existen explícitamente. No hay ningún sitio en que definamos la entidad X, sino que únicamente existen como el conjunto de atributos asociados a un Id
determinado. En Datomic no tendremos una entidad Person
como tal, sino varios atributos asociados a un Id
y esos atributos serán lo que conformen la entidad Person
. Por convención, todos los atributos relacionados con una misma entidad conceptual están prefijados con el nombre de la entidad, por ejemplo tendremos atributos :person/name
, :person/friends
o :person/height
, pero en realidad no es necesario seguir esta convención y podríamos llamarlos como quisiéramos.
Los atributos también son entidades
Una vez que tenemos creada la base de datos, podemos empezar a definir el esquema creando atributos, pero antes de eso es importante entender una cosa: los atributos en Datomic son entidades.
En muchas bases de datos la información sobre el esquema o, en general, la metainformación sobre la base de datos, se almacena en la propia base de datos. Por ejemplo, en SQL Server, la base de datos master
contiene información sobre todas las bases de datos del sistema, y a través de information_schema podemos acceder a información sobre la propia estructura de cada base de datos.
En Datomic esta idea se lleva al extremo, y mientras que en SQL existen «lenguajes» diferentes para la creación del esquema (DDL) y para la manipulación de datos (DML), en Datomic la creación del esquema, es decir, de los atributos, se realiza exactamente igual que la creación del resto de entidades.
Por tanto, un atributo no es más que una entidad (un Id
) que lleva asociados otros atributos que permiten a Datomic saber cómo tratar con él. Una ventaja de esta idea es que permite tratar de forma homogénea la creación del esquema y la inserción de datos, puesto que en el fondo es lo mismo: asociar atributos a entidades.
Para «escribir» en la base de datos la función principal es transact
que recibe una conexión y una lista de operaciones a realizar. En su versión más básica, tendríamos algo así:
(d/transact conn [[:db/add entity-id attribute1 value1] [:db/add entity-id attribute2 value2]])
Si os fijáis en el API, un factor imnportante es que las operaciones están representadas por datos (vectores en este caso) en lugar de utilizar un modelo de objetos específicos o strings. Esto es muy típico en clojure (y supongo que en otros lenguajes funcionales) y permite construir las operaciones que queremos usar en las transacciones utilizando todas las funciones genéricas de manipulación de datos (map
, filter
, etc.).
Como es muy frecuente el caso en que en la misma transacción queremos añadir varios atributos a una misma entidad, transact
permite hacerlo usando un map
:
(d/transact conn [{:db/id entity-id attribute1 value1 attribute2 value2}])
Este código es equivalente al anterior, pero es algo más corto y fácil de leer. Independientemente de qué versión uses, siempre puedes incluir varios map
o vector
en la transacción para que se ejecuten conjuntamente como parte de la misma transacción, en incluso mezclarlos entre sí.
Un esquema de ejemplo
Para poner en práctica todo lo que hemos visto hasta ahora vamos a crear un esquema muy simple. En nuestro esquema tendremos los siguientes atributos:
Nombre | Tipo | Cardinalidad |
---|---|---|
:person/name | string | 1 |
:person/age | integer | 1 |
:person/friends | reference | varios |
Usando transact
, podemos definirlo así:
(d/transact conn [{:db/id #db/id[:db.part/db -1] :db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "Person's name"} {:db/id #db/id[:db.part/db -2] :db/ident :person/age :db/valueType :db.type/long :db/cardinality :db.cardinality/one :db/doc "Person's age"} {:db/id #db/id[:db.part/db -3] :db/ident :person/friends :db/valueType :db.type/ref :db/cardinality :db.cardinality/many :db/doc "Person's friends (refs to another people)"} [:db/add :db.part/db :db.install/attribute #db/id[:db.part/db -1]] [:db/add :db.part/db :db.install/attribute #db/id[:db.part/db -2]] [:db/add :db.part/db :db.install/attribute #db/id[:db.part/db -3]]])
Eso ha sido bastante código. Vamos a centrarnos en un atributo concreto, por ejemplo :person/name
. Lo primero que tenemos es la definición del atributo como tal:
{:db/id #db/id[:db.part/db -1] :db/ident :person/name :db/valueType :db.type/string :db/cardinality :db.cardinality/one :db/doc "Person's name"}
Estamos usando un map
para representar la información del atributo, indicando su nombre (:db/ident
), su tipo (:db/valueType
), su cardinalidad (:db/cardinality
) y (esto es opcional) su documentación (:db/doc
).
Como dijimos hace un momento, los atributos son entidades y, por tanto, tienen un Id
. Lo más extraño del código anterior es, precisamente, la generación de ese Id
. En este caso estamos dejando que lo genere la base de datos dentro de la partición :db.part/db
(lo que es una partición se escapa un poco del alcance de este post, pero puedes leer más en la documentación de Datomic). Podríamos generar el Id
simplemente poniendo #db/id[:db.part/db]
, pero el -1
nos permite referenciar el Id
generado dentro de otra operaciones de la misma transacción.
De esta forma, en la misma transacción podemos instalar el atributo para decirle a Datomic que queremos empezar a utilizarlo en otras entidades, cosa que hacemos con la operación:
[:db/add :db.part/db :db.install/attribute #db/id[:db.part/db -1]
Nuevamente podemos ver cómo todo se trata de forma homogénea. Instalar un atributo consiste en añadir una relación :db.install/attribute
entre la entidad :db.part/db
y el Id
del atributo que acabamos de crear. Aunque todavía no sabemos el valor que se asignará a Id
, podemos referenciarlo gracias a que hemos usado la forma #db/id[:db.part/db -1]
.
Una duda razonable sería por qué para referenciar la entidad :db.part/db
no estamos usando su Id
. Podemos hacerlo así porque al definir un atributo podemos indicar que es un índice único, lo que nos permite usarlo para identificar entidades igual que haríamos con el Id
.
Con lo que ya sabemos sobre transacciones, entidades, ids y atributos, es fácil imaginarse cómo sería el código para añadir un par de personas amigas entre sí:
(d/transact conn [{:db/id #db/id[:db.part/user -1] :person/name "Alejandro" :person/age 53 :person/friends [#db/id[:db.part/user -2]]} {:db/id #db/id[:db.part/user -2] :person/name "Lucas" :person/age 62 :person/friends [#db/id[:db.part/user -1]]}])
Resumen
En este post hemos visto cómo definir un esquema de datos en Datomic y cómo añadir información sobre ese esquema de datos.
Es importante quedarse con la idea de que en Datomic se definen atributos que permiten establecer hechos sobre entidades, y que esos atributos son entidades en si mismas, por lo que la forma de definir el esquema y la forma de insertar información es exactamente la misma.
Además, las transacciones están formadas por un conjunto de operaciones que no son más que estructuras de datos simples (maps
y vectors
), por lo que podemos utilizar toda la potencia del lenguaje (clojure en este caso) para construir las operaciones que lanzaremos contra la base de datos.
En el siguiente post veremos cómo podemos lanzar consultas sobre la base de datos e introduciremos otro de los conceptos diferenciales de Datomic frente a otras bases de datos: la base de datos como valor.