Referensiell integritet på tvers av subgrafer
FS GraphQL API består av en supergraf med flere subgrafer. Hver subgraf eier sine egne data, men utad er alt ett sammenhengende system. Det betyr at data i én subgraf ofte refererer til data som eies av en annen – et utdanningstilbud i opptakssystemet viser for eksempel til en utdanningsinstans som eies av utdanningsregisteret.
Denne siden beskriver hvordan vi sikrer referanseintegritet på tvers av subgrafene, hvorfor vi har valgt logisk replikering mellom Postgres-databasene som mekanisme, og hvilke prinsipper som gjelder når data deles. Den er ment for deg som utvikler en subgraf og trenger å referere til eller bygge på data som eies av andre.
Bakgrunn og begreper
Solide, sammenhengende informasjonsmodeller har vært garantien for langsiktighet og kontinuitet i løsningene våre helt fra start. Vi optimaliserer for høy datakvalitet og krever at det finnes referanseintegritet og skranker i databasene som ivaretar dette – også når referansene går på tvers av delsystemer.
Sentrale begreper:
- Eier (autoritativ kilde) – subgrafen som er masterdatakilde for en gitt type data. All oppretting, endring og sletting skjer hos eieren.
- Referanseintegritet – databasens garanti for at dersom du refererer til noe, så finnes det du refererer til. Internt i en database håndheves dette med fremmednøkler.
- Logisk replikering – Postgres sin innebygde mekanisme for å kopiere endringer i utvalgte tabeller fortløpende fra én database til en annen.
Utfordringen er at fremmednøkler bare virker innenfor én database. Skal opptaksdatabasen kunne garantere at et utdanningstilbud peker på en utdanningsinstans som faktisk finnes, må identitetene til utdanningsinstansene finnes lokalt i opptaksdatabasen. Det er dette hullet logisk replikering tetter: eieren publiserer et minimalt utvalg av sine data, og mottakeren abonnerer på dem og kan definere fremmednøkler mot dem.
Prinsipper for deling av data
Følgende prinsipper gjelder for all replikering av data mellom subgrafer:
- Hver subgraf eier sine egne data. All endring av data skjer via subgrafen som eier dem.
- Replikerte data er read-only hos mottakeren. Mottakerens applikasjon skal aldri kunne skrive til replikerte tabeller, og dette skal håndheves med rettigheter i databasen.
- Repliker så lite som mulig. Bare de tabellene, kolonnene og radene som trengs for å dekke behovet. Postgres støtter kolonnelister og radfiltre i publikasjoner, så minimeringen kan håndheves teknisk – ikke bare som en intensjon.
- Entitetstabeller replikeres for referanseintegritet. Identitetene til entiteter du refererer til, replikeres til egne tabeller slik at du kan definere fremmednøkler mot dem og modellere videre normalisert.
- Denormalisering kun ved dokumentert behov. Data utover identiteter kan replikeres der det er nødvendig for ytelse, typisk for å bygge søkeindekser. Behovet skal dokumenteres.
- Replikerte data re-eksponeres ikke. Data du har fått replikert skal ikke tilgjengeliggjøres i din subgraf – det er eierens subgraf som eksponerer dem i supergrafen. Trenger konsumenter dataene sammen, løses det med føderering.
- All replikering dokumenteres. Hva som replikeres, hvorfor, og hvem som er eier.
Hvorfor vi løser dette i databaselaget
Vi har valgt å bruke databasens egne mekanismer – fremmednøkler og logisk replikering – fremfor å sjekke referanser i applikasjonskode eller synkronisere via egne integrasjonsjobber. Det følger av samme erfaring som ligger bak resten av arkitekturen:
- Databasen står seg bedre over tid enn applikasjonskoden. Informasjonsmodellen for domenet vårt endrer seg saktere enn teknologien rundt. En fremmednøkkel gir en garanti som gjelder uansett hvilken applikasjon eller kodevei som skriver til databasen.
- Integritetssjekker i applikasjonskode er utilstrekkelige. De kan omgås, de må dupliseres i hver kodevei, og de kan ikke garantere noe om data som allerede ligger i databasen.
- Logisk replikering er innebygd i Postgres. Vi slipper å utvikle og forvalte egne synkroniseringsjobber med tilhørende feilhåndtering, omkjøring og overvåking – Postgres håndterer initiell kopi, fortløpende endringer og gjenopptak etter avbrudd.
Rett mekanisme for rett behov
Føderering og replikering er ikke konkurrerende mekanismer – de henger tett sammen og understøtter hverandre. Sett fra konsumentens side er det supergrafen som er ansvarlig for å hente data, og data hentes alltid fra den autoritative kilden. Replikeringen virker i databaselaget og sørger for at identitetene subgrafene refererer til, er gyldige – som er nettopp det fødereringen bygger på. Se Samspillet med fødereringen.
Replikering er altså ikke en generell integrasjonskanal, men databaselagets virkemiddel for integritet og ytelse. API-først-strategien gjelder fortsatt: forretningsdata utveksles via supergrafen. Bruk denne tabellen for å finne rett mekanisme:
| Behov | Mekanisme |
|---|---|
| Vise data fra et annet delsystem i APIet | Føderering i supergrafen – ingen kopi |
| Garantere at en referanse peker på noe som finnes | Replikert entitetstabell + fremmednøkkel |
| Søk eller ytelse som krever oppdaterte data fra andre | Replikering for denormalisering |
| Reagere på forretningshendelser i andre delsystemer | Hendelser via supergrafen |
Merk skillet mellom de to siste: en søkeindeks trenger dataene fortløpende og maskinelt – det er et databasebehov. En forretningsprosess som skal reagere på at noe har skjedd, er et API-behov og skal bruke hendelsene som eierens subgraf eksponerer.
Slik fungerer logisk replikering
Logisk replikering i Postgres følger en publiser/abonner-modell:
- Eieren oppretter en publikasjon (
CREATE PUBLICATION) som angir hvilke tabeller – og eventuelt hvilke kolonner og rader – som tilbys. - Mottakeren oppretter et abonnement (
CREATE SUBSCRIPTION) som kobler seg til eierens database, kopierer eksisterende data (initiell synkronisering), og mottar deretter alle endringer fortløpende. - Endringene leses fra eierens transaksjonslogg (WAL) via en replikeringsslot, som sørger for at ingen endringer går tapt selv om mottakeren er nede en periode.
Tabellene må finnes på forhånd hos mottakeren, med samme skjemakvalifiserte navn som hos eieren og kompatible kolonner – replikeringen matcher tabeller på fullt navn, og tabelldefinisjoner kopieres ikke automatisk. Replikeringen er alltid enveis: fra eier til mottaker, aldri tilbake.
Detaljene – konfigurasjon, roller, nettverk og feilsøking – dekkes i egne veiledninger, se Status og videre arbeid.
Mønster: entitetstabeller for referanseintegritet
Dette er hovedmønsteret, og det du normalt skal bruke. Mottakeren abonnerer på en minimal entitetstabell – i utgangspunktet kun identitetene – og definerer fremmednøkler mot den.
Hos eieren ligger entitetstabellen i domeneskjemaet dens – utdanningsregisteret har valgt
utdanning, så tabellen heter utdanning.utdanningsinstans. Siden replikeringen matcher
tabeller på skjemakvalifisert navn, må mottakeren bruke nøyaktig samme navn: navnerommet følger
eieren.
Alle kolonnene som inngår i entitetens identitet i grafen må replikeres. Dette er kolonnene i
@node sine keyColumns, ref Samspillet med fødereringen. Om
det er en naturlig nøkkel eller en surrogat, er eierens modellvalg; kravet mønsteret stiller, er
at nøkkelen er stabil. For utdanningsinstans er identiteten utdanningsinstansnr – de
organisasjonstildelte kodene kan endres over tid, mens nummeret står fast.
Hos eieren (utdanningsregisteret) publiseres i utgangspunktet kun nøkkelkolonner:
create publication opptak_utdanningsinstans
for table utdanning.utdanningsinstans (utdanningsinstansnr);
Hos mottakeren (opptak) opprettes tabellen med samme skjemakvalifiserte navn, og abonnementet kobles opp:
create schema utdanning;
create table utdanning.utdanningsinstans (
utdanningsinstansnr bigint primary key
);
create subscription utdanningsregisteret_utdanningsinstans
connection '<tilkoblingsstreng til utdanningsregisterets database>'
publication opptak_utdanningsinstans;
Deretter kan opptaksdatabasen modellere normalisert videre, med full referanseintegritet:
create table opptak.utdanningstilbud (
opptakstype_kode text not null,
opptak_kode int not null,
utdanningsinstansnr bigint not null
references utdanning.utdanningsinstans,
-- ...
primary key (opptakstype_kode, opptak_kode, utdanningsinstansnr)
);
Databasen garanterer nå at et utdanningstilbud aldri kan referere til en utdanningsinstans som ikke finnes hos eieren. Den replikerte nøkkelkolonnen inngår direkte i utdanningstilbudets egen primærnøkkel – normalisert modellering fortsetter altså sømløst på toppen av den replikerte entitetstabellen. Entiteter med sammensatte naturlige nøkler håndteres på samme måte: publiser alle nøkkelkolonnene og definer en tilsvarende sammensatt fremmednøkkel – veiledningen viser den varianten i praksis.
Beregnede kolonner (GENERATED ALWAYS AS (…) STORED) kan først publiseres fra Postgres 18.
Trengs en beregnet kolonne hos mottakeren på 17, kan samme beregning defineres
lokalt på toppen av replikerte grunnkolonner.
Opptak har i dag et eget utdanning-skjema med lokalt modellerte og lokalt vedlikeholdte kopier
av flere av utdanningsregisterets tabeller. Den nyeste utdanningstilbud-modellen
(opptak.utdanningstilbud_v2) refererer i tillegg til utdanningsregisterets ID-er som ren tekst
(utdanningsinstans_ekstern_id) – uten fremmednøkkel. Det er nettopp disse to mellomløsningene
mønsteret skal erstatte: replikerte tabeller i stedet for manuelt vedlikeholdte kopier, og
fremmednøkler i stedet for ukontrollerte ID-strenger. Overgangen må også håndtere at
utdanning-navnerommet i opptak i dag er opptatt av de lokale kopiene.
Mønsteret med å legge eksterne identiteter i egne tabeller med fremmednøkler mot dem, har vi brukt lenge. Det nye er at logisk replikering holder disse tabellene vedlike automatisk, i stedet for at hver tjeneste må gjøre det selv.
Samspillet med fødereringen
I supergrafen bruker vi Apollo Federation, der entiteter deles mellom subgrafer ved hjelp av
@key-direktivet.
For at dette skal fungere, må alle subgrafer bruke samme verdi for @key-feltene for den samme
entiteten – det er slik supergrafen vet at det er snakk om den samme utdanningsinstansen på
tvers.
I skjemaene våre er id-feltet en global Node-ID som Graphitron genererer ut fra
@node-direktivet: typeId og de konfigurerte keyColumns pakkes inn i en base64-url-safe
enkodet ID. For at @key-verdiene skal bli identiske på tvers, må subgrafene dermed konfigurere
@node likt for entiteten – samme typeId og samme keyColumns i samme rekkefølge.
Utdanningsregisteret definerer utdanningsinstans med
@node(typeId: "10150", keyColumns: ["utdanningsinstansnr"]), og opptak speiler konfigurasjonen
og eksponerer bare selve referansen:
type Utdanningstilbud {
# ...
utdanningsinstans: Utdanningsinstans
}
type Utdanningsinstans implements Node
@node(typeId: "10150", keyColumns: ["utdanningsinstansnr"])
@key(fields: "id", resolvable: false) {
id: ID! @nodeId
}
Det er her replikeringen og fødereringen møtes: nøkkelkolonnen som replikeres, er den autoritative verdien ID-en bygges av, og fremmednøkkelen garanterer at subgrafen aldri kan lagre – og dermed aldri eksponere – en referanse som ikke finnes hos eieren. Hver eneste ID opptak sender fra seg, lar seg altså resolve.
Innholdet i utdanningsinstansen henter supergrafen fra utdanningsregisterets subgraf – den autoritative kilden. Slik understøtter prinsippene hverandre: replikeringen sørger for integritet og gyldige nøkler i databaselaget, mens fødereringen sørger for at konsumentene alltid får dataene fra eieren. Konsumenten ser aldri de replikerte dataene, bare den sammenhengende grafen.
Mønster: denormalisering for søk
Noen behov lar seg ikke løse med føderering og identiteter alene. Et godt eksempel er søk: opptakssystemet skal tilby gode søk etter utdanningstilbud, men mye av informasjonen om utdanningsmulighetene tilbudene gjelder for, eies av utdanningsregisteret. Et slikt søk krever en søkeindeks bygget på oppdaterte data fra begge, og det kan ikke løses med API-kall i spørringsøyeblikket uten at ytelsen blir uakseptabel.
Da tillater vi replikering av dataene som trengs for å bygge indeksen:
- Repliker kun kolonnene og radene indeksen faktisk trenger, med kolonneliste og eventuelt radfilter i publikasjonen.
- De replikerte tabellene er kilde for indeksen, ikke en del av mottakerens egen datamodell. Denormaliseringen – materialiserte visninger, indekstabeller eller lignende – gjøres lokalt hos mottakeren, på toppen av de replikerte tabellene.
- Prinsippene gjelder fortsatt fullt ut: read-only, ikke re-eksponer eierens data som dine egne, og dokumenter behovet.
Dersom du er i tvil om behovet kvalifiserer, start med føderering og mål ytelsen. Replikering for denormalisering er unntaket som skal begrunnes – ikke utgangspunktet.
Konsekvenser du må designe for
Replikering gir oss fremmednøkler på tvers av subgrafer, men den endrer ikke at det er to selvstendige databaser. Følgende egenskaper må du kjenne til og designe for.
Replikerte data er ferskvare
Replikeringen er asynkron. Normalt er forsinkelsen under et sekund, men ved store endringsvolumer eller avbrudd kan mottakeren ligge etter. Design for at replikerte data kan være marginalt utdaterte, og bruk aldri replikerte data i flyter som krever et transaksjonelt konsistent bilde på tvers av databasene.
Fremmednøkler håndheves ikke under selve replikeringen
Endringer som kommer inn via et abonnement, tas imot med session_replication_role = replica.
Det betyr at fremmednøkkelsjekker ikke kjøres når replikerte rader skrives hos mottakeren –
kun lokale skriv sjekkes. I praksis er den viktigste konsekvensen knyttet til sletting, se under.
Sletting hos eieren
Dersom eieren sletter en rad som mottakeren har fremmednøkler mot, vil slettingen bli replikert uten at mottakerens fremmednøkler stopper den – og mottakeren sitter igjen med referanser til noe som ikke lenger finnes. Dette må håndteres med avtalte sletteregler mellom eier og mottaker. Utgangspunktet vårt er at entiteter som andre kan referere til, ikke slettes fysisk, men markeres som utgåtte (soft delete). Fysisk sletting – for eksempel av personvernhensyn – må koordineres med mottakerne.
Skjemaendringer replikeres ikke
Logisk replikering kopierer data, ikke DDL. Endrer eieren skjemaet for en publisert tabell, kan replikeringen stoppe hos mottakerne inntil de har gjort tilsvarende endring. Å publisere en tabell er derfor en kontrakt: eieren varsler mottakerne om skjemaendringer på publiserte kolonner i god tid, på samme måte som vi håndterer endringer i GraphQL-skjemaene. Ved å publisere med eksplisitt kolonneliste kan eieren fritt endre alt som ikke er publisert.
Replikering er en forpliktelse for eieren
Replikeringsslotten hos eieren holder på transaksjonslogg helt til mottakeren har fått endringene. Er en mottaker nede over tid, hoper loggen seg opp hos eieren og kan i verste fall fylle disken. Både eier og mottaker må derfor overvåke replikeringen – forsinkelse, stoppede abonnementer og slot-størrelse. Hva som skal overvåkes og hvordan, dekkes i veiledningene.
Regler og konvensjoner
- Navnerommet følger eieren – og må avtales. Replikeringen matcher tabeller på
skjemakvalifisert navn, så mottakeren må bruke nøyaktig samme skjema- og tabellnavn som eieren.
Tjenestene våre organiserer databasene i domeneskjemaer, så en replikert
utdanning.utdanningsinstanslegger beslag påutdanning-navnerommet også hos mottakeren. Har mottakeren allerede egne tabeller i et skjema med samme navn – slik opptak har i dag – må navnerommet ryddes som del av overgangen til replikering. - Read-only håndheves med rettigheter. Mottakerens applikasjonsrolle gis kun
SELECTpå replikerte skjemaer. Bare abonnementets eier skal kunne skrive. - Navn identifiserer motparten. Publikasjoner navngis etter mottaker og formål (for eksempel
opptak_utdanningsinstans), abonnementer etter eier og formål (for eksempelutdanningsregisteret_utdanningsinstans). Da er det alltid tydelig ved feilsøking hvem som er i andre enden. Konvensjonen gjelder nye publikasjoner og abonnementer for formålet beskrevet her – utdanningsregisterets eksisterendedbz_publicationer Debezium sin standard og tjener hendelsesloggen, se Status og videre arbeid. - Dokumentasjonen bor hos mottakeren. Det er mottakeren som har behovet, og som dokumenterer hva som replikeres og hvorfor, med lenke fra eierens dokumentasjon.
Status og videre arbeid
Vi bruker Postgres 17 og 18, der logisk replikering er moden funksjonalitet. Mekanismen er
allerede i drift hos oss, men til et annet formål: utdanningsregisteret har en publikasjon
(dbz_publication) som mater en Debezium-basert hendelseslogg (debezium.database_hendelse).
Den publiserer hele tabeller og følger ikke konvensjonene i dette dokumentet – de gjelder
entitetsreplikering mellom subgrafer. Databasene våre kjører på RDS, der logisk replikering
aktiveres med rds.logical_replication-parameteren og rds_replication-rollen.
Kompetansen på logisk replikering er foreløpig begrenset internt, så vi bygger den opp gjennom veiledninger:
Start med Kom i gang med logisk replikering i Postgres – en praktisk innføring der du utforsker hele mekanismen i et lokalt lekemiljø.
Oppskrifter for å tilby data for replikering fra egen subgraf og abonnere på data fra andre i ekte miljøer, kommer under Veiledninger.
SIS-subgrafen kjører på Oracle og er ikke omfattet av mekanismen beskrevet her. Prinsippene for eierskap og deling gjelder like fullt; hvordan data fra SIS gjøres tilgjengelig for replikering, avklares som en del av moderniseringsarbeidet.