Parenteser

Betraktninger fra Mat-teamets grønne enger

Data i passe porsjoner

Nylig ba jeg en database om noe data for en mengde id-er, omtrent sånn:

(defn get-last-served [conn meal-ids]
  (db/q conn
   '{:select [:meal-id (max :served-at)]
     :from :meals
     :where (in :meal-id ?meal-ids)}
   {:params {:meal-ids meal-ids}}))

Altså: gitt alle disse måltids-id-ene, gi meg tilbake en liste med id-en og siste tidspunkt det ble servert.

Problemet kom da jeg ba om for mange måltider på en gang. Denne spørringen skulle nemlig til en eldre databaseserver som ikke syns det var noe særlig å få mer enn 1000 id-er på én gang i en in.

Løsningen ble å batche spørringen min. Så hvordan gjør vi det? Batching er egentlig to operasjoner: del opp input i passe porsjoner, og samle resultatene i én datastruktur.

Så hvordan deler man opp en datastruktur i Clojure? Med partition eller partition-all:

(partition 2 [0 1 2 3 4])
;;=> ((0 1) (2 3))

(partition-all 2 [0 1 2 3 4])
;;=> ((0 1) (2 3) (4))

Som du ser så kan partition finne på å utelate data. Det er fordi den kun returnerer tupler av angitt størrelse (2, i dette tilfellet). Har du en “rest” så blir den ikke med. Dette har sitt bruk, men ikke til å løse batching.

partition-all inkluderer all input, selvom det betyr at den kan returnere tupler med ulikt antall elementer. Det passer bra for oss, som nå har en liste med en passe mengde inputs å sende til database-serveren.

Gitt at vi har en database-tilkobling i conn og en liste med id-er i ids kan vi nå loope over denne lista og hente resultatene for hver enkelt batch:

(map
 (fn [batch]
   (get-last-served conn batch))
 (partition-all 1000 ids))

Dette gir oss en liste med lister av resultater. Disse må samles i én liste. Den aller enkleste måten å gjøre det på er å bytte ut map med mapcatmapcat forventer nemlig at funksjonen du gir den returnerer en liste, og så konkatenerer den sammen alle resultatene til én liste:

(mapcat
 (fn [batch]
   (get-last-served conn batch))
 (partition-all 1000 ids))

Vips, så har vi løst batching! La oss lage en funksjon av det:

(defn batch [f batch-size xs]
  (mapcat f (partition-all batch-size xs)))

Vi kan bruke den sånn:

(defn get-last-served [conn meal-ids]
  (batch
   (fn [batch]
     (db/q conn
      '{:select [:meal-id (max :served-at)]
        :from :meals
        :where (in :meal-id ?meal-ids)}
      {:params {:meal-ids batch}}))
   1000
   meal-ids))

Vakkert!

Christian

Om Clojure

clojure.core er en serie med bloggposter om alle de nyttige verktøyene i `clojure.core`, hjørnesteinen i det rikholdige standardbiblioteket til Clojure. Lyst til å lese videre? Her er det neste innlegget i serien:

Sidestilling med juxt

Kjernebiblioteket i Clojure har rikelig med småfunksjoner man ikke ser hver dag. I dag skal vi se på en hendig liten funksjon med et rart navn. Det er tid for juxt.