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
-
Minska det utrymme databasen behöver
-
Minska risken att vi har inkonsistent data, det vill säga att beroende på var man i databasen kollar, skulle kunna få olika svar
-
Ö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.
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
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:
|
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:
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.
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.
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.
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:
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
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.
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?
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)
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).
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:
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:
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:
students |
|
personnr |
namn |
010811-xxxx |
Karl-Gustaf Sjöblom |
000222-xxxx |
Viking Fors |
020928-xxxx |
Alexis Lindroth |
020402-xxxx |
Karl-Gustaf Sjöblom |
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.
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:
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).
genres |
|
id |
genre |
1 |
Pop |
2 |
Dance |
3 |
Rock |
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:
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.
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:
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 |
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:
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.
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 |
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 |
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 |
|---|---|
|
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 |
|---|---|
|
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 |
| ER-diagram | UML klassdiagram |
|---|---|
|
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:
-
Ta bort pengarna från konto 1.
-
Föra in pengerna på konto 2.
Alternativt
-
Föra in pengarna på konto 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 samstämmig och motsägelsefri. Det får inte finnas några uppgifter i databasen som inte går ihop med varandra. – I transaktionshantering brukar konsistens formuleras som att databasen ska ha ett tillåtet tillstånd både vid början och vid slutet av en transaktion. Consistency är c:et i förkortningen ACID. – Se också referensintegritet och långsam konsistens (eventual consistency). – Ordet: Som översättning av engelska consistency är konsistens omstritt, eftersom det svenska ordet konsistens oftast står för hur trögflytande eller lättflytande 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 likadant varje gång, medan konsistens är när alla delar av en helhet stämmer överens med varandra.
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.
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 innanBEGIN TRANSACTION.
|
I SQLite (och de flesta databashanterare) räcker det att skriva |
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.
-
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.
-
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.
-
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.
-
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:
-
Modellera databasen i ett ER-diagram eller ett UML klassdiagram.
-
Skriv schemat som
CREATE TABLE-satser i SQLite, inklusive primärnycklar, främmande nycklar och deCHECK-begränsningar du tycker behövs. -
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?
-
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))).
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?
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.
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:
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.
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
Outputten ser olika ut men begreppen är liknande. |
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:
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. |
|
Vidareläsning
|
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.
-
Skapa en mapp för övningen och lägg till en
Gemfile:source 'https://rubygems.org' gem 'sqlite3' gem 'faker'Kör
bundle install. -
Skriv ett script
seed.rbmed 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 FakerBlä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
|
|
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
-
Hämta tre e-postadresser ur tabellen: en som ligger först, en sist, och en adress som inte finns.
-
Skriv ett script
bench.rbsom 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?
-
-
Lägg till ett index på
emailoch kör om mätningarna. Vad har förändrats? -
Använd
EXPLAIN QUERY PLANfö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:
-
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? -
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? -
Sortering. Mät
SELECT * FROM users ORDER BY email LIMIT 100;med och utan ett index påemail. Indexet hjälper inte baraWHERE-frågor. Vad händer här, och varför? -
LIKE-mönster. Jämför
WHERE email LIKE 'anna%'medWHERE 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? -
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 kolumnernadebetochkredit
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,datumochbeskrivning -
kontots
nummerochnamn -
debet- ochkredit-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:
-
intäkter: totalt saldo över allaintäkt-konton -
kostnader: totalt saldo över allakostnad-konton -
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_namnochk_typ, båda av typTEXT. Värdena tilldelas senare i funktionen. -
SELECT … INTO …hämtar värden från enSELECTin 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 ävenELSIFfö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: |
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ångochkostnadär saldotSUM(debet) − SUM(kredit) -
För
skuld,eget_kapitalochintäktär detSUM(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:
-
En ny rad i
verifikat -
En debet-rad i
konteringarpåkonto_debetmed beloppetbelopp -
En kredit-rad i
konteringarpåkonto_kreditmed sammabelopp
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:
-
Egen insättning 5 000 kr: Bank / Egen insättning
-
Hyra 3 000 kr: Kontorshyra / Bank
-
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
konteringarvara 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)på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é.
|
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:
SELECT * FROM users WHERE id = 1;
-- id | username | email
-- 1 | grill.korv | grill@korv.se
{
_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
INSERT INTO users (username, email)
VALUES ('grill.korv', 'grill@korv.se');
db.users.insertOne({
username: "grill.korv",
email: "grill@korv.se"
})
Läs
SELECT * FROM users WHERE username = 'grill.korv';
db.users.find({ username: "grill.korv" })
Uppdatera
UPDATE users SET email = 'ny@korv.se' WHERE username = 'grill.korv';
db.users.updateOne(
{ username: "grill.korv" },
{ $set: { email: "ny@korv.se" } }
)
Radera
DELETE FROM users WHERE username = 'grill.korv';
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:
-
Skapa två användare, t.ex.
grill.korvochEva. -
Gå till sidan för en användare och uppdatera e-postadressen.
-
Gå till
/groups, skapa en grupp som heterCrazy Cowboysmed 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:
-
Var ska inläggen lagras? Som en egen
posts-collection? Som en inbäddad lista igroups? Som en inbäddad lista iusers? -
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:
-
En route som tar emot ett nytt inlägg och sparar det.
-
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:
-
Varje operation som ändrar en användare publicerar ett event, en notifikation som säger "användaren med id X uppdaterades".
-
Andra delar av systemet prenumererar (subscribe) på dessa events.
-
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.exstartar 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.
|
Vidareläsning
|
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:
-
Skapa
lib/pluggy/event_bus.exmed en GenServer som tar emot subscribe + publish, som i exemplet. -
Ändra
Pluggy.Models.User.updateså att den publicerar:user_updatedefter Mongo-anropet. -
Skapa
lib/pluggy/subscribers/group_subscriber.exmed en GenServer som prenumererar på:user_updatedoch uppdaterar inbäddadeusername-värden i berörda grupper. -
Lägg till
Pluggy.EventBusochPluggy.Subscribers.GroupSubscriberi barnlistan ilib/pluggy/application.exså de startas automatiskt. -
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
GenServersom 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:
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
|
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 |
|
anna, johan, fatima |
Läsa elev-tabellen, skriva i betyg-tabellen |
|
andrew, fatima |
Allt en lärare får + läsa lönetabellen |
|
karl, viking |
Läsa sina egna betyg |
|
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 |
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:
-
Samma indata ger alltid samma utdata.
-
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 |
|
|
|
|
|
|
|
|
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. |
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 krypteringslogiken (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. 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.
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. |
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ärdiginterpolerade 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:
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. |
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:
-
Vilka är konsekvenserna om en angripare lyckas läsa hela tabellen?
-
Hur skulle tabellen sett ut om lösenorden vore hashade istället för i klartext?
-
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
|
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:
-
Laglighet, korrekthet och öppenhet: det måste finnas en laglig grund för behandlingen, och den registrerade ska informeras om den.
-
Ändamålsbegränsning: uppgifterna får bara användas för det ändamål de samlades in för.
-
Uppgiftsminimering: samla bara in de uppgifter som faktiskt behövs.
-
Korrekthet: uppgifterna ska vara korrekta och uppdaterade.
-
Lagringsminimering: uppgifterna ska sparas så kort tid som möjligt.
-
Integritet och konfidentialitet: uppgifterna ska skyddas mot obehörig åtkomst, förlust och ändring.
-
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:
|
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:
-
Är det en personuppgift? Om ja, är den känslig?
-
Behövs den för tjänsten, eller kan den uteslutas?
-
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