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
© Copyright 2024