magnars

Three Tiny Tidbits That Made Adding Municipalities Effortless

På norsk på Parenteser-bloggen.

At work we recently threw together a new site for food safety posters online. There was a bit of a rush since an on-prem server was about to be shut down, so it was extra satisfying to ship the whole thing in a week. Just like this blog, we built a static site using Christian’s Stasis Powerpack. That definitely helped with the speed.

One thing the old site lacked was an overview of all restaurants by municipality. Luckily, the Norwegian postal service has made a list of all postal codes and their corresponding municipalities publicly available. So we decided to quickly whip up some municipality pages at the last minute. Normally this would mean a tangle of joins, transforms, and glue code – but with Datomic, it turned out to be ridiculously easy.

Tidbit 1 — The Import

We had already pulled in all the food safety inspections and their corresponding restaurants from the dataset on Data Norge. The parsing looked (mostly) like this:

(let [m (zipmap csv-header csv-line)]
  {,,,
   :restaurant/navn (:navn m)
   :restaurant/orgnummer (:orgnummer m)
   :restaurant/poststed {:poststed/postnummer (:postnr m)}
   ,,,})

A poststed (postal place) is its own entity, and :poststed/postnummer is set up as a unique identifier:

;; From the Datomic schema:

{:db/ident :poststed/postnummer
 :db/valueType :db.type/string
 :db/unique :db.unique/identity ;; <--
 :db/cardinality :db.cardinality/one}

Datomic hanldes identity attributes differently, so this line …

 {:poststed/postnummer (:postnr m)}

… turns into an upsert. Meaning: if a poststed entity with this postal code already exists, it’s reused—otherwise, a new one is created.

And because the poststed is declared like this …

 :restaurant/poststed {:poststed/postnummer (:postnr m)}

… it gets automatically linked to the restaurant.

But wait — we don’t have any municipalities yet. Those come from the postal service CSV file, which we import too, but in a separate step. It looks roughly like this:

{:poststed/postnummer (:postnummer m)
 :poststed/navn (:poststed m)
 :poststed/kommune {:kommune/nummer (:kommunekode m)
                    :kommune/navn (:kommunenavn m)}}

;; kommune = municipality

Again, we’re using upserts — two of them this time. If a poststed with that postal code already exists, it’s reused — but enriched with name and municipality. If a municipality with that code already exists, it’s reused as well.

And just like that, we’ve stitched together restaurants → via postal place → to municipality — all through upserts, without me having to “do anything” to wire it up.

Lovely.

PS! I realize I ought to write a little tidbit on Datomic’s delightful system for describing data transactions in this format. It’s coming!

Snippet 2 — Where’s the URL?

As I’ve written before, Datomic models its data as entities and attributes — not tables. If you’re used to tables, it might feel like all the entities are just floating around in an unstructured soup. But that’s a good thing! The world isn’t rectangular.

If you read Christian’s blog post on keys and how to use them, you saw an example of this in action:

{:db/id 17592186046486
 :kommune/nummer "3107"
 :kommune/navn "Fredrikstad"
 :page/uri "/kommune/fredrikstad/"
 :page/kind :page.kind/kommune-page}

Here we’ve got an entity that’s a municipality (kommune) in some contexts and a page in others. Datomic lets you model that without breaking a sweat.

So when I was putting together this link:

[:a.hover:underline {:href "..."}
  (:kommune/navn kommune)]

…I caught myself wondering: “Okay, but what is the actual URL for a municipality page?”

I started thinking about writing a function like (get-kommune-url kommune) to figure it out.

And then it hit me:

[:a.hover:underline {:href (:page/uri kommune)}
  (:kommune/navn kommune)]

Haha! Sometimes things really are that simple.

Datomic’s flexible data model lets entities serve multiple roles without extra ceremony. I didn’t have to juggle multiple layers of abstraction — the data was already where I needed it.

Snippet 3 — The search

Later on, we wanted to include municipality names in the search on the front page. The municipality name was a good search candidate alongside other address-related fields in the index. Here’s the relevant code:

(defn get-searchable-address [restaurant]
  (->> [(-> restaurant :restaurant/adresse :poststed)
        (-> restaurant :restaurant/adresse :linje1)
        (-> restaurant :restaurant/adresse :linje2)]
       (remove empty?)
       (str/join " ")))

Oh no, this function receives only the restaurant. No database to look up the municipality anywhere in sight.

Think for a second — how would you go about also passing the municipality down to this function?

Maybe you’d have to add a JOIN to a SQL query elsewhere? In that case, you’d be looking at a double join: from restaurant to postal place to municipality.

Maybe you’d add the municipality name to some kind of DTO or Restaurant object?

Maybe you’d just pass both the restaurant and the municipality into the function?

Okay, enough daydreaming. Here’s what we actually ended up with:

(defn get-searchable-address [restaurant]
  (->> [(-> restaurant :restaurant/adresse :poststed)
        (-> restaurant :restaurant/adresse :linje1)
        (-> restaurant :restaurant/adresse :linje2)
        (-> restaurant :restaurant/poststed :poststed/kommune :kommune/navn)]
       (filter not-empty)
       (str/join " ")))

Ha!

Thanks to Datomic’s entity API — built on the direct index access we’ve talked about before — the entire database is seamlessly navigable.

No SQL. No unholy INNER JOINs. Just data.

And in the end, it wasn’t about clever code. It was about letting the data model carry the weight.

Datomic Tidbits is a series of blog posts about the exciting and peculiar database Datomic. Did you miss out on the start? Here's the first entry in the series:

An Explosion of Data

Datomic is truly a delightful database to work with. I’m starting the year with a new series of tidbits from this functional, functional database. First out is the data model at its core – and explosions!