Hopp til innhold

Fagstoff

Mer om spørringer og organisering av data i Cloud Firestore

Tanken bak Cloud Firestore er at det skal gå så raskt som mulig å hente ut data, uansett hvor stor databasen er. Derfor er databasen organisert på en annen måte enn det tradisjonelle SQL-databaser er, og du må derfor tenke annerledes når du skal utforme spørringer mot databasen.
Bærbar datamaskin med ringpermrygger i ulike farger over hele skjermen. Foto.
Åpne bilde i et nytt vindu

Fra datamodellering med relasjonsdatabaser (SQL) er du kanskje vant til at data organiseres over flere tabeller som har relasjoner til hverandre. Om vi for eksempel har en database over hoteller og vil registrere hotellrommene på hvert enkelt hotell, ville vi i SQL ha lagd en tabell for hoteller og en tabell for hotellrom. Så hadde vi koblet disse sammen med en en-til-mange-relasjon.

To kvadratiske felt er koblet sammen. Det til venstre har overskriften "Hotell", og det til høyre har overskriften "Hotellrom". Under "Hotell" står det Hotell-ID INT og Hotellnavn VARCHAR(45), Telefon VARCHAR(45), Epost VARCHAR(45) og Adresse VARHCAR(45). Under "Hotellrom" står det Romnummer INT, Romtype VARCHAR(45), Antall_sengeplasser INT, Ledig TINYINT og Hotell_Hotell-ID INT. Illustrasjon.

Samlinger og undersamlinger

I dokumentbaserte databaser (NoSQL) organiseres data på en annen måte. Siden hvert enkelt databaseobjekt er lagret som et frittstående dokument, med i utgangspunktet ingen regler for hvilke data som skal inn i hvert enkelt dokument, kan vi ikke basere oss på koblinger med nøkler og fremmednøkler, slik som i SQL. I dokumentbaserte databaser er dokumenter i stedet organisert i samlinger (collections) og undersamlinger (subcollections). Hotelldatabasen i eksempelet vårt vil da være organisert med alle hotellene i ei samling, og rommene vil være undersamlinger til hvert hotell.

Modell som viser databaser. På øverste nivå er det tre ulike hoteller: Plaza, Grand og Ritz. På nederste nivå er samlinger av de tre hotellene koblet opp mot ulike hotellrom. Illustrasjon.
Åpne bilde i et nytt vindu

Dette kan virke ulogisk om du er vant til relasjonsdatabaser, der det legges mye vekt på struktur, og der det er et viktig prinsipp å unngå dobbeltlagring, men begge modellene følger det samme prinsippet: Ett hotell har flere rom, og ett rom hører til ett hotell.

NoSQL gir deg derimot mer fleksibilitet. Det er ingenting i veien for at flere hoteller har like romnummer, som de ofte har, fordi rommene lagres under hvert hotell, og ID-ene trenger da bare å være unike for hvert hotell. Det er heller ikke noe problem om et hotell ikke har romnummer, men unike navn på rommene, eller kombinasjoner av bokstaver og tall. Det kan også variere hva slags informasjon som er relevant å ha med i databasen ut fra profilen til hotellet. Noen hoteller har kanskje fokus på velvære og vil registrere om rommet har boblebad, steamdusj eller massasjestoler, mens andre vil bare registrere om rommet har bad eller ikke. Det er ikke noe problem i en NoSQL-database.

Mange-til-mange-relasjoner

Hva med mange-til-mange-relasjoner? Se for deg en database for å registrere elever, hvilke fag de har tatt, og hvilken karakter de har fått i de ulike fagene. En datamodell for denne relasjonsdatabasen kan se slik ut:

Modell av en relasjonsdatabase for å registrere elever, fag og karakterer. Et felt har overskriften "Elev". Dette er koblet til et felt med overskriften "Poststed" og et felt med overskriften "Elev_har_Fag". Dette siste feltet er i sin tur koblet til et felt med tittelen "Fag". Illustrasjon.

I Firebase kan dette gjøres på nesten samme måte ved å lage to parallelle samlinger for elever og fag og ei egen samling for å holde orden på hvilke elever som har tatt hvilke fag, og hvilke karakterer de har fått.

Modell av fagdatabase på to nivåer. På det øverste er det to samlinger, den ene med elever og den andre med fag. På det nederste nivået er det ei samling med tittelen "Elev har Fag". Der er en boks merket med "Elev 1 Fag 2", en boks merket "Elev 1 Fag 3" og en boks merket "Elev 2 Fag 1". Illustrasjon.
Åpne bilde i et nytt vindu

Spørringer mot flere samlinger

Forskjellen på dette og en mange-til-mange-relasjon i SQL er hvordan spørringer fungerer. I SQL kunne vi ha brukt ei spørring med JOIN for å hente data fra flere tabeller samtidig. NoSQL har ikke støtte for fremmednøkler og JOIN-operasjoner, så for å hente ut data fra flere tabeller, må vi kjøre flere separate spørringer. I NoSQL kan vi heller ikke definere på forhånd hvilke felt vi vil hente ut, slik som vi kan gjøre i SQL med SELECT.

For å hente ut navn og karakter på alle elever i faget Utvikling kunne SQL-spørringa ha sett slik ut:

SQL-spørring

1SELECT Elev.fornavn, Elev.etternavn, Elev_har_Fag.Karakter
2FROM Fag 
3  JOIN Elev_har_Fag 
4  JOIN Elev
5ON Elev.ElevID = Elev_har_Fag.Elev_ElevID 
6  AND Fag.FagID = Elev_har_Fag.Fag_FagID
7WHERE Fag.Fagnavn = "Utvikling";

I Cloud Firestore må vi lage flere atskilte spørringer, slik:

Firestore-spørring

1// import { getDoc, getDocs, doc, collection, query, where } from "https://www.gstatic.com/firebasejs/9.6.3/firebase-firestore.js";
2
3// Henter alle dokumenter fra samlinga Fag der fagnavn er Utvikling,
4// og lagrer dem i arrayen fagSporring
5const fagSporring = await getDocs(
6  query(
7    collection(db, "Fag"), 
8    where("Fagnavn", "==", "Utvikling")
9  )
10);
11// (I praksis vil spørringa returnere kun ett dokument, 
12// men det blir likevel lagret som en array med ett element.)
13
14// Henter alle dokumenter fra samlinga Elev_har_Fag 
15// (dvs. ID, ElevID, FagID, år og karakter) der FagID 
16// er lik resultatet fra fagSporring, og lagrer dem 
17// i arrayen elevHarFagSporring. 
18const elevHarFagSporring = await getDocs(
19  query(
20    collection(db, "Elev_har_Fag"), 
21    where("FagID", "==", fagSporring[0].id)
22  )
23);
24// Siden getDocs returnerer en array, må vi angi 
25// at vi skal ha data fra element 0 i arrayen 
26// (arrayen har i dette tilfellet bare ett element).
27
28// Oppretter en array for å lagre all data om elever 
29const elevArr = [];
30// Oppretter en (parallell) array for å lagre karakterer
31const karakterArr = [];
32
33// Kjører ei løkke som går gjennom alle dokumenter 
34// fra spørringa elevHarFagSporring
35elevHarFagSporring.forEach((dokument) => {
36  // Henter inn hvert enkelt elevdokument og lagrer i arrayen
37  // elevArr. getDoc henter et dokument med en bestemt ID. 
38  elevArr.push(
39    await getDoc(
40      doc(db, "Elev", dokument.data().ElevID)
41    )
42  );
43  
44  //Henter karakteren til hver elev og lagrer i arrayen karakterArr 
45  karakterArr.push(
46    dokument.data().karakter
47  );
48  // Denne arrayen får da parallelle verdier med ElevArr, 
49  // altså vil karakterene komme i rekkefølge med riktig elev.
50});
51
52// Skriver ut fornavn og etternavn (fra elevArr) og 
53// karakter (fra karakterArr) til alle elever
54for(i = 0; i < elevArr.length; i++){
55  console.log(
56    elevArr[i].data().fornavn, 
57    elevArr[i].data().etternavn, 
58    karakterArr[i]
59  );
60}

Begrensninger på spørringer i Cloud Firestore

Hvis du har brukt SQL før, vet du at du i SQL kan kombinere betingelser, sorterting, gruppering og mer for å få akkurat det resultatet du ønsker fra databasen. På grunn av måten data er organisert i Cloud Firestore på, er det en del begrensninger på hvordan du kan utforme spørringer. Hvis du har en betingelse (where()), kan du ikke sortere (orderBy()) etter noe annet enn den parameteren du brukte i betingelsen.

Eksempel

1// import { collection, query, where, orderBy } from "https://www.gstatic.com/firebasejs/9.6.3/firebase-firestore.js";
2
3query(
4  collection(db, "elever"),
5  where("alder", "<", 18),
6  orderBy("etternavn")
7);

I dette eksempelet vil du få ei feilmelding fordi det er brukt ulike parametere i where() (alder) og orderBy() (etternavn). Dette er fordi ei slik spørring vil ta lengre tid å gjennomføre, og det vil skape mer trafikk til og fra databasen. Det er likevel mulig å gjennomføre ei slik spørring ved å lage en sammensatt indeks (composite index) i Firebase-konsollen. Dette er det mulig å gjøre direkte fra feilmeldinga du får hvis du prøver å kjøre spørringa i eksempelet. Feilmeldinga vil inneholde en lenke til Firebase-konsollen som lar deg opprette akkurat den sammensatte indeksen du trenger, men du bør være forsiktig med å lage for mange, da du bare har mulighet til å lage 50 sammensatte indekser til hver database. Det er mer hensiktsmessig å sortere resultatet du får tilbake (som en array) enn at Firestore sorterer dette for deg.

På samme måte vil du i utgangspunktet ikke kunne kjøre spørringer mot flere undersamlinger samtidig. I hotelleksempelet over vil du for eksempel ikke kunne søke etter ledige dobbeltrom fra flere hoteller samtidig. Dette kan også løses ved å lage en sammensatt indeks i Firebase-konsollen, eller du kan kjøre flere separate spørringer mot hvert hotell og samle resultatene i ei liste (array), som du kan sortere og søke i via ditt eget skript.

Mer informasjon

Firebase tilbyr informasjon om hvordan Firestore-databasen er bygd opp, og hvordan den brukes i videospillelista "Get to know Cloud Firestore" på YouTube.

Her er lenke til to videoer som forklarer prinsippene bak det du har lest om i denne artikkelen:

Relatert innhold

Ved hjelp av datamodellering planlegger man hvordan en database skal bygges opp, ved å sette opp tabeller og angi hvilke forhold disse har til hverandre.

Når vi skal hente data fra Cloud Firestore, kan vi hente ut ett enkelt dokument, alle dokumentene i ei samling, eller vi kan hente data med spørringer.

CC BY-SASkrevet av Karl Arne Dalsaune.
Sist faglig oppdatert 30.05.2022

Læringsressurser

Dynamiske nettsider med NoSQL-databaser