trydatomic

Chapters

Relationships

So far every query has looked at one kind of thing, a Pokemon, and its attributes. Datomic can also connect entities to each other. Our database has a second kind of entity, the types like Fire and Water, along with which types are strong against which. Water is strong against Fire, for example.

Each type points at the types it is strong against. That takes an attribute whose value is another entity, a reference, declared with :db.type/ref:

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

A type is strong against several others, so the cardinality is many. :db/unique :db.unique/identity makes :type/name the type's natural key: at most one type entity can have a given name, and Datomic enforces it. The database also has :type/weak-against (half damage) and :type/no-effect-on (no damage).

Asserting a reference with a lookup ref

A unique attribute like :type/name lets you identify an entity without knowing its entity id, with a lookup ref: a two-element vector like [:type/name "Fire"] that stands in for whichever entity has that value.

Lookup refs let you connect two entities in one transaction, using only values you already know. This adds a new strength for Water without looking up any ids:

(d/transact conn {:tx-data [{:db/id [:type/name "Water"] :type/strong-against [:type/name "Fire"]}]})

Entity ids are assigned by Datomic and can differ between databases, so code shouldn't store them outside it. A unique attribute is stable, and a lookup ref lets you use it anywhere an entity id is expected, including as the value of a reference attribute, as here.

Following a reference

The value of a reference attribute is an entity id. That means a variable can be an entity in one clause and a value in the next. Read the second clause below as "?attacker is strong against ?fire":

Query
[:find ?name :where [?fire :type/name "Fire"] [?attacker :type/strong-against ?fire] [?attacker :type/name ?name]]

Both directions

The same attribute answers the opposite question. Which position the known entity takes decides the direction. Here Fire is on the left, so we get the types Fire is strong against:

Query
[:find ?name :where [?fire :type/name "Fire"] [?fire :type/strong-against ?defender] [?defender :type/name ?name]]

Joining by value

A Pokemon's types are plain strings, like "Ghost", and a type entity has the same string in :type/name. We didn't need a reference between them. As we saw in Anatomy of a query, two clauses that use the same variable have to agree on it. That works on any equal values, whether it is an entity id or a string.

This is a shortcut for the tutorial. In a real schema :pokemon/type would usually be a :db.type/ref to the type entity, which gives referential integrity (no typos that silently join to nothing) and lets pull follow it directly.

Let's use that to answer a bigger question: which Pokemon hit Gengar super-effectively? We'll build the query one step at a time. First, Gengar's types, as strings:

Query
[:find ?defending-name :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name]]

Next, the type entities those strings name. ?defending-name appears in both the :pokemon/type clause and the new :type/name clause, so they have to agree, and the join is by value:

Query
[:find ?defending ?defending-name :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name] [?defending :type/name ?defending-name]]

Now the types strong against them. This is the reference from the start of the chapter, from the attacking type to the defending one:

Query
[:find ?attacking-name :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name] [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name]]

The last step goes back to Pokemon: the ones that have one of those types. The query has hopped across three kinds of links: from Gengar's type strings to their type entities (by value), to the types strong against those (by reference), and back to the Pokemon that have such a type (by value):

Query
[:find ?name :where [?gengar :pokemon/name "Gengar"] [?gengar :pokemon/type ?defending-name] [?defending :type/name ?defending-name] [?attacking :type/strong-against ?defending] [?attacking :type/name ?attacking-name] [?e :pokemon/type ?attacking-name] [?e :pokemon/name ?name]]

Negation across relationships

not-join works across relationships too. Which types have no Pokemon at all? Only ?name is shared between the not-join and the rest of the query:

Query
[:find ?name :where [?type :type/name ?name] (not-join [?name] [?pokemon :pokemon/type ?name])]
TRY

Change "Gengar" to another Pokemon, like "Charizard", in the last join. Or in the first query, change "Fire" to "Ghost" and :type/strong-against to :type/no-effect-on to see which types can't touch Ghost.

You can now
  • Declare a reference attribute with :db.type/ref, and connect entities with a lookup ref instead of an entity id.
  • Follow a reference in either direction, and join by shared value when no reference exists.
  • Chain several hops, by value and by reference, into one query, and negate across a relationship with not-join.