Johdatus tietokantoihin
Learning objectives
- Tiedät mitä käsitteet tietokannanhallintajärjestelmä, tietokanta, ja tietokokoelma tarkoittavat.
- Tunnet muutamia esimerkkejä tietokantojen käytöstä jokapäiväisessä elämässä.
- Tutustut tietokantojen käyttöön ohjelmallisesti.
- Osaat lisätä tietoa tietokannassa olevaan tietokokoelmaan.
- Osaat listata tietokannassa olevan tietokokoelman sisältävät tiedot.
- Tiedät mitä asynkroniset funktiot ovat ja miksi niitä käytetään.
Olemme tähän mennessä tarkastelleet käsitteitä informaatio, data, ja tieto. Olemme tutustuneet muuttujien tyyppeihin ja harjoitelleet käsitteisiin liittyvien muuttujien yhteen sitomista luokkien avulla. Luomamme ohjelmat ovat olleet sellaisia, että niihin syötetty tieto katoaa ohjelman uudelleenkäynnistyksen yhteydessä.
Tutustutaan seuraavaksi tietokantoihin eli tiedon säilömiseen tarkoitettuihin ohjelmistoihin. Tietokantojen avulla voidaan luoda ohjelmia, joihin lisätyt tiedot ovat olemassa myös ohjelman seuraavan käynnistyskerran yhteydessä.
Tietokanta ja tietokannan luominen
Opettelemme seuraavaksi käyttämään Dart-kielelle kehitettyä database-kirjastoa, joka abstrahoi (eli piilottaa) konkreettisen tietokantatoteutuksen. Kirjaston avulla voimme käyttää samaa ohjelmointityyliä useammalle tietokantapalvelulle.
Kirjasto löytyy pakkauksesta database/database.dart
ja se tuodaan käyttöön komennolla import 'package:database/database.dart';
. Materiaalissa olevassa ohjelmointiympäristössä kirjasto on valmiiksi käytössä. Mikäli teet tehtäviä omalla koneellasi, tulee kirjasto ladata paikallisesti käyttöön.
Tietokannan valinta tapahtuu adapterilla, jota voi ajatella ohjelman ja tietokannan väliin asetettavana palana. Adapteri mahdollistaa saman ohjelmointityylin käytön eri tietokannoille.
Alla olevassa esimerkissä käytetään MemoryDatabaseAdapter
-nimistä luokkaa, joka tarjoaa pääsyn muistiin ladattavaan tietokantaan. Tietokanta haetaan komennolla MemoryDatabaseAdapter().database();
-- komento luo luokasta MemoryDatabaseAdapter
olion ja kutsuu sen metodia database
, joka palauttaa tietokantaolion. Mikäli tietokantaa ei ole, se luodaan.
Alla olevassa esimerkissä luodaan tietokantapääsyn tarjoavan tietokanta
-niminen olio, joka tulostetaan.
import 'package:database/database.dart';
main() {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
}
program output
Database(...)
Kutsujen ketjutus
Yllä olevassa esimerkissä tietokannan hakeminen kuvattiin muodossa MemoryDatabaseAdapter().database()
. Vaikka komento esitettiin yhdellä rivillä, on siinä todellisuudessa kaksi erillistä kutsua. Tällaista kutsujen yhteen kytkemistä kutsutaan ketjuttamiseksi (method chaining).
Saman voi halutessaan kirjoittaa kahdella rivillä seuraavalla tavalla.
var adapteri = MemoryDatabaseAdapter();
var tietokanta = adapteri.database();
Vastaavasti, ketjutetun kutsun (ja oikeastaan minkä tahansa kutsun) voi kirjoittaa myös useammalle riville. Tämä tapahtuu seuraavalla tavalla. Sisennyksen koolla ei ole väliä, mutta yleisesti ottaen sisennyksellä pyritään ilmaisemaan yhteen kuuluvia osia.
var tietokanta = MemoryDatabaseAdapter().
database();
Tietokanta voidaan ajatella paikkana, joka sisältää tietokokoelmia. Konkreettinen vertauskuva on esimerkiksi Excel tai Google Sheets. Google Sheets on tietokantoja hallinnoiva ohjelma (eli tietokannanhallintajärjestelmä), yksittäinen dokumentti tietokanta, ja dokumentin välilehti tietokokoelma.

Yksittäiseen dokumenttiin (eli vertauskuvamme tietokantaan) voi luoda useampia välilehtiä (eli vertauskuvamme tietokokoelmia). Kullakin välilehdellä on aina yksi taulukko. Kukin taulukon rivi vastaa yhtä tietokokoelman tietuetta ja taulukon sarakkeet vastaavat tietueesta kerättäviä tietoja. Yllä olevassa kuvassa on henkilöitä sisältävä tietokokoelma. Jokaisesta henkilöstä on kerätty nimi ja syntymävuosi.
Ohjelmallisesti tietokokoelmaan pääsee käsiksi tietokantaolion tarjoamalla collection
-metodilla. Metodille annetaan parametrina kokoelman nimi. Mikäli kokoelmaa ei ole olemassa, sellainen luodaan.
Alla olevassa esimerkissä luodaan ensin tietokantaolio, joka tulostetaan. Tämän jälkeen tietokantaoliolta pyydetään tietokokoelmaolio, joka myös tulostetaan. Esimerkissä käsitellään kokoelmaa nimeltä henkilot
.
import 'package:database/database.dart';
main() {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
}
program output
Database(...) Database(...).collection("henkilot")
Kokoelmaan lisääminen tapahtuu kokoelmaolion metodilla insert
. Lisäämistä voi ajatella uuden rivin lisäämisenä taulukkoon. Metodi insert
saa parametrinaan sanakirjamuotoisen olion -- metodille tulee poikkeuksellisesti määritellä parametrin nimi (tässä data
), johon arvo asetetaan.
Parametri määritellään metodikutsussa muodossa insert(data: arvo)
, missä parametrin nimi on data
ja arvo
parametrille annettava arvo. Alla olevassa esimerkissä kokoelmaan nimeltä henkilot
lisätään uusi henkilö. Henkilön nimi on Ada Lovelace
ja hän on syntynyt vuonna 1815.
import 'package:database/database.dart';
main() {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
henkilokokoelma.insert(data: {
'nimi': 'Ada Lovelace',
'syntymavuosi': 1815
});
}
Question not found or loading of the question is still in progress.
Metodi insert
on tarkka sen hyväksymistä arvoista. Mikäli sanakirjaolion haluaa määritellä erikseen, tulee sanakirjan avainten ja arvojen tyypit määritellä. Avainten tulee olla merkkijonoja (String
), mutta arvot voivat olla minkä tyyppisiä tahansa (Object
). Tällaisen sanakirjan esittely tapahtuu seuraavalla tavalla.
Map<String, Object> sanakirja = {};
Tehtävän lisääminen sanakirjalla, johon on määritelty tyypit, näyttää seuraavalta.
import 'package:database/database.dart';
main() {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
Map<String, Object> sanakirja = {};
sanakirja['nimi'] = 'Ada Lovelace';
sanakirja['syntymavuosi'] = 1815;
henkilokokoelma.insert(data: sanakirja);
}
Metodi insert
palauttaa arvon, jonka voi tulostaa. Tarkastellaan tätä arvoa seuraavaksi.
import 'package:database/database.dart';
main() {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
Map<String, Object> sanakirja = {};
sanakirja['nimi'] = 'Ada Lovelace';
sanakirja['syntymavuosi'] = 1815;
var vastaus = henkilokokoelma.insert(data: sanakirja);
print(vastaus);
}
program output
Database(...) Database(...).collection("henkilot") Instance of '_Future<Document<dynamic>>'
Kun insert
-metodin palauttama arvo tulostetaan, näemme merkkijonon Future
. Tutustutaan tähän seuraavaksi hieman tarkemmin.
Asynkroniset funktiokutsut
Tähän mennessä toteuttamamme ohjelmat ovat olleet synkronisia eli ohjelman suoritus on edennyt loogisesti eteenpäin lause lauseelta. Mikäli jonkin funktion suorittaminen on kestänyt pitkään, funktion suoritusta on odotettu kunnes funktion suoritus on valmis.
Useat ohjelmointikielet tarjoavat tuen asynkronisten funktioiden luomiseen. Kun asynkronista funktiota kutsutaan, funktion sisältämän lähdekoodin suoritus aloitetaan, mutta kutsuva ohjelma ei jää odottamaan funktion suorituksen loppumista. Tällainen ohjelmointikielen ominaisuus on hyödyllinen muunmuassa hitaammissa laskentaoperaatioissa: mikäli laskennan tuloksella ei ole merkitystä seuraavaksi suoritettavien komentojen kannalta, ei laskennan tuloksen valmistumista kannata jäädä odottamaan.
Asynkronisten funktioiden palauttama arvo -- mikäli arvo palautetaan -- on lupaus tulevaisuudessa saatavasta tuloksesta. Dart-kielessä tällaista lupausta kutsutaan nimellä Future. Edellä nähty tulostus '_Future<Document<dynamic>>'
kertoo että funktio on asynkroninen ja se palauttaa dokumentin (Document
) joskus tulevaisuudessa.
Tarkastellaan tätä konkreettisen esimerkin kautta. Alla olevassa ohjelmassa on funktio nimeltä hidasFunktio
, joka palauttaa lupauksen tulevasta laskennasta. Kun funktiota hidasFunktio
kutsutaan, suorittaa se siihen liittyvän lähdekoodin. Käytännössä funktio odottaa 10 sekuntia ja kutsuu sitten funktiota valmis
.
Funktion suoritus aloitetaan kun sitä kutsutaan, mutta main
-funktiossa ei jäädä odottamaan funktion suorituksen valmistumista. Todellisuudessa tällainen hidas funktio tekisi jotain järkevämpää, mutta odottava ohjelma toimii hyvin esimerkkinä asynkronisista funktioista.
Kun suoritat yllä olevan ohjelman, ohjelman tulostus on seuraava. Huomaat, että rivi Laskenta valmis!
tulostuksessa kestää hetki. Rivi tulostetaan myös rivin Funktion main lopussa.
jälkeen.
program output
Funktion main alussa! Funktion main lopussa! Laskenta valmis!
Entäpä jos haluamme odottaa asynkronisen funktion suorituksen valmistumista? Tässä hyödyksi tulevat komennot await
ja async
. Komentoa await
käytetään asynkronisen funktiokutsun yhteydessä. Asettamalla komento await
ennen funktiokutsua kerrotaan, että ohjelman tulee odottaa asynkronisen funktiokutsun suorituksen loppumista. Komentoa await
saa käyttää vain funktioissa, joiden määrittelyssä käytetään avainsanaa async
.
Alla olevassa esimerkissä edellinen ohjelma on toteutettu siten, että ohjelma jää odottamaan funktion hidasFunktio
suorituksen päättymistä.
main() async {
print('Funktion main alussa!');
await hidasFunktio();
print('Funktion main lopussa!');
}
hidasFunktio() {
return Future.delayed(Duration(seconds: 10), valmis);
}
valmis() {
print('Laskenta valmis!');
}
program output
Funktion main alussa! Laskenta valmis! Funktion main lopussa!
Yllä olevassa ohjelmassa olevan funktion hidasFunktio
voi kirjoittaa myös siten, että sen määrittelyssä kerrotaan että kyse on asynkronisesta funktiosta. Tällöin määrittelyyn lisätään async
-avainsana -- ohjelma näyttää nyt seuraavalta.
Question not found or loading of the question is still in progress.
Tiedon lisääminen tietokantaan
Tietokannassa olevaa tietoa käsittelevät tietokantaoperaatiot on toteutettu asynkronisina funktioina. Tämä juontaa juurensa siihen, että tietokantaoperaatiot saattavat olla hitaita -- ainakin tietokoneen nopeuteen suhteutettuna. Siinä missä yksittäisen laskuoperaation suoritus kestää tietokoneella noin nanosekunnin (eli 0,000000001 sekuntia), pienen datamäärän kirjoitus kiintolevylle voi viedä jopa millisekunteja. On siis järkevää mahdollistaa tietokoneen laskentatehon ohjaaminen kirjoitusoperaation valmistumisen odottamisesta johonkin muuhun.
Olemme tietokantaa käsittelevissä ohjelmissamme kiinnostuneita tietokantakyselyiden tuloksista ja onnistumisesta. Tästä eteenpäin main
-funktiossa käytetään async
-määrettä ja tietokannassa olevaa tietoa käsittelevissä kutsuissa await
-määrettä.
Kun lisäämme async
ja await
määreet tietoa tietokantaan lisäävään ohjelmaamme, ohjelma näyttää seuraavalta.
import 'package:database/database.dart';
main() async {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
Map<String, Object> sanakirja = {};
sanakirja['nimi'] = 'Ada Lovelace';
sanakirja['syntymavuosi'] = 1815;
var vastaus = await henkilokokoelma.insert(data: sanakirja);
print(vastaus);
}
Nyt tulostus kertoo, että kokoelmaan nimelta henkilot
on lisätty dokumentti, jonka tunnus on c996ce2854c39c9b4711b5617d3ad81a
. Dokumenttia voi ajatella yksittäisenä taulukon rivinä -- joskin dokumentit voivat todellisuudessa olla monimutkaisia -- ja tunnusta kyseisen rivin yksilöivänä tunnuksena.
program output
Database(...) Database(...).collection("henkilot") Database(...).collection("henkilot").document("c996ce2854c39c9b4711b5617d3ad81a")
Tietokannassa olevan tiedon listaaminen
Kokoelmassa olevien rivien hakeminen onnistuu kokoelman tarjoamalla search
-metodilla. Metodille ei anneta parametreja Haku palauttaa tuloksen, joka sisältää hetkellisen kuvan (snapshots) kokoelmassa olevista dokumenteista ja dokumenttien datasta -- kuvalla tarkoitetaan tässä sitä, että kyselyn tulos on staattinen kuva tietokannan sisällöstä. Tietokannan sisältö saattaa muuttua haun jälkeen esimerkiksi mikäli tietokantaa käsittelee useampi ohjelma, mutta muutokset eivät näy aiemmin tehdyn haun tuloksessa.
Alla on kuvattu hakukomennon käyttö. Lista dokumenteista löytyy haun tuloksena saadun olion (QueryResult
) muuttujasta snapshots
.
var tulos = await henkilokokoelma.search();
var dokumentit = tulos.snapshots;
Listan läpikäynti onnistuu samalla tavalla kuin minkä tahansa listan. Listan elementit ovat dokumenttien "kuvia" (Snapshot
), jotka kuvaavat viimeistä dokumentin versiota haun hetkellä. Listan läpikäynti onnistuu for
-toistolauseella seuraavalla tavalla. Kullakin kuvalla on muuttuja data
, joka sisältää tietokantaan lisäämämme datan sanakirjana. Kuvalla on myös muuttuja document
, joka sisältää kuvaan liittyvän dokumenttiolion.
var tulos = await henkilokokoelma.search();
var dokumentit = tulos.snapshots;
for (var i = 0; i < dokumentit.length; i++) {
var data = dokumentit[i].data;
var dokumentti = dokumentit[i].document;
print(data);
print(dokumentti);
}
Kokonaisuudessaan ohjelma, joka lisää tietokantaan tietoa ja hakee tietokannasta tietoa näyttää seuraavalta.
import 'package:database/database.dart';
main() async {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
Map<String, Object> sanakirja = {};
sanakirja['nimi'] = 'Ada Lovelace';
sanakirja['syntymavuosi'] = 1815;
var vastaus = await henkilokokoelma.insert(data: sanakirja);
print(vastaus);
var tulos = await henkilokokoelma.search();
var dokumentit = tulos.snapshots;
for (var i = 0; i < dokumentit.length; i++) {
var data = dokumentit[i].data;
print(data);
}
}
program output
Database(...) Database(...).collection("henkilot") Database(...).collection("henkilot").document("41ff94235dd8a6a087e8044d9dbc9ede") {nimi: Ada Lovelace, syntymavuosi: 1815}
Tulokset voi ottaa myös talteen listalle, jossa niitä voi käsitellä myöhemmin.
import 'package:database/database.dart';
main() async {
var tietokanta = MemoryDatabaseAdapter().database();
print(tietokanta);
var henkilokokoelma = tietokanta.collection('henkilot');
print(henkilokokoelma);
Map<String, Object> sanakirja = {};
sanakirja['nimi'] = 'Ada Lovelace';
sanakirja['syntymavuosi'] = 1815;
var vastaus = await henkilokokoelma.insert(data: sanakirja);
print(vastaus);
var tulos = await henkilokokoelma.search();
var dokumentit = tulos.snapshots;
var lista = [];
for (var i = 0; i < dokumentit.length; i++) {
var data = dokumentit[i].data;
lista.add(data);
}
print('Hakutuloksia: ${lista.length}');
}
program output
Database(...) Database(...).collection("henkilot") Database(...).collection("henkilot").document("7fb373fa05082bd77a61d35e99faefcd") Hakutuloksia: 1
Mistä tietokannoissa oikein on kyse?
Tietotekniikan ja ohjelmistojen yleistyessä on huomattu, että ohjelmissa toistuu yhä uudestaan ja uudestaan tarve datan tallentamiselle ja hakemiselle. Datan tallentamiseen ja hakemiseen käytetyt ohjelmien osat pyrkivät muunmuassa varmistamaan, että tallennettu data päätyy tietokoneen kovalevylle, että data löytyy kovalevyltä luettavassa muodossa, että datan muokkaaminen on mahdollista, ja että ohjelman päätyminen mahdolliseen virhetilanteessa ja sitä kautta kaatumiseen ei johda datan tuhoutumiseen.
Ohjelmoijat toteuttivat tällaisen toiminnallisuuden aluksi itse lähes jokaiseen vastaavaa toiminnallisuutta tarvitsevaan ohjelmistoon. Tämä oli työlästä ja virhealtista -- osa toteutuksista oli toimivia, osa virheellisiä. Melko nopeasti huomattiin, että datan hallintaan liittyviä osia ei jokaisen kannata toteuttaa itse. Tämä johti siihen, että ohjelmoijat ja ohjelmistoyritykset alkoivat tarjoamaan datan hallintaan käyttämiään ohjelmistoja muiden ohjelmistokehittäjien käyttöön.
Nykyään harva ohjelmoija on toteuttanut datan tallentamiseen ja hakemiseen käytetyn toiminnallisuuden täysin itse. Sen sijaan, datan käsittelyyn käytetään pääosin kirjastoja, aivan kuten mekin tässä materiaalissa teemme.
Nykyään tietokannat ovat jatkuvasti läsnä oleva ilmiö. Kotien sähkön- ja vedenkulutusta seurataan elektronisesti, lehti- ja palvelutilaukset tehdään digitaalisiin järjestelmiin, sairaaloilla on sähköiset potilasrekisterit, verkon kautta tarjotut viihdepalvelut pitävät kirjaa palveluiden käytöstä, yritykset tarjoavat räätälöityjä palveluita digitaalisen käyttäytymisen perusteella, ja niin edelleen.
Vastaavasti palvelut kuten Airbnb ja Uber eivät omista välittämiensä tuotteiden mahdollistavia peruspaloja (asuntoja tai autoja), vaan pitävät yllä tietokantaa vapaana olevista asunnoista ja vapaista autoista. Palveluista kiinnostuneet voivat hakea niitä tietokannan päälle rakennetun käyttöliittymän kautta.
Digitaalisten palveluiden myötä tietokannat ovat myös huomaamattomia. Tämä kurssimateriaali sijaitsee tietokannassa, kurssitehtäviin liittyvät pisteet kirjataan tietokantaan, ja kurssin suoritusmerkintä kirjataan tietokantaan. Kännykässäsikin on hyvin todennäköisesti useita erilaisia tietokantoja; yhteystiedot, kalenteri, herätyskello, aikavyöhykkeet, karttapalvelut, suosikkiverkkosivut, ym.
Tietokannat voivat olla paikallisia, eli ne voivat sijaita samalla koneella tietokantaa käyttävän ohjelmiston kanssa, esimerkiksi kännykässä, tai ne voivat sijaita erillisellä palvelimella, johon otetaan tarvittaessa yhteyttä. Loppukäyttäjän näkökulmasta tietokannan konkreettisella sijainnilla ei ole juurikaan merkitystä, sillä haetun tiedon näkee tyypillisesti käytössä olevan sovelluksen käyttöliittymän kautta.
Hi! Please help us improve the course!
Please consider the following statements and questions regarding this part of the course. We use your answers for improving the course.
I can see how the assignments and materials fit in with what I am supposed to learn.
I find most of what I learned so far interesting.
I am certain that I can learn the taught skills and knowledge.
I find that I would benefit from having explicit deadlines.
I feel overwhelmed by the amount of work.
I try out the examples outlined in the materials.
I feel that the assignments are too difficult.
I feel that I've been systematic and organized in my studying.
How many hours (estimated with a precision of half an hour) did you spend reading the material and completing the assignments for this part? (use a dot as the decimal separator, e.g 8.5)
How would you improve the material or assignments?