Databaser
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. |
Typer av databaser
Det finns i dag två huvudsakliga typer av databaser: Relationsdatabaser och dokument/nod-baserade databaser. Andra ord som används för att skilja på dessa två typer av databaser är SQL (relationella) och NoSQL (dokument/nod-baserade).
I den här kursen kommer vi enbart använda oss av SQL-baserade databaser, det vill säga Relationsdatabaser.
Grundläggande terminologi
Databashanterare
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, hämta och modifiera innehåll i en databas.
Man kan inte kommunicera direkt med en databas; all kommunikation sker via en databashanterare.
I den här kursen kommer vi använda oss av SQLite som DBMS.
Tabeller, rader och kolumner
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.
Entiteter och tabeller
En tabell i en databas kallas för en entitet
Tupler och rader
Varje rad i tabellen kallas för en tupel och beskriver en post i tabellen.
I books-tabellen innehåller varje tupel information om en bok.
Attribut och kolumner
Varje kolumn beskriver en egenskap, eller attribut för posten.
I books-entiteten är id
, title
och page_count
attribut.
Attribut måste vara atomära
Attribut måste vara atomära, det vill säga i varje "cell" eller "fält" får det maximalt finnas ett (1) värde - man får t.ex inte lagra flera titlar i en och samma cell.
|
Tabeller och relationer
En relationsdatabas består av tabeller och relationerna mellan tabellerna.
Primärnyckel och unika rader
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 löses enklast genom att varje tabell har ett attribut som kallas för primärnyckel eller primary key.
När man skapar tabellen talar man om vilket av attributen som kommer vara primärnyckeln.
I tabellen ovan har den som skapat tabellen valt attributet id
som primärnyckel. En primärnyckels värde måste vara unikt för tabellen och kolumnen.
Detta innebär att även om det skulle finnas två böcker som heter "The Stranger", och som båda råkar ha 123 sidor 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 |
Databashanterarens (SQLite) kontrollerar att varje primärnyckel/tupel är unik.
Namngivning av primärnyckeln
I den här kursen kommer vi:
|
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örfattare 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) |
ER-Diagram
För att på ett smidigt sätt kunna modellera, det vill säga designa relationsdatabaser finns Entity Relationship Diagrams (ER-diagram).
ER-diagram illustrerar en databas logiska uppbyggnad, det vill säga vilka entiteter, attribut och relationer som finns i databasen - det beskriver inte den faktiska datan som lagras i databasen.
Entitet
En entitet (engelska: entity) representerar en en typ av sak som lagras i databasen. Entiteter ritas som rektanglar, med namnet (i singular) i mitten.

Attribut
Attribut (engelska: attribute) representerar en egenskap på något som lagras i databasen. Attribut ritas som en ovaler, med namnet i mitten. Alla attribut måste tillhöra en tabell, och man drar ett streck mellan entiteten och attributet för att visa vilken entitet ett attribut tillhör.
Om ett attribut på en entitet är unikt för den entiteten, det vill säga, det får i systemet som databasen modellerar inte finnas två saker som har samma värde på det attributet stryker man under det attributet.

Primärnycklarna
är alltid understrukna.
Främmande nycklar Främmande nycklar ska aldrig ritas ut i ER-diagrammet, deras placering framgår av relationerna (se nästa rubrik) |
Sambandstyp/Relation
Sambandstyper (engelska: relation) visar på kopplingar mellan två entiteter. Sambandstyper ritas som romber. I mitten av romben står ett ett eller flera ord som beskriver entiteternas samband (oftast från ena entitetens perspektiv).
Varje sambandstyp är kopplad med streck till de ingående entiteterna.
Kardinalitetsförhållanden
I varje ände av en sambandstyp framgår dess kardinalitet (engelska: cardinality), som mer exakt beskriver vad entiteterna har för kardinalitetsförhållanden eller samband. Det finns tre typer av kardinalitetsförhållanden:
Ett-till-ett-samband (eller 1:1-samband)
Ett 1:1-samband innebär att de ingående entiteterna kan höra ihop med ett exempel av entiteten i andra änden av sambandet.
Ett-till-många-samband (eller 1:*-samband)
Ett 1:*-samband innebär att de ingående entiteterna kan höra ihop med ett exempel av entiteten i andra änden av sambandet.
En sambandstyp där entiteten i den ena änden av sambandet kan höra ihop med flera exempel av entiten 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.

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 en en-till-många-samband.
Sambandstypen är i exemplet ovan är 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").
SQL
För att prata med databashanteraren använder man frågespråket SQL, Structured Query Language. SQL kan (på svenska) uttalas "ess-ku-ell", (på engelska) "es-queue-ell", som Engelska ordet "sequel" eller som "seek well".
SQL skiljer sig från programmeringsspråk som t.ex Ruby eller Javascript genom att man definerar vilken information man vill ha, och lämnar upp till databashanteraren att avgöra hur den på bästa (snabbaste och säkraste) sätt ska hämta efterfrågad data.
Man brukar säga att Ruby (och de flesta andra programmeringsspråk) är imperativa, dvs man säger till datorn hur och i vilken ordning den ska göra något, medan SQL är deklarativt, det vill säga man deklarerar vilket resultat man vill ha och lämnar till "datorn" att avgöra hur den ska uppnå resultatet.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
titles = [ {name: 'BBQ Book', isbn: '123', page_count: 1337},
{name: 'Grillboken', isbn: '234', page_count: 42},
...
]
long_titles = []
i = 0
while i < titles.length
if titles[i][:page_count] > 500
long_titles << books[i]
end
i += 1
end
p long_titles
1
SELECT * FROM titles WHERE page_count > 500; (1)
1 | Förutsatt att det i databasen finns en tabell vid namn titles |
Select
SELECT * FROM fruits;
SELECT * FROM products WHERE price < 100;
SELECT * FROM songs WHERE play_count > 100 AND length < 120;
Select med join
För att hämta data från en tabell, samt tillhörande data från en tillhörande tabell behöver man använda sig av joins. En join är ett sätt att med en fråga få data från flera tabeller.
Kod-exemplen nedan utgår från fruits och ratings. Det finns en fruits
-tabell och en ratings
-tabell. I ratings-tabellen återfinns fruits
-tabellens primärnyckel som en främmande nyckel:

När man ska skriva en SQL-join är det enklast om man utvecklar SQL-frågan rad för rad:
1
2
SELECT *
FROM fruits
Ovanstående fråga returnerar en array av arrayer:
1
2
3
4
5
6
[
[1, "apple"],
[2, "banana"],
[3, "pear"],
[4, "orange"]
]
1
2
3
SELECT *
FROM fruits
INNER JOIN ratings
Ovanstående fråga returnerar också en array av arrayer.
De första två elementen i varje array är värdena från fruits
-tabellen. De sista tre elementen i varje array är värden från ratings
-tabellen (eftersom det i SQL-frågan först står fruits
och sen ratings
).
Man kan alltså visualisera att arrayen ser ut som följer:
[<fruits.id>,<fruits.name>,<ratings.id>,<ratings.score>,<ratings.fruit_id>]
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[
[1, "apple", 1, 3, 1],
[1, "apple", 2, 3, 2],
[1, "apple", 3, 3, 1],
[1, "apple", 4, 4, 2],
[1, "apple", 5, 5, 1],
[1, "apple", 6, 7, 1],
[1, "apple", 7, 8, 1],
[1, "apple", 8, 9, 3],
[2, "banana", 1, 3, 1],
[2, "banana", 2, 3, 2],
[2, "banana", 3, 3, 1],
[2, "banana", 4, 4, 2],
[2, "banana", 5, 5, 1],
[2, "banana", 6, 7, 1],
[2, "banana", 7, 8, 1],
[2, "banana", 8, 9, 3],
[3, "pear", 1, 3, 1],
[3, "pear", 2, 3, 2],
[3, "pear", 3, 3, 1],
[3, "pear", 4, 4, 2],
[3, "pear", 5, 5, 1],
[3, "pear", 6, 7, 1],
[3, "pear", 7, 8, 1],
[3, "pear", 8, 9, 3],
[4, "orange", 1, 3, 1],
[4, "orange", 2, 3, 2],
[4, "orange", 3, 3, 1],
[4, "orange", 4, 4, 2],
[4, "orange", 5, 5, 1],
[4, "orange", 6, 7, 1],
[4, "orange", 7, 8, 1],
[4, "orange", 8, 9, 3]
]
Dessvärre innehåller den returnerade datan en massa nonsens: databashanteraren har, för varje rad i ratings-tabellen, "hittat på" en rad för varje rad i frukt-tabellen (vi har 8 ratings, och 4 frukter, 8 * 4 ⇒ 32
).
Det vi behöver göra nu är att filtrera ut de värden som faktiskt finns i databasen, det vill säga de rader där den främmande nyckeln är samma som primärnyckeln i huvudtabellen.
I vårt fall är det alltså fruits_id
i ratings
-tabellen (det sista elementet i arrayen) som ska vara sammma som id
i fruits
-tabellen (det första elementet i arrayen).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[
[1, "apple", 1, 3, 1],
[1, "apple", 2, 3, 2],
[1, "apple", 3, 3, 1],
[1, "apple", 4, 4, 2],
[1, "apple", 5, 5, 1],
[1, "apple", 6, 7, 1],
[1, "apple", 7, 8, 1],
[1, "apple", 8, 9, 3],
[2, "banana", 1, 3, 1],
[2, "banana", 2, 3, 2],
[2, "banana", 3, 3, 1],
[2, "banana", 4, 4, 2],
[2, "banana", 5, 5, 1],
[2, "banana", 6, 7, 1],
[2, "banana", 7, 8, 1],
[2, "banana", 8, 9, 3],
[3, "pear", 1, 3, 1],
[3, "pear", 2, 3, 2],
[3, "pear", 3, 3, 1],
[3, "pear", 4, 4, 2],
[3, "pear", 5, 5, 1],
[3, "pear", 6, 7, 1],
[3, "pear", 7, 8, 1],
[3, "pear", 8, 9, 3],
[4, "orange", 1, 3, 1],
[4, "orange", 2, 3, 2],
[4, "orange", 3, 3, 1],
[4, "orange", 4, 4, 2],
[4, "orange", 5, 5, 1],
[4, "orange", 6, 7, 1],
[4, "orange", 7, 8, 1],
[4, "orange", 8, 9, 3]
]
Filtreringen gör vi genom att säga till vilka två kolumner som ska matcha med hjälp av ON
:
1
2
3
4
SELECT *
FROM fruits
INNER JOIN ratings
ON ratings.fruit_id = fruits.id
Vilket returnerar den filtrerade listan:
1
2
3
4
5
6
7
8
9
[
[1, "apple", 1, 3, 1],
[2, "banana", 2, 3, 2],
[1, "apple", 3, 3, 1],
[2, "banana", 4, 4, 2],
[1, "apple", 5, 5, 1],
[1, "apple", 6, 7, 1],
[1, "apple", 7, 8, 1],
[3, "pear", 8, 9, 3]]
Om vi slutligen vill begränsa frågan till en specik frukt lägger vi till en WHERE
sist i frågan:
1
2
3
4
5
db.execute('SELECT *
FROM fruits
INNER JOIN ratings
ON ratings.fruit_id = fruits.id
WHERE fruits.id = ?', 2)
Vilket returnerar enbart resultat för frukten med id
2:
1
2
3
4
[
[2, "banana", 2, 3, 2],
[2, "banana", 4, 4, 2]
]
Joins är jobbiga att komma underfund med, men om man gör dem steg för steg enligt ovan, och testkör frågan för varje steg är det enklare att förstå vad man ska skriva.
Insert
För att lagra data i tabeller används INSERT
INSERT INTO fruits (name, tastiness) VALUES ('pear', 8) (1)
1 | Observera att id inte skickas med. Det skapar databasen själv, baserat på vilka id:n som är upptagna. |
SQL-Injection
SQL är ett gammalt språk från 60-talet, långt innan internet slog igenom. Det är från en enklare tid, när man utgick från att de som kunde skicka data till databasen var välvilliga, och inte ville förstöra eller hämta ut data som de inte egentligen hade rättigheter att komma åt.
Därför innehåller SQL en stor säkerhetslucka som gör det möjligt för en illvillig person att köra godtycklig SQL-kod mot databasen (t.ex lista alla användare eller ta bort tabeller) genom en så kallad SQL-injection attack. Detta innebär att all data som kommer från en användare (dvs inte står skriven som statiska värden i källkoden) måste saneras innan de skickas till databashanteraren
SQLite3-biblioteket i Ruby löser saneringen genom att man skriver ?
i SQL-strängen där man vill lägga in värden från användaren, och sen skicka en array av värden som kommer, på ett säkert sätt, ersätta frågetecknen:
1
db.execute('SELECT * FROM fruits WHERE id = ?', id)
1
db.execute('INSERT INTO users (username, password) VALUES (?,?)', username, password)
Vad kan hända om man interpolerar värden direkt från användaren?
Anta att du tar emot data från användaren, och datan lagras i variabeln name
NEDANSTÅENDE ARBETSSÄTT MED INTERPOLERING ÄR ABSOLUT FÖRBJUDET ATT ANVÄNDA SIG AV. |
db.execute("SELECT * FROM users WHERE name = #{name}") (1)
1 | name kommer från användaren och interpoleras in i SQL-strängen. |
Frågan ser ofarlig ut, men om name
innehåller en speciellt formaterad sträng kan vad som helst ske med databasen.
name = "Haxx0r OR 1 = 1" (1)
db.execute("SELECT * FROM users WHERE name = #{id}") (2)
#=> [[1, "user1", "password1"], [2, "user2", "password2"], [3, "user3", "password3"]] (3)
1 | Strängen innehåller en or -statement där den högra jämförelsen alltid blir sann. |
2 | En SELECT med WHERE returnerar de rader där jämförelsen är true . Men eftersom strängen innehåller en or-statement, och i or-statements behöver bara ena sidan vara true , blir id = 3 OR 1 = 1 alltid true. Det databashanteraren ser är alltså SELECT * FROM USERS WHERE true |
3 | Eftersom true är true för alla rader returneras alla rader från tabellen. |
name = "Haxx0r'; DROP TABLE users; -- "
db.execute("SELECT * FROM users WHERE name = '#{name}';"
db.execute("SELECT * FROM users;" #=> eval error: no such table: users
SELECT *
FROM users
WHERE name = 'Haxxor'; (1)
DROP TABLE users; (2)
-- ';(3)
1 | Strängen innehåller Haxx0r';. ' gör att strängen avslutas, ; avslutar frågan (som kommer köras, men resultatet används inte) |
2 | Drop table statement som stod efter ; i strängen kommer köras |
3 | Strängen avslutas med en kommentar, '; är det som ursprungligen stod efter den interpolerade strängen. |
I SQLite3-biblioteket till Ruby vi använder körs som standard alltid enbart den första frågan i en query, dvs eventuella frågor som står efter ett Men långt ifrån alla bibliotek och eller språk ignorerar eventuella extra frågor. I PHP fanns länge både funktionen |
Requests & Routing
Webbapplikationer fungerar genom att en klient (webbläsare) kommunicerar med en server (i vårt fall Sinatra) med hjälp av protokollet HTTP.
I HTTP finns det två typer av meddelanden: HTTP Request och HTTP Response
Förenklat sker kommunikationen enligt följande:
-
En webbläsare skickar en HTTP request efter en viss resurs på servern.
-
Servern kollar sen om resursen finns.
-
Om resursen finns skickar servern en HTTP response med status 200 och vad nu resursen innerhåller för data (t.ex html).
-
Om resursen inte finns skickar servern en HTTP response med status 404 (och eventuellt html som visar ett fint felmeddelande).
-
-
Klienten tolkar sen responsen (t.ex ritar upp HTML på skärmen)
Resurs
En resurs kan vara t.ex en bild, en css- eller javascript-fil, men det kan också vara en "sida" på en site: t.ex. https://www.fruit-o-matic.com/fruits eller https://fruit-o-matic.com/fruits/apples/3/ där |
En webbapplikation skickar aldrig information utan att den blivit ombedd. För att skicka information till en webbläsare måste webbläsaren först fråga efter informationen. Detta innebär att all kommunikation mellan en webbserver och en webbläsare alltid börjar med en Request
från en klient (webbläsaren) och slutar med en Response
från servern.
Sekvensdiagram
För att beskriva hur en HTTP Request/Response går till brukar man använda sekvensdiagram
Sekvensdiagram beskriver hur olika komponenter (t.ex webbläsare och sinatra-server) skickar "meddelanden" till varandra. Meddelanden visas som text ovanför pilar som går från sändare till mottagare.
Sekvensdiagrammet ovan är onödigt detaljerat, i fortsättningen kommmer vi inte visa de komponenter vi ändå inte har kontroll över. Ett sekvensdiagram för motsvarande aktivitet ser då ut som nedan:
Det finns två typer av HTTP Requests
som en webbläsare kan skicka (det finns egentligen fler, men det är bara dessa två som är relevanta för kursen Webbserverprogrammering 1):
HTTP GET Request
En HTTP GET Request
ber om att få en specifik resurs
på servern, men kan inte skicka med någon data till servern.
HTTP POST Request
En HTTP POST Request
skickar data (från t.ex. ett HTML-formulär) till servern.
Routing
Eftersom en webbserver aldrig gör något om den inte fått en Request
är Requests centrala i en Sinatra-applikation.
I app.rb
skrivar man sina routes, dvs regler som bestämmar vilka resurser som finns på servern och vad den ska göra om den får en HTTP Request (GET eller POST) till en specifik resurs.
1
2
3
4
5
6
7
8
9
10
11
class App < Sinatra::Base (1)
get '/' do (2)
erb :index
end
get '/fruits' do (3)
erb :'fruits/index'
end
end
1 | Talar om att filen innehåller en Sinatra-applikation. |
2 | Konfigurerar en Route för resursen "/" |
3 | Konfigurerar en Route för resursen "/fruits" |
När en Sinatra-applikation tar emot en request från en klient söker den efter en route som matchar requestens verb (GET
eller POST
) och dess resurs (t.ex /fruits
). Koden i första routen som matchar kommer att köras, och resultatet läggs i ett HTTP Response
och skickas tillbaks till klienten som skickat Requesten.
Man kan låtsas att routes i en Sinatra-app fungerar som en lång if-elsif-else-sats, enligt nedan:
1
2
3
4
5
6
7
8
9
if verb == "GET" && resource == "/"
return erb(:index)
elsif verb == "GET" && resource == '/fruits'
return erb(:'fruits/index')
elsif File.exist?("./public#{resource}")
return File.read("./public#{resource}")
else
return (404, "Not Found")
end
Det är inte så det är implementerat egentligen, men tankemodellen fungerar när man ska bestämma vilka routes man ska ha och i vilken ordning i de behöver finnas i.
Det finns två typer av routes:
Statiska Routes
En statisk route innehåller inga dynamiska bitar, det vill säga, den request som kommer måste matcha exakt mot routen som den är skriven i Sinatra.
1
2
3
4
5
6
7
8
9
10
11
get '/' do (1)
... #Lämplig kod
end
get '/weather' do (2)
... #Lämplig kod
end
post '/weather' do (3)
... #Lämplig kod
end
1 | Matchar HTTP GET Requests till / |
2 | Matchar HTTP GET Requests till /weather |
3 | Matchar HTTP POST Requests till /weather |
Dynamiska Routes
En dynamisk route är en route där delar av routen kan skilja från den route som är skriven.
En dynamisk route skapas genom att man ersätter den bit av routen som ska vara dynamisk med ett kolon (:
) följt av ett beskrivande ord.
1
2
3
get '/weather/:city' do |city| (1)
... #Lämplig kod
end
1 | Matchar HTTP GET Requests till /weather/VADSOMHELST/` , t.ex /weather/goteborg eller /weather/oslo eller /weather/3 |
Eftersom man antagligen vill veta vad det var för värde som låg på den dynamiska biten i requesten behöver man fånga upp den.
1
2
3
get '/weather/:city' do |city| (1)
... #Lämplig kod
end
1 | Variabeln city innehåller nu vad som fanns på motsvarande ställe i requesten, text "goteborg", "oslo" eller "3". |
Siffror i dynamiska routes
Det som lagras i variabeln som fångar upp en parameter från en dynamisk route kommer alltid vara en sträng, även om det innehåller siffror.
Om applikationen förnväntar sig ett tal (integer) behöver man omvandla siffran till en integer med |
1
2
3
get '/weather/:city/temperature/:date' do |location, date| (1)
... #Lämplig kod
end
1 | När man har flera parametrar i en dynamisk route mappas parametrar till variabler i den ordning de kommer. Om requesten är GET /weather/goteborg/temperature/2023-09-23 kommer location innehålla 'goteborg' och date innehålla '2023-09-23' . |
Namngivning av parametrar och variabler i dynamiska routes
Parametrar ( |
Formulär och GET-POST-Redirect-Cykeln
Formulär skickar data via POST
-requests. Men en POST
-request ska inte användas för att visa data - det är vad GET
-requests är till för.
För att visa resursen som skapats när en användare skickat in data via ett formulär måste man därför säga till användarens webbläsare vart den ska ta vägen (dvs göra en GET
-request till) efter servern behandlat datan.
Detta görs genom att servern, efter den behandlat datan från formuläret, skickar tillbaks en Response
som talar om vart webbläsaren ska skicka en GET
-request. Webbläsaren skickar sen automagiskt en GET
-request dit servern specificerat.
Formulärbehandlande routes i Sinatra
Givet följande formulär som skickas in:
1
2
3
4
5
<form action='/fruits' method="POST">
<input type='text' name="fruit" placeholder="Fruit name"></input>
<input type='number' name="rating"></input>
<button type='submit'>Create new fruit rating</button>
</form>
För att ta emot datan från formuläret i Sinatra behöver vi skapa en route som matchar formulärets method
och action
- i det här fallet POST
och /fruits
.
1
2
3
4
5
6
7
post '/fruits' do (1)
name = params['fruit'] (2)
rating = params['rating'].to_i (3)
query = 'INSERT INTO fruits (name, rating) VALUES (?,?) RETURNING id'
result = db.execute(query, name, rating).first (4)
redirect "/fruits/#{result['id']}" (5)
end
1 | Den route som datan från formuläret skickas till. Observera post . |
2 | Params innehåller 'fruit' eftersom formuläret har ett input-element med name="fruit" |
3 | All data i params är alltid strängar, om du vill ha något annat behöver man göra en typomvandling (t.ex to_i ) |
4 | Datan från formuläret behandlas på lämpligt sätt (exemplet förutsätter t.ex. att det finns en fruits-tabell) |
5 | Servern skickar ett redirect-response. Klienten kommer automagiskt skicka en ny förfrågan efter resursen |
1
2
3
4
get '/fruits/:id' do |fruit_id| (1)
@fruit = db.execute('SELECT * FROM fruits WHERE id = ?', fruit_id) (2)
erb :'fruits/show' (3)
end
1 | En dynamisk route, som "råkar" matcha routen som redirecten ovan går till. |
2 | Gör lämplig behandling av parametrarna i routen |
3 | Rendera lämplig mall. |
Routes för CRUD
De allra, allra flesta webbapplikationer är så kallade CRUD-applikationer, det vill säga man kan skapa (Create), visa (Read), ändra (Update), och ta bort (Delete) olika resurser.
Eftersom dessa operationer (CRUD) är så vanliga finns det en standard för hur man ska jobba med dem. Det finns 7 olika actions och varje action och resurs motsvarar en route i app.rb
I exemplet nedan utgår vi från en app som är ett digitalt klotterplank, där resursen är message
.
Ett message består av ett id
och content
(textinnehållet i meddelandet).
I appen kan vem som helst kan skriva (Create) ett meddelande och meddelandena kan visas (Read), ändras (Update) och tas bort (Delete) av vem som helst. Följande actions och routes finns för message
, men de routes
och actions
som visas gäller för alla sorters resurser.
Action: index
route : /messages
method: get
index
visar en listning av alla förekomster av den aktuella resursen. Eventuellt kan listan vara filtrerad (t.ex skulle enbart meddelanden som tillhör den aktuella användaren kunna visas, om applikationen hade haft stöd för användare och inloggning).
1
2
3
4
5
6
7
...
# messages#index (1)
get '/messages' do (2)
@messages = db.execute('SELECT * FROM messages') (3)
erb :'messages/index' (4)
end
...
1 | En kommentar som berättar vilken resurs och action routen motsvarar |
2 | Routen för index är /<resursens namn i plural> |
3 | Eftersom index ska visa en listning, hämtar vi alla messages från databasen |
4 | För att strukturera upp våra views skapar vi en mapp per resurs, och döper vy-filen till <action>.erb och sparar i mappen för resursen. |
1
2
3
<% @messages.each do |message| %>
<p><%= message['content'] %></p>
<% end %>
Action: show
route: /messages/:id
method: get
show
visar en specifik instans av den aktuella resursen, identifierad av dess id
. Detta förutsätter att alla instanser av den aktuella resursen har ett unikt id.
1
2
3
4
5
6
7
...
# messages#show
get '/messages/:id' do |id| (1)
@message = db.execute('SELECT * FROM messages WHERE id = ?', id).first (2)
erb :'messages/show'
end
...
1 | Routen för show är /<resursens namn i plural>/<resursens id> . Plocka ut id -parametern med |id| |
2 | Eftersom vi alltid får en array från db.execute och vi bara vill ha en instans i värdet tar vi det första (och enda) elementet från arrayen. |
1
<p><%= @message['content'] %></p>
Action: new
route: /messages/new
method: get
new
visar ett formulär för att skapa en ny instans av den aktuella resursen. Eventuellt kan man istället/även visa formuläret på index
eller någon annanstans.
1
2
3
4
5
6
...
# messages#new
get '/messages/new' do (1)
erb :'messages/new'
end
...
1 | Routen för new är /<resursens namn i plural>/new . |
Varför då? |
1
2
3
4
<form action='/messages/' method='post'> (1)
<input type='text' name='content'></input> (2)
<button type='submit'>Send</button>
</form>
1 | action och method för new -formuläret motsvarar create -routen (se nedan) |
2 | name anger namnet på parametern som skickas till create -routen (se nedan) |
Action: create
route: /messages
method: post
create
är routen new-formuläret postar datan till. Behöver sedan redirecta användaren till lämplig adress.
1
2
3
4
5
6
7
8
9
...
# messages#create
post '/messages/' do (1)
message = params['content'] (2)
query = 'INSERT INTO messages (message) VALUES (?) RETURNING *'
result = db.execute(query, message).first (3)
redirect "/messages/#{result['id']}" (4)
end
...
1 | Routen för create är /<resursens namn i plural> med post som method. |
2 | Hämta ut datan som skickats från new -formuläret |
3 | Spara i databasen, och lagra resultatet |
4 | Gör en redirect till show för den nya instansen (eller gör en redirect nån annan route) |
Action: edit
route: /messages/:id/edit
method: get
edit
visar ett formulär för att uppdatera en befintlig instans av den aktuella resursen. Eventuellt kan man istället/även visa formuläret på show
eller någon annanstans.
1
2
3
4
5
6
7
...
# messages#new
get '/messages/:id/edit' do |id| (1)
@message = db.execute('SELECT * FROM messages WHERE id = ?', id.to_i).first
erb :'messages/edit'
end
...
1 | Routen för edit är /<resursens namn i plural>/<resursens id>/edit . |
1
2
3
4
<form action='/messages/<%=@message['id']%>/update' method='post'> (1)
<input type='text' name='content' value='<%= @message['content']%>'></input> (2)
<button type='submit'>Send</button>
</form>
1 | action och method för edit -formuläret motsvarar update -routen (se nedan) |
2 | value "förifyller" input-elementet med datan från backend). |
Action: update
route: /messages/:id/update
method: post
update
är routen edit-formuläret postar datan till. Behöver sedan redirecta användaren till lämplig adress.
1
2
3
4
5
6
7
8
...
# messages#update
post '/messages/:id/update' do |id| (1)
message = params['content']
db.execute('UPDATE messages SET content = ? WHERE id = ?', message, id)
redirect "/messages/#{id}" (2)
end
...
1 | Routen för update är /<resursens namn i plural>/<resursens id>/update med post som method. |
2 | Redirect till lämpligt ställe |
Action: delete
route: `/messages/:id/delete method: `post
delete
är routen delete-formuläret postar datan till. Delete-formuläret är antagligen bara ett minimalt formulär runt en knapp och ligger antagligen inte i en egen vy-fil (men kanske i index.erb
eller show.erb
):
1
2
3
<form action='/messages/<%= @message['id'] %>/delete' method='post'> (1)
<button type="submit">DELETE</button>
</form>
1 | action och method för delete -formuläret motsvarar delete -routen (se nedan) |
1
2
3
4
5
6
7
...
# messages#delete
post '/messages/:id/delete' do |id| (1)
db.execute('DELETE FROM messages WHERE id = ?', id)
redirect "/messages/"
end
...
1 | Routen för delete är /<resursens namn i plural>/<resursens id>/delete med post som method. |
Registrering & Inloggning
Läs först Kakor, kryptering och sessioner |
För att använda säker inloggning behöver man kunna hasha användares lösenord. Standardlösningen för Sinatra är att använda bcrypt
:
1
2
3
4
5
6
7
source 'https://rubygems.org'
gem 'thin'
gem 'sinatra'
gem 'rerun'
gem 'sqlite3'
gem 'bcrypt'
Lägg till bcrypt
i din Gemfile och kör bundle install
.
Skapa en tabell för användare med åtminstone användarnamn och lösenord.
Registrering
Skapa ett formulär för registrering av användare.
Skapa en route för registreringsformuläret
I routen, använd bcrypt
för att hasha lösenordet innan det lagras i databasen:
1
2
3
4
5
...
cleartext_password = params['password'] (1)
hashed_password = BCrypt::Password.create(cleartext_password) (2)
#spara användare och hashed_password till databasen
...
1 | Hämta det inskrivna lösenordet från params |
2 | Hasha det inskrivna lösenordet. |
Bcrypt kommer automagiskt salta lösenordet och lägga till salten i den hashade strängen.
Inloggning
Skapa ett formulär för inlogging
Skapa en route för inloggningsformuläret
I routen, använd bcrypt
för att jämföra det inmatade lösenordet med det sparade saltade och hashade lösenordet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
username = params['username']
cleartext_password = params['password'] (1)
#hämta användare och lösenord från databasen med hjälp av det inmatade användarnamnet.
user = db.execute('SELECT * FROM users WHERE username = ?', username).first
#omvandla den lagrade saltade hashade lösenordssträngen till en riktig bcrypt-hash
password_from_db = BCrypt::Password.new(user['password'])
#jämför lösenordet från databasen med det inmatade lösenordet
if password_from_db == clertext_password (1)
session[:user_id] = user['id'] (2)
... (3)
else
... (4)
end
...
1 | Bcrypt kommer automagiskt extrahera salten från det lagrade databaset, salta & hasha klartextlösenordet och jämföra resultatet |
2 | Lösenordet stämde, lagra i sessionskakan. |
3 | Övrig kod som behöver ske. |
4 | Lösenordet stämde inte, gör något annat. |
Autorisering (kontroll av användare och rättigheter)
1
2
3
4
...
user_id = session[:user_id] (1)
# Kod för att kontrollera om användaren finns, och/eller har rättighet att se det som ska visas
...
1 | Hämta användaren från sessionen |
Utloggning
-
Skapa ett formulär för utloggning (innehåller antagligen enbart en knapp).
-
Skapa en route för utloggningsformuläret
1
2
3
...
session.destroy
...
Kakor, kryptering och sessioner.
För att bygga säkra webbapplikationer behöver man hålla koll på 5 begrepp: kakor, sessioner, kryptering, hashning och saltning
HTTP är Stateless.
HTTP (och därmed webbservrar som Sinatra) är stateless, dvs de har inget "minne" av tidigare requests, dvs varje requests behandlas i separation från alla andra requests. Detta innebär att en webbläsare inte har något sätt att veta om requests kommer från samma webbläsare. Man har valt att göra HTTP stateless eftersom det skulle ta för mycket resurser (minne) på servern att "komma ihåg" alla klienter som är uppkopplade mot servern - en webbserver kan ju ha miljontals klienter som skickar requests till den. |
Kakor
För att kunna lagra state, trots att HTTP är stateless har man hittat på kakor ("cookies").
En kaka är data från servern som lagras i webbläsaren. Kakor skapas på servern och skickas till webbläsaren i ett HTTP-Response
.
Kakor tillhör alltid en domän, t.ex fruitomatic.com
, eller localhost:9292
. Om webbläsaren har en kaka som tillhör den domän som webbläsaren skickar en HTTP-Request
till kommer kakan alltid att skickas med, för varje request till den domänen.
På servern kan man sen kolla på kakan, och baserat på vad den innehåller, välja att hantera requesten olika.
Kakor tar plats.
Eftersom kakor skickas med varje request är det viktigt att inte lagra onödig data i kakan - dels blir det segare att skicka requesten, dels blir det mer data att tolka på servern. Oftast kommer man enbart lagra användarens id i kakan. |
Kakor är okrypterade.
Innehållet i en kaka är i klartext ("okrypterad"). Detta innebär att användaren kan läsa all information som står i kakan. ![]() |
Kakor kan ändras av användaren.
Användaren kan i webbläsaren ändra allt innehåll i en kaka. Om man t.ex. lagrar användarens id i kakan för att hålla koll på vilken användare som är inloggad kan användaren lätt ändra till en annan användares id och då kommer servern anta att kommande requests kommer från den användaren istället. ![]() |
Kryptering
För att kunna lagra information i kakor, och samtidigt förhindra att användaren kan ändra (eller ens läsa) innehållet i kakan behöver vi kryptera innehållet i kakan.
Förenklat kan man säga att vi tar det vi vill lagra i kakan, t.ex strängen "favourite_fruit ⇒ "apple" och krypterar den till en annan sträng, t.ex "fofavovouroritote_fofroruitot ⇒ apoppoplole".
Krypterade kakor är fortfarande läsbara.
Om vi krypterar innehållet i kakan, kommer användaren fortfarande att kunna läsa innehållet i kakan, men hen kommer inte förstå vad den innehåller. Användaren kan också fortfarande ändra kakans värde, men sannolikheten att hen skulle kunna lyckas ändra den krypterade strängen, till en annan korrekt krypterad sträng, är så liten att man helt kan bortse från den (förutsatt att vi valt en tillräckligt säker krypteringsalgoritm). |
Sessioner
För att hålla koll på state för en besökare på en webapplikation använder man begreppet session.
När en webbläsare första gången skickar en request till servern upprättar man en session för den webbläsaren (användaren). I sessionen kan man sen lagra information om användaren.
I Sinatra använder man så kallade sessionskakor för att hålla koll på sessioner. En sessionskaka är en krypterad kaka som ska användas för att lagra information om användaren.
1
2
3
4
5
6
7
class App < Sinatra::Base
enable :sessions
...
end
Om man använder enable :sessions
i Sinatra kommer varje besökare automagiskt få en krypterad kaka vid första besöket.

För att läsa av och modifiera innehållet i en sessionskaka använder man variabeln session
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class App < Sinatra::Base
enable :sessions
get '/' do
session[:user_id] = 1 (1)
end
get '/home' do
user_id = session[:user_id] (2)
... (3)
end
end
1 | Lagra data i sessionskakan. |
2 | Läs data från sessionskakan. |
3 | Använd datan från sessionskakan, t.ex för att hämta en användare från databasen. |
Sinatra avkrypterar och krypterar automagiskt sessionskakorna. Observera återigen att kakan kommer skickas med från webbläsaren för varje request, så det är viktigt att lagra så lite information som möjligt i kakan.
Hashning
Hashning är ett kryptografiskt begrepp som innebär att man kan kryptera något, som sen inte går att avkryptera. Man kan se det som en envägskryptering. Skillnaden mellan kryptering och hashning är alltså att kryptering går att avkryptera (om man har rätt nyckel).
Sessionskakan måste vara krypterad så vi kan läsa av den, men om vi ska lagra användares lösenord ska de hashas - så vi inte kan läsa dem.
Men, kanske någon tänker nu, om vi inte kan läsa av lösenorden, hur ska vi då kunna kolla om någon anger rätt lösenord för att logga in? Svaret är att vi provar att hasha det de skrev in, och kontrollerar om resultatet av det hashade inskrivna lösenordet matchar det hashade lösenordet vi lagrat i databasen.
Salting
Eftersom samma input till en hashningsalgoritm alltid ger samma input räcker inte hashning av lösenorden. (t.ex skulle input "hemligt" till en tänkt hashningsalgoritm alltid ge "1a2806367d23c7884dfde273882ad6a9" som output).
För att kunna knäcka lösenord skulle man lätt skriva ett program som hashar alla vanliga lösenord, och sen skulle man bakvägen kunna se vilka lösenord som lagrats.
För att öka säkerheten använder man en så kallad password salt. En salt är en slumpad sträng som man lägger till på klartextlösenordet innan det hashas.
Exempelvis skulle vi kunna ta "hemligt", lägga till den slumpade strängen "abc123", få "hemligtabc123" och sen hasha den sammanslagna strängen och få resultatet "ced772b8bee4a1b208efb5bed6f86651". Om vi alltid slumpar olika salt kommer lösenorden, även om de är likadana, få olika hashingsresultat.
Nu har vi ett nytt problem: för att kunna kolla om ett inmatat lösenord matchar en lagrad saltad hash måste vi ju veta vad salten var. Lösningen är att lägga till salten i i hashen, t.ex "ced772b8bee4a1b208efb5bed6f86651+abc123". Vi kan då plocka ut salten ur hashen, lägga till den till det inmatade lösenordet, och sen se om den genererade hashen matchar den lagrade hashen utan den pålagda saltningen.
Templates
En template (svenska: mall) låter oss blanda text (i vårt fall HTML) och Ruby-kod. Man kan skriva templates i template-språket ERB (Embedded Ruby).
Variabler från routes
För att variabler definerade i en route ska vara tillgängliga i en mall behöver variabeln vara en så-kallad instansvariabel. En instansvariabel skapas genom att man sätter ett En route med instansvariabel
|
I en ERB-fil skriver man HTML som vanligt, och där man vill använda Ruby-kod omger man den med <%= din_kod_här %>
eller <% din_kod_här %>
.
Skillnaden mellan kodblock som börjar med <%=
och <%
är att resultatet av block som börjar med <%=
kommer visas på sidan, medans resultatet av block som börjar med <%
inte kommer visas på sidan, men koden kommer att köras.
1
2
<h1>3 + 1 blir <%= 3 + 1 %></h1> (1)
<h1>3 + 1 blir <% 3 + 1 %></h1> (2)
1 | Resultatet kommer synas på sidan. |
2 | Resultatet kommer inte synas på sidan. |
1
2
<h1>3 + 1 blir 4</h1>
<h1>3 + 1 blir </h1>
<% %>
(utan =
) används framförallt för loopar och if-satser, där själva if-satsen eller loopen inte ska visas på sidan. Inuti loopen eller if-satsen använder man <%= %>
för att visa innehåll
1
2
3
4
<% if @weather == 'raining' %> (1)
<img src='/img/rain.png'> (2)
<span>Nederbörd: <%= @precipitation %> mm</span> (3)
<% end %> (4)
1 | Själva if-satsen ska inte synas på sidan, därför används <% … %> . |
2 | Allt mellan if -blocket och end-blocket kommer visas på sidan om if-satsen är true . Om den är false , kommer inget visas. |
3 | <%= … %> används eftersom innehållet i variabeln ska visas. |
4 | end ska inte visas på sidan, därför används <% … %> . |
@weather
är 'raining' och @precipitation
är 3)1
2
<img src='/img/rain.png'>
<span>Nederbörd: 3 mm</span>
1
2
3
4
5
6
7
8
<ol>
<% @cities.each do |city| %> (1)
<li> (2)
<h3><%= city.capitalize %></h3> (3)
<img src='/img/<%= city.downcase %>.png'> (3)
</li>
<% end %> (4)
</ol>
1 | Själva each-loopen ska inte synas på sidan, därför används <% … %> . |
2 | Alla element mellan each -blocket och end-blocket kommer visas på sidan för varje grej i @cities |
3 | <%= … %> används eftersom resultatet av koden ska visas. |
4 | end ska inte visas på sidan, därför används <% … %> . |
@cities
är ['copenhagen', 'gothenburg', 'OSLO']
) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ol>
<li>
<h3>Copenhagen</h3>
<img src='/img/copenhagen.png' />
</li>
<li>
<h3>Gothenburg</h3>
<img src='/img/gothenburg.png' />
</li>
<li>
<h3>Oslo</h3>
<img src='/img/oslo.png' />
</li>
</ol>
Formulär
För att skicka data från användaren till Servern används formulär.
1
2
3
4
5
6
7
<!-- Övrig HTML -->
<form action='/fruits' method="POST">(1)
<input type='text' name="fruit" placeholder="Fruit name"></input>(2)
<input type='number' name="rating"></input>(2)
<button type='submit'>Create new fruit rating</button>(3)
</form>
<!-- Övrig HTML -->
1 | action är vilken route datan ska skickas till på servern, method är vilket HTTP-verb som ska användas (nästan alltid POST ) |
2 | Ett text-input-element. name -attributet anger vilken parameter som kommer innehålla input-elementets värde |
3 | Ett submit-knapp-element. När knappen klickas kommer webbläsaren göra en POST -request till /fruits med datan från formuläret. |
Formulär-element
För att skicka data med ett formulär behöver man olika element att "mata in" datan i.
Följande element är vanliga i formulär:
input
Ofta en ruta man kan mata in värden i. Det finns flera olika typer av input-element: text
, number
, password
, email
, search
, etc
1
2
3
4
5
<!-- i ett form-element -->
<input type='text' name="fruit" placeholder="Fruit name"></input>
<input type='number' name="rating"></input>
<input type='email' name="email"></input>
<input type='search' name="query"></input>
Läs mer om input-elementen på MDN
checkbox
En ruta som kan vara eller inte vara ikryssad. Egentligen en typ av input-element.
1
2
3
<!-- i ett form-element -->
<input type="checkbox" name="hungry" />
<input type="checkbox" name="thirsty" checked />
Läs mer om checkbox på MDN
textarea
Ett text-input-element med flera rader
1
2
<!-- i ett form-element -->
<textarea name="message" />
Läs mer om textarea på MDN
select och option
En select är en slags dropdown-lista, där alternativen ligger i option-element.
1
2
3
4
5
6
7
8
<!-- i ett form-element -->
<select name="fruits">
<option value="">--Välj en frukt--</option>
<option value="banana">Banan</option>
<option value="pear">Päron</option>
<option value="apple">Äpple</option>
<option value="orange">Apelsin</option>
</select>
Läs mer om select på MDN
radio button
Som en select, fast med knappar istället. Enbart en kan vara aktiv åt gången. Egentligen en variant av input.
1
2
3
4
5
<!-- i ett form-element -->
<input type="radio" name="fruit" value="pear" checked />
<input type="radio" name="fruit" value="banana" />
<input type="radio" name="fruit" value="apple" />
<input type="radio" name="fruit" value="orange" />
Läs mer om radio button på MDN
datalist
En slags autocomplete-sök-lista (liknande en select, men man får skriva istället). Egentligen ett input-element som hämtar alternativen från ett datalist-element med options
1
2
3
4
5
6
7
8
9
<!-- i ett form-element -->
<input list="fruits" name="fruit" />
<datalist id="fruits">
<option value="Pear"></option>
<option value="Banana"></option>
<option value="Apple"></option>
<option value="Orange"></option>
</datalist>
Läs mer om datalist på MDN
button
En knapp (för att skicka in formulär t.ex)
1
2
<!-- i ett form-element -->
<button type="submit"/>Skicka</button>
Läs mer om button på MDN
label
Labels ger etiketter till andra form-element. Genom att sätt for
-attributet på en label till ett annat elements id
-attribut kopplas label till element
1
2
3
4
5
6
<!-- i ett form-element -->
<label f [or="fruit">Frukt</label>
<input type="text" id="fruit" name="fruit">
<input type="checkbox" id="favourite" name="favourite">
<label for="favourite">Favorit?</label>
Läs mer om labels på MDN
Arbetsflöde
0. Skriv ner vilka resurser applikationen hanterar och vad de har för attribut.
Exemplet nedan utgår för en applikation där användare kan lägga upp och visa kod-snippets.
Följ sen för varje resurs följande steg:
1. Vilken resurs är det du ska jobba med?
snippets
2. Vad är basrouten för resursen?
/snippets
3. Vilken action är det du ska skapa?
show
4. Surfa till den adress som är aktuell för den resurs och action som ska ske.
Eftersom det är show
som är action, behöver vi id
för resursen. Vi testar med id 1 (detta förutsätter att resursen med id 1 redan finns i databasen, om inte, lägg till i seed.rb
och seeda om.)
Basrouten är /snippets
alltså blir adressen vi ska surfa till /snippets/1
När du surfar till adressen ger Sinatra dig en kod du kan bygga vidare på.
5. Lägg till sinatras exempelkod i app.rb
.
1
2
3
4
5
...
get '/snippets/1' do
'Hello, World!'
end
...
6. Testa om routen fungerar med exempelkoden genom att ladda om sidan i webbläsaren.
Ser du "Hello, World!" i webbläsaren?
Om inte: Är app.rb
sparad? Finns det någon annan route tidigare som "krockar"?
7. Skapa en view
för routen och uppdatera route-koden att använda view
-en och visa exempeldatan där istället.
i views/snippets/show.erb
1
<%= @test %>
i app.rb
1
2
3
4
5
6
...
get '/snippets/1' do
@test = "Working?"
erb :'snippets/show'
end
...
Ladda om webbsidan. Ser du "Working?"
8. I routen, lägg till den kod du tror du egentligen behöver.
i app.rb
1
2
3
4
5
6
7
...
# snippets#show
get '/snippets/:id' do |id|
@snippet = db.execute('SELECT * FROM snippets WHERE id = ?', id.to_i).first
erb :'snippets/show'
end
...
9. Om du pratar med databasen, skriv ut rå-datan i view-en innan du försöker jobba med den strukturerade datan.
i views/snippets/show.erb
1
<%= @snippet.inspect %>
Ser datan ut som du förväntar dig? Om du förväntar dig ett element, är det då ett element, eller är det en array?
Om datan ser fel ut, gå tillbaks till routen och fundera på hur du behöver förändra hur du hämtar/manipulerar datan innan du visar den.
10. Iterera över/visa den strukturerade datan i HTML på rätt sätt.
i views/snippets/show.erb
1
2
3
4
<h1><%= @snippet['title'] %></h1>
<pre> (1)
<code><%= @snippet['code'] %></code> (2)
</pre>
1 | pre -element bevarar radbrytningar etc |
2 | code -element talar om för skärmläsare etc att innehållet är kod |
Ser sidan ut som du förväntar dig? Gå vidare till nästa resurs/action.
Automatisk testning med Cypress
När man utvecklar en applikation testar man kontinuerligt att den fungerar som den ska, genom att manuellt ladda om sidan och se att det man just skapat fungerar som det ska. Detta är dock inte en hållbar lösning i längden, då det tar mycket tid och är lätt att missa något.
Det är också vanligt att man, när man gör en ändring som man tror inte påverkar något annat, råkar förstöra något annat i applikationen, utan att man märker det.
Därför är det viktigt att automatisera testningen av applikationen. Det finns många olika verktyg för att göra detta. I den här kursen kommer vi använda Cypress, som är ett verktyg för att skriva och köra tester på webbapplikationer.
Installation
Cypress ska installeras i Windows, inte i WSL, eftersom det inte fungerar att köra grafiska program i WSL. Alla instruktioner nedan ska alltså köras i Windows.
Installera Node.js
För att köra Cypress behöver du först installera node.js. Gå till nodejs.org/en/download och ladda ner och installera den senaste versionen av Node.js.
Om du är osäker på hur du ska installera Node.js kan du följa instruktionerna på www.geeksforgeeks.org/installation-of-node-js-on-windows/
Installera Cypress
När du har installerat Node.js kan du installera Cypress genom att köra följande kommando i terminalen:
npm install -g cypress
Starta Cypress
När du har installerat Cypress kan du starta det genom att köra följande kommando i terminalen (i src-mappen i ditt projekt):
cypress open
Cypress öppnar nu följande fönster:

Klicka på "browse manually" och leta reda på src-mappen för ditt projekt.
Välj E2E-testing (E2E betyger End-to-End, dvs att allt testas, från databas till användargränssnitt)

Klicka på "Continue"

Välj "Start E2E testing in Chrome"

Välj "Create new spec"

Klicka på "Run the spec"

Testet körs nu i Cypress-fönstret.

I din mappstruktur kommer det nu att skapas en mapp som heter cypress
, testet som skapades i cypress-fönstret ligger i cypress/e2e
. Om du öppnar filen i VS Code ser du testet som kördes:
describe('template spec', () => {
it('passes', () => {
cy.visit('https://example.cypress.io')
})
})
Skriva egna tester
Ett test består av två delar: en describe
-funktion och en it
-funktion. describe
-funktionen beskriver vad testet handlar om, och it
-funktionen beskriver vad som ska testas.
I exemplet ovan beskriver describe
-funktionen att det är ett test för en template, och it
-funktionen beskriver att testet ska passera om sidan example.cypress.io
laddas.
describe`
gör inget mer än att det grupperar massa texter. Argumentet som skickas till describe används bara för att visa vilka test som hör ihop.
it
är det som faktiskt testar något. Argumentet som skickas till it
används för att visa vad som testas i cypress-fönstret.

Ett första test
beforeEach(() => { (1)
cy.visit('http://localhost:9292/thesaurus')
})
describe('Thesaurus', () => { (2)
it('shows a list of words', () => { (3)
cy.get('li').should('exist')
})
it('has links to words', () => { (4)
cy.get('li a').then(words => {
const word = words[0]
cy.visit(word.href)
})
})
it('clicking a word shows a list of related words', () => { (5)
cy.get('a').contains('abbey').click();
cy.contains('church')
cy.contains('monastery')
})
})
1 | beforeEach körs innan varje it . Här besöker vi sidan som vi vill testa. |
2 | describe -funktionen beskriver att det är ett test för thesaurus. |
3 | it -funktionen beskriver att testet ska passera om det finns en lista (förhoppningsvis med ord). |
4 | it -funktionen beskriver att testet ska passera om det finns en länk till ett ord |
5 | it -funktionen beskriver att testet ska passera om det går att klicka på ett ord och få upp en lista med relaterade ord |
Vanliga saker att testa
Visas rätt sak på skärmen?
Det finns två sätt att testa om rätt sak visas på skärmen: cy.contains
och cy.get
.
cy.get
tar en css-selektor som argument. Cypress kommer att leta efter ett element som matchar css-selektorn i DOM:en. Om det finns en matchning kommer testet att passera.
cy.contains
tar en sträng som argument. Cypress kommer att leta efter en matchning för strängen i DOM:en. Om det finns en matchning kommer testet att passera.
cy.contains('Login') (1)
cy.get('button').contains('Login') (2)
cy.get('nav form#logout button').should('have.text', 'Logout') (3)
cy.get('.item').should('have.length', 3) (4)
1 | Testar om texten 'Login' finns någonstans på sidan |
2 | Testar om det finns en knapp med texten 'Login' någonstans på sidan |
3 | Testar om det finns en knapp med texten 'Logout' i en form med id logout i en nav |
4 | Testar om det finns tre element med klassen 'item' på sidan |
Skapas rätt saker när man fyller i formulär?
För att fylla i formulär används cy.get
och .type
tillsammans. .type
tar en sträng som argument och skriver in den i det element som matchar css-selektorn.
cy.get('input[name="comment"]').type('This is my comment') (1)
cy.get('form').submit() (2)
cy.contains('This is my comment') (3)
1 | Skriver in texten 'This is my comment' i ett input-element med namnet 'comment' |
2 | Skickar formuläret |
3 | Testar om texten 'This is my comment' finns någonstans på sidan (som man skickats till efter formuläret skickats) |
Fungerar inloggnignsfunktionen?
cy.get('input[name="username"]').type('admin')
cy.get('input[name="password"]').type('password')
cy.get('form').submit()
cy.contains('Welcome admin')
cy.get('button.logout'].click()
cy.contains('Logga in')
Fungerar länkar?
cy.get('a').contains('Home').click()
cy.contains('Welcome to our website')
cy.href().should('eq', 'http://localhost:9292/')
Fungerar rättighetskontroller?
cy.get('input[name="username"]').type('notadmin')
cy.get('input[name="password"]').type('password')
cy.get('form').submit()
cy.visit('http://localhost:9292/admin')
cy.contains('You do not have permission to access this page')
Fil- och mappstruktur
Ett Sinatraprojekt innehåller följande filer och mappar:
fruit-o-Matic (1)
|
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
├── public
│ ├── css
│ │ └── style.css
│ ├── img
│ │ └── fruit.png
│ └── js
│ └── script.js
└── views
├── fruits
│ ├── index.erb
│ └── show.erb
├── index.erb
└── layout.erb
1 | Varje Sinatraprojekt måste ligga i en egen mapp, i det här fallet fruit-o-matic . |
Gemfile & Gemfile.lock
Gemfile
berättar vilka bibliotek (i Ruby kallas biblioteken för "gems") vår applikation behöver.
För att installera biblioteken som finns i Gemfile
kör man kommandot bundle install
i terminalen i samma mapp som Gemfile
ligger.
När man kör bundle install
kommer filen Gemfile.lock
skapas. Den innehåller vilken version av biblioteken som installerades.
source 'https://rubygems.org' (1)
gem 'thin' (2)
gem 'sinatra' (2)
gem 'rerun' (2)
1 | Talar om för bundler var biblioteken vi ska använda finns. |
2 | Vilka gems (bibliotek) som ska installeras |
app.rb
I app.rb
skapar vi vår applikation och definerar våra routes.
1
2
3
4
5
6
7
8
9
10
11
class App < Sinatra::Base (1)
get '/' do (2)
erb :index
end
get '/fruits' do (3)
erb :'fruits/index'
end
end
1 | Talar om att filen innehåller en Sinatra-applikation. |
2 | Gör så att innehållet i views/index.erb renderas när man surfar till root-url:en ('/'). |
3 | Gör så att innehållet i views/fruit/index.erb renderas när man surfar till '/fruits'. |
config.ru
config.ru
nvänds av rackup
för att starta applikationen (ru
i config.ru
står för rackup
).
require 'bundler' (1)
Bundler.require (2)
require_relative 'app' (3)
run App (4)
1 | Gör så vi kan använda bundler för att läsa in biblioteken i Gemfile . |
2 | Läser in biblioteken i Gemfile . |
3 | Läser in app.rb , |
4 | Säger till rackup att starta applikationen App från app.rb (se rad 1 i app.rb ). |
public-mappen
I public
-mappen lägger man statiska resurser som bilder, css och js - oftast i separata undermappar så det inte blir för rörigt.
views-mappen
I views
-mappen lägger man sina templates/mallar - ofta i separata undermappar så det inte blir för rörigt när man har flera resurser i sin app.
Moduler
Efter ett tag, när applikationen blir större, blir det rörigt att ha all kod i app.rb
. Då behöver man bryta ut koden i moduler.
En modul är en fil som innehåller kod (funktioner) som hör ihop.
I Webbserverprogrammering 1 kommer vi att använda moduler för att bryta ut kod som pratar med databasen. Vi kommer använda en modul för varje tabell i databasen.
Genom att använda moduler kan vi gå från kod i app.rb
som ser ut så här:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class App < Sinatra::Base
def db
if @db == nil
@db = SQLite3::Database.new('./db/db.sqlite')
@db.results_as_hash = true
end
return @db
end
...
get '/fruits' do
@user = db.execute('SELECT * FROM users WHERE id = ?', session[:user_id]).first
@fruits = db.execute('SELECT * FROM fruits')
erb :'fruits/index'
end
end
Till kod som ser ut såhär:
1
2
3
4
5
6
7
8
9
10
11
require_relative 'models/user'
require_relative 'models/fruit'
class App < Sinatra::Base
get '/fruits' do
@user = User.find(session[:user_id])
@fruits = Fruit.all
erb :'fruits/index'
end
end
Detta gör att koden blir mer lättläst och underhållbar, man tappar inte bort sig bland alla SQL-frågor och databasoperationer i app.rb
.
För att åstadkomma detta skapar vi en mapp som heter models
och lägger våra moduler där (moduler som pratar med databasen kallas traditionellt för "models").
En modul är en fil som innehåller en module
-deklaration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module Fruit (1)
def self.all (2)
db.execute('SELECT * FROM fruits')
end
def self.find(id) (2)
db.execute('SELECT * FROM fruits WHERE id = ?', id).first
end
def self.most_popular(amount) (2)
db.execute('SELECT * FROM fruits ORDER BY popularity DESC LIMIT ?', amount)
end
def self.db (3)
if @db == nil
@db = SQLite3::Database.new('./db/db.sqlite')
@db.results_as_hash = true
end
return @db
end
end
1 | Skapar en modellen Fruit i modulen Fruit . |
2 | Skapar metoderna Fruit.all , Fruit.find(id) och Fruit.most_popular(amount) , som vi kan anropa från app.rb (en metod är en funktion som är definerad i en modul). |
3 | Koden för att skapa databasanslutningen flyttas från app.rb . |
Namnet på metoderna (all
, find
, most_popular
) är godtyckliga, de kan heta vad som helst, men det är en bra idé att använda namn som beskriver vad metoden gör.
Linux
Kommandon
- cat
-
Används för att visa innehållet i en fil
cat app.rb
- cd
-
Navigera mellan mappar. Fungerar som i Windows, men blanksteg krävs vid
cd ..
- ctrl+c
-
Avbryter den process som körs för stunden.
- killall
-
Används för att avbryta en process som körs i bakgrunden:
killall ruby
. - ls
-
Lista filer i nuvarande mapp (som
dir
i Windows). - mkdir
-
Skapar en mapp i den aktuella sökvägen:
mkdir grillkorv
- pwd
-
Skriv ut sökvägen till aktuell mapp.
- rm
-
Tar bort en fil (obs den hamnar EJ i papperskorgen):
rm grillkorv.rb
- su
-
Används för att byta användare:
su wsl
byter till användaren "wsl". Gör detta om du ser typroot@**:/mnt….
i terminalen (då bör du se→ wsp
istället) - sudo
-
Används för att köra kommandom med root- (administratörs-)behörighet ex
sudo killall ruby
. - touch
-
Skapar en ny tom fil
touch grillkorv.rb
Installation och konfigurering
Börja med att göra alla uppdateringar i Windows update.
Windows Terminal
Windows Terminal är en bättre terminal än Windows Command Prompt (cmd). Installera från Microsoft Store
WSL
I stort sett all webbserverprogrammering sker nuförtiden på Linux- eller linux-kompatibla operatisystem. Därför är de flesta guider och bibliotek (samlingar med kod som utvecklare kan använda) skrivna för Linux.
Eftersom Windows inte är ett linuxkompatibelt operativsystem kommer vi installera WSL - Windows Subsystem for Linux. WSL är ett sätt att köra virtuella Linuxmaskiner parallelt med Windows.
Ladda ner kursens Linux-image (finns även i Classroom). Spara filen som wsp.tar i mappen du skapat för kursen. Packa upp ZIP-filen till en TAR-fil.
Starta Windows Terminal som administratör (högerklicka på ikonen och väl "Starta som Administratör") Navigera till mappen i Windows Terminal och kör nedanstående kommandon, ett i taget.
Kan du inte starta som Administratör måste du lägga till ditt konto i Windows så du har rättigheter att köra som admin.
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart (1)
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart (2)
wsl --update (3)
wsl --set-default-version 2
wsl --unregister Ubuntu
wsl --import WSP . .\wsp.tar (4)
netsh interface portproxy add v4tov4 listenport=9292 listenaddress=0.0.0.0 connectport=9292 connectaddress=(wsl hostname -I) (5)
1 | Aktivera WSL |
2 | Aktivera stöd för Virtuella Maskiner |
3 | Installera senaste versionen av WSL, ställ in rätt version + avinstallera ev. tidigare versioner av Ubuntu |
4 | Sökvägen till WSP-imagen |
5 | Gör så att datorn forwardar porten till WSP |
Om wsl --update
inte fungerar (märks genom att bara instruktionerna för hur wsl används skrivs ut) har du en för gammal version av Windows. Hämta uppdateringar genom att
-
Se till att Hyper-V och WSL är igång: Kontrollpanelen > Program > Aktivera eller inaktivera Windowsfunktioner
-
Öppna
Inställningar
>Updateringar & Säkerhet
>Windows Update
-
Klicka på "Avancerade Alternativ" (längst ner)
-
Sätt på "Hämta uppdateringar för andra Microsoft-produkter när jag uppdaterar Windows"
-
Gå tillbaks till föregående skärm i inställningar och Klicka på "Check for updates", "Ladda ner" och vänta på att uppdateringarna laddas ner och installeras.
-
Starta om Windows en eller flera gånger allteftersom du blir ombedd. Detta kan ta låång tid och även misslyckas. Prata med mig i så fall.
Slutligen:
Starta den virtuella maskinen: wsl
.
Användarnamn och lösenord för WSL-imagen: wsl
Visual Studio Code
Installera följande extensions:
-
Ruby LSP (av Shopify)
-
erb (av Craig Maslowski)