Fra relationel database til dokument database

Fra relationel database
til dokument database
Kim Jensen (17.07.83) ([email protected])
Vejleder: Jesper Larsson ([email protected])
IT-Universitetet i København
Afsluttende diplomprojekt
Afleveringsdato: 3. sep 2012
Resumé
I dette projekt undersøger jeg migrationen af en eksisterende webapplikation fra at bruge en relationel
database til i stedet at bruge en dokumentdatabase. Dette gøres ved at beskrive væsentlige forskelle
mellem den relationelle database og dokumentdatabasen, og undersøge problemstillinger opstået under
migrationen af en specifik case. Casen der tages udgangspunkt i er en webapplikation baseret på MySQL og
Ruby on Rails som gennem udførslen af casen, vil blive konverteret til at benytte MongoDB i stedet for
MySQL.
Abstract
In this project I study the migration of an existing web application from using a relational database to use a
document data store. This is done by describing significant differences between the relational database and
the document data store, and examining issues occurred during the migration of a specific case. The case is
based on a web application using MySQL and Ruby on Rails which during the implementation of the case,
will be converted to use MongoDB instead of MySQL.
2
Indhold
1
Indledning .................................................................................................................................................. 6
1.1
Motivation for at bruge en NoSQL-database .................................................................................... 6
1.2
Min motivation .................................................................................................................................. 6
2
Problemformulering .................................................................................................................................. 7
3
Metode ...................................................................................................................................................... 7
3.1
Valg af case ........................................................................................................................................ 7
4
Afgrænsning............................................................................................................................................... 8
5
Opgavens opbygning ................................................................................................................................. 8
5.1
Terminologi........................................................................................................................................ 8
5.2
Forudsætninger ................................................................................................................................. 9
6
Forforståelse .............................................................................................................................................. 9
7
Teori ......................................................................................................................................................... 10
7.1
NoSQL .............................................................................................................................................. 10
7.1.1
7.2
Dokumentdatabase ......................................................................................................................... 11
7.2.1
Datastruktur ............................................................................................................................ 12
7.2.2
Query language........................................................................................................................ 13
7.2.3
Integritet .................................................................................................................................. 14
7.2.4
Transaktioner........................................................................................................................... 14
7.3
MongoDB ......................................................................................................................................... 15
7.3.1
Datastruktur ............................................................................................................................ 15
7.3.2
Dataoperationer ...................................................................................................................... 16
7.3.3
Atomiske operationer .............................................................................................................. 17
7.3.4
Aggregeringer og MapReduce ................................................................................................. 18
7.3.5
Replikering ............................................................................................................................... 18
7.3.6
Sharding ................................................................................................................................... 19
7.4
Applikationens tilgang til databasen ............................................................................................... 20
7.4.1
7.5
Active Record ........................................................................................................................... 20
Ruby on Rails ................................................................................................................................... 21
7.5.1
8
Typer af NoSQL-databaser ....................................................................................................... 10
Active Record i Ruby on Rails .................................................................................................. 22
Casen ....................................................................................................................................................... 24
8.1
Begrundelse for valg af case ............................................................................................................ 24
3
9
8.2
Casens tekniske opbygning.............................................................................................................. 24
8.3
Databasens oprindelige opbygning ................................................................................................. 24
8.4
Fremgangsmåde .............................................................................................................................. 25
8.4.1
Fremgangsmåde – database layout......................................................................................... 25
8.4.2
Fremgangsmåde – applikation ................................................................................................ 26
8.4.3
Migrationens faser................................................................................................................... 27
Analyse .................................................................................................................................................... 28
9.1
Gemme data atomisk ...................................................................................................................... 28
9.1.1
Definition af problemet ........................................................................................................... 28
9.1.2
Løsning: Indlejring ................................................................................................................... 28
9.1.3
Evaluering ................................................................................................................................ 30
9.2
Gemme flere dokumenter i isolation .............................................................................................. 31
9.2.1
Definition af problemet ........................................................................................................... 31
9.2.2
Løsning ..................................................................................................................................... 31
9.2.3
Evaluering ................................................................................................................................ 31
9.3
Modificerende dataoperationer ...................................................................................................... 32
9.3.1
Definition af problemet ........................................................................................................... 32
9.3.2
Løsning ..................................................................................................................................... 32
9.3.3
Evaluering ................................................................................................................................ 34
9.4
Cursor read consistency .................................................................................................................. 35
9.4.1
Definition af problemet ........................................................................................................... 35
9.4.2
Løsning ..................................................................................................................................... 35
9.4.3
Evaluering ................................................................................................................................ 35
9.5
Database constraints ....................................................................................................................... 35
9.5.1
Definition af problemet ........................................................................................................... 35
9.5.2
Løsning med unikke nøgler ...................................................................................................... 36
9.5.3
Løsning med indlejrede dokumenter ...................................................................................... 37
9.5.4
Evaluering ................................................................................................................................ 38
9.6
Databasejoins og MongoDB ............................................................................................................ 38
9.6.1
Definition af problemet ........................................................................................................... 38
9.6.2
Oprindelig løsning med ActiveRecord ..................................................................................... 38
9.6.3
Løsning med MongoMapper ................................................................................................... 39
9.6.4
Evaluering ................................................................................................................................ 40
4
9.7
Primærnøgler ændres...................................................................................................................... 41
9.7.1
Definition af problemet ........................................................................................................... 41
9.7.2
Løsning ..................................................................................................................................... 41
9.7.3
Evaluering ................................................................................................................................ 41
9.8
Sikkerhed: Mass assignment ........................................................................................................... 42
9.8.1
Definition af problemet ........................................................................................................... 42
9.8.2
Løsning ..................................................................................................................................... 42
9.8.3
Evaluering ................................................................................................................................ 43
9.9
Databasetilgang via automatiserede test........................................................................................ 43
9.9.1
Definition af problemet ........................................................................................................... 43
9.9.2
Løsning ..................................................................................................................................... 43
9.9.3
Evaluering ................................................................................................................................ 44
9.10
Migration af databaseskema ........................................................................................................... 44
9.10.1
Definition af problemet ........................................................................................................... 44
9.10.2
Løsning ..................................................................................................................................... 44
9.10.3
Evaluering ................................................................................................................................ 45
9.11
Yderligere bemærkninger til analysen............................................................................................. 45
10
Diskussion ............................................................................................................................................ 46
11
Konklusion ........................................................................................................................................... 46
12
Perspektivering .................................................................................................................................... 47
13
Litteraturliste ....................................................................................................................................... 48
5
1 Indledning
1.1 Motivation for at bruge en NoSQL-database
Der har eksisteret mange forskellige databaseteknologier gennem tiden. Blandt de meget populære i dag,
finder man de relationelle databaser, som første gang blev beskrevet af Edgar Codd i hans artikel fra 1970
[1]. Da databasekapløbet skulle afgøres, spillede de kommercielle organisationer en vigtig rolle, og var i høj
grad dem der beviste at den relationelle model var værd at satse på [2].
I dag ser vi store aktører såsom SAP, Google, Disney, Amazon og Facebook [3], [4] interessere sig for nye
databaseteknologier under betegnelsen NoSQL. Alligevel oplever jeg IT-professionelle der argumenterer for
brugen af relationelle databaser, med argumentet om at det man kan i en NoSQL-database, kan man også i
en relationel database. Med sådan et argument er det interessant at kigge på Charlie Bachmans argument,
fra midt 70erne, for at bruge forgængerne, IMS og CODASYL, frem for en relationel database [2]:
a) COBOL-programmører kan ikke forstå nymodens relationelle sprog.
b) Det er umuligt at lave en effektiv implementering af en relationel database.
c) CODASYL kan bruges til at repræsentere tabeller.
Når man kigger på disse tre argumenter for at bruge IMS og CODASYL, ser de ikke særlig relevante ud i
forhold til dagens standard og teknologiske udvikling. Måske vil argumenterne for at bruge en relationel
database føles lige så oldnordiske om nogle få år. Måske vil historien gentage sig selv, blot med nye
teknologier.
Stonebraker [5] argumenterer for at de relationelle databasesystemer hørte til i en anden tid hvor
hardwareressourcer og brugen af IT-systemer var anderledes. Med følgende citat, argumenterer han for at
der skal ny teknologi til at overtage fra den relationelle databaseteknologi, som han kalder forældet:
”Because RDBMSs can be beaten by more than an order of magnitude on the standard OLTP benchmark,
then there is no market where they are competitive. As such, they should be considered as legacy
technology more than a quarter of a century in age, for which a complete redesign and re-architecting is the
appropriate next step.”
Hvis Stonebraker har ret og det viser sig at de relationelle database er på vej på pension om nogle få år, skal
vi være klar til at tage den nye teknologi til os, og vi skal kunne migrere til de nye teknologier på en
skånsom måde.
1.2 Min motivation
Databaser ses ofte tilgået fra en applikation via et mere abstrakt lag, såsom Hibernate eller ActiveRecord,
og i den forbindelse er det min opfattelse at databasesystemets evne til at optimere forespørgsler ikke
udnyttes i særlig høj grad. Hvis databasesystemet bruges, for manges vedkommende, som et ”dumt”
datalager, bør man måske benytte et mere simpelt databasesystem som er bedre optimeret til lige netop
denne opgave. Derudover har min erfaring vist mig at når relationelle databasesystemer vokser til en tilpas
høj kompleksitet, mister man noget fleksibilitet, og det daglige arbejde med at vedligeholde systemet bliver
mere tidskrævende og ligeledes komplekst. Fx kan det at levere den ønskede hastighed og tilgængelighed,
6
via replikering og sharding, være både svært og dyrt med en relationel database. Derfor har de mange
såkaldte NoSQL-databaser, der er dukket op i de seneste par år, interesseret mig ud fra følgende tre
kriterier: De er simple, størstedelen er open source og de skalerer godt over flere fysiske enheder.
Heriblandt, er jeg især interesseret i at undersøge dokumentdatabasens evne til at overtage den
relationelle databases plads, i situationer hvor den relationelle database tidligere har været det mest
oplagte valg. Dette skyldes at dokumentdatabasen, i modsætning til fx Key-Value Stores, har en mere rig
datamodel, som efter min mening, minder mere om hvad man kan finde i en relationel database, og derfor
vil være nemmere at omstille sig til for en person med professionel eller akademisk erfaring med
relationelle databaser.
2 Problemformulering
I et IT-system vælger man ofte en database ved projektets start, og fortsætter med at bruge denne
database i hele systemets levetid. Det anses af mange for at være en omkostnings- og risikofyldt opgave at
lave et skift af den underliggende database [6]. I mange år har den relationelle database været selvskrevet i
de fleste kommercielle IT-systemer, men et stadig større fokus på webtjenester med mange samtidige
brugere og et krav til lav eller ingen nedetid, har åbnet op for brugen af såkaldte NoSQL-databaser. Jeg vil i
dette projekt undersøge overgangen fra at bruge en relationel database til at bruge en dokumentdatabase
og besvare følgende spørgsmål:
Hvordan påvirkes en eksisterende applikations design og arkitektur af en migration fra en relationel
database til en dokumentdatabase?
Hvilke vigtige forskelle mellem den relationelle database og dokumentdatabasen skal man være
opmærksom på?
3 Metode
For at undersøge skiftet fra at bruge en relationel database i et IT-system til i stedet at bruge en
dokumentdatabase, vil jeg tage udgangspunkt i en eksisterende webapplikation der benytter en relationel
database, og tilpasse applikationen til at bruge dokumentdatabasen MongoDB. Undervejs vil jeg observere
hvilken designmæssig indflydelse den nye database har på applikationen. Derudover vil jeg undersøge
hvilke erfaringer andre har haft med at skifte til en dokumentdatabase. For at finde de vigtigste forskelle
mellem den relationelle database og dokumentdatabasen, og for samtidig at forudse hvilke problemer der
kan opstå under udførslen af casen, vil jeg undersøge og sammenligne teori og funktionalitet bag de to
typer databaser.
3.1 Valg af case
I dette projekt tages der udgangspunkt i en case baseret på webapplikationen www.taskjunction.com.
Applikationen er et online projektstyringsværktøj til softwareudviklingsteams med fokus på agile metoder,
så som Scrum.
De to vigtigste teknologier casen gør brug af, i forhold til dette projekt, er Ruby on Rails og MySQL. I
forbindelse med casens udførelse, vil MySQL blive udskiftet med dokumentdatabasen MongoDB. Se afsnit 8
for en mere udførlig beskrivelse af casens opbygning.
7
Jeg har valgt at arbejde med en case der ikke er skabt til formålet, men i stedet er en eksisterende webapplikation, der allerede bruges i produktion. Der er flere grunde til at jeg har valgt lige netop denne case:




Systemet bruges allerede i produktion og er ikke en forsimplet model skabt til formålet.
Casen er ikke omfattet af aftaler om hemmeligholdelse. Applikationens design vil uden videre
kunne offentliggøres i dette projekt.
Jeg har i forvejen stort kendskab til casen. Jeg vil derfor ikke være nødt til at bruge en masse tid på
at sætte mig ind i hvordan den fungerer.
Casens designmæssige opbygning er repræsentativ for mange af de webapplikationer der findes.
4 Afgrænsning
Projektet fokuserer på processen, designet og arkitekturen. Jeg vil ikke forholde mig til hvorvidt det at
bruge en dokumentdatabase er fordelagtigt i forhold til ydelse og drift, ligesom jeg heller ikke vil overveje
andre typer databaser under paraplyen NoSQL. Ligeledes vil jeg heller ikke undersøge migrationens
indflydelse på et udviklingsteam og uddannelsesbehovet heraf.
Jeg vil når der er fokus på databasereplikering, afholde mig fra at gå i dybden. Dette er for ikke at afvige for
meget fra målet, der som nævnt, ikke har drift som hovedområde.
5 Opgavens opbygning
Jeg vil i dette afsnit kort gennemgå hvordan denne rapport er opbygget.
I afsnittene 2 og 3 er problem og metode blevet formuleret. Afgrænsning af opgaven, i forhold til problem
og metode, er beskrevet i afsnit 4. I afsnit 7 vil teori, der forklarer projektets teknologier før og efter
migration, blive præsenteret. Derefter vil projektets case blive gennemgået i afsnit 8. Analyse af casens
udførelse gennemgås efterfølgende i afsnit 9 hvor problemstillinger beskrives med tilhørende løsninger og
evalueringer heraf. I afsnit 10 vil jeg forholde mig kritisk til denne rapport og den tilhørende case. Det er
også her jeg vil relatere projektet til min egen forforståelse forud for projektets start, som er beskrevet i
afsnit 6. Projektets konklusion præsenteres på baggrund af analyse (afsnit 9) og i forhold til problem (afsnit
2) og metode (afsnit 3), i afsnit 11. Rapporten afsluttes med en perspektivering i afsnit 12.
Kodeeksempler, log output og tegninger vil blive brugt gennem hele rapporten, hvor det er relevant for
beskrivelsen af en specifik problemstilling og løsning heraf. Kodeeksemplerne er typisk baseret på Ruby,
men der er ikke nogen forventning om at læseren i forvejen er bekendt med dette programmeringssprog.
Da relevante eksempler er medtaget direkte i rapporten, er der ikke vedhæftet bilag.
5.1 Terminologi
Når der anvendes fagudtryk, vil jeg bruge en blanding af danske og engelske begreber. Inden for mange
områder er det svært at finde korrekte danske begreber. I stedet for at forsøge at oversætte de engelske
begreber og potentielt skabe mere forvirring, har jeg valgt primært at bruge de engelske begreber, som
allerede er godt kendt og som bruges i den refererede litteratur.
8
I forbindelse med MongoDBs datastruktur er der i litteraturen ikke konsistens i brugen af begreberne
”dokument” og ”objekt”, som bliver brugt synonymt. Derfor vil mit sprogvalg til tider afspejle samme form
for inkonsistens.
Begrebet ”databasetransaktion” og ”transaktion” vil blive brugt synonymt for begrebet
”databasetransaktion”, hvis ikke andet beskrevet.
5.2 Forudsætninger
For at læse og forstå denne rapport forventes der en grundlæggende viden om relationelle databaser, SQL
og objektorienteret programmering. De praktiske implementeringer der benyttes, vil blive beskrevet i en
grad, så man med en grundlæggende viden kan forstå hvordan de fungerer i konteksten, og hvilken
betydning teknologierne har for projektet.
6 Forforståelse
Jeg vil i dette afsnit beskrive nogle tanker og forventninger jeg har haft forud for projektets opstart.
Casen er baseret på MySQL som er et meget brugt databasesystem, og derfor findes der mange biblioteker
og frameworks med understøttelse for MySQL. Derudover er det store community omkring MySQL
medvirkende til at fejl bliver løst hurtigt. Derfor har det været min opfattelse at det at skifte fra en meget
kendt og meget brugt teknologi til en mindre brugt teknologi (MongoDB), ville resultere i problemer
relateret hertil. Det kan fx være driverproblemer og inkompatibilitet mellem komponenter.
Jeg har forud for udførelsen af casen ikke haft forventninger om at Ruby ville give problemer. Min
forventning har været at Rubys dynamiske opbygning, i modsætning til fx C#s eller Javas mere statiske
opbygning, passer godt til MongoDBs tilsvarende dynamiske opbygning.
Den case jeg har taget udgangspunkt i, er fra starten bygget på en relationel database som i høj grad følger
normaliseringsreglerne, og har mange relationer mellem tabeller. Det er min generelle opfattelse at de
fleste der har haft succes med NoSQL-databaser, har lavet webapplikationer med en simpel datamodel
hvor kun få dataentiteter har relationer mellem sig, såsom blogs, forums eller sociale netværk. Derfor har
jeg også forventet at mit arbejde med emnet, ville have en mere kompleks karakter.
9
7 Teori
Jeg vil i de følgende afsnit præsentere teori der belyser området omkring dokumentdatabaser samt teori
nødvendigt for at forstå den case der arbejdes med i dette projekt.
7.1 NoSQL
Selvom principperne bag NoSQL er kendt siden 60erne [7], er begrebet NoSQL et relativt nyt begreb der
dækker over en lang række databasetyper som ikke følger en traditionel relationel databaseopbygning.
Begrebet blev første gang brugt af Carlo Strozzi i 1998 [8] om en relationel database der ikke brugte SQL.
Senere er begrebet blevet omdefineret til at omhandle alle ikke-relationelle databaser. Rick Cattell [9]
nævner det svære i at definere NoSQL-begrebet ud fra den betragtning at det er et område der er opstået
løbende.
Rick Cattell benytter følgende definition af NoSQL-begrebet [10].
1.
2.
3.
4.
5.
6.
Kan skalere horisontalt.
Kan replikere og partitionere data over flere servere.
Benytter et mere simpelt sprog end SQL.
Benytter en mere simpel concurrency model end de fleste relationelle databaser.
Effektiv brug af distribuerede indekser.
Kan dynamisk tilføje nye felter til data objekter.
Ifølge Neal Leavitt [7], er fordelen ved at benytte NoSQL, en bedre performance. Dette opnås ved en mere
simpel datamodel og tilsidesættelse af ACID princippet (se også 7.2.4). Som ulemper ved NoSQL, nævner
han manglen på understøttelse af SQL som et problem da omfattende opslag kan blive komplekse og
tidskrævende. Derudover nævner han manglen på understøttelse af ACID og det begrænsede kendskab til
teknologierne bag NoSQL fra erhvervslivet som ulemper ved at bruge NoSQL.
7.1.1 Typer af NoSQL-databaser
I følge nosql-database.org findes der 4 primære former for databaser under NoSQL-paraplyen. I praksis
findes der flere end de 4 typer og hybrider inden for emnet, men de fire neden for er de mest almindelige.
1. Key-Value stores
Databaser med en meget simpel datamodel, bestående af en nøgle og noget data, hvor der er
meget begrænsede muligheder for at udsøge data på andet end primærnøgler eller predefinerede
indekser [11]. I forhold til en relationel database, minder et key-value store på mange måder om en
tabel med to felter: En primærnøgle og et BLOB-felt med data.
2. Document data stores (dokumentdatabaser)
I dokumentdatabasen er data struktureret i objekter uden et foruddefineret skema. Det er oftest
muligt at lave udtræk baseret på alle felter i et objekt via et dertil konstrueret sprog, på mange
måder ligesom man kan med SQL [11]. Læs mere om dokumentdatabasen i 7.2.
3. Wide column data stores / Extensible Record Stores
Inddeler data i tabeller, men i modsætning til de relationelle tabeller, med et variabelt antal
kolonner og med bedre muligheder for at partitionere data over flere noder [9].
10
4. Graf-databaser
Disse databaser er specialiseret i at navigere en kompleks, ofte skemafri, objektgraf med
egenskaber bundet på relationer mellem entiteter [12]. En graf-database er velegnet til at
modellere og analysere venskaber, trafik og netværk.
Der findes mange forskellige implementeringer af NoSQL-databaser som alle har små variationer inden for
deres respektive type NoSQL-database. Se Figur 7-1 for en liste af nogle af de mange implementeringer der
findes. For en mere omfattende liste, se [13].
Relationelle
databaser
MySQL
PostgreSQL
MSSQL
DB2
Sqlite
Firebird
Key-Value stores
DynamoDB
MEMBASE
Riak
Redis
Scalaris
Berkeley DB
Voldemort
Memcache DB
Dokumentdatabaser Wide column data
stores
MongoDB
Hadoop
CouchDB
Cassandra
RavenDB
Hypertable
Terrastore
Amazon SimpleDB
Graf-databaser
Neo4J
InfoGrid
Trinity
BigData
Figur 7-1: Eksempler på forskellige databaseimplementeringer som defineret af nosql-database.org og wikipedia.org
7.2 Dokumentdatabase
I de følgende afsnit vil jeg gennemgå nogle af de primære karakteristika for dokumentdatabasen som har
relation til dette projekt. Dokumentdatabasen er den databasetype der er mest fokus på i dette projekt, og
derfor vil det kun være denne type der vil blive beskrevet i en særlig høj detaljeringsgrad. I afsnit 7.3 bliver
dokumentdatabasen MongoDB gennemgået.
11
7.2.1 Datastruktur
I det relationelle databasesystem kender vi begreberne tabeller, felter, rækker, indekser etc. I en
dokumentdatabase hvor data er struktureret på en anden måde, findes der andre begreber for koncepter
der minder om dem vi kender fra den relationelle database. For at folk med en baggrund inden for
relationelle databaser og SQL skal have nemmere ved at bruge de nye begreber, præsenterer mongodb.org
[14] oversættelsestaksonomien på Figur 7-2 med begreber kendt fra MySQL oversat til begreber brugt i
MongoDB. Det skal dog ikke forstås sådan at begreberne har præcis samme betydning, men nærmere en
tilnærmelse fra et begreb til et andet.
MySQL term
Database
Table
Index
Row
Column
Join
primary key
group by
Mongo term/concept
Database
Collection
Index
BSON document
BSON field
embedding and linking
_id field
Aggregation
Figur 7-2: Skema der oversætter begreber fra MySQL til MongoDB
I modsætning til den relationelle database, som har et fast foruddefineret databaseskema inddelt i tabeller,
kolonner og datatyper, har dokumentdatabasen ikke et skema. Databasen er inddelt i en række collections
indeholdende strukturerede dokumenter som hver især kan have forskellig struktur, også inden for samme
collection. Et dokument kan i denne sammenhæng være struktureret i forskellige formater, mest
almindeligt JSON (JavaScript Object Notation) [15] (se 7.2.1.1) men andre notationer, såsom XML
(Extensible Markup Language) og YAML (Yet Another Markup Language) kan også være en mulighed alt
efter implementering. At dokumenterne er strukturerede og at databasesystemet er i stand til at forstå og
manipulere denne struktur, er den primære forskel mellem en dokumentdatabase og et key/value store
[12].
Fordi dokumentdatabasen er skemaløs, kan to eller flere dokumenter i samme collection have forskellige
strukturer. Det er en stor forskel fra den relationelle database, og kan vise sig at løse nogle komplekse
problemer som den relationelle database har svært ved at løse på en elegant måde. Dette viser Anderson,
Lehnardt og Slater [16] ved hjælp af et eksempel med visitkort. Visitkort er et type dokument som vi alle
kan finde ud af at bruge, selvom to visitkort sjældent har samme struktur. Et kort kan have et fax-nummer,
mens et andet måske har to adresser og et tredje har et privatnummer. Hvis et sådan visitkort skal gemmes
i en relationel database, skal der på forhånd tages højde for alle de forskelligheder der kan være, mens man
i en dokumentdatabase kan lagre visitkortene i samme collection indeholdende de forskelligheder de nu
har.
7.2.1.1 JSON
JSON er et human readable dataformat baseret på et subset af JavaScript syntaksen [15]. I sammenligning
med XML er JSON både mere simpelt og mindre ressourcekrævende at håndtere [17]. Et dokument er
opbygget som name-value par hvor værdien kan bestå af simple typer såsom tal, tekststrenge, kommatal,
boolske værdier og datoer, samt komplekse typer som arrays og objekter. Et eksempel på et JSON
12
dokument i MongoDB, kan være defineret som på Figur 7-3. Bemærk hvordan feltet country definerer et
underobjekt med felterne name og code, og bemærk at feltet location er et array med to værdier.
Denne struktur stemmer i høj grad overens med Dates [18] observation af et objekts struktur i forbindelse
med objektdatabaser, som han beskriver som værende tuples bestående af uforanderlige underobjekter
(heltal, datoer mv.), referencer til foranderlige underobjekter og arrays (inklusiv sæt og lister). Den eneste
forskel fra dokumentet til Dates definition af et objekt, er at dokumentet, ud over ovenstående, kan
indeholde komplekse underobjekter bestående af selvsamme elementer, uden brug af referencer.
{
"_id" : ObjectId("4f566926565f02069f8d8f3f"),
"name" : "Copenhagen",
"population" : 1153615,
"country": {
"name" : "Denmark",
"code" : "DK"
},
"location" : [
55.67594,
12.56553
],
"timezone" : "Europe/Copenhagen"
}
Figur 7-3: Eksempel på JSON dokument i MongoDB
7.2.2 Query language
I den relationelle database bruges der oftest SQL til at udtrække og manipulere data, men da SQL ikke er
implementeret i nogle NoSQL-databaser, skal der bruges et alternativ. Da de forskellige
dokumentdatabaser har forskellige måder at tilgå data på, findes der ikke et ensartet sprog til formålet
[19]. Forsøg på at specificere et standardiseret sprog til udtræk fra en ustruktureret database, er igangsat
med projektet UnQL [20], men i skrivende stund er der ingen praktiske implementeringer.
Et populært alternativ til SQL, blandt dokumentdatabaser, er et RESTful API, bla. understøttet af MongoDB,
CouchDB og RavenDB. MongoDB understøtter, ud over et RESTful API, også et sprog baseret på JavaScript
til definering af dataudtræk, se Figur 7-4 for et eksempel i MongoDBs command shell (se også afsnit 7.3.2).
Langt de fleste eksempler i denne rapport vil benytte sidstnævnte. Til sammenligning kan man på Figur 7-5
se tilsvarende forespørgsler i traditionel SQL. På første linje i Figur 7-4 returneres et komplet JSON objekt
som på Figur 7-3. På linje 2 returneres ikke kun et enkelt objekt, men i stedet en serie af JSON-objekter (se
også 7.3.2.1).
db.cities.findOne({name: "Copenhagen"})
db.cities.find({country.name: "Denmark", population: {$gt: 100000}})
db.cities.update({name:"Copenhagen"}, {$set:{population: 1000000}}, false, true)
Figur 7-4: Eksempel på forespørgsler i MongoDB shell
13
SELECT * FROM cities WHERE name = ’Copenhagen’;
SELECT * FROM cities WHERE country_name = ’Denmark’ AND population > 100000;
UPDATE cities SET population = 1000000 WHERE name = ’Copenhagen’;
Figur 7-5: Eksempel på SQL forespørgsel i en relationel database.
7.2.3 Integritet
Selvom der kan laves referencer mellem to objekter i en dokumentdatabase, sikrer databasen ikke at det
refererede objekt eksisterer. I en relationel database kan dette nemt opnås med brug af fremmednøgler,
men i dokumentdatabasen er man nødsaget til at konstruere den omkringliggende kodebase således at
refererede dokumenter kan mangle.
Da der ikke er noget skema, vil der også kunne opstå uoverensstemmelse mellem strukturerne for de
forskellige dokumenter i en given collection. Der er altså ingen not-null eller datatype constraints som man
kender det fra den relationelle database.
7.2.4 Transaktioner
Mange relationelle databaser bruger transaktioner for at understøtte ACID princippet (Atomicity,
Concurrency, Isolation, Durability) [21]. Transaktionerne bliver brugt til at skabe isolation og sikre integritet
mellem flere samtidige databaseforbindelser. For at opnå bedre performance og bedre muligheder for at
skalere over flere servere, har mange databaser under NoSQL-paraplyen valgt at tilsidesætte ACID
princippet til fordel for BASE princippet (Basically Available, Soft state, Eventually consistent) hvor isolation
og konsistens ikke kan garanteres i samme grad [10]. Dog er det muligt i fx MongoDB at gemme et enkelt
dokument atomisk [22] (se også 7.3.3), mens CouchDB benytter sig af en multiversion concurrency control
model [23].
Argumentet for at droppe ACID til fordel for BASE, understøttes af Brewers CAP teori hvori det fastslås at et
distribueret system, på et givent tidspunkt, maksimalt kan implementere to af de tre begreber, consistency,
availability og partition-tolerance [24]. Som vi kan se på CAP trekanten Figur 7-6, placerer MongoDB sig på
CP aksen. Det gør den i kraft af at MongoDB garanterer eventual consistency, ved at alle skriveoperationer
går igennem en primær node som replikerer data i korrekt rækkefølge til alle sekundære noder. I tilfælde af
network partition hvor den primære node bliver utilgængelig for en eller flere klienter, vil systemet som
helhed, over for disse klienter, være available for read men ikke available for write. Læs mere om
replikering i MongoDB i afsnittet 7.3.4.
Stonebraker [25] forholder sig dog kritisk over for hele CAP teorien, og mener at CAP i NoSQL-kredse,
fejlagtigt bliver brugt som undskyldning for at konstruere et system med mindre fokus på consistency eller
availability. Han mener at fordi network partition sker så relativt sjældent, bør et system hellere være et CA
system, ligesom de relationelle databasesystemer er på Figur 7-6, frem for CP eller AP.
Abu-Libdeh og Escriva [26] argumenterer for at begrebet consistency ikke kan bruges i en all or nothing
kontekst, men i stedet at et system besidder forskellige consistency-egenskaber. At snakke om consistency i
forbindelse med CAP, kan give et alt for simpelt billede af et IT-system. I praksis vil et system have
forskellige consistency-egenskaber ved forskellige handlinger.
14
Brewer nævner selv i en nyere artikel [27] at det at vælge to ud af de tre egenskaber (C, A og P) er
vildledende, fordi både consistency og partition tolerance kan forstås og implementeres på flere forskellige
måder, og at graden af disse egenskaber kan være forskellig alt efter hvilken operation der benyttes i ITsystemet.
Figur 7-6: Visuel repræsentation af CAP, med angivelse af de forskellige databasetyper og -implementeringer. Kilde:
http://blog.nahurst.com/visual-guide-to-nosql-systems
7.3 MongoDB
MongoDB er en open source dokumentdatabase produceret af virksomheden 10gen som også yder
kommerciel support for systemet. MongoDB bygger på at forsimple nogle egenskaber, såsom concurrency
control og ACID, for derved at opnå mere fleksibilitet, flere features, bedre performance og nemmere
anvendelighed [28].
Jeg vil i de følgende afsnit beskrive egenskaber som er gældende for MongoDB. Dette kan både være
egenskaber som kun er gældende for MongoDB og som er gældende for MongoDB og andre
dokumentdatabaser.
7.3.1 Datastruktur
MongoDB gemmer sine data i dokumenter formateret som JSON (se 7.2.1.1) serialiseret til BSON (Binær
repræsentation af JSON) i en databases collections. Data tilgås enten via en driver udviklet til det
udviklingsmiljø man vil tilgå databasen fra eller via en konsol med en JavaScript-baseret syntaks, se Figur
7-4. Som i en relationel database, kan der oprettes indekser og laves referencer (kaldet links) fra et
dokument til et andet. Sidstnævnte er dog uden constraints, det vil sige fremmednøgler håndhæves ikke på
databaseniveau.
15
7.3.2 Dataoperationer
Dataoperationer kan enten laves via det medfølgende shell-værktøj eller via et udviklingsmiljø, her i
projektet Ruby on Rails. I denne rapport kan eksempler stamme fra begge dele, dog vil der i sidst nævnte
flere steder blive suppleret med log output fra Ruby on Rails som viser mere præcist hvilke operationer der
udføres.
Som beskrevet i afsnit 7.2.2, understøtter MongoDB ikke SQL, men benytter i stedet et sprog baseret på
JavaScript til at udtrykke dataoperationer. Find()-operationen minder på mange måder om et SELECT query
i et databasesystem der understøtter SQL, og tillader ad hoc queries der kan bruges til udsøgning af data på
alle indekserede og ikke-indekserede felter i de dokumenter databasen indeholder. På Figur 7-7 ses
eksempler på nogle basale dataoperationer. Ud over disse operationer, eksisterer der også operationer til
aggregeringer (se 7.3.4), atomiske updates (se 7.3.3) og databasevedligehold. I modsætning til relationelle
databaser, er det ikke muligt at konstruere joins på tværs af collections.
/* Udsøg alle projekter med status 1, sorteret på title*/
db.projects.find( { status: 1 } ).sort( { title: 1 } )
/* Udsøg de 10 første projekter, sorteret på title i omvendt rækkefølge */
db.projects.find().sort( { title: -1 } ).limit(10)
/* Udsøg alle tasks hvor status er 2 eller estimate > 10 */
db.tasks.find( { $or: [ { status: 2 }, { estimate: { $gt: 10 } } ] } )
/* Opret ny bruger i collection users */
db.users.insert( { username: ”User 1”, is_admin: false, email: ”[email protected]” } )
Figur 7-7: Eksempler på dataoperationer i MongoDB.
7.3.2.1 Cursors
Man kan undersøge resultatet af find()-metoden nærmere, ved at kalde metoden help() på returværdien,
hvorefter det viser sig at resultatet af find() ikke er en liste, men i stedet en cursor, se Figur 7-8. I bunden
af figuren gøres der brug af metoden forEach() til at gennemløbe resultatet og printe værdien af objektets
felt name til konsollen. I praksis er der endnu flere metoder på cursor-objektet, end hvad help() beskriver,
hvilket ses ved at printe hele cursor-objektet som JSON, tojson(cur) (resultat af denne operation er udeladt
af pladshensyn).
16
> var cur = db.cities.find({country: "Denmark", population: {$gt: 100000}})
> cur.help()
find() modifiers
.sort( {...} )
.limit( n )
.skip( n )
.count() - total # of objects matching query, ignores skip,limit
.size() - total # of objects cursor would return, honors skip,limit
.explain([verbose])
.hint(...)
.showDiskLoc() - adds a $diskLoc field to each returned object
Cursor methods
.forEach( func )
.map( func )
.hasNext()
.next()
> cur.forEach(function(object) { print(object.name) } )
Odense
Copenhagen
Århus
Aalborg
Figur 7-8: Resultatet af en find() operation i MongoDB er i realiteten en cursor.
7.3.3 Atomiske operationer
Med de almindelige opdaterende dataoperatorer, såsom update(), insert() og save(), bliver et helt
dokument gemt, enten som et nyt dokument eller som en erstatning for et andet dokument [22]. Dette er
forskelligt fra SQL hvor UPDATE kan bruges til kun at opdatere et valgfrit antal felter. Til situationer hvor det
er fordelagtigt kun at opdatere en del af et eksisterende dokument, tilbyder MongoDB en række atomiske
operatorer (se Figur 7-9) [29]. Dog er det værd at bemærke at disse operationer kun er atomiske inden for
ét enkelt dokument. Der opnås altså ikke atomiske egenskaber på tværs af flere dokumenter.
Operator
$set
$unset
$inc
$push
$pushAll
$pull
$pullAll
$rename
Beskrivelse
Sæt værdi
Fjern et felt
Forøg værdien af et felt
Tilføj en værdi til et array
Tilføj flere værdier til et array
Fjern en værdi fra et array
Fjern flere værdier fra et array
Omdøb felt
Figur 7-9: Udvalg af atomiske operatorer i MongoDB
På Figur 7-10 ses et eksempel på brug af operatoren $inc. I eksemplet oprettes først et dokument, herefter
lægges 2 til feltet a hvor feltet b har værdien 100. $inc vil kunne bruges på flere felter eller sammen med
andre operatorer fra Figur 7-9 uden risiko for konflikt med andre samtidige opdaterende operationer.
>
>
>
{
db.temp.save({a: 1, b: 100})
db.temp.update({b: 100}, {$inc: {a: 2}})
db.temp.findOne({b: 100})
"_id" : ObjectId("500937fc3449ca09c0776d6c"), "a" : 3, "b" : 100 }
Figur 7-10: Eksempel på brug af den atomiske operator $inc i MongoDB
17
7.3.4 Aggregeringer og MapReduce
Som pendant til GROUP BY i SQL har MongoDBs collections en group metode [30] som når brugt, returnerer
et dokument indeholdende aggregerede data. På Figur 7-11 ses et eksempel på en aggregering i MongoDB
hvor feltet todo summeres for hvert task_status_id hvor project_id er 42. Den tilsvarende løsning i SQL ses
på Figur 7-12.
db.tasks.group({
key: {task_status_id: true},
cond: {project_id: 42},
reduce: function(document, out) { out.todo += document.todo; },
initial: {todo: 0}
})
Figur 7-11: Eksempel på simpel aggregering i MongoDB
SELECT task_status_id, SUM(todo)
FROM tasks
WHERE project_id = 42
GROUP BY task_status_id
Figur 7-12: Tilsvarende aggregering i MySQL
Fordi group på Figur 7-11 returnerer alle resultater som et enkelt dokument, er der visse begrænsninger
[30]. Et dokument i MongoDB er, i skrivende stund, begrænset til en maksimal størrelse på 16MB med
maksimalt 10.000 felter, hvilket betyder at det aggregerede resultat maksimalt kan have 10.000 unikke
resultater. Metoden group er også begrænset til en konfiguration der ikke er inddelt i shards (se 7.3.6). Skal
alle disse begrænsninger omgås, skal man i stedet benytte et MapReduce-udtryk [31] som på Figur 7-13.
Med MapReduce beskrives først hvordan data skal mappes til aggregerede key/value-par (mapfunktionen), hvorefter en metode til at reducere dataene beskrives (reduce-funktionen). Denne form for
dataaggregering er velegnet ved store datamængder, distribueret over flere fysiske noder i et netværk.
var map = function() {
emit(this.task_status_id, this.todo);
}
var reduce = function(key, values) {
var sum = 0;
for (var i = 0; i < values.length; i++)
sum += values[i];
return sum;
}
db.tasks.mapReduce(map, reduce, {query: {project_id: 42}, out: {inline: true}});
Figur 7-13: Tilsvarende MapReduce udtryk i MongoDB
7.3.5 Replikering
MongoDB understøtter to former for replikering, Replica Set og Master-Slave, hvor Replica Set er den
metode som anbefales at bruge i de fleste situationer [32]. Begge metoder benytter sig af asynkron
replikering med én primær node (node = databaseinstans) der modtager alle skrivninger. Den største
forskel ligger i at Replica Set, i modsætning til Master-Slave, understøtter failover hvor en sekundær node, i
18
tilfælde af nedbrud på den primære node, automatisk kan overtage rollen som primær node. Ved begge
former for replikering, kan der udlæses data fra alle sekundære og primære noder.
Replikering foregår ved at alle dokumenter der bliver oprettet, ændret eller slettet på primærnoden, bliver
skrevet til en såkaldt oplog som er en capped collection (first-in-first-out collection med en predefineret
maksimal størrelse). Alle sekundære noder har en forbindelse åben til den primære node hvorfra de via
long polling, synkroniserer nye ændringer til lokale collections og en lokal oplog. Den lokale oplog har til
formål at afgøre hvilken sekundær node der er den mest opdaterede, i tilfælde af nedbrug på
primærnoden.
På Figur 7-14 ses en simplificeret tegning af et replikeringssetup med Replica Set med tre noder.
Applikationsserveren er i stand til at forbinde til en hvilken som helst databasenode, men vil kun være i
stand til at udføre skriveoperationer på den primære node. Ligeledes er applikationsserveren i stand til at
forbinde til en anden databasenode, i tilfælde af at en anden bliver valgt som primær.
Application
Server
Primary
DB
repliker
repliker
ping
ping
ping
Secondary
DB
Secondary
DB
Figur 7-14: Replikering baseret på Replica Set mellem en primær node og to sekundære noder
Replikering og sharding (se næste afsnit) er store emne som vil kunne fylde en hel rapport, og derfor går jeg
ikke i dybden med konfigurationsmuligheder og analyse heraf.
7.3.6 Sharding
Ud over replikering understøtter MongoDB også sharding (data partitionering) som sætter
databasesystemet i stand til at fordele data og skriveoperationer over flere fysiske enheder (shards) [32], se
Figur 7-15. Kort fortalt handler det om at fordele data baseret på en nøgle. Fx kan man forestille sig at man
fordeler en collection indeholdende brugere over to servere, hvor den første halvdel af brugerne er
placeret på den ene server, mens den anden halvdel er placeret på den anden.
Sharding sker automatisk i databasesystemet baseret på en predefineret shard key som er en nøgle der
beskriver på hvilken shard et givent dokument hører hjemme. Hvis man fx har brugt username som shard
key, vil alle brugere med et forbogstav mellem A og O ligge på én shard, mens brugere med et forbogstav
mellem P og Å er placeret på en anden shard. Ved opslag på et bestemt brugernavn er mongorouteren,
som applikationsserveren er forbundet til, i stand til at afgøre hvilken shard forespørgslen skal videresendes
19
til. Forespørges der i stedet baseret på et felt som ikke er en del af en shard key, vil mongorouteren spørge
alle shards parallelt, og sammensætte resultaterne til ét samlet resultat.
Config-servere bruges i en shardet konfiguration til at holde og vedligeholde metadata der beskriver hvilke
klumper af data der er placeret på hvilke shards. Disse config-serveres data anses for værende yderst
kritiske, og derfor er skrivninger til serverne beskyttet af en two-phase commit hvilket betyder at i tilfælde
af nedbrug på en config-server, vil der stadig kunne læses fra og skrives til systemet, men shards vil ikke
kunne omkonfigureres.
Shard B
Shard B Secondary
Secondary
Shard B
Primary
Shard A
Shard A Secondary
Secondary
Shard A
Primary
Config
Server
Mongo Router
Config
Server
Application server
Figur 7-15: Eksempel på en shardet konfiguration med 2 shards konfigureret i replica sets, to config servere, en mongo router og
en application server
7.4 Applikationens tilgang til databasen
Jeg vil i dette afsnit kigge på hvordan integrationen mellem applikation og database kan struktureres. Dette
har betydning i afsnit 7.5.1 og i analysen af casen.
Martin Fowler [33] argumenterer for at opdele en applikation i tre lag: præsentation, forretningslogik og en
datakilde. Præsentationen er den brugergrænseflade brugeren interagerer med, det kan fx være et
website, men det kan også være et Windowsprogram, et kommandolinieværktøj eller en mobil app. I
forretningslogiklaget defineres de regler og den logik der er nødvendig for at applikationen kan fungere.
Der vil her være defineret objekter som skal gemmes på en måde, så de kan tilgås igen på et senere
tidspunkt. Datakilden løser problemet med at objekter både skal kunne gemmes og hentes frem igen på et
senere tidspunkt. Datakilden består både af et databasesystem (relationel database, NoSQL mv.) og et lag i
applikationen med ansvar for at interagere med databasen.
7.4.1 Active Record
Fowler [33] definerer fire primære patterns til håndtering af datakilden, hvoraf et af dem, Active Record,
har betydning for dette projekt. I Active Record har alle objekter i forretningslogiklaget, der skal kunne
gemmes i en database, både den logik der har med forretningsområdet at gøre og den logik der omhandler
det at hente, gemme og slette objektet (Figur 7-16). Objektet repræsenterer typisk data i én tabel, og har
20
ofte en struktur der minder meget om den tabel objektet skal gemmes i. Fowler anbefaler at bruge Active
Record når forretningslogikken ikke er for kompleks. Når logikken og klasserne bliver mere komplekse, i
kraft af nedarvning, objektrelationer eller en objektstruktur der i betydelig grad afviger fra databasens
struktur, anbefaler han at bruge et pattern med mere kompleks mapping mellem database og objekter.
Figur 7-16: Active Record pattern. Kilde: M. Fowler, Patterns of Enterprise Application Architectur
I afsnit 7.5.1 kommer jeg ind på hvordan dette pattern bruges i konteksten af Ruby on Rails.
7.4.1.1 N+1 Problemet
Når man benytter et pattern som Active Record, kan man risikere at være udsat for N+1 problemet [34].
Problemet opstår når en række objekter hentes fra databasen og disse objekter har relationer til andre
objekter som hentes ét ad gangen. Hvis en databaseforespørgsel returnerer 100 objekter som alle refererer
et objekt, vil det resultere i 101 queries mod databasen.
For at undgå problemet, kan man i et databasesystem med understøttelse af SQL, bruge IN-operatoren til
at hente 100 relationer fra databasen via ét enkelt query. For MongoDBs vedkommende hedder
operatoren $in. I begge tilfælde reduceres antallet af databasekald til 2, uanset hvor mange objekter der
returneres i første query.
I forbindelse med udførelsen af casen, beskriver jeg i højere grad hvorfor dette er et vigtigt problem at løse
og hvordan det kan gøres i de to involverede miljøer, se afsnit 9.6.
7.5 Ruby on Rails
Ruby on Rails (RoR) er et Open Source Model View Controller (MVC) webframework designet i
programmeringssproget Ruby, og bygger på en grundfilosofi baseret på convention-over-configuration,
don’t repeat yourself (DRY) og en RESTful arkitektur [35]. RoR blev udviklet af David Heinemeier Hansson i
2003-2004 som et værktøj der skulle gøre repeterende opgaver nemmere, ved at lade konventioner styre
programmørens udviklingsopgaver for på den måde gøre ham i stand til at opnå mere med færre linjer
kode [36].
Frameworket er inddelt i 3 primære dele: controllers, models og views (se Figur 7-17). Derudover vil jeg
kategorisere delene database migrations og unit tests som værende vigtige i konteksten af dette projekt.
21
Figur 7-17: Ruby on Rails primære arkitektur. Kilde: Agile web development with Rails af Sam Ruby
DRY princippet kommer især til udtryk i model-laget som baserer sig på ActiveRecord [33] og en Metadata
Mapping [33] hvor tabelnavne udledes af model-klassers navne, og tabelkolonner udledes af modelklassers properties. Frameworket, som er konstrueret til at håndtere alle relationelle databaser med
understøttelse af SQL, forventer ikke at databasesystemet håndterer håndhævelse af fremmednøgler, notnull constraints og andre former for datavalidering. Det vil sige at alle relationer mellem klasser og
håndhævelse heraf skal defineres direkte i model-klasserne, i form af has-many, has-one og belongs-to
relationer [37].
Databasemigrationer håndteres og versioneres af RoR som abstraherer de forskellige SQL-dialekter via et
Domain Specific Language (DSL) til et fælles sprog, baseret på Ruby [38]. Med migrationer kan man, når
forretningsmodellen udvikler sig i takt med ny funktionalitet, oprette nye tabeller, tilføje felter, oprette
indekser mv. Det gør frameworket i stand til automatisk at rulle databaseændringer på en udviklings-,
produktions- eller testdatabase. Migrationerne beskrives i konteksten af en relationel database og bliver, i
modsætning til visse andre lignende DSL’er (fx WebDSL), ikke udledt direkte af forretningsmodellen [39].
Det lave abstraktionsniveau har den konsekvens at migrationerne kun fungerer sammen med en relationel
database med understøttelse af SQL. Migrationerne kan altså ikke bruges sammen med en
dokumentdatabase eller anden NoSQL-database.
7.5.1 Active Record i Ruby on Rails
En standard RoR applikation inkluderer et bibliotek, kaldet ActiveRecord, som baserer sig på det førnævnte
pattern af samme navn. Selvom Fowler [33] skriver at Active Record ikke er fordelagtigt når det kommer til
brug af referencer, nedarvning og lister, har David Heinemeier Hansson alligevel valgt at implementere
disse concepter i ActiveRecord-biblioteket. Om hvorvidt Fowler er for hurtig til at afvise Active Record’s
brugbarhed, eller om Heinemeiers ActiveRecord bibliotek i praksis ikke implementerer et Active Record
pattern, vil jeg lade stå hen i det uvisse.
ActiveRecord gør det muligt at bruge en relationel database i en webapplikation uden at skrive SQL eller
kode der mapper databasefelter til objekter. Dette er en fordel i et miljø som RoR hvor der er transparent
understøttelse af en lang række forskellige relationelle databaser. For at opnå denne transparens bruges
ActiveRecord sammen med et indbygget query-language hvis begrebsverden minder om den man kender
fra SQL. For et eksempel på brug, se metoden members_of_users_projects i Figur 7-18.
22
def self.members_of_users_projects(user)
User.
joins(:project_members).
where(:project_members => {:project_id => user.projects}).
uniq
end
Figur 7-18: Eksempel på brug af ActiveRecord query language. Metoden returnerer alle brugere som er medlem af samme
projekter som brugeren defineret af parameteren user.
Som tidligere nævnt, baserer RoR sig på en convention-over-configuration filosofi hvilket viser sig når man
kigger på klassen User i Figur 7-19. Fordi klassen User nedarver fra ActiveRecord, vil den automatisk kunne
gemmes i en tabel med navnet ”users”, og består automatisk af properties baseret på de felter denne
tabels metadata definerer. På Figur 7-20 ses et eksempel på hvordan denne klasse kan bruges og hvordan
den transparente databaseadgang udnyttes.
class User < ActiveRecord::Base
has_many :project_members
has_many :projects, :through => :project_members
validates :email, :name, :presence => true
validates_uniqueness_of :email
... /CODE REMOVED/ ...
end
Figur 7-19: Eksempel på klassen User med referencer til klassen Project og ProjectMembers.
user = User.find 10
user.name = "Jens Jensen"
user.save
projects = user.projects.where(:project_status_id => PROJECT_STATUS_OPEN)
Figur 7-20: Eksempel på brug af et objekt af typen User og dets reference til projekter.
23
8 Casen
Casen der tages udgangspunkt i er webapplikationen www.taskjunction.com. Det er et online
projektstyringsværktøj til administrering af softwareudviklingsprojekter efter agile principper, såsom
Scrum. Værktøjet tillader brugere at oprette en række projekter og invitere andre medlemmer til
projekterne. I projekterne kan der oprettes opgaver som inddeles i iterationer. Projektledere og udviklere
kan løbende holde styr på projektets fremgang via et taskboard (kendt fra Scrum), testboard, burn down
charts og andre statistikker. Ud over en webgrænseflade, er der også en webservicegrænseflade som
blandt andet bliver brugt af en mobil app til Android.
8.1 Begrundelse for valg af case
Casen er valgt ud fra det synspunkt at den oprindeligt er lavet til at benytte en relationel database. Der vil
derfor forekomme brug af funktioner der ikke er tilgængelige i en dokumentdatabase. Dette kan f.eks.
være håndhævelse af fremmednøgler, not-null constraints, datatyper samt brug af transaktioner og joins.
Casen gør det muligt at afprøve skiftet fra en relationel brug, til en dokumentbaseret brug, i en situation
der repræsentativ for opgaven og som ikke er forsimplet.
Både MongoDB og Ruby on Rails bygger på nogle fælles grundlæggende filosofier omkring convention-overconfiguration, open source samt nem og fleksibel brug. Der er et stærkt community bag begge teknologier,
og der findes den nødvendige viden til at få de to teknologier til at fungere sammen. Det har derfor været
min opfattelse, forud for udførelsen, at casen kan udføres i praksis.
8.2 Casens tekniske opbygning
Applikationen er baseret på RoR version 3.2.5 og en MySQL database hvor alle tabeller, for at få
understøttelse af transaktioner og fremmednøgler, benytter sig af InnoDB storage engine. Når casen er
blevet gennemført, vil systemet i stedet bestå af en RoR applikation der benytter MongoDB version 2.0.6
som underlæggende database. Alle eksisterende data vil blive konverteret til den nye datastruktur.
8.3 Databasens oprindelige opbygning
På Figur 8-1 ses et diagram over den oprindelige MySQL-databases tabelstruktur. Bemærk, for at gøre
diagrammet mere simpelt er relationer til tabeller af meget statisk karakter fjernet fra diagrammet. Disse
tabeller ses i bunden af diagrammet.
De vigtigste dele af diagrammet er Projects, Iterations, Tasks, TestCases og Users som repræsenterer de
grundlæggende forretningsdata. Helt grundlæggende kan man sige at systemet indeholder nogle brugere
som er medlem af nogle projekter med tilhørende tasks som kan være inddelt i iterationer.
Tabellerne iteration_statistics, iteration_test_statistics og project_statistics indeholder aggregerede data
om hvor mange tasks og test cases der findes i de forskellige statuskategorier. Tabellerne eksisterer for
hurtigere at kunne udlæse statistik fra databasen samt for at kunne lagre statistik i et tidsperspektiv. Data i
disse tabeller vedligeholdes automatisk af applikationen, når relevante data ændres.
24
Figur 8-1: Oversigt over tabeller i oprindelig MySQL database.
8.4 Fremgangsmåde
8.4.1 Fremgangsmåde – database layout
På Figur 8-2 kan man se hvordan de 22 MySQL tabeller grupperes i et mindre antal MongoDB collections.
Der er en række tabeller som udgår, hvoraf den mest oplagte er tabellen schema_migrations da der med
MongoDB ikke er noget databaseskema at holde styr på. De seks tabeller i højre side af diagrammet
kategoriserer jeg som statiske type-tabeller som er tabeller der fx beskriver en prioritet eller en status.
25
Disse tabeller indeholder primært et id og en tekst. Det giver mening i en normaliseret relationel database,
men i dokumentdatabasen har jeg i stedet valgt ar disse entiteter skal være repræsenteret af objekter
instantieret i forretningslaget ved runtime.
Selvom det ikke er indtegnet, er der referencer imellem de otte collections. Der er primært referencer
mellem projects, iterations, users og tasks. Fordi MongoDB ikke har indbygget understøttelse af
dokumentreferencer, er der her tale om bløde referencer som håndhæves af applikationen.
Bemærk hvordan test_cases og task_discussions, som før migrationen var selvstændige entiteter med
referencer til tasks, nu er blevet en integreret del af collectionen tasks. Det sker ud fra den betragtning at
test cases og task discussions altid bliver betragtet gennem en task, og ved at sammenlægge disse entiteter
fjernes behovet for key constraints mellem tasks og test cases. Se evt. 9.5.3.
Figur 8-2: Oversigt over hvordan tabeller bliver samlet i nye collections i MongoDB.
8.4.2 Fremgangsmåde – applikation
Det er umiddelbart ikke muligt at konstruere applikationen så den både kan fungere med en relationel
database og en dokumentdatabase. Det skyldes at ActiveRecord biblioteket, som modellen baserer sig på,
kun kan benyttes med en relationel database, med understøttelse af SQL. ActiveRecord skal derfor
udskiftes med et tilsvarende bibliotek der understøtter MongoDB. Pt findes der to udbredte biblioteker,
MongoMapper og MongoId, hvoraf jeg har valgt at bruge MongoMapper. Derudover kan det indbyggede
query language, som bruges til at forespørge data fra databasen og automatisk instantiere tilsvarende
objekter, heller ikke bruges i sin oprindelige form. MongoMapper implementerer sit eget query language, i
stil med hvad ActiveRecord implementerer, som er blevet brugt som erstatning.
Fordi casen benytter ActiveRecord, vil et klassediagram over relevante klasser, minde meget om
databasediagrammet på Figur 8-1, og er derfor udeladt fra denne rapport. De klasser der ikke er
26
repræsenteret af en tabel i databasediagrammet, har ikke betydning for dette projekts databaserelaterede
formål.
Det indbyggede koncept omkring migrationer af databaseskemaændringer er overflødigt når der bruges
MongoDB. Eftersom MongoDB ikke har noget databaseskema, er det ikke nødvendigt at holde metadata
opdateret på samme måde. Dog har det vist sig nødvendigt at håndtere vedligeholdelse af indekser på en
struktureret måde hvilket uddybes i 9.10.
I forbindelse med automatiske tests er det muligt at opsætte statiske test fixtures som definerer nogle
standardiserede data som de enkelte tests kan regne med eksisterer i databasen når en test køres. Det var
nødvendigt at udskifte hele biblioteket til håndtering af test fixture med et andet, hvilket uddybes i 9.9.
Fordi et modelobjekts properties ikke er defineret i kildekoden bag objektet, dannes properties på
baggrund af databasens skema ved runtime. Men fordi MongoDB er skemaløst, kan denne metode ikke
bruges. I stedet skal properties og typer defineres i modelobjekterne, på en måde som MongoMapper kan
håndtere når data hentes og gemmes i databasen.
8.4.3 Migrationens faser
I tabellen nedenfor beskriver jeg, hvilke faser migrationen gennemgik.
1
Konfiguration
2
Reimplementering af
model-klasser
3
4
Tests konverteres
Datakonvertering
5
Udskiftning af Query
Language
Analysering af løsning
6
Konfigurationen blev ændret fra at bruge ActiveRecord til at bruge
MongoMapper.
Alle klasser der nedarvede fra ActiveRecord blev ændret til at bruge
MongoMapper biblioteket. Det var ikke nok bare at ændre nedarvningen.
Det var også nødvendigt at ændre definitionen af klasserne så de passede
ind i den nye struktur og brugte det nye biblioteks metoder til at beskrive
felter, referencer og underdokumenter.
Tests og test fixtures baseret på en relationel model blev udskiftet.
Eksisterende testdata blev konverteret. Et script, der kunne konvertere den
eksisterende MySQL database og indsætte data i den nye MongoDB
database, blev produceret.
Der blev lavet tilpasninger de steder hvor det eksisterende query language
ikke var kompatibelt med det nye.
Til sidst blev løsningen analyseret og sammenlignet med den oprindelige
MySQL løsning.
27
9 Analyse
Jeg vil i de kommende afsnit beskrive problemstillinger og løsninger hertil, opstået undervejs i migrationen
af casen fra MySQL til MongoDB. Jeg fokuserer på en analyse af konkrete løsninger på datalogiske
problemstillinger som jeg har skønnet af høj betydelighed for sammenligningen af de to områder.
Kodeeksempler og logoutput, simplificeret til at konkretisere de vigtige dele af et givet problem, vil blive
brugt hvor det skønnes nødvendigt.
9.1 Gemme data atomisk
9.1.1 Definition af problemet
MongoDB kan højst gemme eller opdatere ét dokument atomisk. Et eksempel på hvornår det er nødvendigt
at gemme flere rækker atomisk, i den relationelle database, er når en task skifter status. Når det sker,
beregnes en samlet statistik for hele projektet hvor sammentællinger gemmes i en tabel i MySQL som på
Figur 9-1 i 5 individuelle rækker for hvert projekt.
+----+------------+----------------+----------+------+------------+
| id | project_id | task_status_id | estimate | todo | task_count |
+----+------------+----------------+----------+------+------------+
| 26 |
6 |
1 |
18.0 | 23.0 |
21 |
| 27 |
6 |
2 |
0.0 | 0.0 |
0 |
| 28 |
6 |
3 |
0.0 | 0.0 |
2 |
| 29 |
6 |
4 |
0.0 | 0.0 |
6 |
| 30 |
6 |
5 |
22.0 | 18.0 |
94 |
+----+------------+----------------+----------+------+------------+
Figur 9-1: Tabellen project_statistics i MySQL databasen
9.1.2 Løsning: Indlejring
Løsningen på problemet er at indlejre alle data i et samlet dokument i en struktur som på Figur 9-2, for
derved at opnå atomiske egenskaber på den samlede datastruktur.
{
"_id": ObjectId("9872943ABC0928403"),
"project": ObjectId("98092383EF9238EF"),
"task_statistics": [
{"task_status_id": 1, "estimate": 18.0, "todo": 23.0, "task_count": 21},
{"task_status_id": 2, "estimate": 0.0, "todo": 0.0, "task_count": 0},
{"task_status_id": 3, "estimate": 0.0, "todo": 0.0, "task_count": 2},
{"task_status_id": 4, "estimate": 0.0, "todo": 0.0, "task_count": 6},
{"task_status_id": 5, "estimate": 22.0, "todo": 18.0, "task_count": 94}
]
}
Figur 9-2: Struktur for project_statistics
Det at danne den nye objektstruktur er signifikant anderledes når MongoDB benyttes frem for MySQL
hvilket kan ses på Figur 9-3 (MySQL) og Figur 9-4 (MongoDB). Eksemplet er en anelse kompliceret og
kræver derfor lidt forklaring, dog er det ikke nødvendigt at forstå alle kodelinierne for at forstå eksemplet.
Formålet med metoden, er for Figur 9-3 at gemme statistik i en struktur som på Figur 9-1, mens Figur 9-4
gemmer i en struktur som på Figur 9-2. For bedre at kunne illustrere forskelligheder og ligheder, er det
blevet forsøgt at bruge enslydende variabelnavne og udføre de enkelte trin i samme rækkefølge. Resultatet
af det sammentællende databasekald gemmes i variablen group_result, opdelt i forskellige task-statusser.
Syntaksen for at lave denne gruppering af data er i betydende grad forskellig fra den ene figur til den
28
anden, men resultatet er det samme. Efterfølgende gennemløbes statuses for at skabe et objekt for hver
række i resultatet. Det er her den store forskel er, for på Figur 9-3 gemmes hvert objekt som sin egen række
i databasen (save i #20 og transaction commit i #22), hvorimod på Figur 9-4 gemmes rækkerne som
indlejrede dokumenter i et andet dokument som derefter gemmes via et enkelt kald til save() (#04). På den
måde opnås der samme funktionalitet og skrive-isolering i begge systemer.
01: def self.update_statistics(project)
02:
ProjectStatistic.transaction do
03:
statuses = TaskStatus.all
04:
group_result = project.tasks.group(:task_status_id).
05:
select([:task_status_id, "sum(estimate) as sum_estimate",
06:
"sum(todo) as sum_todo", "count(*) as task_count"])
07:
statistics = project.statistics.to_a
08:
09:
statuses.each do |status|
10:
sum = group_result.find{|res| res.task_status_id == status.id}
11:
default = {:project => project, :task_status => status,
12:
:estimate => 0, :todo => 0, :task_count => 0}
13:
stat = statistics.find{|s| s.task_status_id == status.id} ||
14:
ProjectStatistic.new(default)
15:
unless sum.nil?
16:
stat.estimate = sum.sum_estimate || 0
17:
stat.todo = sum.sum_todo || 0
18:
stat.task_count = sum.task_count || 0
19:
end
20:
stat.save!
21:
end
22:
end
23: end
Figur 9-3: Brug af transaktion i MySQL til beregning af statistik via group_by som derefter gemmes i databasen
01: def self.update_statistics(project)
02:
ps = find_or_init_by_project(project)
03:
ps.task_statistics = ProjectTaskStatistic.calculate_statistics(project)
04:
ps.save!
05: end
06:
07: def self.calculate_statistics(project)
08:
statuses = TaskStatus.items
09:
group_result = Task.collection.group({
10:
:key => [:task_status_id],
11:
:cond => {:project_id => project.id},
12:
:reduce => "function(obj,prev) { prev.sum_estimate += obj.estimate || 0;
prev.sum_todo += obj.todo || 0; prev.task_count += 1; }",
13:
:initial => {:sum_estimate => 0, :sum_todo => 0, :task_count => 0}
14:
})
15:
16:
statuses.map do |status|
17:
default = {"sum_estimate" => 0, "sum_todo" => 0, "task_count" => 0}
18:
sum = group_result.find{|res| res["task_status_id] == status.id} || default
19:
ProjectTaskStatistic.new(
20:
:task_status => status,
21:
:estimate => sum["sum_estimate"] || 0,
22:
:todo => sum["sum_todo"] || 0,
23:
:task_count => sum["task_count"] || 0
24:
)
25:
end
26: end
Figur 9-4: Beregning af statistik i MongoDB uden brug af transaktioner, men i stedet ved at gemme et dokument med
underdokumenter atomisk
29
Metoden i Figur 9-4 ser umiddelbart ud til at levere samme resultat som den tilsvarende løsning med
MySQL, og fungerer som forventet i mit testmiljø, men som beskrevet i 7.3.4 har metoden visse
begrænsninger. På Figur 9-5 ses tilsvarende metode, men med brug af et MapReduce udtryk som ikke har
samme begrænsninger som metoden i Figur 9-4. Bemærk at der mellem <--JS og JS indlejres JavaScript til
brug for map og reduce metoder.
01: def self.update_statistics(project)
02:
ps = find_or_init_by_project(project)
03:
ps.task_statistics = ProjectTaskStatistic.calculate_statistics(project)
04:
ps.save!
05: end
06:
07: def self.calculate_statistics(project)
08:
map = <<-JS
09:
function() {
10:
emit(this.task_status_id, {task_count: 1, sum_estimate: this.estimate || 0, sum_todo:
this.todo || 0});
11:
}
12:
JS
13:
14:
reduce = <<-JS
15:
function(key, values) {
16:
var result = {task_count: 0, sum_estimate: 0, sum_todo: 0}
17:
values.forEach(function(value){
18:
result.task_count += value.task_count;
19:
result.sum_estimate += value.sum_estimate;
20:
result.sum_todo += value.sum_todo;
21:
});
22:
return result;
23:
}
24:
JS
25:
26:
task_stats = Task.collection.map_reduce(map, reduce, {
27:
:query => {:project_id => project.id},
28:
:raw => true,
29:
:out => {:inline => true}
30:
})["results"]
31:
32:
TaskStatus.items.map do |status|
33:
default = {"value" => {"sum_estimate" => 0, "sum_todo" => 0, "task_count" => 0}}
34:
sum = (task_stats.find{|x| x["_id"] == status.id} || default)["value"]
35:
ProjectTaskStatistic.new(
36:
:task_status => status,
37:
:estimate => sum["sum_estimate"] || 0,
38:
:todo => sum["sum_todo"] || 0,
39:
:task_count => sum["task_count"] || 0
40:
)
41:
end
42: end
Figur 9-5: MongoDB aggregering med MapReduce udtryk.
9.1.3 Evaluering
Indlejring fungerer rigtig godt i denne sammenhæng, fordi de data som statistikken består af, altid bruges
som et samlet objekt. Der opstår på intet tidspunkt en situation hvor kun dele af objektet er interessant for
en given funktionalitet. Dog skal der mere kompleks programmering til at danne de aggregerede data i
MongoDB, da man i stedet for at kunne bruge en mere simpel group-by syntaks i SQL, er nødt til at beskrive
et MapReduce udtryk som består af en blanding af Ruby og JavaScript. Det føles umiddelbart
overkompliceret ved en aggregering så simpel som i dette eksempel. Ud over den mere omfattende måde
at danne statistikken på, er der ikke en nævneværdig indvirkning på hverken applikationen eller
brugeroplevelsen.
30
9.2 Gemme flere dokumenter i isolation
9.2.1 Definition af problemet
Mange steder i applikationen gemmes kun ét objekt ad gangen hvilket gør brugen af en eksplicit
transaktion overflødig, men der findes visse metoder der gemmer mere end ét objekt på én gang hvor
transaktionsegenskaber er ønsket.
Et eksempel herpå ses på Figur 9-6 hvor en transaktion startes eksplicit (start #03, slut #09) hvorefter en
række tasks opdateres og gemmes.
01: def before_save_iteration
02:
if iteration_status_id_changed? && iteration_status_id == 5
03:
Task.transaction do
04:
tasks.select{|task| task.task_status_id == 3}.each do |task|
05:
task.task_status_id = 5
06:
task.save!
07:
end
08:
update_statistics
09:
end
10:
end
11: end
Figur 9-6: Metode der bruger transaktioner til at gemme i MySQL via ActiveRecord fra klassen Iteration
9.2.2 Løsning
Jeg har i denne situation ikke kunne finde en brugbar løsning, da det i MongoDB ikke er muligt at lave
atomiske dataoperationer på mere end ét dokument ad gangen. Resultatet heraf blev at fjerne
transaktionshåndteringen fra metoden. Opdateringen af tasks sker derfor i denne metode ikke længere i
isolation fra andre samtidige transaktioner. Det betyder at hvis der opstår en fejl undervejs, vil ikke alle
tasks være blevet opdateret i databasen.
Det kan være teknisk muligt at konstruere applikationen og mongodatabasen så ACID-egenskaber opnås
[40], men da ”hjemmelavede” databasetransaktioner frarådes af Stonebraker og Cattell [11], vælger jeg
ikke at eksperimentere med det.
9.2.3 Evaluering
Lige netop i denne situation er risikoen forbundet med den manglende transaktionsunderstøttelse ikke
særlig stor. En samtidig bruger vil evt. kunne se at ikke alle tasks i en iteration har skiftet status, men ved en
efterfølgende opdatering af websiden, vil alt se korrekt ud. Der er også den risiko at metoden kan fejle
halvvejs gennem loopet, men da operationen er så simpel som den er, anser jeg risikoen for værende
meget lille.
I andre systemer, fx banksystemer, vil dette formentlig være et langt større problem som ikke kan løses
med MongoDB. I sådanne situationer er en dokumentdatabase uden transaktionshåndtering ikke velegnet.
31
9.3
Modificerende dataoperationer
9.3.1 Definition af problemet
Hvis to brugere retter det samme dokument samtidig, vil kun den sidst gemte ændring blive gemt. I
forbindelse med denne case er det i langt de fleste tilfælde en risiko der er værd at løbe, men der er
situationer hvor dokumenter bliver store og komplicerede, og hvor det er en fordel at kunne gemme en del
af dokumentet, uden risiko for konflikt med andre opdaterende dataoperationer. Fx har klassen Task et
array af indlejrede dokumenter der til sammen repræsenterer en diskussion. Når der skal tilføjes et nyt
diskussionsindlæg, er det fristende at hente det komplette dokument fra databasen, tilføje indlægget og
gemme dokumentet (Figur 9-7). Men det vil betyde at hvis to brugere samtidig tilføjer et indlæg, vil kun det
seneste blive gemt. I stedet vil en opførsel som på Figur 9-8 være at foretrække.
Figur 9-7: App Server #2's tilføjelse til et array i Doc A bliver ignoreret når App Server #1 gemmer sin version af Doc A
Figur 9-8: To samtidige ændringer til Doc A hvor begge ændringer bliver bevaret.
9.3.2 Løsning
På Figur 9-9, den oprindelige MySQL løsning, ser man hvordan et diskussionsindlæg (TaskDiscussion) bliver
oprettet (#02), beriget med data (#02-#04) og gemt (#05). Berigelsen opretter, i dette tilfælde, automatisk
en reference til den korrekte task. Det resulterende databasekald ses i bunden af figuren.
32
01: def create
02:
@task_discussion = TaskDiscussion.new(params[:task_discussion])
03:
@task_discussion.user = @current_user
04:
@task_discussion.number = (TaskDiscussion.maximum("number", :conditions => ["task_id = ?",
@task_discussion.task_id]) || 0 ) + 1
05:
if @task_discussion.save
06:
redirect_to task_url(@task_discussion.task_id)
07:
else
08:
render "new"
09:
end
10: end
SQL (0.3ms) INSERT INTO `task_discussions` (`created_at`, `number`, `task_id`, `text`, `updated_at`,
`user_id`) VALUES ('2012-06-15 13:42:49', 5, 122, 'This is a new test comment', '2012-06-15 13:42:49',
1)
Figur 9-9: Oprettelse af TaskDiscussion ved hjælp af ActiveRecord og MySQL. Nederst ses det resulterende databasekald.
Den tilsvarende metode med brug af MongoDB ses på Figur 9-10. Dog skal man her lægge mærke til det
resulterende databasekald som fylder meget fordi det er det komplette dokument, baseret på klassen Task,
med tilhørende underdokumenter der gemmes i databasen, selvom det kun er en lille del af dokumentet,
der har ændringer.
Ser man i stedet på Figur 9-11, kan man se hvordan MongoDB, via de atomiske operationer (se 7.3.3), kan
tilføje et ekstra element til et eksisterende array i et eksisterende dokument, uden at skulle sende hele
dokumentet til databasen. Selvom forskellen i de to metoder (primært linie #05 og #06) er meget minimal,
er der stor forskel på resultatet. Figur 9-9 og Figur 9-11 er sammenlignelige da de begge formår at gemme
TaskDiscussion objektet i databasen, uden at opdatere eksisterende data, og med understøttelse af
tilføjelser til samme dokument fra flere samtidige brugere.
33
01: def create
02:
@task_discussion = TaskDiscussion.new(params[:task_discussion])
03:
@task_discussion.user = @current_user
04:
@task_discussion.number = (@task.task_discussions.map{|td| td.number}.sort.last || 0) + 1
05:
@task.task_discussions << @task_discussion
06:
if @task_discussion.valid? && @task.save
07:
redirect_to task_url(@task.id)
08:
else
09:
render "new"
10:
end
11: end
MONGODB (0ms) taskjunctiondevelopment['tasks'].update({:_id=>BSON::ObjectId('4fa03f3eeb327f4f5600006d')},
{"_id"=>BSON::ObjectId('4fa03f3eeb327f4f5600006d'), "title"=>"A task number 22",
"project_id"=>BSON::ObjectId('4fa03f3eeb327f4f5600002b'),
"created_user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'),
"updated_user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "owner_user_id"=>nil,
"task_status_id"=>1, "description"=>"sdcsc", "task_type_id"=>1, "iteration_id"=>nil,
"created_at"=>2012-01-15 10:10:55 UTC, "updated_at"=>2012-05-30 18:15:45 UTC,
"task_discussions"=>[{"_id"=>BSON::ObjectId('4fc65d66eb327f0f740000c4'), "text"=>"mclsdmc",
"user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "number"=>1, "created_at"=>2012-05-30
17:48:22 UTC, "updated_at"=>2012-05-30 18:15:45 UTC},
{"_id"=>BSON::ObjectId('4fc666fceb327f0f7400014e'), "text"=>"dssc",
"user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "number"=>3, "created_at"=>2012-05-30
18:29:16 UTC}, {"_id"=>BSON::ObjectId('4fc667daeb327f0f7400018f'), "text"=>"sdsdc",
"user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "number"=>4, "created_at"=>2012-05-30
18:32:58 UTC}, {"_id"=>BSON::ObjectId('4fdb3177452a65248400003b'), "text"=>"New comment on task.",
"user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "number"=>5, "created_at"=>2012-06-15
12:58:31 UTC}], "testcases"=>[{"_id"=>BSON::ObjectId('4fc66ce2eb327f12d400009b'), "title"=>"test
again", "test_status_id"=>1, "description"=>"", "test_comment"=>"", "created_at"=>2012-05-30
18:54:26 UTC, "tested_by_user_id"=>nil}]})
Figur 9-10: Oprettelse af TaskDiscussion ved hjælp af MongoDB og MongoMapper uden brug af modificerende dataoperator.
Resulterende databasekald ses nederst.
01: def create
02:
@task_discussion = TaskDiscussion.new(params[:task_discussion])
03:
@task_discussion.user = @current_user
04:
@task_discussion.number = (@task.task_discussions.map{|td| td.number}.sort.last || 0) + 1
05:
if @task_discussion.valid? && @task.push(:task_discussions => @task_discussion.to_mongo)
06:
redirect_to task_url(@task.id)
07:
else
08:
render "new"
09:
end
10: end
MONGODB (0ms) taskjunctiondevelopment['tasks'].update({:_id=>{"$in"=>[BSON::ObjectId('4fa03f3eeb327f4f5600006d')]}},
{"$push"=>{:task_discussions=>{"_id"=>BSON::ObjectId('4fdb3177452a65248400003b'), "text"=>"New comment
on task.", "user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000005'), "number"=>5, "created_at"=>2012-0615 12:58:31 UTC}}})
Figur 9-11: Metode der gør det samme som ovenstående, men ved hjælp af atomisk operator. Resulterende databasekald ses nederst.
9.3.3 Evaluering
Den SQL-baserede udgave er den mest simple, både set ud fra koden i RoR og den resulterende SQL, men
løsningen i MongoDB på Figur 9-11 kommer meget tæt på at levere et tilsvarende resultat i begge
henseender. Udgaven på Figur 9-10 kan kun bruges hvis antallet af samtidige skrivninger til det samme
dokument er begrænset, så risikoen for kollision formindskes. For brugeren er Figur 9-9 og Figur 9-11 lige
gode, mens Figur 9-10 vil kunne resultere i tab af data.
34
9.4 Cursor read consistency
9.4.1 Definition af problemet
Når der mod en MongoDB collection udføres et find-kald som vil returnere mange dokumenter, er der en
teoretisk mulighed for at få returneret det samme dokument mere end én gang [41]. Det skyldes at
MongoDB ikke understøtter transaktioner og at MongoDB ikke returnerer selve dokumenterne, men i
stedet en cursor (se 7.3.2.1) som driveren eller shell’en bruger til at hente de egentlige dokumenter fra
databasen. Der opstår et problem hvis et dokument der allerede er læst af cursoren, bliver redigeret
sideløbende med cursorens levetid, og dokumentets nye størrelse gør at det skal relokeres på disken. I
sådanne tilfælde er der en sandsynlighed for at det samme dokument figurerer mere end én gang i listen af
resultater.
9.4.2 Løsning
MongoDB tilbyder en snapshot operator (se Figur 9-12) [41] som garanterer at et dokument ikke returneres
mere end én gang, men brugbarheden er desværre begrænset. For det første kan resultatet ikke sorteres,
for det andet kan der ikke bruges andre indekser end primærindekset _id. Ud over dette forhold, er den
officielle dokumentation generelt uklar når det kommer til spørgsmålet om hvordan cursoren vil reagere på
dokumenter der oprettes eller slettes mens cursoren er aktiv.
db.users.find().snapshot() /* Vil fungere */
db.users.find().sort({username: 1}).snapshot() /* Vil ikke fungere */
db.users.find({username: ”bruger1”}).snapshot() /* Vil fungere, men vil ikke bruge indekset på
username */
Figur 9-12: Brug af snapshot i MongoDB.
9.4.2.1
9.4.3 Evaluering
Løsningen i MongoDB kan, på grund af manglende brug af indekser, ikke bruges som en del af IT-løsningen.
Funktionaliteten kan udelukkende bruges til datareplikering og vedligeholdelse. Man må formode at
risikoen for at dette problem viser sig i produktion er forsvindende lille, og det er derfor nødvendigt at
vurdere om det er en risiko man kan leve med. Er meget konsistente læsninger vigtige for brugen af
applikationen, vil MySQL eller anden database der overholder ACID princippet, være en bedre løsning. I
konteksten af den applikation som denne case beskæftiger sig med, er det min vurdering at denne form for
inkonsistens ikke vil have en betydelig indvirkning på brugen af applikationen.
9.5 Database constraints
9.5.1 Definition af problemet
Som nævnt i 0 håndterer dokumentdatabaser, her under MongoDB, hverken not-null constraints eller
fremmednøgler, og på grund af databasens manglende skemadefinitioner, findes der heller ikke datatype
constraints. Selvom applikationslaget håndterer alle disse typer constraints, vil det være teknisk muligt at
skabe inkonsistens når flere brugere benytter systemet samtidig. En bruger vil eksempelvis kunne oprette
et nyt dokument A, der refererer dokumentet B, mens en anden bruger sletter dokumentet B samtidig. Et
eksempel på dette har jeg illustreret på Figur 9-13. To app servere, i dette tilfælde RoR, modtager hver
deres HTTP forespørgsel samtidig (#1 og #2). App server 1 kontrollerer i #3 om dokumentet B findes, inden
35
det i #8 opretter dokumentet A med reference til B. I mellemtiden har app server #2 slettet dokumentet B
og alle referencer til det i #5 og #6, men fordi databasen ikke håndhæver constraints, vil #8 resultere i at
dokumentet A, der refererer det nu slettede dokument B, blive gemt i databasen.
Figur 9-13: Inkonsistente dokumentreferencer i en dokumentdatabase uden håndtering af constraints på databaseniveau
9.5.2 Løsning med unikke nøgler
Der er én type constraints som MongoDB selv kan håndhæve, og det er unikke indekser [42]. Alle
opdaterende kald til MongoDB er som standard fire-and-forget, det vil sige, hvis der sendes en gem
forespørgsel, venter man ikke på at se om databasen returnerer en fejl eller ej. Det er dog muligt eksplicit,
at sende en forespørgsel som et safe-mode kald, som venter på svar fra databasen. De steder hvor jeg har
haft brug for at ændre eller oprette værdier omfattet af et unikt indeks, har jeg brugt safe-mode kald.
På Figur 9-14 ses et eksempel på klassen User og den tilsvarende controller UserController der har til
formål at gemme en ny bruger i databasen. Bemærk, eksemplerne er forsimplet for at understrege hvad
der er vigtigt i denne sammenhæng. I #04 defineres feltet email som værende unikt, påkrævet og i et
format der overholder et bestemt regulært udtryk. Disse tre valideringer sikres i #18, når objektet user
gemmes i databasen. Dog er disse tre valideringer håndteret af appikationen (RoR), og uden #08 som
definerer et unikt indeks, vil databasen ikke afvise et dokument med en ikke unik e-mail-adresse. Metoden
ensure_indexes i #07 eksekveres ikke automatisk, men som en del af et build script (i RoR kaldet rake). Når
det unikke indeks eksisterer og en ikke-unik e-mail-adresse gemmes, vil #08 returnere en fejl, såfremt :safe
er sat til true. Er værdien af :safe ikke true, vil der ikke blive returneret en fejl, men dokumentet vil stadig
ikke blive gemt.
36
01: class User
02:
include MongoMapper::Document
03:
key :password, String
04:
key :email, String, :required => true, :unique => true, :format => /\A([^@\s]+)@((?:[-a-z09]+\.)+[a-z]{2,})\Z/i
05:
key :name, String, :required => true
06:
07:
def self.ensure_indexes
08:
User.ensure_index [[:email, 1]], :unique => true
09:
User.ensure_index [[:task_summary_mail_time, 1], [:task_summary_mail_time_hour, 1]]
10:
end
11: end
12:
13: class UsersController < ApplicationController
14:
def create
15:
@user = User.initialize_by_email(params[:user][:email])
16:
@user.attributes = params[:user]
17:
@user.change_password(params[:password], params[:confirm_password])
18:
if @user.errors.empty? && @user.save(:safe => true)
19:
respond_with @user do |format|
20:
format.html {redirect_to :controller => "users", :action => "done"}
21:
end
22:
else
23:
respond_with @user { |format|}
24:
format.html {render "new"}
25:
end
26:
end
27:
end
29: end
Figur 9-14: Forsimplet eksempel på klassen User og tilsvarende UserController med metode til at gemme en ny User
Bemærk, fordi det er driveren der håndterer brugen af safe mode, er log-outputtet fra ovenstående kode
ens, uanset om :safe => true benyttes eller ej.
9.5.3 Løsning med indlejrede dokumenter
Løsningen fra 9.1.2 hvor dokumenter indlejres i andre dokumenter, er også relevant i denne sammenhæng.
På Figur 9-15 ses hvordan jeg har indlejret data for TaskDiscussion og Testcase i en samlet Task på
henholdsvis linie #05 og #13. Disse dataentiteter var i den relationelle model, som det kan ses på Figur
9-16, gemt i hver deres tabel med fremmednøgler imellem. Ved at indlejre på denne måde, vil alle
relaterede TaskDiscussions og Testcases blive slettet hvis den overordnede task slettes.
01: {
02:
"_id" : ObjectId("4fa03f3feb327f4f56000103"),
03:
"title" : "Ensret brugernavn til altid at være stort",
04:
"project_id" : ObjectId("4fa03f3feb327f4f56000090"),
05:
"task_discussions" : [
06:
{
07:
"_id" : ObjectId("4fa03f3feb327f4f56000104"),
08:
"text" : "Denne opgave er en god ide",
09:
"user_id" : ObjectId("4fa03f3eeb327f4f56000005"),
10:
"number" : 1
11:
}
12:
],
13:
"testcases" : [
14:
{
15:
"_id" : ObjectId("4fa03f3feb327f4f56000105"),
16:
"title" : "Kontroller at brugernavne aldrig er med småt og altid er med stort.",
17:
"test_status_id" : 1
18:
}
19:
]
20: }
Figur 9-15: Indlejring af dokumenter for at undgå referencer
37
Figur 9-16: Struktur for Task, Testcase og TaskDiscussion i den relationelle model, med referencer imellem
9.5.4 Evaluering
Ovenstående løsninger i MongoDB viser hvordan man, i visse tilfælde, kan opnå samme håndhævelse af
constraints som i MySQL. Med de indlejrede dokumenter opnås en funktionalitet der på mange måder
svarer til en brug med fremmednøgler med cascading delete i MySQL. At databasen ikke selv kan
håndhæve alle former for constraints, gør at databasen på et tidspunkt formentlig vil indeholde
inkonsistente data, der ikke lever op til applikationens valideringsregler. Dette er et resultat af ikke at have
noget foruddefineret databaseskema. Slutbrugerens oplevelse af systemet vil formentlig ikke blive påvirket
i særlig høj grad. Jeg forventer kun at brugeren vil opleve problemer hvis applikationen ikke i tilstrækkelig
grad håndterer situationen hvor et refereret dokument eller en værdi mangler eller er ugyldig.
9.6 Databasejoins og MongoDB
9.6.1 Definition af problemet
I MySQL kan man konstruere joins mellem tabeller, og fx kun hente data fra en tabel hvor om det gælder at
en refereret række i en anden tabel, har en værdi der lever op til et bestemt kriterie. Ydermere er det
muligt via RoR’s ActiveRecord at specificere hvilke refererede tabeller der skal hentes fra databasen via
joins, så man slipper for et N+1 problem (se 7.4.1.1). Det vil sige hvis man for alle rækker i tabellen Tasks,
har brug for brugerne defineret via feltet owner_user_id, kan man via ActiveRecord og MySQL hente både
fra tabellen Tasks og Users via ét SQL query. Et tilsvarende kald uden denne funktionalitet, vil resultere i et
opslag i tabellen Users for hver række der hentes i tabellen Tasks. Det er ikke muligt i MongoDB at lave en
forespørgsel der involverer et join. Der kan kun hentes data fra én collection ad gangen.
9.6.2 Oprindelig løsning med ActiveRecord
På Figur 9-17 ses hvordan brugere og projekter er relateret til hinanden i den relationelle database. En
optimal SQL for at hente alle brugere der er medlem af de samme projekter som en given bruger er
medlem af ses på Figur 9-18. De forespørgsler der bliver lavet mod databasen i denne case er dog ikke altid
optimeret i samme grad. Det skyldes at biblioteket, ActiveRecord for MySQL og MongoMapper for
MongoDB, håndterer det at opbygge en databaseforespørgsel hvilket ikke altid bliver optimeret i
tilstrækkelig grad. I afsnit 9.6.3 kan man se hvordan N+1 problemet viste sig i MongoDB løsningen, inden
der blev lavet den nødvendige optimering.
38
Figur 9-17: Model af hvordan brugere er tilknyttet projekter via en mange-til-mange relation
SELECT DISTINCT
u.*
FROM project_members pm1
INNER JOIN project_members pm2 ON pm2.project_id = pm1.project_id
INNER JOIN users u ON u.id = pm2.user_id
WHERE
pm1.user_id = :user_id;
Figur 9-18: SQL der kan hente alle brugere der er medlem af et eller flere projekter som :user_id er medlem af
På Figur 9-19 ses den RoR kildekode der resulterer i den SQL forespørgsel der ses på Figur 9-20.
Forespørgslen adskiller sig fra den optimale SQL ved at være opdelt i to mindre SQLer, hvor først alle
brugerens projekter hentes, hvorefter brugere hentes på baggrund af medlemskaber til disse projekter. Vi
kan se på RoR kildekoden at én linie kode kan resultere i mere end én forespørgsel mod databasen.
def self.members_of_users_projects(user)
User.joins(:project_members).where(:project_members => {:project_id => user.projects}).uniq
end
Figur 9-19: RoR metode for at hente alle brugere i en relationel database som er medlem af et eller flere af de samme projekter
som user.
Project Load (0.1ms) SELECT `projects`.* FROM `projects` INNER JOIN `project_members` ON
`projects`.`id` = `project_members`.`project_id` WHERE `project_members`.`user_id` = 1
User Load (0.1ms) SELECT DISTINCT `users`.* FROM `users` INNER JOIN `project_members` ON
`project_members`.`user_id` = `users`.`id` WHERE `project_members`.`project_id` IN (1, 2, 6,
13, 16, 18, 19)
Figur 9-20: Resulterende SQLer fra ovenstående metode.
9.6.3 Løsning med MongoMapper
Når den samme funktionalitet skal implementeres i MongoDB, er det ikke muligt at benytte databasejoins.
På grund af det, er man er nødt til at lave mindst ét kald til databasen per collection der skal hentes data
fra. Fordi objekterne af klassen ProjectMember i denne model ikke ligger i deres egen collection, men er en
del af samme collection som Project, er det kun 2 collections der er i spil. På Figur 9-21 ses en forespørgsel
gennem MongoMapper der minder meget om den vi lige har set på Figur 9-19, men hvis vi kigger på de
resulterende databasekald på Figur 9-22, kan vi se at der er betydeligt flere. Vi er her løbet ind i N+1
problemet hvor hver bruger hentes én ad gangen.
39
def self.members_of_users_projects(user)
user.projects.flat_map{|p| p.project_members.map{|pm| pm.user}}.uniq
end
Figur 9-21: Uoptimeret udgave af metode der kan hente brugere fra MongoDB som er medlem af et eller flere af de samme
projekter som user.
MONGODB (0ms)
taskjunction['projects'].find({:"project_members.user_id"=>BSON::ObjectId('4fa03f3eeb327f4f5600
0005')}).sort([["title", 1]])
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4fa03f3eeb327f4f56000029')})
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa935beb327f1315000050')})
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa9360eb327f1315000060')})
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa9367eb327f1315000072')})
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa936ceb327f1315000086')})
MONGODB (0ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa9372eb327f131500009c')})
MONGODB (1ms) taskjunction['users'].find({:_id=>BSON::ObjectId('4faa9377eb327f13150000b4')})
Figur 9-22: Resulterende databasekald fra ovenstående metode.
For at opnå samme antal databasekald som i implementeringen med ActiveRecord, har jeg optimeret
metoden baseret på MongoMapper til at lave to lignende forespørgsler hvor først projekter hentes og
derefter brugere, resulterende i tilsvarende 2 kald.
def self.members_of_users_projects(user)
User.find(user.projects.flat_map{|p| p.project_members.map{|pm| pm.user_id}}.uniq)
end
Figur 9-23: Optimeret udgave af ovenstående metode.
MONGODB (0ms)
taskjunction['projects'].find({:"project_members.user_id"=>BSON::ObjectId('4fa03f3eeb327f4f56000
005')}).sort([["title", 1]])
MONGODB (0ms)
taskjunction['users'].find({:_id=>{"$in"=>[BSON::ObjectId('4fa03f3eeb327f4f56000005'),
BSON::ObjectId('4fa03f3eeb327f4f56000029'), BSON::ObjectId('4faa935beb327f1315000050'),
BSON::ObjectId('4faa9360eb327f1315000060'), BSON::ObjectId('4faa9367eb327f1315000072'),
BSON::ObjectId('4faa936ceb327f1315000086'), BSON::ObjectId('4faa9372eb327f131500009c'),
BSON::ObjectId('4faa9377eb327f13150000b4')]}})
Figur 9-24: Resultat af ovenstående metode. Bemærk at der laves samme antal databasekald som i ActiveRecord-versionen.
9.6.4 Evaluering
Med MySQL er det muligt at optimere databaseforespørgslen bedre hvis den designes uden om
ActiveRecord hvor ved der opnås bedre performance. Hvis man lader det være op til ActiveRecord at danne
forespørgslen, bliver den resulterende SQL mindre forudsigelig. Selv med MongoMapper er det nødvendigt
at være opmærksom på de forespørgsler der sendes til databasen fra biblioteket, og lave de nødvendige
optimeringer. Min erfaring er, uanset om der bruges ActiveRecord eller MongoMapper, kan man opnå en
tilfredsstillende performance og undgå N+1 problemet, men ikke uden optimeringer der er specifikke for
den pågældende databasetype. Selv med et bibliotek som ActiveRecord eller MongoMapper er det
nødvendigt at have grundlæggende viden om hvordan databasen fungerer.
40
9.7 Primærnøgler ændres
9.7.1 Definition af problemet
Alle primærnøgler i MySQL databasen er defineret som unsigned integer-felter med navnet Id. Felterne er i
MySQL databasen et fortløbende heltal, mens det tilsvarende felt i MongoDB, som standard, er en
hexdecimal streng [43] som minder om en kortere udgave af en UUID (Universally unique identifier). Disse
felter bruges i høj grad overalt i applikationen og især i url’er. Fx vil urlen /tasks/13 referere til en task med
id 13. Selvom det er muligt at bruge de eksisterende felters værdier, kan der være fordele i at bruge nye
værdier genereret af MongoDB, hvorved det sikres at værdierne er unikke, selv i et distribueret miljø med
flere replikaer (se 7.3.5) eller shards (se 7.3.6).
9.7.2 Løsning
Jeg skulle beslutte hvordan alle dokumenters unikke primærnøgler skulle dannes. Der fandtes, i den
relationelle database, allerede primærnøgler hvor et felt med navnet ID havde et autogenereret heltal unikt
for den givne tabel. I forbindelse med konverteringen var der to muligheder:


Beholde de gamle heltalsbaserede nøgler.
Danne nye universelt unikke nøgler.
Fordelen ved at beholde nøglerne som de er, er at referencer mellem objekter forbliver de samme, og en
konvertering af data bliver derfor meget nemmere. Derudover, fordi nøglerne bliver brugt til at identificere
et objekt via en URL, fx. domain.com/tasks/37, vil alle eksisterende dybe links fortsætte med at fungere
efter migrationen.
Ulempen ved at beholde nøglerne i den oprindelige form, er at de strider mod konventionen for brug af
primærnøgler i MongoDB. Det er også usikkert om alle frameworks kan håndtere heltal som nøgler.
Derudover kan disse nøgler heller ikke bruges i et driftsmiljø med flere replikerede noder (se 7.3.5). Når der
bruges mere end én node, skal hver node kunne danne egne nøgler, uden først at kommunikere med
resten af noderne for at sikre en unik nøgle.
Jeg valgte at generere nye nøgler til alle objekter der blev konverteret. Det stillede større krav til
datakonverteringen, idet tabellerne skulle konverteres i en bestemt rækkefølge, for at sikre at de
refererede objekter havde fået nye nøgler inden en reference kunne oprettes. Det vil sige at
konverteringen startede med de objekter der lå nederst i objekttræet, for derefter at konvertere opad i
træet. At alle objekterne tilsammen kunne betragtes som et træ, og at der ikke var cirkulære referencer,
var en stor fordel.
Det kunne også have været en mulighed at gemme den gamle nøgle i et andet felt i dokumentet, så begge
nøgler var til stede i alle dokumenter. På den måde ville dybe links stadig kunne fungere.
9.7.3 Evaluering
Det er svært at sammenligne de to løsninger, da de adresserer to vidt forskellige problemer. I MySQL drejer
det sig udelukkende om at danne nøgler der er unikke for den givne tabel, mens løsningen i MongoDB
adresserer det faktum at MongoDB er et distribueret system hvor flere dokumenter kan fødes samtidig i
samme collection, men på forskellige shards (se 7.3.6). Det stiller krav til at nøglerne kan dannes
distribueret frem for via en enkel primær node.
41
9.8 Sikkerhed: Mass assignment
9.8.1 Definition af problemet
Den relationelle database begrænser indirekte antallet af felter der kan skrives til databasen. Hvis der bliver
forsøgt gemt et felt der ikke allerede er defineret af databasens metadata, vil hele opdateringen af rækken
producere en fejl. Som nævnt i 7.2.1 har dokumentdatabasen ikke noget foruddefineret skema, hvilket
åbner op for muligheden for automatisk oprettelse af nye felter som derefter bliver gemt i databasen. Er
applikationen ikke beskyttet mod det, vil en ondsindet bruger kunne ændre i en HTML form, ved at tilføje
ekstra felter som den ondsindede bruger derefter vil få gemt i databasen.
9.8.2 Løsning
For at beskrive løsningen, vil jeg tage udgangspunkt i klasserne User og UserController, se forsimplet
kodeeksempel på Figur 9-25, og forklare problemet i flere detaljer. På #15 bliver alle attributter som
variablen params indeholder kopieret til objektet user. Det er her værd at bemærke, at params er en hash
med key/value pairs. Det vil sige hvis params indeholder en key med navnet ”name”, vil værdien af denne
blive kopieret til et felt med samme navn på objektet user. Hvis params indeholder en key som ikke allerede
findes i user, vil et nyt felt automatisk blive oprettet i #15 og gemt i databasen i #16.
Løsningen her findes i #08 hvor en white list over felter bliver defineret. Listen definerer hvilke felter der
automatisk kan sættes via massetildelingen i #15. Med en SQL-baseret database, kan man være fristet til
udelukkende at definere en black list (samme syntax, blot metoden attr_protected i stedet), idet ikke
eksisterende felter automatisk er black listet.
01: class User
02:
include MongoMapper::Document
03:
04:
key :password, String
05:
key :email, String, :required => true, :unique => true, :format => /\A([^@\s]+)@((?:[-a-z09]+\.)+[a-z]{2,})\Z/i
06:
key :name, String, :required => true
07:
08:
attr_accessible :name, :email
09: end
10:
11: class UsersController < ApplicationController
12:
def update
13:
@user = User.find! params[:id]
14:
restrict_access unless @user.allow_update?(@current_user)
15:
@user.attributes = params[:user]
16:
if @user.save(:safe => true)
17:
respond_with @user do |format|
18:
format.html {redirect user_url(@user)}
19:
end
20:
else
21:
respond_with @user do |format|
22:
format.html {render "edit"}
23:
end
24:
end
25:
end
26: end
Figur 9-25: Forsimplet udgave af klasserne User og UserController
42
9.8.3 Evaluering
Det vil have alvorlige konsekvenser hvis man i MongoDB glemmer at lave en white list over felter der kan
bruges i mass assignment. Konsekvenserne vil være mere alvorlige end ved brug af fx MySQL eller anden
lignende database. Med det sagt, er det dog værd at nævne at løsningen vil fungere både med en
dokumentdatabase og en relationel database, og vil kunne fungere som en best practice for alle
databasetyper.
9.9 Databasetilgang via automatiserede test
9.9.1 Definition af problemet
Forud for at køre automatiserede unit tests er det nødvendigt at opsætte testdata, også kaldet test fixtures.
Det er nødvendigt at sikre ensartede test fixtures, hver gang en test udføres, og at en test ikke indirekte
påvirker en anden test når testdata manipuleres.
9.9.2 Løsning
Når ActiveRecord bruges sammen med en SQL-baseret database, kan test fixtures sættes op via YAML-filer
(se Figur 9-26). Filerne indlæses automatisk i de korrekte tabeller med referencer til andre tabeller, forud
for testframeworkets opstart. For at sikre konsistente testdata mellem tests, bruges transactional teardown
[44] som sikrer at der udføres en rollback på databasetransaktionen i stedet for en commit, efter hver test.
På den måde sikrer man ens testdata for alle tests.
Med MongoDB er der ikke den samme mulighed. For det første understøtter biblioteket til automatisk
opsætning af test fixtures via YAML ikke MongoDB. For det andet kan man, på grund af manglende
understøttelse af databasetransaktioner, ikke bruge transactional teardown. Testdata opsættes i stedet via
factories (se Figur 9-27), der kan oprette testdata når der opstår behov for dem. For at sikre at tests ikke
påvirker test fixtures som er nødvendige for udførslen af andre tests, har alle tests der manipulerer data,
fået et dedikeret sæt test fixtures. At genindlæse en ny testdatabase mellem udførslen af hver test er en
mulighed, men det vil påvirke performance i negativ grad, og skalerer dårligt med antallet af nye tests.
task_01:
project: dummy_project_1
title: Task title 01
description: Long description of task 01
task_status_id: 1
task_type_id: 1
task_priority_id: 4
created_user: holger
owner_user: holger
estimate: 30
todo: 30
created_at: 2011-07-26T14:59:34+02:00
updated_at: 2011-07-26T14:59:34+02:00
Figur 9-26: Eksempel på ActiveRecord test fixture i YAML format.
43
factory :task_01, :class => Task do
id "1094274387"
association :project, factory: :dummy_project_1
title "Task title 01"
description "Long description of task 01"
task_status_id 1
task_type_id 1
task_priority_id 4
association :created_user, factory: :holger
association :owner_user, factory: :holger
estimate 30
todo 30
created_at Time.parse("2011-07-26T14:59:34+02:00")
updated_at Time.parse("2011-07-26T14:59:34+02:00")
end
Figur 9-27: Eksempel på factory til brug for tests med MongoDB i Ruby.
9.9.3 Evaluering
Brugen af test fixtures og transactional teardown i en relationel database er en integreret del af RoR, og er
derfor nemt at opsætte og bruge. Med MongoDB er det mere omfattende at opsætte de nødvendige test
fixtures. På grund af den manglende understøttelse af databasetransaktioner eller alternativ hertil i
MongoDB, er det svært at holde test fixtures konsistente, og fordi løsningen dikterer at testdata der
manipuleres af en test, ikke må bruges af andre tests, stiger risikoen for menneskelige fejl.
Test fixtures formateret som på Figur 9-26 er både lettere at skrive og læse end test fixtures formateret
som på Figur 9-27. Man kan argumentere for at den ene syntaks kan konverteres til den anden, men om
det er besværet værd når der blot er 20-30 af denne type test fixtures, er op til den enkelte at vurdere.
9.10 Migration af databaseskema
9.10.1 Definition af problemet
I en SQL-baseret database, med et fast foruddefineret skema, kan man finde sig nødsaget til at versionere
individuelle ændringer til databasens metadata for at sikre at databasens metadata er up to date, i
forbindelse med løbende vedligehold og ændringer.
9.10.2 Løsning
Som beskrevet i 7.5, består RoR frameworket bl.a. af et bibliotek til håndtering af databaseændringer i en
SQL-baseret database. Det fungerer ved at man danner filer med timestamps som filnavne, hvori
databaseændringer beskrives via Ruby. Se et eksempel på en databasemigration på Figur 9-28 hvor feltet
last_login_ip tilføjes til tabellen users. Kommandolinjeværktøjet ”rake” bruges derefter til at opdatere en
test-, udviklings- eller produktionsdatabase til en given databaseversion.
Fordi MongoDB ikke har noget foruddefineret skema, er det derfor ikke nødvendigt at holde metadata ved
lige på samme måde. Min erfaring fra casen har vist mig at det kun er nødvendigt at oprette de nødvendige
indekser, hvilket for denne case vedkommende, betyder 10 linjer kode. Dog kan det i en driftssituation, vise
sig nødvendigt at lave datamanipulation når objektmodellen ændres for at opfylde nye krav, fx hvis et felt
ændrer navn. Versionering og sikring af scripts der udfører denne datamanipulation, må håndteres på
anden vis, fx via et bibliotek til formålet, eller hvis mængden er tilpas lille, på en mere lavpraktisk facon.
44
class AddLastLoginIpToUsers < ActiveRecord::Migration
def self.up
add_column :users, :last_login_ip, :string, :limit => 16
end
def self.down
remove_column :users, :last_login_ip
end
end
Figur 9-28: ActiveRecord migration der tilføjer feltet last_login_ip til tabellen users.
9.10.3 Evaluering
Løsningen med databasemigrationer i ActiveRecord fungerer godt når man benytter en af de understøttede
databaser (MySQL, PostgreSQL, SQLite, SQL Server, Sybase og Oracle) [45], og det gør det muligt at benytte
forskellige databaser i test- og udviklingsmiljø. Fx har jeg i det oprindelige design benyttet SQLite i
testmiljøet og MySQL i udviklings- og produktionsmiljø. Benyttes en anden relationel database end
førnævnte understøttede databaser, fx DB2 eller Interbase/Firebird, vil der kun være delvis eller ingen
understøttelse, og biblioteket kan måske ikke benyttes.
Fordi MongoDB ikke har et fast foruddefineret skema, bliver hele databasemigrationsbiblioteket stort set
overflødigt. Det er min erfaring at man som udvikler i højere grad kan koncentrere sig om udviklingen af
forretningsobjekter, uden i særlig høj grad at tage hensyn til hvordan disse skal gemmes i en database.
9.11 Yderligere bemærkninger til analysen
Det at finde og implementere løsninger på datalogiske problemer, har ikke været den eneste tidskrævende
opgave i forbindelse med udførslen af casen. Det at finde, opsætte og lære at bruge de nødvendige 3. parts
biblioteker (ud over MongoDB, MongoDB driveren og Ruby on Rails), har været en undervurderet opgave.
Selvom der findes meget information om hvordan en given problemstilling kan løses ved hjælp af det valgte
bibliotek, bliver brugen af denne information kompliceret i takt med at biblioteker og teknologier udvikler
sig, og inkompatibilitet opstår. For at få de automatiserede tests til at understøtte en dokumentdatabase
(se 9.9), skulle jeg bruge uforholdsmæssigt meget tid på at finde og lære at bruge nye biblioteker.
Ud over udskiftning af biblioteker og tilpasningerne beskrevet i analyseafsnittene 9.1 til 9.10, var det ikke, i
betydelig grad, nødvendigt at lave tilpasninger i webapplikationens design og arkitektur. Dette skyldes
formentlig applikationens høje abstraktionsniveau og den begrænsede brug af SQL.
Fordi jeg har afgrænset mig fra (se 4) at analysere systemets ydelse, har jeg ikke lavet en decideret
performancetest af systemet, før og efter migrationen. Stikprøver viser dog at migrationen ikke har haft
nogen betydelig indflydelse på performance, hverken med eller uden brug af replikering. Stikprøverne er
lavet med en database med ca. 300.000 rækker/dokumenter og kun få samtidige brugere, kørende på en
almindelig kontor-PC.
Hele webapplikationen blev gennem dette projekt migreret over på MongoDB, hvorefter der for
slutbrugerens vedkommende, ikke er nogle nævneværdige forskelle at bemærke. Det var muligt at
konvertere alle eksisterende data til dokumentdatabasen uden nogen former for datatab.
Dokumentdatabasens skemafrie arkitektur gjorde i øvrigt datakonverteringen til en nem og hurtigt
overstået opgave.
45
10 Diskussion
Problemformuleringen (se 2) og indledningen (se 1) til dette projekt forholder sig til dokumentdatabaser på
et generelt plan, mens resten af rapporten i høj grad benytter sig af den specifikke
dokumentdatabaseimplementering MongoDB. Rapporten har til tider været farvet af at min metode
beskæftiger sig med en case hvori MongoDB indgår. Området omkring dokumentdatabaser under
overbegrebet NoSQL, er et relativt nyt område hvor det endnu er svært at beskrive forhold og egenskaber i
generelle termer. Derfor har det været nødvendigt at tage udgangspunkt i eksisterende implementeringer,
heriblandt især MongoDB. Det vil sige at hvad der er gældende for MongoDB, ikke nødvendigvis er
gældende for andre databasesystemer der går under betegnelsen dokumentdatabase.
Man kan argumentere for at min case til tider er for simpel til at udfordre brugen af en anden
databasetype. Fx indeholder casen ikke underklasser der nedarver fra superklasser eller datastrukturer der
kræver mange samtidige JOIN’s for at få et brugbart resultat. Ligeledes har der ikke været brugt
funktionalitet som kun er tilgængelig i MySQL, ej heller stored procedures, triggers eller views. Ved en mere
kompleks objektstruktur eller relationel struktur, vil forskellene på brugen af dokumentdatabasen og den
relationelle database måske være mere markante.
I min forforståelse (se 6) vurderer jeg inden påbegyndelsen af projektet at min eksisterende objektmodel
og datastruktur er avanceret nok, til at blive brugt i dette projekt. Selvom projektet måske er mere
avanceret end mange af de succeshistorier man til tider læser på Internettet, viste det sig alligevel at
projektet gerne måtte have haft en mere avanceret brug af databasesystemet.
At dokumentdatabasen ikke har noget fast foruddefineret skema, er et faktum der til tider er
underrepræsenteret i dette projekt. Dette skyldes ikke at den skemaløse arkitektur er ubetydelig, men
nærmere at det er en egenskab der blot eksisterer og ikke stiller betydelige krav til brugeren af
databasesystemet.
11 Konklusion
Jeg har gennem mit arbejde med dokumentdatabaser, her under MongoDB, belyst væsentlige forskelle
mellem den relationelle database og dokumentdatabasen/MongoDB (se 7.2, 7.3, 9). Derudover har jeg,
gennem udarbejdelse og analyse af min case, fundet frem til problemstillinger som kan være at finde i en
migrationsproces fra relationel database til dokumentdatabase. For hver problemstilling har jeg afprøvet og
beskrevet brugbare løsningsforslag, hvor muligt.
Som beskrevet i 10, har jeg erfaret at fordi dokumentdatabaserne har forskellige egenskaber, og fordi min
case har beskæftiget sig med en specifik implementering af en dokumentdatabase (MongoDB), kan man
ikke konkludere noget om dokumentdatabaser som helhed. I stedet kan man lave konklusioner på
baggrund af MongoDB som dokumentdatabase og som erstatning for en typisk relationel database, såsom
MySQL.
På baggrund af min analyse kan man se at en af de væsentligste forskelle, er manglen på understøttelse af
transaktioner i dokumentdatabasen (se 9.1, 9.2, 9.3, 9.4). Selv de automatiserede tests blev mere
komplicerede af manglen på transaktioner (se 9.9). Dog er det værd at bemærke at fordi MongoDB består
46
af et tilpas stort sæt af features, findes der i mange tilfælde alternative løsninger. Om manglen på
transaktioner er et større eller mindre problem i andre implementeringer af dokumentdatabaser eller i
andre typer applikationer, kan man ikke sige noget om ud fra dette projekt.
En anden væsentlig forskel er at dokumentdatabasen, i modsætning til en relationel database, ikke har et
fast databaseskema (se 9.10). Migrationen og udviklingsarbejdet bliver lettet af ikke at skulle holde et
databaseskema synkroniseret med applikationens forretningslogik. Dog betyder det at visse egenskaber,
såsom database constraints, må undværes (se 9.5).
Fordi hele applikationen kunne migreres, uden omfattende ændringer til design og arkitektur (se 9.11), kan
man konkludere at MongoDB kan bruges som erstatning for MySQL, i applikationer der minder om den der
er brugt i denne case. Dog er det værd at bemærke at det er nødvendigt at sætte sig godt ind i hvordan det
specifikke databasesystem fungerer, og optimere brugen herefter. Det kom især til udtryk i 9.6 hvor
analyse af logoutput viste at selvom applikationen kom frem til det rigtige resultat, kunne brugen af
databasen optimeres. Det samme viste sig i 9.8 hvor en forhastet udviklingsproces ville kunne resultere i en
forringet sikkerhed og en øget risiko for misbrug af system.
12 Perspektivering
På baggrund af min analyse af at migrere en applikation fra at bruge en relationel database til at bruge en
dokumentdatabase, er det interessant at lave en tilsvarende migration til en anden databasetype under
NoSQL-begrebet (se 7.1.1), fx et key-value store eller en graf-database. Der findes NoSQLdatabasesystemer med et langt mindre featureset hvor det at konvertere applikationen, vil stille større krav
til udvikleren, og vil formentlig resultere i større ændringer i applikationen end set i min case.
De mange forskellige NoSQL-databasesystemer (122, ifølge nosql-databases.org 9. august 2012) kan gøre
det svært at vælge den mest fordelagtige til et givet IT-projekt. Det vil have værdi, i højere grad end nu, at
kortlægge databasesystemernes individuelle styrker og svagheder i forhold til kategoriseringer af ITsystemer eller datalogiske problemstillinger.
Noget tyder på at valget af databaseteknologi i fremtiden er polyglot [46] [47] [48], hvilket vil sige at et ITprojekt vil benytte mere end én databaseteknologi, for på den måde at bruge den bedste database til et
givet underområde. Selvom der ikke er noget nyt i konceptet, er udnyttelsen af flere databaseteknologier,
herunder relationelle og de nye NoSQL-teknologier, et område der er meget lidt undersøgt, og værdien af
en sådan arkitektur kan derfor være svær at få et overblik over. Det kunne være interessant at se hvordan
resultatet af en tilsvarende case kunne udforme sig, hvis kun dele af projektet blev ændret til at bruge en
anden databasetype.
47
13 Litteraturliste
[1]
E. F. Codd, »A relational model of data for large shared databanks,« ACM, nr. 13, Juni 1970.
[2]
M. Stonebraker og J. Hellerstein, »What Goes Around Comes Around,« Readings in Database Systems
(Fourth Edition), pp. 2-41, 2005.
[3]
MongoDB.org, »Production Deployments,« [Online]. Available:
http://www.mongodb.org/display/DOCS/Production+Deployments. [Senest hentet eller vist den 01 04
2012].
[4]
Wikipedia, »NoSQL,« [Online]. Available: http://en.wikipedia.org/wiki/NoSQL. [Senest hentet eller vist
den 01 04 2012].
[5]
M. Stonebraker, S. Madden, D. J. Abadi, N. Hachem og P. Helland, »The End of an Architectural Era (It’s
Time for a Complete Rewrite),« VLDB '07 Proceedings of the 33rd international conference on Very
large data bases, pp. 1150-1160, 2007.
[6]
J. Bisbal, D. Lawless, B. Wu, J. Grimson, V. Wade, R. Richardson og D. O. Sullivan, »An Overview of
Legacy Information System Migration,« Software Engineering Conference, 1997. Asia Pacific ... and
International Computer Science Conference 1997. APSEC '97 and ICSC '97. Proceedings, pp. 529-530, 2
12 1997.
[7]
N. Leavitt, »Will NoSQL Databases Live Up to Their Promise?,« Computer, nr. 43, pp. 12-14, 2010.
[8]
C. Strozzi, »NoSQL - A Relational Database Management System,« 1998. [Online]. Available:
http://www.strozzi.it/cgi-bin/CSA/tw7/I/en_US/nosql/Home%20Page. [Senest hentet eller vist den 8 7
2012].
[9]
R. Cattell, »Relational Databases, Object Databases, Key-Value Stores, Document Stores, and Extensible
Record Stores: A Comparison,« December 2010.
[10] R. Cattell, »Scalable SQL and NoSQL Data Stores,« ACM SIGMOD Record, årg. 39, nr. 4, pp. 12-27, 12
2010.
[11] M. Stonebraker og R. Cattell, »Ten Rules for Scalable Performance in “Simple Operation” Datastores,«
Communications of the ACM , årg. 54, nr. 6, pp. 72-80, 2011.
[12] S. Weber, »NoSQL Databases«.
[13] »NoSQL Databases,« [Online]. Available: http://nosql-database.org. [Senest hentet eller vist den 22 07
2012].
48
[14] MongoDB.org, »SQL to Mongo Mapping Chart,« 2012. [Online]. Available:
http://www.mongodb.org/display/DOCS/SQL+to+Mongo+Mapping+Chart. [Senest hentet eller vist den
30 03 2012].
[15] »json.org,« [Online]. Available: http://www.json.org/.
[16] J. C. Anderson, J. Lehnardt og N. Slater, CouchDB: The definitive guide, O’Reilly, 2010.
[17] N. Nurseitov, M. Paulson, R. Reynolds og C. Izurieta, »Comparison of JSON and XML Data Interchange
Formats: Case Study,« 2009.
[18] C. J. Date, An Introduction to Database Systems (8th Edition), Addison Wesley, 2003.
[19] R. Hecht og S. Jablonski, »NoSQL Evaluation: A Use Case Oriented Survey,« 2011 International
Conference on Cloud and Service Computing, pp. 336-341, 2011.
[20] UnQL, »UnQL Specification,« 16 08 2011. [Online]. Available: http://unqlspec.org. [Senest hentet eller
vist den 28 07 2012].
[21] M. Stonebraker og U. Çetintemel, »“One Size Fits All”: An Idea Whose Time Has Come and Gone,« In
Proceedings of the 21st International Conference on Data Engineering (ICDE '05). IEEE Computer
Society, Washington, DC, USA, pp. 2-11, 2005.
[22] K. Chodorow og T. Brock, »MongoDB: Updating,« 30 05 2012. [Online]. Available:
http://www.mongodb.org/display/DOCS/Updating/. [Senest hentet eller vist den 20 07 2012].
[23] CouchDB, »Technical Overview,« [Online]. Available: http://couchdb.apache.org/docs/overview.html.
[Senest hentet eller vist den 11 3 2012].
[24] S. Gilbert og N. Lynch, »Brewer’s Conjecture and the Feasibility of Consistent, Available, PartitionTolerant Web Services,« ACM SIGACT News 33, pp. 51-59, 2002.
[25] M. Stonebraker, »Clarifications on the CAP Theorem and Data-Related Errors,« 21 10 2010. [Online].
Available: http://voltdb.com/company/blog/clarifications-cap-theorem-and-data-related-errors.
[Senest hentet eller vist den 11 06 2012].
[26] H. Abu-Libdeh og R. Escriva, »A Call for Clarity on Consistency«.
[27] E. Brewer, »CAP Twelve Years Later: How the" Rules" Have Changed,« Computer-IEEE Computer
Magazine, nr. 45, pp. 23-29, 2012.
[28] D. Merriman, »Philosophy,« 2011. [Online]. Available:
http://www.mongodb.org/display/DOCS/Philosophy. [Senest hentet eller vist den 13 3 2012].
[29] D. Merriman, »Atomic Operations,« 29 7 2011. [Online]. Available:
49
http://www.mongodb.org/display/DOCS/Atomic+Operations. [Senest hentet eller vist den 11 3 2012].
[30] R. Murphy og M. Dannenberg, »MongoDB Aggregation,« MongoDB, 20 06 2012. [Online]. Available:
http://www.mongodb.org/display/DOCS/Aggregation. [Senest hentet eller vist den 16 07 2012].
[31] J. Dean og S. Ghemawat, »MapReduce: Simplified data processing on large clusters,« Communications
of the ACM - 50th anniversary issue: 1958, pp. 107-113, 2008.
[32] K. Barker, MongoDB in Action, 1 red., Manning, 2012.
[33] M. Fowler, Patterns of enterprise application architecure, Addison Wesley, 2003.
[34] G. Fink, »Select N+1 Problem – How to Decrease Your ORM Performance,« 2010. [Online]. Available:
http://www.codeproject.com/Articles/102647/Select-N-1-Problem-How-to-Decrease-Your-ORMPerfor. [Senest hentet eller vist den 20 06 2012].
[35] RubyOnRails, »Getting Started with Rails,« [Online]. Available:
http://guides.rubyonrails.org/getting_started.html. [Senest hentet eller vist den 16 03 2012].
[36] M. Craig, T. Nguyen, B. Luu, E. M. Manarang og C. Williams, »Ruby on Rails,« University of Calgary
SENG 403 Dr. Kremer, 2012.
[37] Ruby on Rails, »A Guide to Active Record Associations,« [Online]. Available:
http://guides.rubyonrails.org/association_basics.html. [Senest hentet eller vist den 24 03 2012].
[38] Ruby on Rails, »Ruby on Rails Guides: Migrations,« [Online]. Available:
http://guides.rubyonrails.org/migrations.html. [Senest hentet eller vist den 18 07 2012].
[39] S. D. Vermolen, G. Wachsmuth og E. Visser, »Generating Database Migrations for Evolving Web
Applications,« GPCE '11 Proceedings of the 10th ACM international conference on Generative
programming and component engineering, pp. 83-92, 2011.
[40] A. Girbal, »The MongoDB Cookbook: Perform Two Phase Commits,« [Online]. Available:
http://cookbook.mongodb.org/patterns/perform-two-phase-commits/. [Senest hentet eller vist den 28
07 2012].
[41] MongoDB, »How to do Snapshotted Queries in the Mongo Database,« 2011. [Online]. Available:
http://www.mongodb.org/display/DOCS/How+to+do+Snapshotted+Queries+in+the+Mongo+Database.
[Senest hentet eller vist den 13 06 2012].
[42] R. Murphy, »Indexes,« 2012. [Online]. Available:
http://www.mongodb.org/display/DOCS/Indexes#Indexes-unique%3Atrue. [Senest hentet eller vist
den 17 03 2012].
50
[43] R. Murphy, »Object IDs,« 2011. [Online]. Available:
http://www.mongodb.org/display/DOCS/Object+IDs. [Senest hentet eller vist den 17 03 2012].
[44] G. Meszaros, xUnit Test Patterns: Refactoring Test Code, Addison Wesley, 2007.
[45] Ruby On Rails, »ActiveRecord:Migrations,« [Online]. Available:
http://api.rubyonrails.org/classes/ActiveRecord/Migration.html. [Senest hentet eller vist den 15 7
2012].
[46] M. Fowler, »PolyglotPersistence,« 16 11 2011. [Online]. Available:
http://martinfowler.com/bliki/PolyglotPersistence.html. [Senest hentet eller vist den 19 06 2012].
[47] S. Penchikala, »Scott Leberknight on Polyglot Persistence,« 27 07 2009. [Online]. Available:
http://www.infoq.com/news/2009/07/leberknight-polyglot-persistence. [Senest hentet eller vist den
19 07 2012].
[48] Heroku, »NoSQL, Heroku, and You,« 20 07 2010. [Online]. Available:
http://blog.heroku.com/archives/2010/7/20/nosql/. [Senest hentet eller vist den 19 07 2012].
51