1. Introduktion

1.1. Introduktion till databaser

1.1.1. Vad är en databas?

En databas är en välorganiserad, det vill säga uppbyggd enligt nedskrivna regler, centraliserad, samling av data.

Data vs information.

Information är behandlad data - det vill säga data som någon kan använda för att tillföra något slags värde i någon slags process eller sammanhang.

Man kan säga att data blir information när någon tittar på eller behandlar den.

1.1.2. Typer av databashanteringsystem

En Databashanterare/Databashanteringssystem, eller DBMS (Database Management System), är mjukvara som gör det möjligt att på ett effektivt och säkert sätt definera och modifiera innehåll i en databas.

Man kan inte kommunicera direkt med en databas; all kommunikation sker via databashanteraren.

Det finns i dag två huvudsakliga typer av databaser: Relationsdatabaser och dokument/nod-baserade. Andra ord som används för att skilja på dessa två typer av databaser är SQL (relationella) och NoSQL (dokument/nod-baserade).

2. Relationsdatabaser

2.1. Introduktion till Relationsdatabaser

Relationsdatabaser bygger på relationsalgebra. Som tur är behöver man inte förstå all bakomliggande teori för att kommunicera med en databashanterare.

Det finns flera olika databashanterare för relationsdatabaser, t.ex

Relationsdatabaser lagrar all data i tabeller, och dessa tabeller har kopplingar, eller relationer till varandra.

2.1.1. Fördelar med relationsdatabaser

Garanterad data-konsistens

Relationsdatabaser har olika metoder (se ACID) för att garantera att datan i databasen, vid varje tillfälle, kommer vara korrekt formaterad.

Garanterad data-integritet

Relationsdatabaser har olika metoder (se ACID) för att garanter att databasen kommer innehålla korrekt data.

Normalisering av data

Normalisering, eller normaliserad data innebär att man minskar förekomsten av redundant data.

Normalisering låter oss

  1. Minska det utrymme databasen behöver

  2. Minska risken att vi har inkonsistent data, det vill säga att beroende på var man i databasen kollar, skulle kunna få olika svar

  3. Öka prestandan

Validering av data

I de flesta databashanterare kan man skriva regler för hur rader och kolumner i tabeller ska se ut.

T.ex kan man bestämma att alla rader i användartabellen måste innehålla ett användarnamn, och att varje användarnamn måste vara unikt. Databashanteraren gör det i så fall omöjligt att skapa rader i användatabellen som saknar användarnamn, eller där användarnamnet redan finns på någon annan rad i tabellen.

En bebrövad standard

SQL är ett väldokumenterat språk som väldigt många utvecklare behärskar. Det är därför lätt för nya utvecklare att sätta sig in i ett nytt system.

Säkerhet och integritet

De flesta databashanterare innehåller avancerade kontroller av vem som har rättighet att utföra olika operationer i databasen.

2.1.2. Nackdelar med Relationsdatabaser

Osäkerhet kring lämplig datastruktur

Om man är osäker på hur man ska strukturera sin data kan det vara svårt att få till effektiva datastrukturer i en relationsdatabas.

Föränderliga datastrukturer

Om ens data och datastrukturer ofta förändras kan en relationsdatabas vara olämplig då det är bökigt att ändra om strukturen i en relationsdatabas och fortfarande uppfylla alla regler och garantier.

Big Data

När man har stora (riktigt stora) mängder data - så kallad Big Data - kan alla regler och garantier en relationsdatabas har sänka prestandan.

Kontinuerliga dataströmmar

Även i de fall där man kontinuerligt har en stor ström med ny inkommande data, kan alla regler och kontroller kan göra att prestandan blir lidande.

Decentralisering

Det kan även vara komplicerat att dela upp databasen på olika servrar, t.ex. genom så kallad "sharding".

2.2. Tabeller och relationer

En relationsdatabas består av en eller flera tabeller. Varje tabell har ett namn, och består av rader och kolumner.

Table 1. Exempel på en tabell för att hålla koll på böcker

books

id

title

page_count

1

'Catch 22'

464

2

'To Kill a Mockingbird'

336

3

'1984'

328

4

'The Stranger'

123

Tabellen ovan har namnet books, och har 4 rader och 3 kolumner.

2.2.1. Tupler

Varje rad i tabellen kallas för en tupel och beskriver en post i tabellen. I vårt exempel lagrar varje tupel information om en bok.

2.2.2. Attribut

Varje kolumn beskriver en egenskap, eller attribut för posten. I vårt fall är "id", "title" och "page_count" attribut.

Attribut måste vara atomära, det vill säga i varje "cell" eller "fält" får det maximalt finnas ett värde - man kan t.ex inte lagra flera titlar i en och samma cell.

2.2.3. Primärnyckel

Varje rad i en tabell måste vara unik, det vill säga, i varje tabell får det inte förekomma två rader där samtliga kolumner är identiska (detta är en följd av relationsalgebran).

Detta löses genom att varje tabell har en attribut som kallas för primärnyckel eller primary key. I tabellen ovan är det attributet med namnet id som är primärnyckel

När man skapar tabellen talar man om vilken av attributen som kommer vara primärnyckeln.

Detta innebär att om någon annan skulle skriva en bok som heter the stranger, och som råkar ha 123 sidor även den kommer raden fortfarande vara unik, eftersom den kommer få en unikt värde på primärnyckeln

Table 2. Varje tupel är fortfarande unik, trots att det finns två böcker med samma titel och sidantal.

books

id

title

page_count

1

'Catch 22'

464

2

'To Kill a Mockingbird'

336

3

'1984'

328

4

'The Stranger'

123

5

'The Stranger'

123

Det är databashanterarens roll att kontrollera att varje tupel är unik.

Namngivning av primärnyckeln

I den här boken kommer vi:

  • alltid använda namnet id för primärnyckeln, men primärnyckeln kan heta vad som helst.

  • alltid använda automatiskt inkrementerande heltal för primärnyckeln, men primärnyckeln kan vara vad som helst.

2.2.4. Relationer

För att knyta samman två tabeller behöver vi skapa en relation, det vill säga, en koppling, mellan dem.

books

id

title

page_count

1

'Catch 22'

464

2

'To Kill a Mockingbird'

336

3

'1984'

328

4

'The Stranger'

123

5

'Closing Time'

382

6

'Animal Farm'

218

7

'The Plague '

312

8

'Coming Up for Air'

393

authors

id

name

nationality

birth_year

shoe_size

1

'Joseph Heller'

'American'

1923

42

2

'Harper Lee'

'American'

1926

36

3

'George Orwell'

'English'

1903

41

4

'Albert Camus'

'French'

1913

44

I exemplet med böcker och författare behöver vi skapa en en-till-många-relation mellan authors och books - en för fattare kan ha skrivit många böcker (men en bok kan bara ha en författare).

Främmande nyckel

För att skapa en relation mellan två tabeller använder man en främmande nyckel. Man skapar en främmande nyckel genom att kopiera in värdet från primärnyckeln i en-änden av relationen i en ny kolumn i många-änden av relationen.

I vårt fall innebär det att vi ska lägga till primärnyckeln från author-tabellen i books-tabellen (en för fattare kan ha skrivit flera böcker).

books

id

title

page_count

author_id

1

'Catch 22'

464

1

2

'To Kill a Mockingbird'

336

2

3

'1984'

328

3

4

'The Stranger'

123

4

5

'Closing Time'

382

1

6

'Animal Farm'

218

3

7

'The Plague '

312

4

8

'Coming Up for Air'

393

3

Namngivning av den främmande nyckeln

Den främmande nyckelns kolumn kan heta precis vad som helst, men i den här boken kommer den främmande nyckelns kolumn-namn alltid döpas enligt följande: namnet på en-ändens tabellnamn i singular följt av ett understreck och sen id (author_id)

2.3. Normalisering

Normalisering av relationsdatabaser är en teori för hur man får en effektiv struktur på sin databas.

Genom att följa en given metodik kan man

  • Minska redundans

  • Minska risken för oönskade bieffekter (data-anomalier)

  • Öka prestandan i databasen

2.3.1. Exempel

Vi återanvänder bok-exemplet från föregående avsnitt:

Table 3. Exempel på en tabell för att hålla koll på böcker

books

id

title

page_count

1

'Catch 22'

464

2

'To Kill a Mockingbird'

336

3

'1984'

328

4

'The Stranger'

123

Säg att vi vill även vill lagra författarens namn. Vi lägger därför till en kolumn:

books

id

title

page_count

author

1

'Catch 22'

464

'Joseph Heller'

2

'To Kill a Mockingbird'

336

'Harper Lee'

3

'1984'

328

'George Orwell''

4

'The Stranger'

123

'Albert Camus'

Inga problem, men säg att vi även vill lagra författarens nationalitet, födelseår och skostorlek.

books

id

title

page_count

author

nationality

birth_year

shoe_size

1

'Catch 22'

464

'Joseph Heller'

'American'

1923

42

2

'To Kill a Mockingbird'

336

'Harper Lee'

'American'

1926

36

3

'1984'

328

'George Orwell'

'English'

1903

41

4

'The Stranger'

123

'Albert Camus'

'French'

1913

44

Nu börjar tabellen se lite konstig ut: Tabellen heter "books" men innehåller attributen "nationality", "birth_year" och "shoe_size". De låter inte som attribut (egenskaper) en bok har. Vi kan förstås lägga in "author_" framför varje attribut som är kopplat till en författare snarare än en bok:

books

id

title

page_count

author

author_nationality

author_birth_year

author_shoe_size

1

'Catch 22'

464

'Joseph Heller'

'American'

1923

42

2

'To Kill a Mockingbird'

336

'Harper Lee'

'American'

1926

36

3

'1984'

328

'George Orwell'

'English'

1903

41

4

'The Stranger'

123

'Albert Camus'

'French'

1913

44

Det känns bättre, men det är fortfarande konstigt att om man vill veta vilken skostorlek en författare har så ska man kolla i boktabellen. Men vad händer när vi börjar lagra flera böcker för en författare?

books

id

title

page_count

author

author_nationality

author_birth_year

author_shoe_size

1

'Catch 22'

464

'Joseph Heller'

'American'

1923

42

2

'To Kill a Mockingbird'

336

'Harper Lee'

'American'

1926

36

3

'1984'

328

'George Orwell'

'English'

1903

41

4

'The Stranger'

123

'Albert Camus'

'French'

1913

44

5

'Closing Time'

382

'Joseph Heller'

'American'

1923

42

6

'Animal Farm'

218

'George Orwell'

'English'

1903

41

7

'The Plague '

312

'Albert Camus'

'French'

1913

44

8

'Coming Up for Air'

393

'George Orwell'

'English'

1903

41

Nu förekommer varje författares nationalitet, födelseår och skostorlek flera gånger - vi har med andra ord redundant data i databasen.

Vi vill i största möjliga utsträckning undvika redundant data, och behöver därför normalisera datan, genom att dela upp datan i två tabeller - books och authors

authors

id

name

nationality

birth_year

shoe_size

1

'Joseph Heller'

'American'

1923

42

2

'Harper Lee'

'American'

1926

36

3

'George Orwell'

'English'

1903

41

4

'Albert Camus'

'French'

1913

44

books

id

title

page_count

1

'Catch 22'

464

2

'To Kill a Mockingbird'

336

3

'1984'

328

4

'The Stranger'

123

5

'Closing Time'

382

6

'Animal Farm'

218

7

'The Plague '

312

8

'Coming Up for Air'

393

Nu har vi normaliserat datan; data om författare i en tabell, och data om böcker i en annan. Ingen data är duplicerad. Dessvärre vet vi inte längre vilken författare som skrivit vilken bok.

För att hålla koll på vilken författare som skrivit vilken bok behöver vi lägga till en främmande nyckel i books-tabellen:

Normalisering leder vanligtvis till att tabeller delas upp och struktureras om. Tabeller kan uppfylla olika normalformer, beroende på hur långt normaliseringen drivs. De vanligaste normalformerna är första normalformen (1NF), andra normalformen (2NF) och tredje normalformen 3NF.

Om man följer stegen ovan kommer man rätt långt i normaliseringsprocessen, och det är mycket möjligt att ens databas blir helt normaliserad. Det är dock fortfarande viktigt att förstå den bakomliggande teorin bakom normalisering av relationsdatabaser. De problem som ändå uppstår kan man ofta hantera om man följer den enkla grundregeln "en typ av sak per tabell, och en sån sak per rad".

Men man behöver ändå förstå de bakomliggande teorierna och begreppen; det är inte alltid så lätt att inse vad de där "sakerna" som man ska ha en per rad egentligen är.

Ibland kan man även behöva analysera en existerande eller föreslagen design, och då måste man förstå, och kunna förklara, vilka problem den medför. Kanske måste man göra en avvägning mellan de nackdelar en dåligt normaliserad design medför, och arbetet att göra om databasen.

2.3.2. Termer och Begrepp

För att kunna ta till sig och tillämpa teorin bakom normalformerna behöver man först lära sig och behärska ett par begrepp:

Tilläggsanomali

En tilläggsanomali (insertion anomaly) innebär att man inte kan lägga till data i en rad i en tabell innan man har information nog att fylla alla celler i raden.

Table 4. En tabell som visar på möjliga tilläggsanomalier

students

name

address

course_code

course_name

adam

adamvägen 1

E1

engelska

adam

adamvägen 1

S1

svenska

adam

adamvägen 1

T1

tyska

beata

beatavägen 1

E1

engelska

beata

beatavägen 1

T1

tyska

I tabellen ovan går det inte (utan att införa null-värden) att

  • lägga till (insert) en kurs utan att det finns en elev inskriven i kursen.

  • skriva in en elev utan att också lägga till eleven i en kurs

Detta är exempel på insertion anomalys. Insertion anomalys uppstår när man har "felaktiga" funktionella beroenden (se nedan) i tabellen.

Uppdateringsanomali

En uppdateringsanomali (update anomaly) kan uppstå när man upprepar samma information på olika ställen i databasen. Det finns då en risk att man missar att ändra datan på flera olika ställen.

Table 5. En tabell som visar på en uppdateringsanomali

students

name

address

course_code

course_name

adam

adamvägen 1

E1

engelska

adam

adamvägen 1

S1

svenska

adam

adamvägen 1

T1

tyska

beata

beatavägen 1

E1

engelska

beata

göstavägen 1

T1

tyska

I tabellen ovan har adressen för beata endast uppdaterats i en av raderna. Update anomalys kan uppstå när man har "felaktiga" funktionella beroenden (se nedan) i tabellen.

Borttagningsanomali

En borttagningsanomali (deletion anomaly) innebär att man, när man tar bort en rad i en tabell, kan råka ta bort annan information som inte återkommer någon annanstans i databasern.

Table 6. En tabell som visar på en möjlig borttagninsanomali

students

name

address

course_code

course_name

adam

adamvägen 1

E1

engelska

adam

adamvägen 1

S1

svenska

adam

adamvägen 1

T1

tyska

beata

beatavägen 1

E1

engelska

beata

göstavägen 1

T1

tyska

Om man i tabellen ovan tar bort all information om "adam" så kommer även course_name och course_code för "svenska" försvinna från databasen, eftersom ingen annan elev är inskriven i denna kurs. Deletion anomalys kan uppstå när man har "felaktiga" funktionella beroenden (se nedan) i tabellen.

Funktionellt Beroende

Ett funktionellt beroende (fb) är ett beroende mellan attribut i en tabell. Låt oss anta att vi har en tabell med personnummer och namn:

Table 7. En tabell med ett funktionellt beroende mellan personnummer och namn

personer

personnummer

namn

970811-xxxx

Karl-Gustaf Sjöblom

560222-xxxx

Viking Fors

590928-xxxx

Alexis Lindroth

820402-xxxx

Karl-Gustaf Sjöblom

I tabellen ovan finns ett funktionellt beroende mellan personnummer och namn. Detta innebär att för ett givet personnummer kommer alltid samma namn returneras, men det omvända är inte sant - ett namn kommer inte garanterat returnera samma personnummer. Med andra ord: om samma personnummer skulle förekomma på två rader i tabellen, så måste värdena namnen också vara lika.

Detta funktionella beroende kan beskrivas som personnummer → namn vilket utläses som "personnumer bestämmer namn" I det här exemplet är personnummer determinanten, eftersom den bestämmer eller determinerar namnet.

Generaliserat brukar man skriva ut funktionella betenden som A → B

Table 8. En tabell med flera funktionella beroenden

students

name

address

course_code

course_name

adam

adamvägen 1

E1

engelska

adam

adamvägen 1

S1

svenska

adam

adamvägen 1

T1

tyska

beata

beatavägen 1

E1

engelska

beata

beatavägen 1

T1

tyska

I tabellen students ovan finns två olika, av varandra oberoende, funktionella beroenden:

  • name → address

  • course_code → course_name

Ett funktionellt beroende kan innehålla flera determinanter, det vill säga ett flera attribut kan tillsammans bestämma värdet på ett (eller flera) andra attribut.

Table 9. En tabell med funktionella beroenden med flera determinanter

songs

title

artist_name

album

year

length

path

genre

TiK ToK

Kesha

Tik Tok

2010

03:20

tiktok.mp3

Pop

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Pop

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Dance

Hot N Cold

Katy Perry

Hot N Cold

2008

03:43

hotncold.mp3

Pop

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Pop

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Rock

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Pop

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Rock

Everlong

Xandra

Everlong

2018

04:00

xeverlong.mp3

Pop

Förutsatt att ingen artist får ge ut en låt med samma titel i olika album finns i songs-tabellen ovan (bland annat) följande funktionella beroenden:

  • {title, artist_name, album, year, length, genre} → path

  • {title, artist_name, album, year, length} → path

  • {title, artist_name, album, year} → path

  • {title, artist_name, album} → path

  • {title, artist_name} → path

Vilka andra funktionella beroenden kan du hitta?

Fullt Funktionellt Beroende

Ett fullt eller fullständigt funktionellt beronde (ffb) är ett funktionellt beroende där man inte kan ta bort någon av determinanterna utan att det funktionella beroendet upphör att gälla. Detta innebär att alla funktionella beroenden som enbart har en determinant är fullständiga beroenden.

Fullt funktionella beroenden skrivs ut som A ⇒ B

I songs-tabellen ovan är {title, artist_name} ⇒ path ett fullt funktionellt beroende, eftersom man inte kan ta bort varesig title eller artist_name utan att det funktionella beroendet försvinner.

Vilka ffb skulle kunna finnas i tabellen nedan?

Table 10. En konstig tabell med funktionella beroenden

strange_table

A

B

C

D

1

4

10

100

2

5

20

50

3

6

20

200

1

4

10

200

2

6

20

0

3

6

20

300

1

4

10

null

2

6

20

50

3

6

20

50

Transitivt Beroende

Om a → b och dessutom b → c så är c transitivt beroende av a via b (förutsatt att b eller c inte bestämmer a)

Table 11. En tabell med transitionella beroenden

employees

id

name

position

school_name

school_address

school_phone

1

Johan Bengtsson

Teacher

NTI Johanneberg

Origovägen 4

031-741 21 00

2

Anna Persson

Teacher

NTI Johanneberg

Origovägen 4

031-741 21 00

3

Fatima Alzanbouri

Principal

NTI Johanneberg

Origovägen 4

031-741 21 00

4

Inge Naning

Teacher

NTI Södertörn

Röntgenvägen 9

08-556 408 50

5

Minna Salo

Teacher

NTI Södertörn

Röntgenvägen 9

08-556 408 50

6

Andrew Gao

Principal

NTI Södertörn

Röntgenvägen 9

08-556 408 50

I tabellen ovan gäller (bland annat) id → school_name men även school_name → {school_address, school_phone} Detta innebär att school_address och school_phone är transitionellt beroende av id via school_name

Supernyckel

En supernyckel (engelska: super key) består av ett eller flera attribut som tillsammans kan användas för att unikt identifiera en tupel i tabellen, det vill säga: Det kombinationen av värden bland de ingående attributen måste vara unika inom tabellen. I songs-tabellen ovan finns (bland annat) följande supernycklar:

  • title, artist_name, album, year, length, genre

  • title, artist_name, album, year, length

  • title, artist_name, album, year

  • title, artist_name, album

  • title, artist_name

Kandidatnycklar

En kandidatnyckel är en supernyckel utan redundanta attribut, det vill säga den supernyckel där vi inte kan ta bort fler attribut utan att vi inte längre kan garantera att raden kommer vara unik. I songs-tabellen ovan skulle kandidatnyckeln bestå av title tillsammans med artist_name

Primärnyckel

En primärnyckel (engelska: primary key) är den av kandidatnycklarna (om det finns flera) designern av databasen har valt ut att använda för interaktion med en given tabell.

Icke-nyckel-attribut

Ett icke-nyckel-attribut (engelska: non-key attribute) är ett attribut som inte är ingår i någon av tabellens kandidatnycklar

I songs-tabellen är därmed följande attribut icke-primär-attribut: album, year, length, path, genre.

2.3.3. Normalformerna

Första Normalformen (1NF)

Regler för Första Normalformen (1NF)

  • Varje attribut i en tabell måste vara atomärt (dvs innehålla ett enda värde)

  • Varje tupel i en tabell måste vara unik, det vill säga det får i tabellen inte finnas två identiska rader.

Första normalformen är egentligen den enda normalform en relationsdatabas måste uppfylla för att fungera (men för att undvika uppdateringsanomalier (update anomalies) så är det rekommenderat att normalisera tom 3NF).

Table 12. En students-tabell som inte når upp till 1NF

students

personnr

namn

courses

010811-xxxx

Karl-Gustaf Sjöblom

EN7, SV3, MA4

000222-xxxx

Viking Fors

EN7, MA4

020928-xxxx

Alexis Lindroth

020402-xxxx

Karl-Gustaf Sjöblom

EN5, MA2, KE1, PROG1

I students-tabellen ovan innehåller course-attributet flera värden, det vill säga, det är inte atomärt.

Man skulle kunna lösa problemet med att lägga in fler kolumner för kurser, t.ex course1, course2, course3, etc. Detta skulle dock betyda att de elever som inte läser maximalt antalt kurser kommer ha en eller flera kolumn utan värde i.

Om man ska lägga in elever med fler kurser än vad man räknat med skulle vara maximalt antal kurser skulle det inte att gå. Det kommer också att uppstå problem med frågeställningen i tabellen:

tänk följande fråga för att ta reda på om någon läser engelska 7
SELECT *
FROM students
WHERE course1 = 'EN7' OR
      course2 = 'EN7' OR
      course3 = 'EN7' OR
      course4 = 'EN7' OR
      course5 = 'EN7' OR
      ...

Man skulle även kunna lösa problemet genom att skapa en kolumn för varje kurs som finns, och som värde ha true/false/null baserat på om eleven läser kursen eller inte. Detta skulle innebära en väldig massa onödiga false eller nullvärden, och så fort en ny kurs läggs till eller tas bort skulle man behöva modifiera tabellen - en synnerligen dyr (tids- och resursmässigt) och otymplig operation.

En möjlig lösning som skulle uppfylla 1NF är att varje elev förekommer på flera rader:

Table 13. En students-tabell som når upp till 1NF

students

personnr

namn

courses

010811-xxxx

Karl-Gustaf Sjöblom

EN7

010811-xxxx

Karl-Gustaf Sjöblom

SV3

010811-xxxx

Karl-Gustaf Sjöblom

MA4

000222-xxxx

Viking Fors

EN7

000222-xxxx

Viking Fors

MA4

020928-xxxx

Alexis Lindroth

020402-xxxx

Karl-Gustaf Sjöblom

EN5

020402-xxxx

Karl-Gustaf Sjöblom

MA2

020402-xxxx

Karl-Gustaf Sjöblom

KE1

020402-xxxx

Karl-Gustaf Sjöblom

PROG1

Om man ser tillbaks till tilläggs/uppdaterings/borttagningsanomalierna kan man dock se att detta inte är en bra lösning.

Ett bättre alternativ är att uppfylla 1NF genom att dela upp tabellen i två olika tabeller:

Table 14. en students-tabell som når upp till 1NF

students

personnr

namn

010811-xxxx

Karl-Gustaf Sjöblom

000222-xxxx

Viking Fors

020928-xxxx

Alexis Lindroth

020402-xxxx

Karl-Gustaf Sjöblom

Table 15. en courses-tabell som når upp till 1NF

courses

personnr

course

010811-xxxx

EN7

010811-xxxx

SV3

010811-xxxx

MA4

000222-xxxx

EN7

000222-xxxx

MA4

020402-xxxx

EN5

020402-xxxx

MA2

Även om denna uppdelning är bättre ger den inte ett bra skydd mot anomalier - hur lägger man t.ex. till en kurs som ingen student läser, eller vad händer när ingen student längre läser t.ex. "EN7"?

Ännu bättre hade antagligen varit att dela upp tabellen i tre tabeller varav en relationstabell, men det bryr sig inte 1NF om.

Övningsuppgift

Normalisera nedanstående tabell så den uppfyller 1NF.

friends

name

phone_numbers

Adam

123, 234

Beata

345, 456

Charles

567, 678, 789

Andra Normalformen (2NF)

Regler för Andra Normalformen (2NF)

  • Alla regler för 1NF är uppfyllda

  • Alla icke-nyckel-attribut är fullt funktionsberoende (ffb) av tabellens kandidat/primärnyckel

Tabeller som uppfyller 2NF har mindre redundans är tabeller i 1NF men de kan fortfarande lida av uppdateringsanomalier.

Eftersom alla funktionella beroenden där det finns en ensam determinant är fullständiga funktioneller beroenden, innebär det att alla tabeller som upfyller 1NF och har en primärnyckel bestående av enbart 1 attribut automatiskt uppfyller 2NF. Detta är även det enklaste sättet att garantera 2NF.

Table 16. songs-tabellen

songs

title

artist_name

album

year

length

path

genre

TiK ToK

Kesha

Tik Tok

2010

03:20

tiktok.mp3

Pop

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Pop

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Dance

Hot N Cold

Katy Perry

Hot N Cold

2008

03:43

hotncold.mp3

Pop

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Pop

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Rock

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Pop

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Rock

Everlong

Xandra

Everlong

2018

04:00

xeverlong.mp3

Pop

I songs-tabellen är album, year, length samt path icke-nyckel-attribut som har ett fullt funktionellt beroende av vår kandidatnyckel {title, artist_name }. genre, däremot, har inte ett fullt funktionellt beroende på kandidatnyckeln - samma kandidanyckel kan användas för att få tillbaks olika genres. Detta innebär att vår tabell inte ännu uppfyller 2NF.

Vi börjar med att ange ett nytt attribut (id) som primärnyckel:

Table 17. songs-tabellen med ny primärnyckel

songs

id

title

artist_name

album

year

length

path

genre

1

TiK ToK

Kesha

Tik Tok

2010

03:20

tiktok.mp3

Pop

2

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Pop

2

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

Dance

3

Hot N Cold

Katy Perry

Hot N Cold

2008

03:43

hotncold.mp3

Pop

4

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Pop

4

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

Rock

5

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Pop

5

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

Rock

6

Everlong

Xandra

Everlong

2018

04:00

xeverlong.mp3

Pop

Eftersom primärnycklar måste vara unika inom en tabell har vi ett problem: id 2, 4 samt 5 förekommer flera gånger i tabellen. Detta är inte tillåtet.

För att komma åt detta behöver vi bryta ut genre till en egen tabell, då det är genre som är orsaken till att ett samma id förekommer flera gånger i tabellen.

För att garantera att genre-tabellen uppnår 2NF ser vi samtidigt till att ge den en primärnyckel (id) (eftersom genre i vår modell inte har några andra attribut kopplade till sig behövs det inte egentligen en ny primärnyckel, men det är enklast att oftast göra likadant).

Table 18. genres-tabellen

genres

id

genre

1

Pop

2

Dance

3

Rock

Table 19. songs-tabellen utan genre

songs

id

title

artist_name

album

year

length

path

1

TiK ToK

Kesha

Tik Tok

2010

03:20

tiktok.mp3

2

Alors on danse

DJ Team

Hits Dance Club

2010

03:27

alors.mp3

3

Hot N Cold

Katy Perry

Hot N Cold

2008

03:43

hotncold.mp3

4

My Hero

Foo Fighters

The Colour and the Shape

1997

04:20

myhero.mp3

5

Everlong

Foo Fighters

The Colour and the Shape

1997

04:11

everlong.mp3

6

Everlong

Xandra

Everlong

2018

04:00

xeverlong.mp3

Vi har nu ett nytt problem: Vi kan inte längre se vilken/vilka genres en låt tillhör.

I enligt med diskussionen om kurser i avsnittet om 1NF går det inte att lägga till några genre-attribut i songs-tabellen, och samma gäller eventuella songs-attribut i genre-tabellen. Eftersom vi i genres-tabellen har en primärnyckel med en ensam determinant får samma genre inte förekomma på flera rader.

Detta innebär att vi i detta fall behöver skapa en tredje-tabell:

Table 20. genres-songs-tabellen

genres

song_id

genre_id

1

1

2

1

2

2

3

1

4

1

4

3

5

1

5

3

6

1

genres-songs-tabellen innehåller följande funktinonella beroenden: {genre_id, song_id} ⇒ {genre_id, song_id}. Även här skulle vi, om vi ville, kunna stoppa in en ny primärnyckel med endast en determinant, men i det här fallet behövs det inte (eftersom genre-songs är en relationstabell utan icke-nyckel-attribut).

Nu uppfyller samtliga 3 tabeller 2NF.

Övningsuppgift

Normalisera nedanstående tabell som hjälper ett bibliotek hålla koll på sina låneböcker så den uppfyller 2NF

books

title_id

isbn

loan_barcode

title

author

condition

1

978-91-27-14196-4

1000001

The Hitchiker’s Guide to the Galaxy

Douglas Adams

Good

1

978-91-27-14196-4

1000002

The Hitchiker’s Guide to the Galaxy

Douglas Adams

Good

1

978-91-27-14196-4

1000003

The Hitchiker’s Guide to the Galaxy

Douglas Adams

Poor

2

978-05-52-17189-2

1000004

Good Omens

Neil Gaiman

Good

2

978-05-52-17189-2

1000004

Good Omens

Terry Pratchett

Good

2

978-05-52-17189-2

1000005

Good Omens

Neil Gaiman

Mint

2

978-05-52-17189-2

1000005

Good Omens

Terry Pratchett

Mint

Tredje Normalformen (3NF)

Regler för Tredje Normalformen (3NF)

  • Alla regler för 2NF är uppfyllda

  • Inget icke-nyckel-attribut är transitivt beroende av primärnyckeln.

Table 21. En tabell med transitionella beroenden

employees

name

position

school_name

school_address

school_phone

Johan Bengtsson

Teacher

NTI Johanneberg

Origovägen 4

031-741 21 00

Anna Persson

Teacher

NTI Johanneberg

Origovägen 4

031-741 21 00

Fatima Alzanbouri

Principal

NTI Johanneberg

Origovägen 4

031-741 21 00

Inge Naning

Teacher

NTI Södertörn

Röntgenvägen 9

08-556 408 50

Minna Salo

Teacher

NTI Södertörn

Röntgenvägen 9

08-556 408 50

Andrew Gao

Principal

NTI Södertörn

Röntgenvägen 9

08-556 408 50

De transitionella beroendena i tabellen ovan (name → school_name och school_name → school_address) innebär redundant data i tabellen.

Genom att bryta ut de transitionella beroendena (och därmed den redundanta datan) i egna tabeller kan vi uppnå 3NF:

Table 22. En employees-tabell utan transitionella beroenden

employees

name

position

school_name

Johan Bengtsson

Teacher

NTI Johanneberg

Anna Persson

Teacher

NTI Johanneberg

Fatima Alzanbouri

Principal

NTI Johanneberg

Inge Naning

Teacher

NTI Södertörn

Minna Salo

Teacher

NTI Södertörn

Andrew Gao

Principal

NTI Södertörn

Table 23. En schools-tabell utan transitionella beroenden

schools

school_name

school_address

school_phone

NTI Johanneberg

Origovägen 4

031-741 21 00

NTI Södertörn

Röntgenvägen 9

08-556 408 50

Övningsuppgift

Vaktmästaren på en gymnasieskola behöver en databas för att hålla koll på elevernas skåp och nycklar.

Vaktmästaren berättar följande:

  • Varje skåp har ett skåpnummer.

  • Varje skåp står i en kolumn.

  • Varje skåp har en höjd (botten, mitten eller toppen).

  • En elev kan ha ett (men endast ett) skåp.

  • Det kan finnas tomma skåp.

  • Varje skåp har en låscylinder.

  • Det finns låscylindrar som inte sitter i ett skåp.

  • Varje låscylinder har en eller flera nycklar.

  • Varje nyckel har ett nyckelnummer.

  • Varje låscylinder tillhör en nummerserie.

  • Varje nummerserie har en huvudnyckel.

  • Skåpets nummer överensstämmer inte med låscylinderns nummer (och eleverna vet inte vad låscylindern har för nummer).

  • Om en elev slarvar bort nyckeln till sitt skåp behöver vaktmästaren byta cylindern mot en ledig cylinder med kvarvarande reservnycklar och den "nya" cylindern får inte tidigare suttit i samma skåp.

  • Vaktmästaren behöver hålla koll på borttappade nycklars nyckelnummer så hen vet vilken cylinder nyckeln tillhör om nyckeln skulle hittas senare.

  • Om samtliga nycklar till ett skåp slarvats bort behöver skåpets låscylinder bytas, och den gamla låscylindern markeras som förverkad (men om en nyckel till en förverkad cylinder senare hittas behöver man kunna ta bort förverkningsmarkeringen).

  • För att undvika att elever i samma klass har skåpen ovan/under varandra behöver vaktmästaren även veta elevens klass.

För närvarande använder vaktmästaren ett excelark för att hålla koll på detta. Det ser ut som följer:

lockers

Omvandla excelarket till en relationsdatabas där samtliga tabeller uppfyller 3NF.

Boyce-Codd Normalform (BCNF)

Regler för Boyce-Codd Normalform (BCNF)

  • Alla regler för 3NF är uppfyllda

  • Varje attribut i tabellen måste vara kopplat till nyckeln, hela nyckeln och inget annat.

Table 24. En schools-tabell med ny primärnyckel

schools

id

school_name

school_address

school_phone

1

NTI Johanneberg

Origovägen 4

031-741 21 00

2

NTI Södertörn

Röntgenvägen 9

08-556 408 50

Table 25. En employees-tabell med två främmande nycklar

employees

id

name

position_id

school_id

1

Johan Bengtsson

1

1

2

Anna Persson

1

1

3

Fatima Alzanbouri

2

1

4

Inge Naning

1

2

5

Minna Salo

1

2

6

Andrew Gao

2

2

Table 26. En positions-tabell

positions

id

name

1

Teacher

2

Principal

Övningsuppgift

Uppdatera elvskåpsdatabasen (om nödvändigt) så samtliga tabeller uppfyller BCNF.

2.4. Datamodellering

När man bygger en applikation som kräver en databas, t.ex en applikation för att hålla koll på böcker, brukar man börja med att göra en beskrivning av hur problemdomänen fungerar.

Denna beskrivning på hög nivå kan kallas en konceptuell eller begreppsmässig beskrivning. När man pratar om relationsdatabaser använder man ofta ordet schema för att beskriva denna konceptuella eller logiska modell av problemdomänen.

För att på ett smidigt sätt kunna modellera, det vill säga designa relationsdatabaser, finns två vanliga notationer som vi tittar på parallellt i det här kapitlet:

  • ER-diagram (Entity Relationship Diagrams) — togs fram specifikt för databasmodellering.

  • UML klassdiagram — kommer från objektorienterad systemdesign men används ofta också för databaser, särskilt när modellen ska användas både för applikationskoden och databasen.

Båda visar samma sak: vilka entiteter (eller klasser), attribut och sambandstyper (eller relationer) som finns i databasen — de beskriver inte den faktiska datan som lagras.

Hänvisning: Ni har troligen sett UML klassdiagram i en kurs om objektorientering eller systemutveckling. Här tittar vi specifikt på hur de används för databasmodellering.

I resten av kapitlet visar vi varje koncept i båda notationer parallellt: ER-diagram (handritade) till vänster och UML klassdiagram (renderat med mermaid) till höger.

2.4.1. Entitet / klass

En entitet (engelska: entity) — eller klass i UML — representerar en typ av sak som lagras i databasen. I båda notationerna ritas den som en rektangel med namnet (i singular) i mitten.

ER-diagram UML klassdiagram
entities
classDiagram class Book class Author

2.4.2. Attribut

Attribut (engelska: attribute) representerar en egenskap på något som lagras i databasen. Alla attribut måste tillhöra en entitet (eller klass).

I ER-diagram ritas varje attribut som en oval, med namnet i mitten, kopplad med ett streck till sin entitet. I UML listas attributen istället inuti klassrektangeln, vanligen i ett mellanavsnitt under klassnamnet.

Om ett attribut är unikt för entiteten — det vill säga att det i systemet som databasen modellerar inte får finnas två saker som har samma värde på det attributet — är det en primärnyckel. I ER markeras primärnyckeln med understrykning. I UML används ofta en stereotyp<<PK>> — efter attributnamnet.

ER-diagram UML klassdiagram
entities with attributes
classDiagram class Book { +id : int «PK» +name : string +page_count : int } class Author { +id : int «PK» +name : string +birth_year : int +nationality : string }

Främmande nycklar

Främmande nycklar ska aldrig ritas ut som attribut — varken i ER-diagram eller i UML klassdiagram. Deras placering framgår av sambanden mellan entiteterna (se nästa rubrik).

2.4.3. Sambandstyp / relation

Sambandstyper (engelska: relation) visar kopplingar mellan två entiteter.

I ER-diagram ritas relationen som en romb kopplad med streck till de ingående entiteterna; i mitten av romben står ett eller flera ord som beskriver sambandet (oftast från ena entitetens perspektiv). I UML är det istället ett enkelt streck mellan de två klasserna, med en etikett på strecket.

Kardinalitet / multiplicitet

I varje ände av en sambandstyp framgår dess kardinalitet (engelska: cardinality), eller multiplicitet som det kallas i UML — som mer exakt beskriver hur många exempel av varje sida som kan delta i sambandet.

Det finns tre grundläggande typer av kardinalitetsförhållanden:

Ett-till-ett-samband (eller 1:1-samband)

Ett 1:1-samband innebär att varje exempel av entiteten i den ena änden av sambandet hör ihop med ett exempel av entiteten i andra änden.

Ett-till-många-samband (eller 1:*-samband)

Ett 1:*-samband innebär att entiteten i den ena änden av sambandet kan höra ihop med flera exempel av entiteten i den andra änden, men varje exempel i den andra änden hör bara ihop med ett exempel av den första entiteten.

Många-till-Många-samband (eller *:*-samband)

Ett *:*-samband innebär att ett exempel av var och en av de ingående entiteterna kan höra ihop med flera exempel av de övriga ingående entiteterna.

UML är mer expressivt: Förutom 1, och 0..1 stödjer UML även 1.. (en eller fler), 0.. (samma som ) och m..n (mellan m och n exempel). Om du behöver uttrycka exakta gränser passar UML ofta bättre.

ER-diagram UML klassdiagram
entities with attributes and relations
classDiagram class Book { +id : int «PK» +name : string +page_count : int } class Author { +id : int «PK» +name : string +birth_year : int +nationality : string } Author "1" --> "1..*" Book : wrote

I exemplet ovan kan man utläsa att en författare kan skriva många (men minst 1) böcker, men en bok kan bara ha en författare. Det är med andra ord ett en-till-många-samband.

Sambandstypen är i exemplet ovan namngiven från författarens perspektiv ("wrote"), men skulle lika gärna kunna vara skriven från en boks perspektiv (t.ex. "written by" eller "belongs to").

2.4.4. Aggregation och komposition

UML har två särskilda relationstyper som ER-diagram inte har en direkt motsvarighet till:

  • Aggregation (öppen romb) — "har en"-relation där delarna kan existera utan helheten. Ett bibliotek "har" böcker, men böckerna finns även om biblioteket läggs ner.

  • Komposition (fylld romb) — "består av"-relation där delarna inte kan existera utan helheten. Ett dokument "består av" sidor — när dokumentet tas bort försvinner sidorna.

I databastermer motsvarar komposition ofta ON DELETE CASCADE på den främmande nyckeln — när "helheten" raderas försvinner "delarna" automatiskt.

UML klassdiagram
classDiagram Library "1" o-- "*" Book : aggregation Document "1" *-- "*" Page : composition

2.4.5. Övningar

Datorregister

Alla elever på NTI Johanneberg får låna en dator. Det finns ett inventeringssystem med en databas där alla elever, datorer och lån finns registrerade.

Systemet håller koll på:

  • Datorers modell och serienummer

  • Elevers namn och personnummer

  • Vilken elev som har lånat vilken dator

  • Samtliga serviceärenden för en specifik dator, och vem datorn tillhörde när serviceärendet startades.

Rita upp de entiteter, attribut och sambandstyper som databasen behöver — gärna i båda notationerna.

Matkort

I årskurs tre får eleverna på NTI-Johanneberg matkort. Företaget som skapar och säljer matkorten till skolorna har (förhoppningsvis) en databas som håller koll på skolor, kort, och låntagare.

De behöver kunna hålla kolla på:

  • Skolors adress och kontaktperson (så de kan skicka ut korten till rätt adress).

  • Korts serienummer och pinkod

  • Elevers namn och klass

  • Vilket kort som tillhör vilken elev

  • Vilken skola och klass en elev går i

Rita upp de entiteter, attribut och sambandstyper som databasen behöver — gärna i båda notationerna.

2.5. Transaktioner

I en databas kan ofta många transaktioner vara beroende av varandra.

Säg till exempel att du skriver mjukvara för att hålla koll på bankkonton, och du vill föra över pengar från ett konto till ett annat konto. Då måste du:

  1. Ta bort pengarna från konto 1.

  2. Föra in pengerna på konto 2.

Alternativt

  1. Föra in pengarna på konto 2.

  2. Ta bort pengarna från konto 1.

Säg att något går fel i steg 2 - kanske har någon angett ett felaktigt kontonummer, eller under tiden som instruktionerna utförs har konto 2 tagits bort. I ett system utan transaktionssäkerhet skulle pengarna bara ha försvunnit i tomma intet (eller i alternativ 2, skapats från tomma intet)

I de flesta databashanterare finns därför transaktioner - om man lägger sina instruktioner i en transaktion kontrollerar databasen att samtliga instruktioner i transaktionen faktiskt genomförs. Skulle felet i steg 2 hända i en transaktion kan man göra en så-kallad rollback, och därmed skulle inget av stegen i transaktionen utföras - och pengarna skulle finnas kvar.

2.5.1. ACID

Atomicity

Atomicitet (engelska: atomicity) innebär att en transaktion antingen genomförs i sin helhet (samtliga steg) eller så genomförs den inte alls. Om en transaktion avbruts innan alla steg genomförts ska databashanteraren kunna garantera att databasen är i samma state som innan transaktionen påbörjades.

Consistency

"Consistency" är ett ord som saknar en tydlig svensk motsvarighet

(consistency)* –– om databaser: det att all information i databasen är sam­stäm­mig och mot­sägelse­fri. Det får inte finnas några upp­gifter i databasen som inte går ihop med var­andra. – I trans­aktions­hantering brukar kon­si­stens formuleras som att data­basen ska ha ett tillåtet till­stånd både vid början och vid slutet av en tran­saktion. Consistency är c:et i för­kort­ning­en ACID. – Se också referens­integritet och lång­sam konsistens (even­tu­al con­sis­tency). – – Ordet: Som över­sättning av engelska consistency är konsistens omstritt, eftersom det svenska ordet konsistens oftast står för hur trög­flytande eller lätt­flytande något är. Men på svenska har ordet också, när det gäller logiska resonemang, samma betydelse som engelska consistency, alltså att resonemanget är logiskt och konsekvent. (Inkonsistent är ett etablerat svenskt ord, och det används inte om mat.) Skillnaden mellan konsekvens och konsistens kan sägas vara att konsekvens är att göra lika­dant varje gång, medan konsistens är när alla delar av en helhet stämmer över­ens med var­andra.

— https://it-ord.idg.se/ord/konsistens/

Consistency garanterar att databasen alltid kommer vara "konsistent", dvs att en transaktion enbart kommer genomföras om databasen, efter transaktionen är genomförd, kommer vara i ett giltigt tillstånd. Man kan på svenska prata om "datavalidering" & "referensintegritet".

Data integrity is a fundamental component of information security. In its broadest use, “data integrity” refers to the accuracy and consistency of data stored in a database, data warehouse, data mart or other construct. The term – Data Integrity - can be used to describe a state, a process or a function – and is often used as a proxy for “data quality”. Data with “integrity” is said to have a complete or whole structure. Data values are standardized according to a data model and/or data type. All characteristics of the data must be correct – including business rules, relations, dates, definitions and lineage – for data to be complete. Data integrity is imposed within a database when it is designed and is authenticated through the ongoing use of error checking and validation routines. As a simple example, to maintain data integrity numeric columns/cells should not accept alphabetic data.

— https://www.veracode.com/blog/2012/05/what-is-data-integrity

I de flesta databashanterare kan man skriva regler för hur rader och kolumner i tabeller ska se ut.

T.ex kan man bestämma att alla rader i användartabellen måste innehålla ett användarnamn, och att varje användarnamn måste vara unikt. Databashanteraren gör det i så fall omöjligt att skapa rader i användatabellen som saknar användarnamn, eller där användarnamnet redan finns på någon annan rad i tabellen

I de flesta databashanterare kan man även skriva regler för hur tabeller hör ihop. Detta gör det möjligt att garantera att kopplingarna mellan tabellerna stämmer, och att datan uppdateras på ett korrekt sätt.

Till exempel kan man se till att en användares alla meddelanden tas bort när användaren tas bort, eller att man enbart kan lägga till elever i klasser som faktiskt finns.

Isolation

Isolation garanterar att även om transaktioner utförs parallellt kommer databasen hamna i samma state som om transaktionerna utförts sekventiellt. Detta innebär att en transaktion aldrig får se den "tillfälliga data" som skapas medans en annan transaktion utförs (innan transaktionen är klar).

Durability

Durability innebär att när en transaktion är genomförd kommer all data finnas sparad och är korrekt även om databaservern kraschar (vid t.ex ett strömavbrott). Skulle databashanteraren krascha mitt i en transaktion kommer datan återställas till läget innan kraschen.

2.5.2. Rollback

När en transaktion ska utföras görs det en "Commit" på transaktionen. Databashanteraren ser nu till att alla steg i transaktionen faktiskt genomförs. Skulle transaktionen av någon anledning inte genomföras till fullo genomför databashanteraren en "rollback", det vill säga ser till att databasen är i samma state som innan transaktionen påbörjades.

Transaktioner kan avbrytas av bland annat följande skäl:

  • Transaktionen skulle innebära att databasen skulle hamna i ett felaktigt state.

  • Databashanteraren kraschar mitt under transaktionen. I de flesta databashanterare finns en loggfil databashanteraren kan använda för att vid återstart se till att databasen är i rätt state.

  • Användaren avbryter transaktionen

2.5.3. Transaktioner i SQL

I SQL hanteras transaktioner med tre nyckelord:

  • BEGIN TRANSACTION — startar en ny transaktion. Allt som skrivs efter detta är "preliminärt" tills transaktionen avslutas.

  • COMMIT — avslutar transaktionen och gör alla ändringar permanenta.

  • ROLLBACK — avslutar transaktionen utan att spara något — databasen ser ut precis som innan BEGIN TRANSACTION.

I SQLite (och de flesta databashanterare) räcker det att skriva BEGIN; istället för BEGIN TRANSACTION; — formerna är synonyma.

Exempel: överföring mellan konton

Anta följande tabell:

CREATE TABLE accounts (
  id      INTEGER PRIMARY KEY,
  owner   TEXT NOT NULL,
  balance INTEGER NOT NULL CHECK (balance >= 0) (1)
);

INSERT INTO accounts (owner, balance) VALUES
  ('Anna',   5000),
  ('Bertil', 1000);
1 CHECK-begränsningen säkerställer att inget konto kan hamna på negativt saldo.

För att flytta 2 000 kr från Anna till Bertil — utan att riskera att pengarna försvinner mitt i — skriver vi de två UPDATE-satserna inuti en transaktion:

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 2000 WHERE id = 1;
UPDATE accounts SET balance = balance + 2000 WHERE id = 2;
COMMIT;

Om något går fel — exempelvis om vi försöker flytta 6 000 kr (mer än Anna har) — bryter första UPDATE-satsen mot CHECK-begränsningen balance >= 0. Då vill vi avbryta hela transaktionen istället för att gå vidare:

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 6000 WHERE id = 1;  -- misslyckas: CHECK-fel
ROLLBACK;

Efter ROLLBACK ser kontona ut precis som innan vi körde BEGIN TRANSACTION.

Vidareläsning

2.5.4. Övningar

Identifiera ACID-egenskapen

För varje scenario nedan, ange vilken av ACID-egenskaperna (Atomicity, Consistency, Isolation, Durability) som skulle ha förhindrat problemet — och förklara kort varför.

  1. En överföring drogs från konto A men hann aldrig läggas in på konto B innan strömmen gick. När databasen startade om igen var pengarna borta.

  2. Två kassörer på samma butik säljer samtidigt den sista enheten av en vara. Båda försäljningarna registreras, och lagersaldot blir -1.

  3. En användare tar bort sitt konto, men dess inlägg ligger kvar och pekar på ett användar-ID som inte längre finns.

  4. En bank bekräftade en överföring för en kund. En timme senare kraschade servern, och när den startade om var överföringen borta.

Bibliotekssystem

Du har fått i uppdrag att designa databasen bakom ett mindre bibliotek. Verksamheten behöver hålla reda på böcker (med flera exemplar av populära titlar), medlemmar, samt vilka exemplar som är utlånade till vem och när.

Lös följande, i den ordning som passar dig:

  1. Modellera databasen i ett ER-diagram eller ett UML klassdiagram.

  2. Skriv schemat som CREATE TABLE-satser i SQLite, inklusive primärnycklar, främmande nycklar och de CHECK-begränsningar du tycker behövs.

  3. Identifiera vilka av systemets operationer som behöver skyddas av en transaktion. Motivera utifrån ACID-egenskaperna — vilken egenskap är det främst som hade brustit utan transaktion?

  4. Implementera minst en av dessa operationer som en SQL-transaktion. Visa ett exempel på indata och förväntat resultat — och vad som händer om något går fel mitt i.

Tänk på att modellen och transaktionen hänger ihop: om du t.ex. modellerar book och loan som separata entiteter blir utlåningen en annan transaktion än om du har en räknare available_copies direkt på book.

Eget scenario

Välj ett av följande scenarier — eller ett eget förslag — och gör samma fyra steg som i bibliotekssystem-uppgiften:

  • Webbshop — kunder lägger ordrar med flera produkter; lager ska minskas och betalning registreras.

  • Spel med inventarier — spelare kan byta föremål med varandra; båda spelarnas inventarier uppdateras samtidigt.

  • Gymsystem — medlemmar bokar pass; antalet platser per pass är begränsat och får inte överskridas.

När du är klar — försök komma på ett scenario i din valda domän där isolation-egenskapen specifikt är det som hindrar ett problem (alltså ett race condition mellan två samtidiga transaktioner).

2.6. Indexering

När mängden i en databas växer försämras prestandan vid sökningar i databasen. Detta beror helt enkelt på att det finns fler rader databashanteraren måste söka igenom för att hitta den eller de rader som stämmer in på SQL-frågan.

Följande exempel med linjär/binärsökning är inte helt sant, men kan användas som en mental modell för att förstå varför och hur index fungerar.

Man kan tänka sig att databashanteraren gör en linjär sökning genom tabellen. En linjär sökning har kostnaden O(n). Detta innebär att om 10 000 rader skulle ta en tiondels sekund att söka igenom skulle 100 000 rader ta en sekund att söka igenom.

För att snabba upp genomsökningen av en tabell behövs en annan algoritm än en linjär sökning. En enkel men väldigt effektiv algoritm är binärsökning (med kostnaden O(log(n))).

Table 27. Linjär vs binärsökning, antal jämförelser i värsta fall

Antal rader

Linjär (worst case)

Binärsökning (worst case)

10

10

4

100

100

7

10 000

10 000

14

1 000 000

1 000 000

20

1 000 000 000

1 000 000 000

30

Vid en miljard rader krävs alltså maximalt 30 jämförelser med binärsökning, men upp till en miljard med linjär.

Vill du se det här i kod? Implementera själv binärsökning över en sorterad lista, låt funktionen räkna antalet jämförelser den gör, och pröva mot listor med 10, 100 och 10 000 element. Stämmer det med tabellen?

Men för att binärsökningsalgoritmen ska fungera måste datan vara sorterad. Och en tabell i en databas kan bara vara sorterad på ett sätt. Eller?

Table 28. En employees-tabell sorterad efter en autoinkrementerande primärnyckel

employees

id

first_name

last_name

1

Johan

Bengtsson

2

Anna

Persson

3

Fatima

Alzanbouri

4

Inge

Naning

5

Minna

Salo

6

Andrew

Gao

Tabellen till vänster är definerad med id som en autoinkrementerande primärnyckel (postgres: serial). Detta innebär att tabellen redan är indexerad efter id. Frågor som använder id:t för att söka efter en specifik användare kan därmed använda sig av binärsökningsalgoritmen för att snabbt hitta användaren.

Exempelfrågor
SELECT * FROM employees WHERE id = 4 (1)
SELECT * FROM employees WHERE last_name = 'Naning' (2)
1 Denna fråga använder id vid sökningen och kan använda en mer effektiv algoritm
2 Denna fråga använder last_name vid sökningen och måste använda en linjär sökning

Så länge tabellen är liten och/eller vi primärt använder id vid våra sökningar i employees-tabellen är det inga problem. Men om tabellen växer väldigt mycket och om många sökningar görs på t.ex. efternamn kan vi behöva tänka om och införa ytterligare index.

Index skapas med hjälp av sql-kommandot CREATE INDEX:

CREATE INDEX employee_lastname ON employees(last_name); (1)
1 employee_lastname är namnet du döper indexet till - det kan heta vadsomhelst.

Man kan tänka att det, när man skapar ett index, skapas en ny tabell:

Table 29. En indexeringstabell för employee_lastname

employee_lastname

last_name

employee_id

Alzanbouri

3

Bengtsson

1

Gao

6

Naning

4

Persson

2

Salo

5

Tabellen till vänster är sorterad efter efternamn, och innehåller en främmande nyckel som låter oss hämta användaren. Eftersom tabellen är sorterad kan vi nu mer effektivt söka på efternamn.

Exempelfrågor
SELECT * FROM employees WHERE id = 4 (1)
SELECT * FROM employees WHERE last_name = 'Naning' (2)
1 Denna fråga använder id vid sökningen och kan använda en effektiv algoritm
2 Eftersom vi nu skapat ett index för last_name, kommer även denna fråga använda en effektiv algoritm

2.6.1. Verifiera att indexet används

Hur vet man att databashanteraren faktiskt använder indexet man skapat? I SQLite skriver man EXPLAIN QUERY PLAN framför sin fråga och får tillbaka en kort beskrivning av hur databasen tänker hämta datan. Jämför samma fråga före och efter att man skapat ett index på last_name:

-- Utan index:
EXPLAIN QUERY PLAN SELECT * FROM employees WHERE last_name = 'Naning';
-- => SCAN employees (1)

CREATE INDEX employee_lastname ON employees(last_name);

-- Med indexet på plats:
EXPLAIN QUERY PLAN SELECT * FROM employees WHERE last_name = 'Naning';
-- => SEARCH employees USING INDEX employee_lastname (last_name=?) (2)
1 Full genomsökning. SQLite läser igenom hela tabellen rad för rad (linjär sökning).
2 Indexet används. SQLite slår upp raden via indexet istället för att skanna.

Tumregel: SCAN = full genomsökning, SEARCH USING INDEX = uppslagning via index.

EXPLAIN i andra databashanterare

EXPLAIN QUERY PLAN är SQLite-specifik syntax. Andra databashanterare gör samma sak med andra kommandon:

  • PostgreSQL: EXPLAIN <fråga> ger frågeplanen. EXPLAIN ANALYZE <fråga> kör också frågan och visar faktiska körtider per steg.

  • MySQL: EXPLAIN <fråga>.

Outputten ser olika ut men begreppen är liknande. Seq Scan i Postgres motsvarar SQLites SCAN, och Index Scan motsvarar SEARCH USING INDEX.

2.6.2. Nackdelar

Nackdelar med att ha index är att vid varje ny användare vi lägger till måste vi indexera om indexeringstabellerna:

Lägga till en ny användare
INSERT INTO employees (first_name, last_name) VALUES ('Kim', 'Ekblad'); (1)
1 Detta får till följd att employee_lastname-indextabellen måste byggas om.

Att bygga om en indexeringstabell är en relativt kostsam operation. Det gör att det går långsammare att lägga till rader i en tabell med index.

Ett index är dessutom, i den enkla modellen ovan, en duplicerad och sorterad kopia av en kolumn. Det innebär att varje index tar plats på disken. På tabeller med många index kan indexen tillsammans uppta mer plats än själva tabellen.

För att veta om det är värt att lägga till ett index bör man samla in statistik kring hur ofta det man vill indexera används vid sökningar, och hur ofta indexeringstabellerna behöver uppdateras.

Index i verkligheten

Förklaringen i avsnittet ovan är en förenkling. Olika databashanterare implementerar index på olika sätt, men de flesta använder en datastruktur som heter B-träd. B-träd är optimerade för disk-läsningar och hanterar både snabba sökningar och uppdateringar bra.

2.6.3. Övningar

Övningarna kommer i tre delar. Först bygger du ett dataset med ~1 000 000 rader med hjälp av Faker. Sedan tidmäter du sökningar med och utan index. Till sist utforskar du några varianter på egen hand.

Skapa ett dataset

För att se indexens påverkan på riktigt behöver du en tabell som är stor nog att skillnaden ska gå att mäta. Faker är en Ruby-gem som genererar realistisk testdata (namn, e-postadresser, städer, datum osv.) i den mängd du behöver.

  1. Skapa en mapp för övningen och lägg till en Gemfile:

    source 'https://rubygems.org'
    gem 'sqlite3'
    gem 'faker'

    Kör bundle install.

  2. Skriv ett script seed.rb med följande startkod, och fyll i loopen som genererar och sätter in raderna:

    require 'sqlite3'
    require 'faker'
    
    ROWS = 1_000_000
    DB_PATH = 'users.sqlite'
    
    File.delete(DB_PATH) if File.exist?(DB_PATH)
    db = SQLite3::Database.new(DB_PATH)
    db.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT, city TEXT)")
    
    # Fyll i: generera och sätt in ROWS rader med Faker

    Bläddra bland Fakers generatorer. Lägg gärna till fler kolumner om du har lust, eller byt domän helt: Star Wars-karaktärer, kaffebeställningar, pokémon. Faker har allt du kan tänka dig (och en hel del du inte kan tänka dig).

Bulkinserts är mycket snabbare inuti en transaktion. Utan en omslutande transaktion gör SQLite en commit efter varje enskild INSERT, vilket gör att en miljon insättningar tar betydligt längre tid än när de packas ihop i en enda transaktion.

db.execute("BEGIN TRANSACTION")
1_000_000.times { db.execute("INSERT INTO users (...) VALUES (?, ?, ?)", ...) }
db.execute("COMMIT")

Räkna med 3-4 minuter på en modern laptop för att fylla tabellen med en miljon rader. Det är Faker som tar tid, inte SQLite. Kör först med 100 000 rader (cirka 20 sekunder) för att verifiera att scriptet fungerar, och bumpa upp till en miljon när du vet att det är rätt.

Mät åtkomsttid med och utan index
  1. Hämta tre e-postadresser ur tabellen: en som ligger först, en sist, och en adress som inte finns.

  2. Skriv ett script bench.rb som tidmäter en sökning på email = ? för var och en av de tre adresserna. Kör varje sökning några gånger och ta medeltiden.

    Reflektera
    • Skiljer sig tiden för första vs sista raden? Varför?

    • Hur lång tid tar det att leta efter en rad som inte finns? Stämmer det med din mentala modell av en linjär sökning?

  3. Lägg till ett index på email och kör om mätningarna. Vad har förändrats?

  4. Använd EXPLAIN QUERY PLAN för att verifiera att SQLite verkligen använder indexet vid sökningarna ovan.

Utforska vidare

Välj minst två av följande och förklara dina observationer med hjälp av EXPLAIN QUERY PLAN och tidmätning:

  1. Skrivkostnad. Bygg om datasetet, först utan något index och sedan med ett index på email, och mät tiden för bulkinsättningen i båda fallen. Hur stor är skrivkostnaden?

  2. Sammansatt index. Skapa ett sammansatt index på (city, name). Kör en sökning enbart på city, en enbart på name, och en på båda. Vilka av dem gynnas av indexet, och vilken inte? Varför?

  3. Sortering. Mät SELECT * FROM users ORDER BY email LIMIT 100; med och utan ett index på email. Indexet hjälper inte bara WHERE-frågor. Vad händer här, och varför?

  4. LIKE-mönster. Jämför WHERE email LIKE 'anna%' med WHERE email LIKE '%@gmail.com'. Använder båda indexet? Vad är skillnaden, och varför kan databashanteraren inte göra något åt det andra fallet?

  5. Primärnyckeln. Sökningar på id är alltid snabba. Verifiera detta för första, sista och en obefintlig rad, och förklara varför det är så.

2.7. Vyer

En vy (engelska: view) är en sparad SELECT-fråga som beter sig som en tabell. När man SELECT:ar från en vy körs själva frågan på nytt, så datan i vyn är alltid lika aktuell som datan i de underliggande tabellerna.

2.7.1. Fördelar

Kapsla in komplicerad SQL

En komplex SELECT med flera JOIN, filter och beräkningar kan gömmas bakom en vy. Applikationen läser från vyn med en enkel SELECT, och själva komplexiteten ligger på ett ställe i databasen.

Enklare gränssnitt

Genom en vy kan man exponera en denormaliserad bild av flera tabeller (en "join-färdig" version), så att klienter slipper själva förstå hur datan är spridd över relationsstrukturen.

Åtkomstkontroll

Man kan ge rättighet att läsa en vy utan att ge rättighet till de underliggande tabellerna. Klassiskt exempel: visa en betyg_publika-vy som filtrerar bort vissa kolumner, utan att ge åtkomst till den underliggande betyg-tabellen.

Gemensam definition

Om flera applikationer behöver samma härledda fråga kan vyn vara enda sanningens källa. Ändras logiken behöver ändringen bara göras på ett ställe.

2.7.2. Nackdelar

Prestanda

Varje SELECT från en vanlig vy kör hela den underliggande frågan på nytt. För dyra vyer som körs ofta blir kostnaden kännbar. Postgres och de flesta andra databashanterare har därför materialized views som lagrar resultatet och uppdateras manuellt eller schemalagt.

Begränsade uppdateringar

Vissa vyer går inte att skriva till. Om vyn aggregerar (GROUP BY, SUM) eller använder JOIN vet databasen inte hur ett INSERT eller UPDATE mot vyn ska översättas till de underliggande tabellerna.

Extra abstraktionslager

När något går fel måste man förstå två saker: vyn och frågan den bygger på. För enkla vyer är det inget problem, men en vy som bygger på en annan vy som bygger på en tredje kan bli svår att felsöka.

2.7.3. Syntax

I Postgres skapas en vy med CREATE VIEW:

CREATE VIEW verifikat_rader AS
SELECT
  v.id, v.datum, v.beskrivning,
  k.konto, ko.namn AS konto_namn,
  k.debet, k.kredit
FROM verifikat v
JOIN konteringar k ON k.verifikat_id = v.id
JOIN konton ko ON ko.nummer = k.konto;

Sedan använder man den som vilken tabell som helst:

SELECT * FROM verifikat_rader WHERE datum = '2026-01-15' ORDER BY id, konto;

För dyra vyer som körs ofta finns materialized views som lagrar resultatet på disk:

CREATE MATERIALIZED VIEW manadsrapport AS
SELECT ...;

REFRESH MATERIALIZED VIEW manadsrapport;

Materialized views måste refreshas manuellt (eller via ett schemalagt jobb) när underliggande data ändras.

Vidareläsning

2.7.4. Övningar

Övningarna i det här kapitlet, samt i kapitel 2.8 (Stored Procedures) och 2.9 (Triggers), bygger alla på samma scaffold och databas: en bokföringsapplikation för en enskild näringsverksamhet. Du sätter upp scaffolden här och återanvänder den i de följande två kapitlen.

Databasen har tre tabeller (samma upplägg som ni jobbade med i Entreprenörskap):

  • konton: en kontoplan (kontonummer, namn, typ, cachat saldo)

  • verifikat: ett verifikat per affärshändelse

  • konteringar: en eller flera rader per verifikat, med kolumnerna debet och kredit

Dubbel bokföring säger att summan av debet och kredit över ett verifikat alltid ska vara lika.

Sätt upp miljön

SQLite stödjer inte vyer av det här slaget (eller stored procedures, eller triggers), så övningarna körs mot Postgres. Vi använder Docker för att slippa lokal installation.

Hämta docker-compose.yml och starta databasen:

docker compose up -d

Anslut med psql:

docker compose exec db psql -U student -d bokforing

Verifiera att schemat är på plats. \dt ska lista de tre tabellerna, och kontoplanen ska vara seedad:

SELECT nummer, namn, typ FROM konton ORDER BY nummer;
 nummer |          namn           |     typ
--------+-------------------------+--------------
 1010   | Kassa                   | tillgång
 1030   | Kundfordringar          | tillgång
 1040   | Varubehållning          | tillgång
 1930   | Bank                    | tillgång
 2010   | Eget kapital            | eget_kapital
 2013   | Eget uttag              | eget_kapital
 2018   | Egen insättning         | eget_kapital
 2030   | Leverantörsskuld        | skuld
 2610   | Utgående moms           | skuld
 2640   | Ingående moms           | tillgång
 3001   | Försäljning vara 25%    | intäkt
 3011   | Försäljning tjänst 25%  | intäkt
 4010   | Inköp varor 25%         | kostnad
 5011   | Kontorshyra             | kostnad
 5410   | Förbrukningsinventarier | kostnad
 5900   | Marknadsföring          | kostnad
(16 rows)

Alla saldo-värden ska vara 0. verifikat och konteringar är tomma.

Aktiva konton

Skriv en vy aktiva_konton som visar nummer, namn och typ för konton där saldot inte är 0. Eftersom inga bokföringar gjorts ännu kommer vyn vara tom — den blir intressant först när du gör övningarna i de följande kapitlen.

Verifikat med konteringar

Skriv en vy verifikat_rader som joinar verifikat, konteringar och konton så att varje rad visar:

  • verifikatets id, datum och beskrivning

  • kontots nummer och namn

  • debet- och kredit-beloppen

Använd vyn för att hitta alla rader för ett visst datum eller ett visst konto — utan att själv behöva skriva ihop JOIN-frågan varje gång.

Utforska vidare: resultaträkning

Skriv en vy resultaträkning som visar tre värden för företaget:

  1. intäkter: totalt saldo över alla intäkt-konton

  2. kostnader: totalt saldo över alla kostnad-konton

  3. resultat: intäkter minus kostnader

Vyn blir bara intressant när det finns bokföringar och konton.saldo är populerad. Återkom hit efter att du skrivit bokför-proceduren i kapitel 2.8 och saldo-triggern i kapitel 2.9.

2.8. Stored Procedures

En lagrad procedur (engelska: stored procedure) är kod (oftast i form av funktioner) som kan lagras i databashanteraren, och sedan anropas och köras. Vilket språk funktionen skrivs i beror på databashanteraren. En del har en SQL-liknande syntax, men andra använder sig av Java eller C.

2.8.1. Fördelar

Kompilering och optimering

Eftersom stored procedures lagras direkt i databashanteraren behöver inte databashanteraren tolka den inkommande SQL-strängen, utan kan kompilera och optimera funktionen när den skapas.

Minskad nätverkstrafik

Även mängden data som behöver skickas över nätverket till databashanteraren kan minska. Istället för att skicka hela SQL-strängen behöver man enbart anropa funktionen (och om funktionen tar några parametrar, skicka med dessa).

Affärslogik i databasen

Stored procedures gör det möjligt att lägga in mycket av den funktionalitet som annars skulle legat i en klient/webbserver direkt i databashanteraren. Detta minskar risken för att buggar i klient/webbserver påverkar databasens innehåll.

Rättigheter

En stored procedure kan köras med andra rättigheter än den som anropar proceduren. Om en användare i vanliga fall inte har rättigheter att göra en DELETE kan möjligheten fortfarande finnas att göra det med en stored procedure, som någon (förhoppningsvis) kontrollerat att den inte gör något oönskvärt.

Skydd från SQL-injektion

Stored procedures tar parametrar via typade argument istället för att klistras ihop till SQL-strängar. Det ger samma skydd mot SQL-injektion som parametriserade frågor. Förutsättningen är att proceduren inte själv bygger dynamisk SQL med användarinmatning, eftersom skyddet då försvinner.

2.8.2. Nackdelar

Affärslogik i databasen

Det kan vara svårt att veta var i applikationen logik finns om en del av logiken ligger i databashanteraren.

Fastlåsning i databashanteringssystem

De flesta databashanterare har olika, inkompatibla språk och ramverk för stored procedures.

Specialistkompetens krävs

Den som skriver stored procedures behöver specialistkompetens, då de inbegriper nya språk och tankesätt.

2.8.3. Syntax i Postgres

I Postgres finns två varianter: funktioner (CREATE FUNCTION) som returnerar ett värde och kan användas i SELECT-frågor, och procedurer (CREATE PROCEDURE, från Postgres 11) som anropas med CALL och inte returnerar något.

En enkel funktion som returnerar antal rader i konton-tabellen:

CREATE FUNCTION antal_konton() RETURNS INTEGER AS $$
  SELECT COUNT(*)::INTEGER FROM konton;
$$ LANGUAGE SQL;

SELECT antal_konton();

Funktioner som ska göra mer än ett enskilt SELECT (t.ex. fatta beslut eller jobba med lokala variabler) skrivs i procedurspråket PL/pgSQL. Här är en funktion som returnerar namn + typ för ett konto, eller 'Okänt konto' om numret inte finns:

CREATE FUNCTION konto_beskrivning(konto_nummer TEXT) RETURNS TEXT AS $$
DECLARE
  k_namn TEXT;
  k_typ  TEXT;
BEGIN
  SELECT namn, typ INTO k_namn, k_typ
    FROM konton WHERE nummer = konto_nummer;
  IF k_namn IS NULL THEN
    RETURN 'Okänt konto';
  ELSE
    RETURN k_namn || ' (' || k_typ || ')';
  END IF;
END;
$$ LANGUAGE plpgsql;

SELECT konto_beskrivning('1930');  -- Bank (tillgång)
SELECT konto_beskrivning('9999');  -- Okänt konto

Tre nya byggstenar:

  • DECLARE-blocket reserverar lokala variabler. I exemplet två stycken: k_namn och k_typ, båda av typ TEXT. Värdena tilldelas senare i funktionen.

  • SELECT …​ INTO …​ hämtar värden från en SELECT in i variablerna istället för att returnera dem som ett resultat. Flera kolumner och variabler kan kombineras som ovan.

  • IF …​ THEN …​ ELSE …​ END IF; är PL/pgSQL:s standardförgrening. Det finns även ELSIF för flervägsförgreningar.

För kortare logik, där man bara vill välja mellan värden, finns även CASE-uttrycket. Det fungerar både inuti SELECT och i PL/pgSQL:

SELECT nummer, namn,
  CASE
    WHEN typ IN ('tillgång', 'kostnad') THEN 'debet-konto'
    ELSE 'kredit-konto'
  END AS kategori
FROM konton ORDER BY nummer LIMIT 5;

CASE är ofta mer kompakt än IF/ELSE när logiken bara är "välj värde baserat på villkor".

Ett tredje verktyg värt att känna till: COALESCE(x, y, …​) returnerar det första argumentet som inte är NULL. Användbart för att kortfattat hantera värden som kanske saknas. Till exempel ger COALESCE(SUM(debet), 0) resultatet 0 om det inte finns några rader att summera, istället för NULL.

En procedur som nollställer hela databasen (praktiskt mellan testkörningar):

CREATE PROCEDURE nollställ_databasen() AS $$
BEGIN
  DELETE FROM konteringar;
  DELETE FROM verifikat;
  UPDATE konton SET saldo = 0;
END;
$$ LANGUAGE plpgsql;

CALL nollställ_databasen();

LANGUAGE SQL används när kroppen är en eller flera vanliga SQL-satser. LANGUAGE plpgsql är Postgres-egna procedurspråk och behövs så fort du vill ha kontrollstrukturer som IF, loopar eller felhantering.

Vidareläsning

2.8.4. Övningar

Övningarna återanvänder samma scaffold och databas som du satte upp i kapitel 2.7 (Vyer). Om du inte gjort den övningen ännu, gå tillbaka och kör docker compose up och psql-stegen där först.

Function: saldo

Skriv en function saldo(nummer) som returnerar det aktuella saldot för ett konto, beräknat från konteringar-tabellen (inte från den cachade saldo-kolumnen).

Tänk på att kontots typ avgör hur saldot beräknas:

  • För tillgång och kostnad är saldot SUM(debet) − SUM(kredit)

  • För skuld, eget_kapital och intäkt är det SUM(kredit) − SUM(debet)

Så länge konteringar är tom ska alla saldon vara 0. Lägg in en rad manuellt och verifiera att din funktion ger rätt svar.

Procedure: bokför

Skriv en procedure bokför(datum, beskrivning, konto_debet, konto_kredit, belopp) som registrerar ett verifikat med två konteringar:

  1. En ny rad i verifikat

  2. En debet-rad i konteringarkonto_debet med beloppet belopp

  3. En kredit-rad i konteringarkonto_kredit med samma belopp

Eftersom samma belopp används på båda sidor är dubbel bokföring garanterad av sig självt. Du behöver inte räkna efter.

Testa:

  1. Egen insättning 5 000 kr: Bank / Egen insättning

  2. Hyra 3 000 kr: Kontorshyra / Bank

  3. Försök bokföra med ett kontonummer som inte finns. Vad händer? Sparas verifikatet ändå, eller rullas allt tillbaka?

Använd din saldo-function för att verifiera saldona på de berörda kontona.

Utforska vidare: försäljning med moms

Verkliga försäljningar har tre konteringar: brutto in på Bank, netto till Försäljning, och moms till Utgående moms. Det går inte att beskriva med bokför-proceduren ovan.

Skriv en specialiserad bokför_försäljning(datum, brutto) som tar bruttobeloppet (vad kunden betalar) och själv räknar ut moms och netto för en 25%-vara, för att sedan registrera de tre konteringarna i ett verifikat.

Verifiera med en försäljning på 1 250 kr: saldot på Bank ska gå upp 1 250, Utgående moms 250 och Försäljning vara 25% 1 000.

Tillbaka till resultaträkning

Gå tillbaka till resultaträkning-vyn du sketch:ade i kapitel 2.7 (Vyer). Nu finns det bokföringar i databasen. Kör några bokföringar (t.ex. ett par bokför_försäljning och en hyra) och kontrollera att SELECT * FROM resultaträkning; ger förväntat svar.

Notera dock att resultaträkning-vyn läser från konton.saldo-kolumnen, som fortfarande är 0 även efter dina bokföringar. För att fylla konton.saldo automatiskt behöver vi en trigger — vi tar oss an det i nästa kapitel.

2.9. Triggers

En trigger är en funktion som databasen kör automatiskt när något händer i en tabell (typiskt en INSERT, UPDATE eller DELETE). Till skillnad från en procedure, som anropas explicit med CALL, kör en trigger sig själv som sidoeffekt av en annan operation.

2.9.1. Fördelar

Automatisk konsistens

En trigger garanterar att vissa följdåtgärder alltid sker när en tabell ändras. Klienten behöver inte komma ihåg att uppdatera en relaterad tabell, eftersom databasen sköter det själv.

Central regel-enforcement

Om flera applikationer eller tjänster skriver till samma tabell behöver varje klient inte upprepa samma logik. Triggern ligger på tabellen, inte i klientkoden, och fångar varje skrivning oavsett källa.

Audit-loggar utan ändring av skrivkod

Ett vanligt mönster: en trigger på en tabell skriver varje ändring till en separat *_audit-tabell. Loggningen kan läggas till efter att applikationen är skriven, utan att en enda rad i den ursprungliga skrivkoden behöver ändras.

2.9.2. Nackdelar

Osynlig magi

Triggers kör sig själva. Det är både deras stora styrka och deras stora fara: logik som "bara händer" är lätt att glömma bort när man felsöker. En utvecklare som ser en oväntad rad i konteringar_audit måste veta att en trigger finns för att förstå varifrån den kom.

Kostnad per skrivning

En trigger körs vid varje påverkad rad. På bulkinserts (t.ex. en miljon rader) körs triggern en miljon gånger. För tunga triggers blir det en mätbar prestandakostnad.

Kaskaderande triggers

Om en trigger ändrar en tabell som har sin egen trigger, kommer den triggern också att köras. Med några nivåers kaskader blir det mycket svårt att resonera kring vad som händer när en enda INSERT körs.

2.9.3. Syntax

I Postgres skapas en trigger i två steg: först definieras en triggerfunktion (en vanlig CREATE FUNCTION som returnerar typen TRIGGER), sedan binds den till en tabell med CREATE TRIGGER.

Säg att vi vill logga varje ny kontering till en separat audit-tabell:

CREATE TABLE konteringar_audit (
  id           SERIAL PRIMARY KEY,
  kontering_id INTEGER NOT NULL,
  konto        TEXT NOT NULL,
  debet        NUMERIC(12,2),
  kredit       NUMERIC(12,2),
  loggad_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE FUNCTION log_kontering() RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO konteringar_audit (kontering_id, konto, debet, kredit)
  VALUES (NEW.id, NEW.konto, NEW.debet, NEW.kredit);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER konteringar_logging
  AFTER INSERT ON konteringar
  FOR EACH ROW
  EXECUTE FUNCTION log_kontering();

Inuti triggerfunktionen finns specialvariabeln NEW som representerar raden som just lades till. (För UPDATE och DELETE finns även OLD som representerar hur raden såg ut innan ändringen.) FOR EACH ROW säger att funktionen ska köras en gång per påverkad rad, och AFTER INSERT att den ska köras direkt efter att INSERT-satsen genomförts.

Nu får varje INSERT INTO konteringar automatiskt en motsvarande rad i konteringar_audit. Klienten behöver inte veta att loggningen sker.

Vidareläsning

2.9.4. Övningar

Övningarna återanvänder samma scaffold och databas som i kapitel 2.7 (Vyer) och 2.8 (Stored Procedures).

Trigger: hålla saldot uppdaterat

Funktionen saldo() från kapitel 2.8 räknar om saldot från grunden varje gång den anropas. På en stor databas är det dyrt. Istället vill vi att konton.saldo ska vara en cachad kolumn som hålls i synk automatiskt.

Skriv en trigger på konteringar som uppdaterar konton.saldo när en ny kontering läggs till. Tecknet beror på kontots typ, samma logik som i din saldo-function.

Verifiera genom att köra ett par CALL bokför(…​) och titta på konton.saldo-kolumnen direkt:

SELECT nummer, namn, saldo FROM konton WHERE saldo <> 0 ORDER BY nummer;

Tänk på:

  • Skapa triggern innan ni gör några bokföringar, annars missar den de verifikat som redan finns. Vad gör ni om triggern skapades sent och cache:n är fel?

  • Vad händer om en kontering uppdateras (UPDATE) eller tas bort (DELETE)? Behöver triggern täcka fler händelser, eller är det ok att låta konteringar vara append-only?

  • Vad blir snabbare i längden, den cachade kolumnen eller saldo()-funktionen? Vad blir säkrare?

Tillbaka till resultaträkning

resultaträkning-vyn från kapitel 2.7 (Vyer) läser från konton.saldo-kolumnen. Med triggern på plats uppdateras konton.saldo automatiskt vid varje bokföring, så vyn återspeglar nu företagets ekonomi i realtid.

Kör några bokföringar via bokför och bokför_försäljning (från kapitel 2.8) och kontrollera att SELECT * FROM resultaträkning; ger förväntat svar. Du behöver inte göra något manuellt för att hålla siffrorna aktuella.

Affärslogik i databasen

Nu har du sett tre verktyg som flyttar logik in i databasen: vyer (sparade frågor), stored procedures (sparad kod) och triggers (kod som körs automatiskt). Var och en av dem har sin plats.

Tänk själv eller diskutera med en kursare: vilka av reglerna i ett bokföringssystem hör hemma som procedure/function/trigger/vy i databasen, och vilka hör hellre hemma i applikationskoden? Motivera utifrån för- och nackdelarna i de tre kapitlen.

Konkreta exempel att diskutera:

  • "Saldon ska summera till noll över varje verifikat": invarianten ni precis byggde. I procedure, i en CHECK-constraint, eller i applikationen?

  • "Bankkontot får inte bli negativt": CHECK (saldo >= 0)konton? I procedure? I applikationskoden?

  • "Verifikat får inte bokföras på framtida datum": trigger? Procedure-validering? Klient?

  • "Försäljning över 50 000 kr kräver godkännande av en revisor": databasen eller en helt annan systemkomponent?

  • "Resultaträkning och balansräkning": vyer, eller frågor i applikationskoden? Vad vinner / förlorar du på respektive val?

3. Backup och återställning

3.1. Backup och Snapshots

De flesta databashanterare har inbyggd funktionalitet för att ta säkerhetskopior på databasen. I de flesta fall förvaras dessa backups på en annan dator och- gärna i en annan serverhall.

4. Dokumentdatabaser

4.1. Introduktion

NoSQL (kortform för "Not only SQL") är ett samlingsnamn för databashanterare som inte använder relationsmodellen. Det finns flera familjer:

  • Dokumentdatabaser lagrar JSON-liknande dokument. Exempel: MongoDB, CouchDB, Elasticsearch.

  • Nyckel-värde-databaser lagrar enkla nyckel-värde-par. Exempel: Redis, DynamoDB.

  • Kolumndatabaser organiserar data per kolumn istället för per rad. Exempel: Cassandra, HBase.

  • Grafdatabaser lagrar noder och kanter, optimerade för nätverksstrukturer. Exempel: Neo4j, ArangoDB.

Det här kapitlet fokuserar på dokumentdatabaser. Det är den typ av NoSQL-databas som studenter oftast möter i fält, och den modell som ligger närmast objektorienterad programmering. Ett "dokument" är i princip en map/dict/hash som man sparar.

4.1.1. Relationsdatabaser och dokumentdatabaser sida vid sida

För att förstå varför man väljer det ena eller andra hjälper det att lista vad respektive modell är bra och dålig på.

Relationsdatabaser

Fördelar:

  • Garanterad datakonsistens via ACID-transaktioner.

  • Stark dataintegritet via schema, främmande nycklar och CHECK-begränsningar.

  • Normalisering minskar utrymme, dubblettrisk och underhållskostnad.

  • Inbyggd validering av datatyper och regler.

  • En beprövad, standardiserad fråge-syntax (SQL) sedan 1970-talet.

Nackdelar:

  • Schemat behöver designas i förväg. Om datastrukturen ändras kostar det arbete att migrera.

  • Mindre lämpat för data med stor variation, t.ex. dokument där olika instanser har olika fält.

  • Svåra att fördela över många servrar utan att förlora ACID-garantierna. Stora relationsdatabaser körs därför ofta på en enda kraftfull server. Den bilden börjar luckras upp av nyare verktyg som Turso och LiteFS (replikerar SQLite över världen), CockroachDB (distribuerad SQL med ACID via konsensusprotokoll), och mönstret att varje kund får en egen liten databas istället för att dela en stor.

  • Mindre lämpat för enormt stora datamängder ("Big Data") och kontinuerliga dataströmmar.

Dokumentdatabaser

Fördelar:

  • Inget schema krävs i förväg. Dokument kan ha varierande struktur.

  • Föränderliga datastrukturer hanteras smidigt. Nya fält kan läggas till över tid utan migrationer.

  • Designade för horisontell skalning över många servrar.

  • Lämpat för Big Data, kontinuerliga dataströmmar och decentraliserade datasamlingar.

Nackdelar:

  • Ingen, eller "eventual", konsistens. Två klienter kan en kort tid se olika versioner av samma data.

  • Ingen, eller "eventual", integritet. Det blir klientens ansvar att hålla data konsekvent.

  • Data dupliceras ofta (mer om det i avsnittet om embed vs reference). När en uppgift ändras kan flera kopior behöva uppdateras.

  • Svårt eller omöjligt att validera dokumentens struktur centralt.

  • Ingen gemensam standard. Varje produkt har sitt eget frågespråk eller API.

4.2. Dokumentmodellen

Den enklaste mentala modellen för en dokumentdatabas är denna:

Om du kan använda en av nedanstående datatyper, så kan du använda en dokumentdatabas. Det är samma idé.

Språk Datatyp Literal

Python

dict

{ "username": "grill.korv" }

Ruby

Hash

{ username: "grill.korv" }

JavaScript

Object

{ username: "grill.korv" }

C#

Dictionary<K, V>

new Dictionary<string,string> { ["username"] = "grill.korv" }

Java

HashMap<K, V>

Map.of("username", "grill.korv")

PHP

Associative array

[ "username" ⇒ "grill.korv" ]

Elixir

Map / Struct

%{username: "grill.korv"} eller %User{username: "grill.korv"}

I en relationsdatabas lagras data i rader med fasta kolumner. I en dokumentdatabas lagras data som dokument, som i praktiken är JSON-objekt med fri struktur.

Samma användardata, en gång relationellt och en gång som dokument:

Relationsdatabas
SELECT * FROM users WHERE id = 1;
-- id | username   | email
--  1 | grill.korv | grill@korv.se
Dokumentdatabas
{
  _id:      ObjectId("5ccfe88f9fb5ba190b0a4483"),
  username: "grill.korv",
  email:    "grill@korv.se"
}

Dokument får innehålla nästan vad som helst, inklusive listor och nästlade objekt:

{
  _id:      ObjectId("5ccfe88f9fb5ba190b0a4483"),
  username: "grill.korv",
  email:    "grill@korv.se",
  profile: {
    bio:      "Älskar att grilla.",
    location: "Stockholm"
  },
  roles: ["editor", "moderator"]
}

Det är samma data som hade krävt en users-tabell plus en profiles-tabell plus en user_roles-länktabell i en relationsdatabas. Här är allt packat i ett enda dokument.

4.2.1. Collections

Dokument lagras i collections (svenska: samlingar). En collection motsvarar en tabell i en relationsdatabas, men kräver inget schema. Dokumenten i en collection får ha olika strukturer.

I praktiken brukar dokument i samma collection ändå se ungefär likadana ut.

4.2.2. Var ligger schemat egentligen?

Marknadsföringen av dokumentdatabaser lyfter ofta "schemaless" och "föränderliga datastrukturer" som starka fördelar. Inget schema att designa i förväg, inga migrationer, snabbare utveckling.

Men datan har alltid en form. Applikationen som läser ett user-dokument förväntar sig att hitta vissa fält där, och kraschar eller beter sig konstigt om de saknas eller har fel typ. Det finns alltså ett schema, men det är underförstått och utspritt i applikationskoden istället för centralt definierat i databasen.

Det får konsekvenser:

  • När du lägger till ett fält måste alla läsare av dokumentet hantera att det ibland finns (nya dokument) och ibland inte (gamla dokument).

  • Har du flera applikationer eller tjänster som läser samma collection måste var och en uppdateras vid varje schemaförändring. Utan databasschema finns ingen central plats där "den nya formen" är definierad. Ingen kompilator eller CHECK-constraint hjälper dig hitta de ställen som behöver uppdateras.

  • Ett "schemalöst" projekt landar ofta i samma migrationsarbete som ett relationellt, fast nu sker det i applikationskoden istället för i databasen. Det blir mindre synligt och svårare att verifiera.

Dokumentdatabasens "frihet" är alltså en omfördelning av ansvar, inte en eliminering av det. Den är fortfarande rätt val för en del problem, men "vi slipper designa scheman" är sällan en sann motivering.

4.3. MongoDB

I det här kapitlet använder vi MongoDB som konkret exempel. Det är den mest spridda dokumentdatabasen och en bra utgångspunkt för att förstå hur de fungerar. MongoDB Atlas erbjuder en gratisplan om du vill prova online. För övningarna i kapitlet kör vi en lokal Mongo-server via Docker.

4.3.1. CRUD-operationer

För varje vanlig SQL-operation finns en MongoDB-motsvarighet.

Skapa
SQL
INSERT INTO users (username, email)
VALUES ('grill.korv', 'grill@korv.se');
MongoDB
db.users.insertOne({
  username: "grill.korv",
  email:    "grill@korv.se"
})
Läs
SQL
SELECT * FROM users WHERE username = 'grill.korv';
MongoDB
db.users.find({ username: "grill.korv" })
Uppdatera
SQL
UPDATE users SET email = 'ny@korv.se' WHERE username = 'grill.korv';
MongoDB
db.users.updateOne(
  { username: "grill.korv" },
  { $set: { email: "ny@korv.se" } }
)
Radera
SQL
DELETE FROM users WHERE username = 'grill.korv';
MongoDB
db.users.deleteOne({ username: "grill.korv" })

4.3.2. Index och sökning

MongoDB stödjer index på samma sätt som relationsdatabaser:

db.users.createIndex({ username: 1 }) (1)
db.users.createIndex({ "profile.location": 1 }) (2)
1 Index på fältet username (1 = stigande, -1 = fallande).
2 Index på ett nästlat fält. Relationsdatabaser stödjer ingen nästling alls; sådan data modelleras alltid som en separat tabell från början.

4.3.3. Aggregering

Istället för GROUP BY har MongoDB en aggregation pipeline: en sekvens av bearbetningssteg där datan flödar igenom.

db.users.aggregate([
  { $match: { active: true } },                       (1)
  { $group: { _id: "$role", count: { $sum: 1 } } },   (2)
  { $sort:  { count: -1 } },                          (3)
  { $limit: 5 }                                       (4)
])
1 Filtrera (motsvarar WHERE).
2 Gruppera och räkna (motsvarar GROUP BY + COUNT).
3 Sortera fallande.
4 Begränsa till fem rader.
Vidareläsning

4.3.4. Övning: sätt upp miljön

Övningarna i resten av kapitlet bygger på ett färdigt Elixir-projekt som pratar med MongoDB. Resten av boken har använt Ruby/Sinatra som lingua franca för webbserver-kod. Det här kapitlet byter språk till Elixir med ramverket Pluggy, byggt på Plug och Cowboy (samma Pluggy som ni jobbade med i webbserver-kursen i början av TE4).

Varför bytet? Sista delen av kapitlet handlar om publish/subscribe-events för att hålla inbäddade kopior i synk. I Ruby kräver det mönstret ett bibliotek. I Elixir är pub/sub bara hur språket fungerar i grunden. Mer om det när vi kommer dit.

Du behöver inte skriva någon Plug-, Cowboy- eller mallkod själv; allt webb-relaterat finns redan. Din uppgift är att jobba med MongoDB-anropen i lib/pluggy/models/ och, senare, lägga till en EventBus och prenumeranter.

Hämta scaffolden: dokumentdatabaser.zip. Packa upp den i en egen mapp.

Starta MongoDB:

docker compose up -d

Installera Elixir-biblioteken och starta appen:

mix deps.get
iex -S mix

Öppna http://localhost:3000 i webbläsaren. Du ska se en (tom) lista över användare och ett formulär för att lägga till nya.

Verifiera grundläggande CRUD genom webbgränssnittet:

  1. Skapa två användare, t.ex. grill.korv och Eva.

  2. Gå till sidan för en användare och uppdatera e-postadressen.

  3. Gå till /groups, skapa en grupp som heter Crazy Cowboys med båda användarna som medlemmar.

Öppna också en mongosh-konsol mot din lokala Mongo-server och inspektera datan direkt:

docker compose exec mongo mongosh test
db.users.find()
db.groups.find()

Hur ser dokumenten ut? Vad innehåller groups-dokumenten under fältet users? Det är det som leder vidare till nästa avsnitt.

4.4. Modellering: embed eller reference

I en relationsdatabas är det självklart hur man kopplar två entiteter: en främmande nyckel. En posts-rad har author_id som pekar på en users-rad. Klart.

I en dokumentdatabas har du ett val. Du kan göra något liknande, eller du kan bädda in den ena entiteten direkt i den andra. Båda strategierna är giltiga, men de har olika trade-offs.

4.4.1. Exemplet

Säg att vi modellerar ett system där användare tillhör grupper, t.ex. för en kursplattform eller Toolie’s gruppslumpare. En användare har ett username. En grupp har ett groupname och ett antal medlemmar. En medlem kan tillhöra flera grupper, en grupp har flera medlemmar.

Det här går att lagra på två sätt.

Variant 1: reference

Användarna lagras i en collection. Grupperna lagras i en annan och innehåller bara id:n till sina medlemmar.

db.users.insertMany([
  { _id: "0x70...700", username: "grill.korv" },
  { _id: "0x70...701", username: "Eva" }
])

db.groups.insertOne({
  _id:       "0x60...700",
  groupname: "Crazy Cowboys",
  user_ids:  ["0x70...700", "0x70...701"]
})

För att visa gruppens medlemmar måste klienten göra två frågor. Först hämta gruppen, sedan slå upp användarna utifrån listan med id.

Variant 2: embed

Grupperna innehåller hela användarobjektet (eller relevanta delar av det), inte bara id:t. Användarna finns ändå kvar i sin egen collection — den är "sanningen" — men en kopia av relevanta fält bäddas in i gruppen.

db.groups.insertOne({
  _id:       "0x60...700",
  groupname: "Crazy Cowboys",
  users: [
    { _id: "0x70...700", username: "grill.korv" },
    { _id: "0x70...701", username: "Eva" }
  ]
})

Nu räcker en enda fråga för att visa gruppen med dess medlemmar. Men username finns på två ställen i databasen. Om Eva byter namn behöver båda kopiorna uppdateras.

4.4.2. När vilken?

Tumregler för när embedding lönar sig:

  • När de inbäddade dokumenten är små.

  • När den inbäddade datan sällan ändras.

  • När du nästan alltid läser dem tillsammans med ytter-dokumentet.

  • När "eventual consistency" är acceptabelt. Om en kopia är lite gammal är det ok.

  • När antalet inbäddade dokument växer långsamt eller är begränsat.

Tumregler för när reference lönar sig:

  • När de refererade dokumenten är stora.

  • När datan ändras ofta.

  • När du ofta läser ytter-dokumentet utan inner-dokumenten.

  • När du behöver "omedelbar konsistens". Klienten måste alltid se senaste versionen.

  • När antalet relaterade dokument är stort eller obegränsat.

I praktiken kombinerar man ofta båda: man bäddar in de få fält man oftast behöver (t.ex. _id och username), och använder id:t som referens för att hämta det fulla dokumentet vid behov. Det är exakt vad scaffold-appen i kapitlet gör.

4.4.3. Övning: embed eller reference

Du ska utöka scaffold-appen från föregående avsnitt med en ny funktion. Användarna ska kunna posta korta inlägg i en grupp (tänk en enkel diskussionstråd).

Varje inlägg har:

  • En författare (referens till eller kopia av en användare).

  • En textkropp.

  • En tidsstämpel.

Innan du skriver någon kod, fundera över:

  1. Var ska inläggen lagras? Som en egen posts-collection? Som en inbäddad lista i groups? Som en inbäddad lista i users?

  2. Om du väljer att lagra inlägg i grupper (inbäddat eller med referenser), hur visar du författaren? Inbäddat eller via referens till users?

Skriv ner ditt val och en motivering utifrån tumreglerna ovan, innan du implementerar.

Implementera sedan:

  1. En route som tar emot ett nytt inlägg och sparar det.

  2. En route som visar de senaste 10 inläggen i en grupp.

När du är klar: vad skulle hända i din lösning om Eva ändrar sitt användarnamn? Påverkas inläggen?

4.5. Konsistens via events

Embedding är snabbt att läsa men har en uppenbar svaghet. När källans data ändras går de inbäddade kopiorna ur synk.

Säg att Eva i exemplet från föregående avsnitt ändrar sitt användarnamn från "Eva" till "evac". Användardokumentet i users uppdateras. Men gruppen Crazy Cowboys har en inbäddad kopia av Eva med det gamla namnet, och det är den klienten ser när den listar gruppen.

4.5.1. Lösningen: publish/subscribe

I många dokumentdatabaser löser man detta med ett event-baserat tillvägagångssätt:

  1. Varje operation som ändrar en användare publicerar ett event, en notifikation som säger "användaren med id X uppdaterades".

  2. Andra delar av systemet prenumererar (subscribe) på dessa events.

  3. När gruppens prenumerant får besked att Eva uppdaterats, går den igenom alla grupper där Eva är inbäddad och uppdaterar de inbäddade kopiorna.

På så vis håller sig inbäddad data automatiskt aktuell, utan att den kod som uppdaterar användaren behöver veta något om grupper.

Det är här Elixir-bytet betalar tillbaka. I många språk behöver man ett pub/sub-bibliotek för det här mönstret. I Elixir är pub/sub bara processer som skickar meddelanden till varandra, vilket är hur språket fungerar i grunden.

4.5.2. En enkel EventBus

Tar man pub/sub till sin minsta beståndsdel landar man på en GenServer som håller reda på vilka processer som prenumererar på vilka events:

defmodule Pluggy.EventBus do
  use GenServer

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  def init(_), do: {:ok, %{}}

  def subscribe(event), do: GenServer.call(__MODULE__, {:subscribe, event, self()})
  def publish(event, args), do: GenServer.cast(__MODULE__, {:publish, event, args})

  def handle_call({:subscribe, event, pid}, _from, state),
    do: {:reply, :ok, Map.update(state, event, [pid], &[pid | &1])}

  def handle_cast({:publish, event, args}, state) do
    state |> Map.get(event, []) |> Enum.each(&send(&1, {event, args}))
    {:noreply, state}
  end
end

Femton rader. Inget bibliotek behövs.

4.5.3. Publicera från modellen

I användarmodellen blir det enkelt att lägga in:

defmodule Pluggy.Models.User do
  alias Pluggy.EventBus

  # ... (find, all, create som tidigare) ...

  def update(id, params) do
    Mongo.update_one(:mongo, "users",
      %{"_id" => BSON.ObjectId.decode!(id)},
      %{"$set" => Map.drop(params, ["id", "_id"])})
    EventBus.publish(:user_updated, id) (1)
  end
end
1 När User.update(…​) körs publiceras :user_updated med användarens id.

4.5.4. Prenumerera i en GenServer

På andra hållet finns en prenumerant som lyssnar på :user_updated:

defmodule Pluggy.Subscribers.GroupSubscriber do
  use GenServer
  alias Pluggy.{EventBus, Models}

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def init(_) do
    EventBus.subscribe(:user_updated) (1)
    {:ok, %{}}
  end

  def handle_info({:user_updated, user_id}, state) do
    user = Models.User.first(user_id)

    Mongo.update_many(:mongo, "groups",
      %{"users._id" => BSON.ObjectId.decode!(user_id)},      (2)
      %{"$set" => %{"users.$.username" => user["username"]}}) (3)

    {:noreply, state}
  end
end
1 Vid uppstart prenumererar GenServer:n på :user_updated.
2 Hitta alla grupper där användaren är inbäddad.
3 Uppdatera den inbäddade username i varje sådan grupp.

För att processerna ska köras vid uppstart läggs de till i lib/pluggy/application.ex:

children = [
  {Mongo, name: :mongo, url: "mongodb://localhost:27017/test"},
  {Pluggy.EventBus, []},                                          (1)
  {Pluggy.Subscribers.GroupSubscriber, []},                       (2)
  {Plug.Cowboy, scheme: :http, plug: Pluggy.Router, options: [port: 3000]}
]
1 EventBus startas innan prenumeranter.
2 GroupSubscriber prenumererar på :user_updated vid uppstart.

4.5.5. Varför det här är inbyggt i Elixir

I Ruby skulle samma mönster kräva ett bibliotek som wisper, ett globalt händelseregister, och subscribe-anrop som körs vid applikationsstart. Det fungerar, men varje del är något som biblioteket lägger till ovanpå språket.

I Elixir är varje del redan en språkfunktion:

  • Processer som skickar meddelanden är hur OTP fungerar.

  • GenServer är en byggsten i standardbiblioteket.

  • En supervisor i application.ex startar både EventBus och prenumeranter automatiskt.

Mönstret som kallas "publish/subscribe" går här inte ovanpå språket utan genom det. Det är hela poängen med att byta till Elixir för det här kapitlet. Idén är densamma som i Ruby plus wisper, men maskineriet är inbyggt och består av delar du redan kan.

4.5.6. Övning: lägg till en event-prenumerant

Scaffold-appen i tidigare övningar har en enkel arkitektur utan events. Nu ska du bygga om den för att använda event-baserad konsistens, enligt mönstret i koden ovan.

Steg:

  1. Skapa lib/pluggy/event_bus.ex med en GenServer som tar emot subscribe + publish, som i exemplet.

  2. Ändra Pluggy.Models.User.update så att den publicerar :user_updated efter Mongo-anropet.

  3. Skapa lib/pluggy/subscribers/group_subscriber.ex med en GenServer som prenumererar på :user_updated och uppdaterar inbäddade username-värden i berörda grupper.

  4. Lägg till Pluggy.EventBus och Pluggy.Subscribers.GroupSubscriber i barnlistan i lib/pluggy/application.ex så de startas automatiskt.

  5. Testa: ändra Evas namn via webbgränssnittet och kontrollera att gruppens listning visar det nya namnet, utan att du behövt lägga till någon kod i User.update-vägen som vet om grupper.

Fundera till sist:

  • Vilka andra events skulle vara värda att hantera? Om en användare raderas, vad ska hända med deras inbäddade kopior i grupper? Skulle du ta bort dem? Markera dem som inaktiva? Lämna dem orörda?

  • Om systemet körs på flera servrar parallellt räcker det inte med en GenServer som bara känner till sin egen process. Vilka alternativ finns då? (Phoenix.PubSub och MongoDB Change Streams är två sätt.)

5. Säkerhet och integritet

5.1. Introduktion till databassäkerhet

Datalagring handlar inte bara om att designa effektiva tabeller och skriva snabba frågor. Så fort en databas innehåller verklig data (kunduppgifter, betyg, lönelistor, journaler) kommer en hel rad ytterligare frågor in i bilden:

  • Vem har rätt att läsa, ändra eller ta bort vilken data?

  • Hur skyddar vi datan om någon stjäl databasservern eller en backup-disk?

  • Hur lagrar vi lösenord på ett sätt som inte avslöjar dem för en angripare?

  • Vad får vi enligt lagen lagra om personer, och hur länge?

I det här kapitlet går vi igenom hur man hanterar dessa frågor i praktiken: hur man styr åtkomst med auktorisering, hur man hanterar känsliga uppgifter, och vilka lagar som styr vad vi får och inte får göra med personuppgifter.

5.1.1. CIA: tre grundläggande principer

Inom IT-säkerhet pratar man ofta om CIA-triaden, en förkortning för tre grundläggande egenskaper som ett system förväntas upprätthålla.

Confidentiality (sekretess)

Information ska bara vara åtkomlig för de personer som har rätt att se den. Att en obehörig kan läsa en kund-tabell är ett brott mot sekretessen, även om hen inte ändrar något.

Integrity (data-integritet)

Information ska vara korrekt och får inte ändras av obehöriga eller av misstag. Att en angripare ändrar ett saldo i en banktabell, eller att en bugg gör att en användarpost skrivs över, är båda brott mot data-integriteten.

Availability (tillgänglighet)

Information ska vara tillgänglig när den behövs. Att en databas ligger nere när användarna försöker logga in är ett brott mot tillgängligheten, även om datan i sig är intakt.

Två olika "integriteter"

Ordet integritet används på svenska i två helt olika betydelser, och det är lätt att blanda ihop dem:

  • Data-integritet (engelska: data integrity): att data är korrekt och oförändrad. Det är i i CIA-triaden.

  • Personlig integritet (engelska: privacy): en persons rätt att själv bestämma över information om sig själv. Det är vad GDPR (se sista avsnittet) handlar om.

Båda är viktiga inom datalagring, men de betyder olika saker.

5.2. Auktorisering och behörigheter

Auktorisering (engelska: authorization) handlar om vad en redan inloggad användare har rätt att göra. Detta skiljer sig från autentisering (engelska: authentication), som handlar om att avgöra vem användaren är.

Autentisering vs auktorisering
  • Autentisering svarar på frågan "vem är du?" (oftast genom användarnamn och lösenord).

  • Auktorisering svarar på frågan "vad får du göra?" (t.ex. läsa, skriva eller ta bort).

5.2.1. Användare och roller

I de flesta databashanterare kan man skapa flera olika användare i databasen. Varje användare har sitt eget användarnamn och lösenord, och kan ges olika rättigheter.

Att i en stor organisation tilldela rättigheter individuellt till varje användare blir snabbt ohanterligt. Därför grupperar man rättigheter i roller. En roll är ett namngivet knippe av rättigheter som man sedan tilldelar till en eller flera användare.

Exempel på roller i en skoladministration

Roll

Användare

Rättigheter

larare

anna, johan, fatima

Läsa elev-tabellen, skriva i betyg-tabellen

rektor

andrew, fatima

Allt en lärare får + läsa lönetabellen

elev

karl, viking

Läsa sina egna betyg

vaktmastare

inge

Läsa och skriva i skåp-tabellen

Lägg märke till att en användare kan ha flera roller. Fatima är både larare och rektor i exemplet ovan. Hennes totala rättigheter är då unionen av rollernas rättigheter.

5.2.2. GRANT och REVOKE

I SQL ger man rättigheter med GRANT och tar bort rättigheter med REVOKE.

GRANT SELECT ON students TO anna; (1)
GRANT INSERT, UPDATE ON grades TO anna; (2)
GRANT ALL PRIVILEGES ON lockers TO inge; (3)

REVOKE INSERT ON grades FROM anna; (4)
1 Anna får läsa från students-tabellen.
2 Anna får lägga till och uppdatera rader i grades-tabellen.
3 Inge får göra allt mot lockers-tabellen.
4 Anna får inte längre lägga till rader i grades-tabellen.

Vanliga rättigheter som kan tilldelas är SELECT, INSERT, UPDATE och DELETE, samt rättigheter för att modifiera schemat (CREATE, DROP, ALTER).

5.2.3. Roller i SQL

I de flesta databashanterare kan man även skapa själva rollerna direkt i databasen:

CREATE ROLE larare; (1)

GRANT SELECT ON students TO larare; (2)
GRANT INSERT, UPDATE ON grades TO larare;

GRANT larare TO anna; (3)
GRANT larare TO johan;
GRANT larare TO fatima;
1 Skapa en ny roll.
2 Tilldela rättigheter till rollen.
3 Tilldela rollen till en användare.

När man senare vill ändra vad lärare får göra behöver man bara uppdatera rollen. Alla användare som tillhör rollen påverkas automatiskt.

5.2.4. Principen om minsta privilegium

En grundregel inom säkerhet är principen om minsta privilegium (engelska: principle of least privilege): varje användare och varje program ska ha precis de rättigheter som krävs för uppgiften, varken mer eller mindre.

Om en webbapplikation bara behöver läsa från en produkter-tabell ska användaren applikationen ansluter med inte ha rättighet att skriva i tabellen. Skulle applikationen råka ut för en SQL-injektion (se nästa avsnitt) eller annan bugg så är skadan begränsad.

Använd inte databasens administratörskonto från din applikation.

Det är frestande att alltid ansluta som postgres eller root eftersom det "bara fungerar". Men ett administratörskonto kan göra allt, inklusive att radera hela databasen. Skapa istället en separat användare för varje applikation, och ge den enbart de rättigheter den behöver.

5.2.5. Övningsuppgift

Tänk dig att du bygger ett bibliotekssystem med tabellerna books, loans och members.

Tre olika program ansluter till databasen:

  • En publik webbsida där besökare kan söka i bokkatalogen.

  • En utlåningsterminal där bibliotekarier registrerar lån och återlämningar.

  • En administratörsapplikation där chefen lägger till nya böcker och nya medlemmar.

Skriv CREATE ROLE- och GRANT-satser för var och en av de tre programmen, så de har precis de rättigheter de behöver, varken mer eller mindre.

5.3. Säker lagring av känsliga uppgifter

Många databaser innehåller uppgifter som måste skyddas extra noga: lösenord, personnummer, hälsouppgifter, kontonummer. Att en angripare kommer åt en sådan databas kan få förödande konsekvenser för de personer datan handlar om.

I det här avsnittet går vi igenom de tre vanligaste metoderna för att skydda känsliga uppgifter: hashning, kryptering och parametriserade frågor.

5.3.1. Hashning av lösenord

En hashfunktion är en matematisk funktion som tar en sträng (t.ex. ett lösenord) och omvandlar den till en till synes slumpmässig sträng av fast längd. Hashfunktioner har två viktiga egenskaper:

  1. Samma indata ger alltid samma utdata.

  2. Det är (i praktiken) omöjligt att utifrån utdatan räkna ut vad indatan var.

Exempel på hashning (sha-256, förkortat)

Indata

Utdata

hej

9c4ec4…​0f1be3

hej!

2cf99c…​88b5a4

hejsan

e1e7ce…​6b3d24

Hej

2178d3…​f8e791

Lägg märke till att även en väldigt liten ändring i indatan (t.ex. ett utropstecken eller en stor bokstav) ger en helt annan utdata.

När en användare registrerar sig hashar vi lösenordet och sparar enbart hashen i databasen. När användaren senare loggar in hashar vi det inskrivna lösenordet och jämför med hashen i databasen. Om hasharna stämmer får användaren logga in.

Skulle databasen läcka ut har angriparen bara hasharna, inte lösenorden.

Använd inte vanliga hashfunktioner som SHA-256 eller MD5 för lösenord!

Vanliga hashfunktioner är designade för att vara snabba, vilket betyder att en angripare kan testa miljarder lösenord per sekund. Använd istället en lösenordshashfunktion som är avsiktligt långsam, t.ex. bcrypt, scrypt eller argon2.

Salt

Ett salt är en slumpmässig sträng som läggs till lösenordet innan det hashas. Saltet sparas tillsammans med hashen i databasen.

Detta gör att två användare som råkar ha samma lösenord ändå får olika hashar i databasen, vilket gör det betydligt svårare för en angripare att använda förberäknade tabeller (s.k. rainbow tables) för att knäcka lösenord.

Moderna lösenordsbibliotek som bcrypt hanterar saltet automatiskt; du behöver inte hantera det själv.

5.3.2. Kryptering

Hashning är envägs: man kan hasha, men man kan aldrig "av-hasha". Det är därför hashning passar för lösenord. Vi behöver ju aldrig läsa tillbaka det riktiga lösenordet.

Kryptering är tvåvägs: man kan kryptera en text till en obegriplig sträng, och man kan dekryptera den tillbaka, om man har rätt nyckel.

Kryptering används där datan faktiskt behöver kunna läsas tillbaka, t.ex. personnummer, kontonummer eller textinnehållet i ett krypterat meddelande.

Kryptering i vila

Kryptering i vila (engelska: encryption at rest) innebär att datan är krypterad när den ligger lagrad på disk. Skulle någon stjäla disken eller en backup-kopia av databasen är datan obegriplig utan rätt nyckel.

De flesta moderna databashanterare har inbyggt stöd för Transparent Data Encryption (TDE), där hela databasen krypteras automatiskt på disk.

Kryptering vid överföring

Kryptering vid överföring (engelska: encryption in transit) innebär att datan är krypterad när den skickas mellan klient och databasserver. Detta sker normalt med TLS (Transport Layer Security), samma teknik som används för HTTPS-trafik på webben.

Utan kryptering vid överföring kan vem som helst på samma nätverk lyssna av trafiken och se både SQL-frågor och svar i klartext.

Kryptering på applikationsnivå

Både kryptering i vila och kryptering vid överföring sker på infrastrukturnivå, alltså databasen själv ser klartext. De skyddar mot stulna diskar och avlyssning på nätverket, men inte mot:

  • En databasadministratör som läser data direkt i databasen.

  • En angripare som fått tag på rätt åtkomstuppgifter och loggar in mot databasen.

  • Personalen hos den molnleverantör som driftar databasen.

För riktigt känsligt innehåll väljer en del tjänster att kryptera datan innan den når databasen. Exempel: Basecamp/HEY för e-post och projektinnehåll, Signal för meddelanden, och lösenordshanterare som 1Password. Databasen lagrar då bara obegripliga teckensträngar; bara applikationen (med rätt nyckel) kan dekryptera dem.

I Ruby/Rails kan det se ut så här:

class Message < ApplicationRecord
  encrypts :body (1)
end

m = Message.create(body: "Hemligt projekt") (2)
puts Message.find(m.id).body (3)
1 Talar om för Rails att fältet body ska krypteras.
2 Innan raden lagras i databasen krypteras body automatiskt.
3 När body läses tillbaka dekrypteras den automatiskt, så länge applikationen har rätt nyckel.

I C# / Entity Framework Core kan man göra samma sak via en value converter:

public class Message
{
    public int Id { get; set; }
    public string Body { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Message>()
        .Property(m => m.Body)
        .HasConversion( (1)
            plain  => Encryption.Encrypt(plain),
            cipher => Encryption.Decrypt(cipher)
        );
}
1 HasConversion registrerar två funktioner: en som körs innan värdet skrivs till databasen, och en som körs när det läses tillbaka. Själva krypterings­logiken (Encryption.Encrypt/Encryption.Decrypt) skriver man antingen själv eller hämtar in via ett NuGet-paket.
Vidareläsning
Avvägningar

Kryptering på applikationsnivå har dock påtagliga nackdelar:

  • Du kan inte söka i krypterad data med SQL. WHERE body LIKE '%projekt%' fungerar inte längre, eftersom databasen bara ser den krypterade datan.

  • Indexering blir meningslöst, eftersom samma indata oftast krypteras till olika utdata varje gång.

  • Sortering fungerar inte på krypterade fält.

  • Nyckelhantering blir kritisk: tappar du nyckeln är datan obegriplig för alltid, även för dig själv.

Därför används tekniken oftast enbart på de allra känsligaste fälten (t.ex. innehållet i ett meddelande eller en patientjournal), inte på fält man behöver söka eller sortera på (t.ex. e-postadress eller efternamn).

Deterministisk kryptering

Det finns en variant som kallas deterministisk kryptering, där samma indata alltid ger samma utdata. Då kan man söka på exakt-matchning (t.ex. WHERE email = 'anna@example.com'), eftersom applikationen kan kryptera söksträngen och jämföra med det krypterade värdet i databasen.

Nackdelen är att en angripare som ser den krypterade datan kan se vilka rader som har samma värde, vilket i sig kan avslöja en hel del.

5.3.3. SQL-injektion

SQL-injektion är en av de äldsta och mest kända säkerhetsbristerna inom datalagring. Den uppstår när en applikation klistrar ihop SQL-frågor med inmatning från användaren utan att hålla isär datan från koden.

Ett klassiskt exempel (Ruby/Rails)
username = params[:username] (1)
password = params[:password]

sql = "SELECT * FROM users WHERE username = '#{username}' AND password = '#{password}'"

db.execute(sql) (2)
1 Användarens inmatning från ett formulär.
2 Frågan körs som om den vore en helt vanlig SQL-fråga.
Samma sårbarhet (C# / Entity Framework Core)
var username = Request.Form["username"];
var password = Request.Form["password"];

var sql = $"SELECT * FROM Users WHERE Username = '{username}' AND Password = '{password}'";

var users = db.Users.FromSqlRaw(sql).ToList(); (1)
1 Med FromSqlRaw följer den färdig­interpolerade strängen rakt in i databasen, exakt samma sårbarhet som i Ruby-exemplet.

Om användaren skriver admin' -- som användarnamn (lägg märke till citationstecknet och de två minustecknen) blir SQL-frågan:

SELECT * FROM users WHERE username = 'admin' --' AND password = ''

Allt efter -- behandlas som en kommentar av databashanteraren, så lösenordskontrollen körs aldrig. Angriparen är inloggad som administratör, utan att ens veta vad lösenordet är.

Förebyggande: parametriserade frågor

Lösningen är att aldrig klistra ihop SQL-strängar med inmatning. Istället skickar man inmatningen som parametrar, separat från själva SQL-koden:

Ruby/Rails: säker variant
sql = "SELECT * FROM users WHERE username = ? AND password = ?" (1)
db.execute(sql, [username, password]) (2)
1 SQL-koden innehåller ? som platshållare istället för värden.
2 Värdena skickas separat till databashanteraren.
C# / Entity Framework Core: säker variant
var users = db.Users.FromSqlInterpolated( (1)
    $"SELECT * FROM Users WHERE Username = {username} AND Password = {password}"
).ToList();

// Eller, mer idiomatiskt i EF Core, via LINQ:
var user = db.Users
    .Where(u => u.Username == username && u.Password == password) (2)
    .FirstOrDefault();
1 FromSqlInterpolated ser ut som vanlig string-interpolation, men EF Core förvandlar automatiskt värdena till parametrar.
2 LINQ-frågor parametriseras alltid automatiskt. Det är en av huvudpoängerna med en ORM.

Eftersom databashanteraren får SQL-koden och datan separat blandas de aldrig ihop. Skriver användaren admin' -- så söks det efter en användare som faktiskt heter admin' -- (med citationstecken och allt), vilket inte finns.

Stored procedures (se kapitlet om relationsdatabaser) ger ett liknande skydd mot SQL-injektion, eftersom enbart funktionsanrop med parametrar skickas till databasen.

Vidareläsning

5.3.4. Övningsuppgift

I tabellen nedan lagras användarnamn och lösenord i klartext.

users

id

username

password

1

anna

hejsan123

2

johan

qwerty

3

fatima

tomten321

Resonera dig fram till svaret på följande frågor:

  1. Vilka är konsekvenserna om en angripare lyckas läsa hela tabellen?

  2. Hur skulle tabellen sett ut om lösenorden vore hashade istället för i klartext?

  3. Två av användarna har valt vanliga lösenord. Hur skulle ett salt göra det svårare för en angripare att utnyttja det?

5.4. Lagar och integritet

Att lagra data om personer är inte bara en teknisk fråga. Det är också en juridisk fråga. I Sverige och resten av EU regleras hanteringen av personuppgifter främst av Dataskyddsförordningen, mer känd som GDPR.

5.4.1. Vad är en personuppgift?

En personuppgift är all information som direkt eller indirekt kan kopplas till en levande person.

Direkta personuppgifter:

  • Namn

  • Personnummer

  • Adress

  • Telefonnummer

  • E-postadress

  • Foto

Indirekta personuppgifter (uppgifter som blir personuppgifter i kombination med annan information):

  • IP-adress

  • Bilars registreringsnummer

  • Användar-id i ett system

  • Cookie-id

Anonymisering vs pseudonymisering
  • Anonymiserad data går inte att koppla tillbaka till en person, ens av den som lagrade datan. Anonymiserad data omfattas inte av GDPR.

  • Pseudonymiserad data är ersatt med ett id eller en kod, men kan fortfarande kopplas tillbaka via en separat tabell. Pseudonymiserad data är fortfarande personuppgifter och omfattas av GDPR.

Känsliga personuppgifter

Vissa personuppgifter räknas som särskilt känsliga och får i princip inte hanteras alls utan starka skäl och skyddsåtgärder:

  • Etniskt ursprung

  • Politiska åsikter

  • Religiös eller filosofisk övertygelse

  • Medlemskap i fackförening

  • Hälsouppgifter

  • Sexuell läggning

  • Genetiska och biometriska uppgifter

5.4.2. GDPR: Dataskyddsförordningen

Dataskyddsförordningen (engelska: General Data Protection Regulation, GDPR) är en EU-förordning som trädde i kraft den 25 maj 2018. Den ersatte den tidigare svenska personuppgiftslagen (PUL).

Eftersom GDPR är en förordning (inte ett direktiv) gäller den direkt som lag i alla EU-länder utan att den behöver översättas till nationell lagstiftning.

De grundläggande principerna

GDPR bygger på sju grundläggande principer för all hantering av personuppgifter:

  1. Laglighet, korrekthet och öppenhet: det måste finnas en laglig grund för behandlingen, och den registrerade ska informeras om den.

  2. Ändamålsbegränsning: uppgifterna får bara användas för det ändamål de samlades in för.

  3. Uppgiftsminimering: samla bara in de uppgifter som faktiskt behövs.

  4. Korrekthet: uppgifterna ska vara korrekta och uppdaterade.

  5. Lagringsminimering: uppgifterna ska sparas så kort tid som möjligt.

  6. Integritet och konfidentialitet: uppgifterna ska skyddas mot obehörig åtkomst, förlust och ändring.

  7. Ansvarsskyldighet: den som behandlar uppgifterna ska kunna bevisa att man följer principerna ovan.

Den registrerades rättigheter

GDPR ger den person uppgifterna handlar om (den registrerade) ett antal rättigheter:

  • Rätt till information om att och hur uppgifterna behandlas.

  • Rätt till tillgång: att få veta vilka uppgifter som lagras om en själv.

  • Rätt till rättelse: att få felaktiga uppgifter rättade.

  • Rätt till radering ("rätten att bli bortglömd"): att få sina uppgifter raderade.

  • Rätt till begränsning: att tillfälligt frysa behandlingen.

  • Rätt till dataportabilitet: att få sina uppgifter i ett format som kan flyttas till en annan tjänst.

  • Rätt att invända mot viss behandling, t.ex. direktreklam.

Roller enligt GDPR

GDPR definierar två centrala roller:

  • Personuppgiftsansvarig (engelska: data controller): den som bestämmer varför och hur personuppgifter ska behandlas. Vanligen ett företag eller en myndighet.

  • Personuppgiftsbiträde (engelska: data processor): den som behandlar uppgifter på uppdrag av den personuppgiftsansvarige, t.ex. en molnleverantör.

Om skolan använder en molntjänst för betygshantering är skolan personuppgiftsansvarig och molnleverantören personuppgiftsbiträde. De måste teckna ett personuppgiftsbiträdesavtal som reglerar hur uppgifterna får behandlas.

5.4.3. Tillsyn och påföljder

I Sverige är Integritetsskyddsmyndigheten (IMY) tillsynsmyndighet för GDPR. Det är dit man vänder sig om man misstänker att en organisation hanterar personuppgifter felaktigt.

Vid allvarliga överträdelser kan IMY utfärda sanktionsavgifter på upp till 20 miljoner euro eller 4 % av en organisations globala årsomsättning, det av de två som är högst.

GDPR har lett till ett antal stora bötesbelopp. Bland de mest uppmärksammade:

  • Meta (Facebooks moderbolag): 1,2 miljarder euro (2023) för olaglig dataöverföring till USA.

  • Amazon: 746 miljoner euro (2021) för bristfällig hantering av annonsering.

  • Klarna: 7,5 miljoner kronor (2022) för otydlig information till kunder.

5.4.4. Konsekvenser för en databasdesigner

Som databasdesigner och utvecklare påverkar GDPR ditt dagliga arbete på flera sätt.

Designa för dataminimering

Innan du lägger till en kolumn, fråga dig själv om uppgiften faktiskt behövs. Behöver verkligen varje användarregistrering inkludera födelsedatum?

Designa för radering

En användare kan begära att få alla sina uppgifter raderade. Om datan är spridd över många tabeller via främmande nycklar kan det vara komplicerat att städa upp ordentligt.

Använd ON DELETE CASCADE med försiktighet och planering. Det kan både hjälpa (genom att städa relaterad data) och stjälpa (genom att radera mer än man tänkt).

Logga åtkomst till känsliga uppgifter

Om en organisation behandlar känsliga personuppgifter förväntas det normalt att åtkomsten loggas. Vem läste vilken patientjournal när?

Lagra inte gammal data i onödan

Om kund-uppgifter sparats i 15 år trots att kunden inte handlat på 10 år är det troligen en överträdelse av lagringsminimeringen. Bygg in automatiska gallringsregler i applikationen.

5.4.5. Övningsuppgift

Tänk dig att du designar databasen för en webbplats där användare kan boka pass på ett gym.

För var och en av nedanstående uppgifter, bedöm:

  1. Är det en personuppgift? Om ja, är den känslig?

  2. Behövs den för tjänsten, eller kan den uteslutas?

  3. Hur länge bör uppgiften sparas efter att kunden avslutat sitt medlemskap?

Uppgifterna är:

  • Förnamn och efternamn

  • Personnummer

  • E-postadress

  • Lösenord

  • Folkbokföringsadress

  • Hälsodeklaration ("Har du diabetes? Hjärtproblem?")

  • Bokningshistorik

  • IP-adress vid inloggning

  • Foto för medlemskortet