Tietokannan käsittely
Learning objectives
- Kertaat tietokannan käyttöä.
- Tiedät, että selaimessa on tietokanta, jota ohjelmat voivat käyttää.
- Osaat kirjoittaa ohjelman, joka käyttää selaimessa olevaa tietokantaa.
Tarkastellaan seuraavaksi tietokannan käyttöä selainsovelluksissa. Tutustutaan ensin muistiin ladattavan tietokannan käyttöön, jonka jälkeen käsittelemme selaimen tarjoaman tietokannan hyödyntämistä.
Suurin ero näiden kahden tietokantatyypin välillä on se, että muistiin ladattavan tietokannan sisältö katoaa aina kun sivu ladataan uudestaan, kun taas selaimen tarjoama tietokannan sisältö on käytössä sivun uudelleenlatauksen jälkeenkin. Selaimen tarjoama tietokanta mahdollistaa sovellukset, joihin lisätty tieto säilyy useamman käyttökerran yli.
Suosittelemme, että käyt kurssin Data ja tieto ennen tämän osan suorittamista.
Tietokannassa olevan tiedon näyttäminen
Tarkastellaan ensin sovellusta, joka näyttää tietokannassa olevat tiedot käyttäjälle. Luomme ensin tietokannan ja henkilot
-nimisen kokoelman henkilöiden säilömiseen. Tämän jälkeen kokoelmaan lisätään kaksi henkilöä. Kun henkilöt on lisätty, kokoelmassa olevat henkilöt näytetään sivulla olevalla listalla.
Ohjelma yhdistää aiemmin harjoittelemiamme asioita. Lisäämme ja haemme henkilöt tietokokoelmasta Data ja tieto -kurssin oppeja noudattamalla. Tietokokoelmasta haetut henkilöt lisätään järjestämättömälle listalle: jokaiselle henkilölle luodaan tämän kurssin Vuorovaikutteisen toiminnallisuuden lisääminen -osaa noudattaen listaelementti, jonka tekstiksi asetetaan henkilön nimi.
Tietokannassa olevan tiedon hakeminen
Tarkastellaan seuraavaksi tietokannassa olevan tiedon hakemiseen tarkoitettua toiminnallisuutta. Tiedon hakemiseen käytetään filttereitä, joiden avulla määritellään rajauksia haettaville tiedoille. Mikäli haluamme hakea tietoa avainsanan perusteella -- eli esimerkiksi henkilöitä, joiden nimessä esiintyy tietty merkkijono -- käytämme KeywordFilter
-luokkaa. Luokka KeywordFilter
löytyy kirjastosta package:database/filter.dart
.
Alla oleva esimerkki sisältää tietokannan luomisen, tietokokoelman hakemisen, sekä tiedon hakemisen tietokannasta. Esimerkissä haetaan kaikki henkilöt, joiden nimi sisältää merkkijono la
.
var tk = MemoryDatabaseAdapter().database();
var henkilot = tk.collection('henkilot');
var kysely = Query(
filter: MapFilter({
'nimi': KeywordFilter('la')
})
);
var tulos = await henkilot.search(query: kysely);
Selaimessa toimivassa ohjelmistossa tiedon hakeminen tietokannasta on luontevaa asettaa osaksi hakutapahtumaa käsittelevää funktiota. Tällainen funktio saa aina parametrinaan muuttujan, joka sisältää tapahtuman tiedot.
Alla oleva ohjelma sisältää ensimmäisen yrityksen hakutoiminnallisuutta sisältävän ohjelman luomiseen. Ohjelman HTML-dokumentti sisältää tekstikentän sekä listaelementin ja näyttää seuraavalta.
<input type='text' id='haku'></input>
<ul id='henkilot'></ul>
Haun toteuttava ohjelma lisää tekstikenttään näppäilytapahtumia käsittelevän kuuntelijan. Näppäilytapahtumia käsittelevä kuuntelija hakee tekstikentän arvoa tietokokoelman dokumenttien nimi
-attribuutista jokaisen näppäinpainalluksen jälkeen.
import 'package:database/database.dart';
import 'package:database/filter.dart';
import 'dart:html';
main() async {
var tietokanta = MemoryDatabaseAdapter().database();
var kokoelma = tietokanta.collection('henkilot');
await lisaaKokoelmaan(kokoelma);
querySelector('#haku').onKeyUp.listen(hae);
}
lisaaKokoelmaan(kokoelma) async {
await kokoelma.insert(data: {'nimi': 'Ada Lovelace'});
await kokoelma.insert(data: {'nimi': 'Blaise Pascal'});
}
hae(e) async {
var tk = MemoryDatabaseAdapter().database();
var henkilot = tk.collection('henkilot');
InputElement hakukentta = querySelector('#haku');
var haettava = hakukentta.value;
var kysely = Query(
filter: MapFilter({
'nimi': KeywordFilter(haettava)
})
);
var tulos = await henkilot.search(query: kysely);
var dokumentit = tulos.snapshots;
for (var i = 0; i < dokumentit.length; i++) {
var dokumentti = dokumentit[i];
var data = dokumentti.data;
lisaaHenkiloListalle(data);
}
}
lisaaHenkiloListalle(henkilo) {
var henkilolista = querySelector('#henkilot');
var listaelementti = Element.li();
listaelementti.text = henkilo['nimi'];
henkilolista.children.add(listaelementti);
}
Kun ohjelmaa kokeillaan, se ei toimi. Riippumatta mitä tekstikenttään kirjoittaa, yhtäkään henkilöä ei näytetä listalla.
Ongelmassa on kyse siitä, että kutsu MemoryDatabaseAdapter().database()
luo uuden muistissa sijaitsevan tietokannan. Yllä olevassa ohjelmassa tietokanta luodaan sekä main
-funktiossa että joka kerta funktiota hae
kutsuttaessa (eli jokaisen näppäinpainalluksen yhteydessä).
Aiemmissa ohjelmissamme tietokantaa on käsitelty siten, että tietokanta (tai tietokokoelma) on annettu parametrina sitä käyttävälle funktiolle. Haluamme tässäkin antaa tietokannan (tai tietokokoelman) parametrina hausta vastaavalla funktiolle. Ongelmana on kuitenkin se, että tapahtumia käsittelevät funktiot saavat parametrinaan tiedot tapahtumasta, mutta ei muita tietoja. Mikäli tapahtumien käsittelyyn tarkoitettuun funktioon lisätään parametreja, sitä ei voi asettaa elementille tapahtumien kuuntelijaksi.
Ongelman voi ratkaista anonyymien funktioiden avulla. Muokataan ohjelmaa siten, että tapahtumien kuunteluun käytetty anonyymi funktio kutsuu henkilöiden hakuun käytettyä funktiota.
Alla olevassa ohjelmointiympäristössä on muokattu versio edeltävästä esimerkistä. Nyt hakufunktiota on muokattu siten, että se saa parametrinaan tietokokoelman. Vastaavasti, tapahtumia kuuntelevassa funktiossa kutsutaan hakufunktiota -- hakufunktiolle annetaan parametrina tietokokoelma. Tietokokoelman antaminen hakufunktiolle onnistuu, sillä tietokokoelma on luotu main
-funktiossa, jossa anonyymi funktio ja sen toiminnallisuus määritellään.
Voit kokeilla ohjelmaa alla olevassa ohjelmointiympäristössä.
Ohjelmaa kokeilemalla huomaamme, että se ei vieläkään toimi toivotulla tavalla. Useita hakuja kokeilemalla huomaamme, että hakutulokset lisätään aina edellisten tulosten perään. Hakemalla useita nimiä saattaa päätyä esimerkiksi seuraavannäköiseen tulokseen.
Edellisessä esimerkissä ja tehtävissä tulokset voidaan nähdä useampaan kertaan. Tämä ei ole toivottavaa, sillä se voi luoda väärän kuvan tietokannan sisällöstä sekä ohjelman toiminnasta. Kukin tietokannassa oleva henkilo (tai mikä tahansa muu asia kuten vihannes) tulisi nähdä korkeintaan kerran.
Ongelmassa on kyse siitä, että tietoa haettaessa uudet tulokset lisätään aina listan loppuun. Muokataan hakua siten, että hakufunktion alussa käyttäjälle näytettävä lista tyhjennetään -- tämä onnistuu kutsumalla elementtiin liittyvän children
-muuttujan metodia clear
. Alla olevassa esimerkissä henkilöitä sisältävä lista tyhjennetään ennen hakutulosten läpikäyntiä ja henkilöiden lisäämistä listalle.
hae(kokoelma) async {
InputElement hakukentta = querySelector('#haku');
var haettava = hakukentta.value;
var kysely = Query(
filter: MapFilter({
'nimi': KeywordFilter(haettava)
})
);
var tulos = await kokoelma.search(query: kysely);
var dokumentit = tulos.snapshots;
querySelector('#henkilot').children.clear();
for (var i = 0; i < dokumentit.length; i++) {
var dokumentti = dokumentit[i];
var data = dokumentti.data;
lisaaHenkiloListalle(data['nimi']);
}
}
Selaimen tietokanta
Tähän mennessä olemme käyttäneet muistiin ladattavaa tietokantaa. Muistiin ladattavan tietokannan heikkous on se, että tietokannassa olevat tiedot poistetaan ohjelman sammuessa (eli esimerkiksi kun selainikkuna suljetaan). Tutustutaan seuraavaksi selainten tarjoaman tietokannan käyttöön.
Lähes jokainen selain tarjoaa paikallisen tietokannan. Tällainen tietokanta on käytettävissä ohjelman käynnistyskertojen yli, jolloin esimerkiksi selaimen uudelleenkäynnistyksen yhteydessä (tai sivun uudelleenlatauksen yhteydessä) selaimen tietokantaan tallennetut tiedot ovat yhä käytettävissä.
Data ja tieto -kurssilla tutuksi tullut tietokantakirjasto tarjoaa valmiit välineet selaimen tietokannan käyttämiseen. Selaimen tietokannan saa käyttöön tuomalla ohjelmaan kirjaston package:database/browser.dart
ja käyttämällä luokasta BrowserDatabaseAdapter
tehtyä tietokantaa luokasta MemoryDatabaseAdapter
tehdyn tietokannan sijaan.
Huom! Mikäli haluat käyttää tietokannan toiminnallisuuksia kuten tulosten rajaamista, tulee käytössä olla myös kirjastot
package:database/database.dart
japackage:database/filter.dart
.
Alla olevassa ohjelmointiympäristössä on esimerkki selaimen tietokantaa käyttävästä ohjelmasta. Aina kun ohjelma käynnistetään uudestaan (eli painetaan ohjelmointiympäristön Run-nappia), tietokantaan lisätään uusi dokumentti. Tiedot säilyvät selaimen tietokannassa myös vaikka lataat tämän sivun uudestaan.
Selaimen tietokanta ja sen "sijainti"
Selaimen tietokanta tallennetaan käyttäjän tietokoneen kiintolevylle. Selain suojelee tietokantoihin pääsyä verkko-osoitteen perusteella: tietokanta on käytettävissä vain siinä verkko-osoitteessa missä se on luotu. Esimerkiksi tämän materiaalin esimerkeissä luotavat ja käytetyt tietokannat ovat käytettävissä vain tähän materiaaliin liittyvässä verkko-osoitteessa.
Aiemmissa esimerkeissä tietokantaa käsittelevät funktiot saivat parametrina kokoelman (tai tietokannan). Tämän avulla vältettiin tilanne, missä kutsulla MemoryDatabaseAdapter().database()
luotiin aina uusi tietokanta. Selaimen tietokantaa käytettäessä tietokannan voi halutessaan "luoda" jokaisessa funktiossa erikseen -- tietokanta joko luodaan, tai mikäli se on olemassa, noudetaan ohjelman käyttöön.
Alla olevassa esimerkissä on ohjelma, missä tietokantaa käsitellään kahdessa eri tapahtumien käsittelyyn tarkoitetussa funktiossa. Toisessa funktiossa tietokantaan lisätään uusi dokumentti, ja toisessa listataan tietokannassa olevat dokumentit. Tietokanta tuodaan ohjelman käyttöön kummassakin funktiossa, eikä tapahtumienkäsittelijöille määritellä anonyymejä funktioita.
Tiedon lisääminen ja poistaminen
Dynaamista toiminnallisuutta käsittelevissä esimerkeissä lisääminen ja poistaminen toteutettiin siten, että tiedon lisäämisen yhteydessä tiedon sisältävään elementtiin määriteltiin tiedon poistava tapahtumankuuntelija.
Tämä toteutettiin alla olevan esimerkin kuvaamalla tavalla. Funktio lisaa
luo uuden listaelementin ja asettaa sille tekstin. Tämän jälkeen listaelementtiin lisätään tapahtumankuuntelija -- mikäli listaelementtiä klikataan, listaelementti poistetaan. Lopulta listaelementti lisätään sivulla olevalle listalle.
import 'dart:html';
main() {
querySelector('#nappi').onClick.listen(lisaa);
}
lisaa(e) {
var elementti = Element.li();
elementti.text = 'Hei!';
elementti.onClick.listen((e) {
elementti.remove();
});
querySelector('#lista').children.add(elementti);
}
Tarkastellaan seuraavaksi vastaavanlaista ohjelmaa, joka käyttää tietokantaa. Alla olevassa esimerkissä on toteutettuna edellistä esimerkkiä matkiva ohjelma, jossa funktio lisaa
lisää dokumentin tietokantaan, luo uuden listaelementin, ja asettaa listaelementtiin sekä listaelementin että tietokannassa olevan dokumentin poistavan tapahtumankäsittelijän.
Konkreettinen dokumentin poistaminen tapahtuu dokumentin delete
-metodia kutsumalla. Alla olevassa esimerkissä dokumenttina käytetään uuden dokumentin lisäämiseen käytetyn await kokoelma.insert
-kutsun palauttamaa arvoa.
Kokeile ohjelman käyttöä. Huomaat, että voit lisätä sekä poistaa dokumentteja.
Yllä oleva ohjelma toimii ensisilmäyksellä hyvin. Siinä on kuitenkin ongelma: ohjelmassa ei näytetä tietokannassa valmiina olevia tietoja, eikä niitä voi poistaa. Tämä johtaa tilanteeseen, missä tietokantaan jää tietoa mikäli selain käynnistetään uudestaan.
Korjataan tilanne. Lisätään ohjelmaan funktio, joka lisää tietokannassa olevat tiedot listalle. Alla olevassa ohjelmassa haetaan tietokanta ja kaikki tietokannassa olevat dokumentit. Tämän jälkeen dokumentit käydään yksitellen läpi -- listalle lisätään uusi listaelementti jokaista dokumenttia kohden.
Tietokantahaun tuloksista pääsee käsiksi yksittäisiin dokumentteihin muuttujan document
avulla. Kuten edellisessä esimerkissä, dokumentin poistaminen tietokannasta tapahtuu dokumentin metodilla delete
.
listaa() async {
var tietokanta = BrowserDatabaseAdapter().database();
var kokoelma = tietokanta.collection('kokoelma');
var tulos = await kokoelma.search();
var dokumentit = tulos.snapshots;
var lista = querySelector('#lista');
for (var i = 0; i < dokumentit.length; i++) {
var data = dokumentit[i].data;
var dokumentti = dokumentit[i].document;
var listaElementti = Element.li();
listaElementti.text = data['teksti'];
listaElementti.onClick.listen((e) async {
listaElementti.remove();
await dokumentti.delete();
});
lista.children.add(listaElementti);
}
}
Kun funktiota listaa
kutsutaan, ohjelmassa näytetään nekin tietokannassa olevat tiedot, joita tietokantaan ei ole lisätty ohjelman suorituksen aikana (eli sivun ollessa auki). Tarkastellessamme funktioita lisaa
ja listaa
huomaamme kuitenkin paljon toisteisuutta. Kummassakin funktiossa luodaan listaelementti, asetetaan listaelementin arvo, lisätään listaelementille tapahtumankuuntelija, ja lisätään listaelementti listalle. Tehdään tätä toiminnallisuutta varten erillinen funktio lisaaListalle
.
Toteutetaan funktio siten, että se saa parametrinaan tekstielementille asetettavan tekstin ja dokumentin.
lisaaListalle(teksti, dokumentti) {
var lista = querySelector('#lista');
var listaElementti = Element.li();
listaElementti.text = teksti;
listaElementti.onClick.listen((e) async {
listaElementti.remove();
await dokumentti.delete();
});
lista.children.add(listaElementti);
}
Nyt funktioista lisaa
ja listaa
saadaan hieman selkeämmät.
lisaa(e) async {
var lisattavaTeksti = 'Hei!';
var tietokanta = BrowserDatabaseAdapter().database();
var kokoelma = tietokanta.collection('kokoelma');
var dokumentti = await kokoelma.insert(data: {
'teksti': lisattavaTeksti
});
lisaaListalle(lisattavaTeksti, dokumentti);
}
listaa() async {
var tietokanta = BrowserDatabaseAdapter().database();
var kokoelma = tietokanta.collection('kokoelma');
var tulos = await kokoelma.search();
var dokumentit = tulos.snapshots;
for (var i = 0; i < dokumentit.length; i++) {
var data = dokumentit[i].data;
var dokumentti = dokumentit[i].document;
lisaaListalle(data['teksti'], dokumentti);
}
}
Voit kokeilla ohjelmaa alla olevassa ohjelmointiympäristössä.
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?