# Replikering, del 1: hent et fuldt udtræk ind i din database

> Hent alle postnumre, veje og adresser som stream, gem dem i SQLite eller Postgres, og gem sekvensnummeret, du skal synkronisere videre fra.
>
> https://danadresse.dk/guides/replikering-foerste-udtraek · verified 2026-11-02
Har du brug for alle danske adresser i din egen database, fx til joins, batchjobs eller offline brug, så bruger du replikerings-API'et. Det sker i to trin: først et fuldt udtræk af hver entitet, derefter løbende hændelser med ændringerne. Denne guide dækker første trin. [Del 2](/guides/replikering-haendelser) dækker hændelserne.

Endpoints er de samme som i DAWA's replikering: `/replikering/senestesekvensnummer`, `/replikering/udtraek?entitet=…` og `/replikering/haendelser?entitet=…`. Vi leverer dem selv fra vores egen kopi af adresseregistret. Alle kald undtagen `senestesekvensnummer` kræver en nøgle, og et udtræk tæller som ét kald, uanset hvor mange rækker det indeholder.

Du starter altid med et fuldt udtræk. Vores hændelseslog går kun tilbage til 2026, og kilden skiftede fra DAWA til Datafordeleren, da DAWA lukkede 1. oktober 2026. Du kan altså ikke bygge en kopi op af hændelser alene eller fortsætte fra en gammel DAWA-cursor.

## 1. Vælg entiteter og rækkefølge

Udtrækket findes for disse entiteter: `postnummer`, `kommune`, `region`, `sogn`, `retskreds`, `politikreds`, `opstillingskreds`, `afstemningsomraade`, `supplerendebynavn`, `navngivenvej`, `vejstykke`, `adgangsadresse`, `adresse` og `bygning`. De fleste skal kun bruge seks:

| Entitet | Primærnøgle | Hvad det er |
|---|---|---|
| `postnummer` | `nr` | Postnumre |
| `kommune` | `kode` | Kommuner |
| `navngivenvej` | `id` | Veje med navn |
| `vejstykke` | `kommunekode` + `vejkode` | Vejens del i én kommune |
| `adgangsadresse` | `id` | Bygningens indgang med husnummer og koordinat |
| `adresse` | `id` | Den fulde adresse med etage og dør |

Hent dem i den rækkefølge, så det, der bliver refereret til, er på plads først. En `adresse` peger på sin `adgangsadresse` via `adgangsadresseid`, og en `adgangsadresse` peger på `postnr`, `kommunekode` og `navngivenvej_id`. Se [Adresse eller adgangsadresse](/guides/adresse-vs-adgangsadresse) for datamodellen.

## 2. Gem sekvensnummeret, før du starter

Kald `/replikering/senestesekvensnummer` før det første udtræk og gem tallet. Det er dit startpunkt for hændelser bagefter: du henter hændelser fra det tal + 1. (`/replikering/senesteSekvensnummer` med stort S virker også.)

```json id=seneste-response run=no
{"sekvensnummer": 207940624, "tidspunkt": "2026-11-02T12:23:10.000Z"}
```

Rækkefølgen er vigtig. Udtrækket af `adresse` kan tage et kvarter, og data ændrer sig imens. Tager du sekvensnummeret før udtrækket, henter du bagefter alle ændringer fra det tidspunkt. Nogle af dem står allerede i udtrækket, men det gør ikke noget, fordi du gemmer med upserts. Tager du sekvensnummeret efter udtrækket, kan du miste ændringer, der skete undervejs.

## 3. Se formatet

Som standard er udtrækket ét JSON-array, sendt som en stream, ligesom hos DAWA. Sætter du `&ndjson` på URL'en, får du NDJSON i stedet: én JSON-række pr. linje (`application/x-ndjson`). NDJSON er nemmest at læse række for række, så eksemplerne her bruger det. Hent de første tre postnumre:

```python id=udtraek-peek run=live expect="København K"
import json
import os

import requests

BASE = "https://api.danadresse.dk"
H = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]}

seneste = requests.get(f"{BASE}/replikering/senestesekvensnummer", headers=H, timeout=10).json()
print("seneste sekvensnummer:", seneste["sekvensnummer"])

# ndjson: én række pr. linje. Uden den får du ét stort JSON-array.
with requests.get(f"{BASE}/replikering/udtraek", params={"entitet": "postnummer", "ndjson": ""},
                  headers=H, stream=True, timeout=(5, 30)) as r:
    r.raise_for_status()
    for i, linje in enumerate(r.iter_lines()):
        print(json.loads(linje))
        if i == 2:
            break
```

Rækkerne er kolonnerne i vores egne tabeller, så feltnavnene er ikke præcis DAWA's attributnavne fra replikeringen. `/replikering/datamodel` viser felterne og primærnøglen for hver entitet; læs den i stedet for at gætte på navne. En `adresse`-række ser sådan ud:

```json id=adresse-row run=no
{"id": "000021c5-e9ee-411d-b2d8-ec9161780ccd", "status": 1, "adgangsadresseid": "0a3f5086-e9cd-32b8-e044-0003ba298018", "etage": "2", "doer": "tv", "oprettet": "2018-08-02T12:47:31", "aendret": "2018-08-02T12:47:31"}
```

Bemærk, at udtrækket bruger feltnavne uden æ, ø og å (`doer`, `aendret`), og at `adgangsadresse` har både `lat`/`lon` (WGS84) og `etrs89_x`/`etrs89_y` (UTM32). Udtrækket indeholder alle statusser: 1 gældende, 3 foreløbig, 2 nedlagt og 4 henlagt. Filtrér på `status`, hvis du kun vil vise aktive adresser.

## 4. Stream udtrækket ind i SQLite

Scriptet herunder henter hver entitet som stream og skriver i batches af 5.000 rækker. Hele rækken gemmes som JSON, så du ikke skal vedligeholde et skema for hvert felt, og primærnøglen læses fra `/replikering/datamodel`:

```python id=udtraek-sqlite run=syntax
"""foerste_udtraek.py: fuldt udtræk af adressedata ind i SQLite.

    python foerste_udtraek.py                      # alle entiteter i ENTITETER
    python foerste_udtraek.py postnummer kommune   # kun disse
"""
import json
import os
import sqlite3
import sys
import time

import requests

BASE_URL = os.environ.get("DANADRESSE_BASE_URL", "https://api.danadresse.dk")
HEADERS = {"X-Api-Key": os.environ["DANADRESSE_API_KEY"]}

# Rækkefølge: det, der bliver refereret til, før det, der refererer
ENTITETER = ["postnummer", "kommune", "navngivenvej", "vejstykke", "adgangsadresse", "adresse"]
BATCH = 5000


def opret_tabeller(db: sqlite3.Connection, entiteter: list[str]) -> None:
    for e in entiteter:
        # pk = primærnøglen som tekst; data = hele rækken som JSON
        db.execute(f"CREATE TABLE IF NOT EXISTS {e} (pk TEXT PRIMARY KEY, status INTEGER, data TEXT NOT NULL)")
    db.execute("CREATE TABLE IF NOT EXISTS replikering_cursor "
               "(entitet TEXT PRIMARY KEY, sekvensnummer INTEGER NOT NULL)")
    db.commit()


def hent_json(path: str) -> dict:
    r = requests.get(BASE_URL + path, headers=HEADERS, timeout=(5, 30))
    r.raise_for_status()
    return r.json()


def hent_udtraek(db: sqlite3.Connection, entitet: str, noegle: list[str]) -> int:
    sql = (f"INSERT INTO {entitet} (pk, status, data) VALUES (?, ?, ?) "
           "ON CONFLICT(pk) DO UPDATE SET status = excluded.status, data = excluded.data")
    antal = 0
    rows: list[tuple] = []
    # stream=True: rækkerne skrives, mens de kommer, i stedet for at ligge i hukommelsen.
    # Read-timeout gælder mellem to bidder af svaret, ikke hele overførslen.
    # ndjson: én række pr. linje, så iter_lines() kan læse rækkerne én ad gangen.
    with requests.get(f"{BASE_URL}/replikering/udtraek", params={"entitet": entitet, "ndjson": ""},
                      headers=HEADERS, stream=True, timeout=(5, 120)) as r:
        r.raise_for_status()
        for linje in r.iter_lines():
            if not linje:
                continue
            raekke = json.loads(linje)
            pk = "-".join(str(raekke[k]) for k in noegle)
            rows.append((pk, raekke.get("status"), json.dumps(raekke, ensure_ascii=False)))
            if len(rows) >= BATCH:
                db.executemany(sql, rows)
                db.commit()
                antal += len(rows)
                rows.clear()
    if rows:
        db.executemany(sql, rows)
        db.commit()
        antal += len(rows)
    return antal


def main(entiteter: list[str]) -> None:
    db = sqlite3.connect("adresser.db")
    opret_tabeller(db, entiteter)
    model = hent_json("/replikering/datamodel")  # primærnøgle pr. entitet

    # 1. Gem sekvensnummeret FØR udtrækket. Ændringer under udtrækket hentes
    #    bagefter som hændelser og overskriver idempotent.
    start = hent_json("/replikering/senestesekvensnummer")["sekvensnummer"]
    print("start-sekvensnummer:", start)

    for entitet in entiteter:
        t0 = time.monotonic()
        antal = hent_udtraek(db, entitet, model[entitet]["key"])
        # 2. Cursor pr. entitet: herfra skal hændelser hentes næste gang
        db.execute("INSERT INTO replikering_cursor (entitet, sekvensnummer) VALUES (?, ?) "
                   "ON CONFLICT(entitet) DO UPDATE SET sekvensnummer = excluded.sekvensnummer",
                   (entitet, start))
        db.commit()
        print(f"{entitet}: {antal} rækker på {time.monotonic() - t0:.0f} s")


if __name__ == "__main__":
    main(sys.argv[1:] or ENTITETER)
```

Typisk output for de små entiteter:

```text id=udtraek-output run=no
start-sekvensnummer: 207940624
postnummer: 1108 rækker på 0 s
kommune: 99 rækker på 1 s
navngivenvej: 112484 rækker på 31 s
vejstykke: 113877 rækker på 29 s
```

`adgangsadresse` og `adresse` har millioner af rækker. Regn med 10 til 30 minutter for hver, afhængigt af din forbindelse og din database. Afbrydes forbindelsen undervejs, så kør scriptet igen for den entitet. Upserts gør, at rækker, du allerede har, bare bliver overskrevet.

Hvorfor stream? `requests.get(...).json()` på hele `adresse`-udtrækket ville læse flere gigabyte ind i hukommelsen, før den første række blev gemt. Med `&ndjson` behandler `iter_lines()` én linje ad gangen. Foretrækker du standardformatet med JSON-array, så læs det med en streamende JSON-parser som `ijson` (`ijson.items(r.raw, "item")`), aldrig med `.json()`.

## 5. Postgres i stedet for SQLite

I Postgres er `jsonb` og `ON CONFLICT` det naturlige valg. Brug `||` i opdateringen, så felter fra senere hændelser bliver flettet ind i stedet for at erstatte hele rækken:

```sql id=postgres run=no
CREATE TABLE adresse (
  pk     text PRIMARY KEY,
  status smallint,
  data   jsonb NOT NULL
);
CREATE INDEX adresse_adgangsadresseid ON adresse ((data->>'adgangsadresseid'));

-- Én række pr. linje i udtrækket (psycopg: cur.executemany(sql, rows))
INSERT INTO adresse (pk, status, data) VALUES (%s, %s, %s::jsonb)
ON CONFLICT (pk) DO UPDATE
  SET status = EXCLUDED.status,
      data   = adresse.data || EXCLUDED.data;

CREATE TABLE replikering_cursor (entitet text PRIMARY KEY, sekvensnummer bigint NOT NULL);
```

Til meget store mængder er `COPY` til en midlertidig tabel efterfulgt af én `INSERT … SELECT … ON CONFLICT` hurtigere end `executemany`.

## 6. Hold kopien opdateret

Når udtrækket er færdigt, står start-sekvensnummeret i `replikering_cursor` for hver entitet. Derfra henter du ændringer med `/replikering/haendelser?entitet=…`. Det er beskrevet i [Replikering, del 2: hændelser](/guides/replikering-haendelser). Vil du have besked, når der er nyt, i stedet for at spørge regelmæssigt, så se [Webhooks](/guides/webhooks-opsaetning).

Entiteter, der sjældent ændrer sig og ikke har løbende hændelser (fx `kommune`, `region` og `sogn`), kan du hente igen med et nyt udtræk, fx en gang om måneden.

> [!NOTE]
> DAWA's gamle sti `/replikering/{entitet}` (fx `/replikering/adgangsadresser`) giver det samme udtræk, også med `&ndjson`. `bygning` findes kun som `/replikering/udtraek?entitet=bygning`.

Data kommer fra Klimadatastyrelsen via Datafordeleren. Gemmer du en kopi, så husk kildeangivelsen "Indeholder data fra Klimadatastyrelsen (CC BY 4.0)".
