Tehtävät
Toisen osan oppimistavoitteet

Ymmärtää että tietoa voidaan käsitellä erilaisilla abstraktiotasoilla. Osaa luoda SQL-kielellä yhden tietokantataulun sisältävän tietokannan, lisätä tietokantatauluun tietoa ja tehdä kyselyitä tietokantatauluun. Osaa luoda ohjelman, joka kommunikoi tietokannanhallintajärjestelmän kanssa valmiin rajapinnan kautta. Tuntee käsitteen SQL-injektio. Tietää menetelmiä SQL-injektioon perustuvien hyökkäysten tekemiseen ja osaa toisaalta puolustautua niiltä.

Erilaiset näkymät tietoon

Edellisessä osassa tietoa tarkasteltiin sen tallennustavan kautta. Käsittelimme menetelmiä tallentaa ja lukea tietoa tiedostosta. Tietokannanhallintajärjestelmän toteutuksen ja tarjoaman käsittelyn näkökulmasta esimerkkimme liittyivät fyysiseen toteutukseen, eli miten ja missä muodossa tieto lopulta tallennetaan.

Tietokannanhallintajärjestelmistä ja tallennettavasta tiedosta puhuttaessa tietoa käsitellään tyypillisesti kolmella eri abstraktiotasolla: käsitteellisellä abstraktiotasolla, rakenteellisella abstraktiotasolla ja fyysisellä abstraktiotasolla.

Käsitteellinen abstraktiotaso (conceptual level) on kuvaus tietokantaan tallennettavista käsitteistä ja niiden yhteyksistä. Käsitteellisellä abstraktiotasolla kuvaus esiintyy esimerkiksi tekstidokumenteissa, puheissa, tai vaikkapa multimediaesityksissä.

Pankin tallennustoiminnallisuudesta kertova asiakas saattaa kertoa tileistä ja niiden omistajista seuraavaa: Tileillä on omistajia, joista jokainen yksilöidään henkilöturvatunnuksen avulla. Em. kuvaus sisältää käsitteet tili, omistaja ja henkilöturvatunnus. Tämän lisäksi käsitteisiin liittyy suhteita, jotka osittain ilmenevät kuvaksesta ja osittain vaativat aihealueen ymmärrystä. Esimerkiksi tili ja omistaja ovat erillisiä käsitteitä ja niiden välinen yhteys kuvaa omistajuutta. Omistajan ja henkilöturvatunnuksen välinen yhteys on taas riippuvainen olemassaolosta -- Omistajan henkilöturvatunnusta ei ole olemassa ilman omistajaa. Omistajalla on henkilöturvatunnus.

Käsitteiden nimeäminen kuvaavammiksi on myös mahdollista. Edeltävässä kuvauksessa ollut käsite omistaja kannattaisi todennäköisesti nimetä uudelleen henkilöksi.

Rakenteellinen abstraktiotaso (logical level, structural level) on kuvaus tietokantaan tallennettavan tiedon rakenteesta. Se voi olla esimerkiksi luokkakaavio, tietokantakaavio (palaamme näihin myöhemmin kurssilla) tai SQL-kielellä tehdyt tietokantataulujen luomislauseet.

Edeltävän esimerkin perusteella voisi puhua kahdesta taulusta: Taulu Tili ja Taulu Henkilo. Henkilöä kuvaavaan tauluun kuuluisi myös henkilöturvatunnus, jolloin taulu merkittäisiin esimerkiksi muodossa Taulu Henkilo(henkiloturvatunnus). Tämän lisäksi henkilön ja tilin suhdetta kuvattaisiin jollain tavalla -- esimerkiksi "Henkilöllä on yksi tai useampi tili".

Fyysinen abstraktiotaso (physical level, internal level) kuvaa konkreettista tiedon tallentamistapaa kiintolevylle. Tämä sisältää tiedon tietokantaa kuvaavan tai kuvaavien tiedostojen sijainnista, käytettävistä tietorakenteista, ymym. Fyysinen abstraktiotaso on tyypillisesti järjestelmäkohtainen ja riippuu myös ainakin osittain tallennettavan tiedon muodosta.

Structured Query Language (SQL)

Materiaalin ensimmäisen osan esimerkeissä tarkasteltiin tietokoneen kiintolevyllä sijaitsevan tiedon käsittelyä ohjelmallisesti -- käytännössä esimerkki käsitteli fyysistä näkymää tiedon tallennukseen. Esimerkeissä määritettiin tiedon rakenne sekä muuttujien maksimipituus: jos muuttujan todellinen arvo ei vastaa maksimipituutta, tyhjä tila täytetään välilyönneillä. Tällä tavoin ohjelmoija voi olettaa, että tiettyyn muuttujaan liittyvä arvo alkaa aina samasta kohtaa. Laajemmin ajatellen, ohjelmoija tietää myös, että samaisen muuttujan arvo on samassa kohdassa jokaiselle tallennetulle tietueelle (tai oliolle).

Ensimmäisen viikon esimerkeissä tiedon käsittelijän tai tiedon hakijan tulee tuntea käsite indeksi sekä osata hyödyntää indeksiä tiedon hakemisessa. Samalla, jos tiedon hakija haluaa vaikkapa useamman muuttujan arvon, tulee ohjelmaa sekä siinä käytettyjä indeksejä päivittää sopivasti.

Kun tietokantoja käytetään osana jokapäiväistä työtä, edellä kuvattu tiedon käsittely ei ole kovin mielekästä. Se vaatii käytännössä ohjelmointiosaamista sekä ymmärrystä tallennetusta tiedosta ja tiedon fyysisestä esitysmuodosta. Tiedon fyysinen esitysmuoto vaihtelee tallennettavan tiedon mukaan, joten tiedon käsittelyyn tarvitaan parempi ratkaisu. Tätä ongelmaa ja työläyttä ratkaisemaan on luotu useampia korkeamman abstraktiotason esitystapoja, joita käytetään tietokannassa olevan tiedon hakemiseen ja tiedon muokkaamiseen.

Tällä kurssilla keskitytään Structured Query Language (SQL) -kieleen. Structured Query Language (jatkossa SQL) on 1980-luvulla standardoitu kieli tietokantakyselyiden tekemiseen. SQL-kielen avulla voidaan määritellä tallennettavan tiedon muoto, luoda ja muokata tietokantatauluja, lisätä tietoa tietokantatauluihin, muokata tietokantatauluissa olevaa tietoa sekä hakea tietoa tietokannoista. Merkittävä osa tällä hetkellä käytössä olevista tietokannanhallintajärjestelmistä mahdollistaa SQL-kielellä tehtyjen kyselyiden käyttämisen tietokannanhallintajärjestelmässä olevien tietokantojen ja tietokantataulujen käsittelyyn. Voidaan ajatella, että SQL-kielellä tehdyt kyselyt ovat kuvattu käsitteellisellä abstraktiotasolla, eli SQL-kieltä käytettäessä tiedon lopulliseen esitysmuotoon kiintolevyllä ei oteta kantaa.

Vuosien mittaan standardista on julkaistu useita versioita, joista viimeisin on vuodelta 2016. Tietokannanhallintajärjestelmät ja niiden eri versiot noudattavat SQL-kielen standardeja vaihtelevasti. Yhtä tietokannanhallintajärjestelmää varten luodut kyselyt eivät ole aina suoraan siirrettävissä toiseen tietokannanhallintajärjestelmään. On siis syytä huomioida että tietokannanhallintajärjestelmästä toiseen siirryttäessä joudutaan usein myös tekemään SQL-kyselyihin (pieniä) muutoksia. Tyypillisimpiä tietotyyppejä, joiden käsittelytapa vaihtelee eri tietokannanhallintajärjestelmien välillä ovat päivämäärät.

Kurssin toisessa osassa tutustutaan yhden tietokantataulun käsittelyyn SQL-kielellä. Opettelemme luomaan tietokantataulun, lisäämään tietokantatauluun tietoa, hakemaan tietokantataulusta tietoa sekä päivittämään ja poistamaan tietokantataulussa olevaa tietoa.

Käytämme tässä osassa SQLite-nimistä tietokannanhallintajärjestelmää.

SQLiten lataaminen ja käyttöönotto

SQLiten saa ladattua osoitteesta https://www.sqlite.org/download.html. Kun olet tallentanut (ja asentanut) SQLiten, käynnistä käyttöjärjestelmässä terminaali, ja kirjoita komento sqlite3 tietokanta.db.

Kyseinen komento luo tietokanta.db-nimisen tiedoston, joka sisältää käsittelemäsi tietokannan, ja avaa yhteyden kyseiseen tietokantaan.

Suorittamalla kyselyn "SELECT 1" sqlite tulostaa arvon 1.

kayttaja@kone:~/kansio/$ sqlite3 tietokanta.db
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> SELECT 1;
1
sqlite>

Voit käyttää kyseistä tietokantaa esimerkkien testaamiseen.

SQL-kyselyiden muodosta

SQL-kieli on "case insensitive", eli sillä, että onko kysely kirjoitettu isoilla vai pienillä kirjaimilla kei ole kyselyn suorituksen kannalta merkitystä. Voimme kirjoittaa komennon SELECT yhtä hyvin muodossa select tai Select -- sama pätee myös taulujen ja sarakkeiden nimille.

Noudatamme tällä kurssilla seuraavaa käytäntöä:

  1. Kaikki SQL-kielen komennot, kuten SELECT, FROM ja WHERE, kirjoitetaan isolla.
  2. Taulujen nimet kirjoitetaan isolla alkukirjaimella. Esimerkiksi Henkilo ja Opiskelija.
  3. Taulujen sarakkeet eli attribuutit kirjoitetaan pienellä. Esimerkiksi nimi ja syntymavuosi.

Tietokantataulun luominen

Tietokantataulu luodaan SQL-kielen CREATE TABLE lauseella, jota seuraa luotavan taulun nimi, ja suluissa tietokantataulun sarakkeiden tiedot pilkulla eroteltuna.

CREATE TABLE TAULUN_NIMI (
    sarakkeen 1 tiedot,
    sarakkeen 2 tiedot,
    sarakkeen 3 tiedot
)

Tietotyypit

Tietokantaan säilöttävä tieto voi olla montaa eri muotoa, esimerkiksi merkkijonoja, numeroita, binäärimuodossa olevia tiedostoja sekä päivämääriä. Tietokannan suunnittelijan tehtävänä on päättää kunkin sarakkeen tiedon tyyppi.

Sarakkeen tiedon tyyppi määrää minkämuotoista tietoa sarakkeen arvoksi voi tallentaa. Käytännössä tietokantataulua luotaessa sarakkeen määrittelyssä annettavat tiedot kertovat tietokannanhallintajärjestelmälle siitä, että minkälaista tietoa sarakkeeseen voidaan lisätä, ja toisaalta samalla minkälaista tietoa sarakkeeseen ei voida lisätä. Sarakkeen tyyppi vaikuttaa myös asioihin, joita sarakkeen arvoilla voi tehdä -- esimerkiksi keskiarvon laskeminen merkkijonotyyppisiä arvoja sisältävästä sarakkeesta ei ole kovin järkevää.

Tyypillisesti käytetyn tietotyypit ovat seuraavat:

  • varchar(n) korkeintaan n merkin pituinen merkkijono.
  • integer kokonaisluku
  • float liukuluku eli desimaaliluku
  • date päivämäärä, tallentaa vuoden, kuukauden ja päivän
  • timestamp aikaleima, tallentaa vuoden, kuukauden, päivän, tunnit, minuutit ja sekunnit -- mahdollisesti myös tarkempia arvoja
Lisää tietotyyppejä

Eri tietokannanhallintajärjestelmät kuten SQLite, MySQL ja PostgreSQL tarjoavat hieman erilaisia tietotyyppejä ohjelmoijan käyttöön. Seuraavissa dokumenteissa kerrotaan näistä enemmän.

Sarakkeen tietojen määrittely

Jokaisesta sarakkeesta kerrotaan sarakkeen nimi, sarakkeeseen tulevan tiedon tyyppi sekä tarvittaessa tietotyypille varattavan tilan koko -- esimerkiksi merkkijonoja tallennettaessa kerrotaan tallennettavan merkkijonon maksimipituus. Jokaisella sarakkeella tulee olla nimi sekä tyyppi. Jos sarakkeen tyyppiä ei määritellä tietokantataulua luotaessa, jotkut tietokannanhallintajärjestelmät määrittelevät sen automaattisesti, toiset taas näyttävät virheilmoituksen.

Sarakkeen tiedot ovat muotoa sarakkeen_nimi sarakkeen_tietotyyppi. Esimerkiksi syntymävuosi määritellään seuraavasti.

syntymavuosi integer

Korkeintaan 200 merkkiä pitkä nimi määriteltäisiin taas seuraavasti.

nimi varchar(200)

Henkilön syntymävuoden ja nimen tallentamiseen tarkoitettu taulu määriteltäisiin seuraavasti. Alla on oletettu, että nimi ei ole koskaan yli 200 merkkiä pitkä.

CREATE TABLE Henkilo (
    syntymavuosi integer,
    nimi varchar(200)
)

Yllä luodun esimerkkitaulun sisältö voisi olla esimerkiksi seuraavanlainen.

syntymävuosi nimi
1997 Pihla
1993 Joni

Tietokantataulun poistaminen

Tietokantataulun poistaminen onnistuu DROP TABLE TAULUN_NIMI -lauseella, missä taulun nimi on poistettavan taulun nimi. Esimerkiksi edellä luodun Henkilo-taulun poistaminen onnistuisi seuraavasti.

DROP TABLE Henkilo

Huomaathan, että tietokantataulun poistaminen poistaa myös kaiken tietokantataulussa olevan datan.

Olemassaolevien tietokantataulujen listaaminen

Olemassaolevien tietokantataulujen listaamiseen ei ole yhtä kaikissa tietokannanhallintajärjestelmissä toimivaa tapaa. Osoitteessa http://onewebsql.com/blog/list-all-tables oleva sivu listaa muutamien tietokannanhallintajärjestelmien syntaksit tietokantataulujen listaamiseen.

SQLiten tietokantataulut saadaan selville komennolla SELECT * FROM sqlite_master.

SELECT * FROM sqlite_master
Sarakkeiden tietotyypin selvittäminen

Attribuutin -- tai sarakkeen -- tietotyypin kysymiseen ei myöskään ole yhtä tapaa, vaan tapa liittyy käytettävään tietokannanhallintajärjestelmään. Käyttämässämme SQLite-versiossa sarakkeen tyypin saa selville kyselyllä PRAGMA TABLE_INFO(TAULUN_NIMI), missä TAULUN_NIMI on tarkasteltavan taulun nimi.

Esimerkiksi

PRAGMA TABLE_INFO(Henkilo)

Tiedon lisääminen tietokantatauluun

Tiedon lisääminen tietokantatauluun tapahtuu INSERT INTO -lauseella. Lausetta INSERT INTO seuraa kohdetaulun nimi, jonka jälkeen määritellään sarakkeet, joihin arvot asetetaan. Näitä seuraa vielä konreettiset arvot.

INSERT INTO TAULUN_NIMI (sarake1, sarake2, sarake3)
    VALUES ('merkkijono hipsuissa','numero ei', 123)

Oletetaan, että käytössämme on edellisessä aliluvussa luotu taulu Henkilo, jossa on sarakkeet syntymavuosi ja nimi. Uuden henkilon lisääminen tapahtuu seuraavasti.

INSERT INTO Henkilo (syntymavuosi, nimi)
    VALUES (1923, 'Edgar Frank Codd')

Yllä olevassa esimerkissä tietokantatauluun Henkilo lisätään uusi rivi. Sarakkeeseen syntymävuosi tulee arvo 1923 ja sarakkeeseen nimi tulee merkkijono 'Edgar Frank Codd'.

Vastaavasti vuonna 1947 syntyneen Raymond Boycen lisääminen tietokantatauluun Henkilo onnistuu seuraavasti.

INSERT INTO Henkilo (syntymavuosi, nimi)
    VALUES (1947, 'Raymond Boyce')

Tiedon hakeminen tietokantataulusta

Tiedon hakeminen tietokantataulusta onnistuu SELECT-lauseella. Avainsanaa SELECT seuraa haettavat sarakkeet, tietokantataulun nimi sekä mahdollisesti rajausehto tai rajausehtoja.

Ilman rajausehtoa kyselyn rakenne on seuraava.

SELECT sarake1, sarake2 FROM TAULUN_NIMI

Rajausehdon kanssa kyselyn rakenne on seuraava.

SELECT sarake1, sarake2 FROM TAULUN_NIMI
    WHERE rajausehto

Oletetaan, että käytössämme on seuraava taulu, jonka nimi on Henkilo.

syntymavuosi nimi
1997 Pihla
1993 Joni
1947 Raymond
1923 Edgar

Jos haluamme listata kaikki taulun henkilöt, kysely olisi muotoa.

SELECT syntymavuosi, nimi FROM Henkilo

Valinta henkilöihin, jotka ovat syntyneet ennen vuotta 1950 onnistuu seuraavasti.

SELECT syntymavuosi, nimi FROM Henkilo WHERE syntymavuosi < 1950
Kaikki sarakkeet

Hakukyselyn SELECT-komentoa seuraava sarakelistaus voidaan korvata tähtimerkillä * jos halutaan hakea kaikki tietokantataulun sarakkeet. Olettaen, että taulussa Henkilo on vain sarakkeet syntymavuosi ja nimi, kysely:

SELECT syntymavuosi, nimi FROM Henkilo WHERE syntymavuosi < 1950

Voidaan korvata kyselyllä

SELECT * FROM Henkilo WHERE syntymavuosi < 1950

Jos sarakkeen arvot ovat merkkijonoja, kuten nimi, voi hakuehdossa käyttää LIKE-operaatiota. Tämän avulla hakutuloksia voi rajata osittaisen merkkijonon avulla. Esimerkiksi kysely SELECT * FROM Henkilo WHERE nimi LIKE '%a%' hakee kaikki henkilöt, joiden nimessä esiintyy a-kirjain.

Loogiset operaatiot

Rajausehtoihin voi lisätä loogisia operaatioita kyselyjen tulosten rajaamiseksi. Operaatio 'ja', eli kahden rajausehdon yhdistäminen, toimii avainsanalla AND. Esimerkiksi kysely SELECT * FROM Henkilo WHERE nimi = 'Ted' AND syntymavuosi = 1920 listaa vain ne henkilöt, joiden nimi on 'Ted' ja joiden syntymävuosi on 1920.

Operaation 'tai' lisääminen on myös mahdollista. Esimerkiksi kysely SELECT * FROM Henkilo WHERE nimi = 'Matti' OR nimi = 'Maija' listaisi kaikki ne henkilöt, joiden nimi on Matti tai Maija.

Kyselyissä toimivat myös suurempi kuin > ja pienempi kuin < -operaatiot.

Ehtoja voi myös yhdistää, jonka lisäksi suluilla voi rajata suoritusjärjestystä. Tutki kyselyä SELECT * FROM Kurssisuoritus WHERE (kurssi = 'Ohjelmoinnin perusteet' OR kurssi = 'Ohjelmoinnin jatkokurssi') AND arvosana = 3 ja mieti mitä se tekee.

Tiedon päivittäminen

Tietokantataulussa olevan tiedon päivittäminen onnistuu UPDATE-lauseella. Komentoa UPDATE seuraa tietokantataulun nimi, avainsana SET, jota seuraa sarakekohtaiset uudet arvot. Lopuksi kyselyyn lisätään rajausehto, jonka perusteella rajataan muutettavia rivejä.

UPDATE TAULUN_NIMI
    SET sarake1='uusiarvo', sarake2=1234
    WHERE sarake3='rajausarvo'

Esimerkiksi 'Joni'-nimisen henkilön nimen päivittäminen muotoon 'Joni S' onnistuu seuraavasti.

UPDATE Henkilo
    SET nimi='Joni S'
    WHERE nimi='Joni'

Tiedon poistaminen tietokantataulusta

Tiedon poistaminen tietokantataulusta onnistuu DELETE FROM -lauseella. Lauseeseen määritellään lisäksi tietokantataulu, mistä tietoa poistetaan, ja mahdollisesti ehtoja, jotka rajaavat poistettavia rivejä. Yksinkertaisimmillaan komennolla poistetaan kaikki rivit annetusta taulusta seuraavasti.

DELETE FROM TAULUN_NIMI

Poistettavien rivien rajaaminen tapahtuu WHERE-ehdolla, jota seuraa poistettavien arvojen rajaus. Esimerkiksi kaikki vuonna 1920 syntyneet henkilöt poistettaisiin tietokantataulusta Henkilo seuraavalla komennolla.

DELETE FROM Henkilo WHERE syntymavuosi = 1920

Poistoehtoon voi rakentaa loogisen lauseen, joka sisältää AND ja OR -määreitä. Näiden avulla poistorajausta voi tehdä laajemmin. Alla olevassa esimerkissä poistetaan henkilö (tai henkilöt), joiden syntymävuosi on 1947 ja joiden nimi on 'Raymond Boyce'.

DELETE FROM Henkilo WHERE syntymavuosi = 1947 AND nimi = 'Raymond Boyce'

Pienempi kuin ja suurempi kuin -vertailuoperaatiot sekä erisuuri kuin vertailuoperaatiot ovat myös mahdollisia. Alla olevassa esimerkissä poistetaan kaikki henkilöt, joiden syntymävuosi on pienempi kuin 2000 ja joiden nimi ei ole 'Boyce-Codd'.

DELETE FROM Henkilo WHERE syntymavuosi < 2000 AND nimi != 'Boyce-Codd'

Yhteenveto

Operaatio Avainsana Esimerkki
Tietokantataulun luominen CREATE
CREATE TABLE Opiskelija (
    opiskelijanumero integer,
    nimi varchar(60),
    sahkopostiosoite varchar(40)
)
Tietokantataulun poistaminen DROP
DROP TABLE Opiskelija
Tiedon lisääminen INSERT
INSERT INTO
    Opiskelija (opiskelijanumero, nimi, sahkopostiosoite)
    VALUES (1008286, 'Ari', 'posti@osoite.net');
Tiedon hakeminen SELECT
SELECT nimi FROM Opiskelija
Tiedon päivittäminen UPDATE
UPDATE Opiskelija
    SET nimi='Ari V'
    WHERE opiskelijanumero=1008286
Tiedon (rivien) poistaminen DELETE
DELETE FROM Opiskelija
    WHERE opiskelijanumero=1008286

Huom! Suurin osa tästä tehtävästä tulee tehdä SQLite-ohjelmalla. Materiaalista löytyy aiemmin ohjeet kyseisen ohjelman asentamiseen. Jos käytät Linux- tai MacOS-käyttöjärjestelmää, sqlite on todennäköisesti valmiiksi asennettuna koneellesi. Jos latasit viikon tehtäväpohjat ennen 14.9., tietokantatiedostoissa on todennäköisesti bugi. Bugi korjaantuu kun lataat pohjan uudestaan (poista tehtävä ja lataa se uudestaan).

Etsi tehtäväpohjan sisältävä kansio ja mene sen alikansioon db. Kyseisessä kansiossa on valmiina tiedosto esimerkki.db.

Luo kansioon db tietokanta levy.db. Avaa tietokanta sqliten avulla, ja luo tietokantaan tietokantataulu nimeltä Kappale, joka sisältää seuraavat sarakkeet:

  • nimi (merkkijono, maksimipituus 100 merkkiä)
  • artisti (merkkijono, maksimipituus 80 merkkiä)
  • levytysvuosi (kokonaisluku)
  • pituus (kokonaisluku)

Lisää tämän jälkeen tietokantatauluun Kappale seuraavat kappaleet.

Nimi Artisti Levytysvuosi Pituus
Capito Tutto Kummeli 1994 124
Kanada Kummeli 1994 119
Tango Vibrato Kummeli 1994 117

Huom! Suurin osa tästä tehtävästä tulee tehdä SQLite-ohjelmalla. Materiaalista löytyy aiemmin ohjeet kyseisen ohjelman asentamiseen.

Etsi tehtäväpohjan sisältävä kansio ja mene sen alikansioon db. Kyseisessä kansiossa on valmiina tiedosto palkkatilastot.db. Kyseisessä tiedostossa on yksityisen sektorin mediaanipalkat. Palkkatilastot on noudettu tilastokeskuksen sivuilta osoitteesta http://www.stat.fi/til/yskp/2014/yskp_2014_2015-08-20_tie_001_fi.html.

Selvitä tiedostosta seuraavat tiedot:

  • Mikä on ohutlevyseppien kuukausittainen mediaaniansio?
  • Kuinka monen työtehtävän mediaaniansio on alle 1900 euroa kuussa?
  • Kuinka monen työtehtävän mediaaniansio on yli 7000 euroa kuussa?
  • Minkä työtehtävän mediaaniansio on tasan 3300 euroa kuussa?

TMC:n tehtäväpohjassa on valmiit kohdat vastauksille. Kun tiedoston Tilastoja.java käynnistää nyt, ohjelma tulostaa.

Ohutlevyseppien mediaaniansio: 0
Työtehtäviä, joiden mediaaniansio on alle 1900 euroa: 0
Työtehtäviä, joiden mediaaniansio on yli 7000 euroa: 0
Työtehtävä, jonka mediaaniansio on tasan 3300 euroa kuussa: ???

Kun olet selvittänyt tilastot, muokkaa ohjelmaa niin, että ohjelma tulostaa edellä toivotut lukumäärät (ja työtehtävän) oikein.

Tehtäväpohjassa on myös tiedosto palkkatilastot_varalla.db siltä varalta, että muokkaat vahingossa käsiteltävää tietokantatiedostoa.

Tietokannan käsittely ohjelmallisesti

Lähes jokainen ohjelmointikieli tarjoaa jonkinlaisen rajapinnan tietokantakyselyiden tekemiseen. Nämä rajapinnat suoraviivaistavat kyselyiden tekemistä tietokantoihin ja tietokannanhallintajärjestelmien käyttöönoottoa, sillä rajapintaa noudattamalla yhteydenotto tietokannantallintajärjestelmään on lähes samankaltaista käytetystä tietokannanhallintajärjestelmästä riippumatta.

Java-kielessä tähän tehtävään on Java Database Connectivity (JDBC) -rajapinta. JDBC tarjoaa tuen tietokantayhteyden luomiseen sekä kyselyiden suorittamiseen tietokantayhteyden yli. Jotta JDBCn avulla voidaan ottaa yhteys tietokantaan, tulee käytössä olla tietokannanhallintajärjestelmäkohtainen ajuri, jonka vastuulla on tietokantayhteyden luomiseen liittyvät yksityiskohdat sekä tietokannanhallintajärjestelmän sisäisten kyselytulosten muuntaminen JDBC-rajapinnan mukaiseen muotoon.

JDBC-ajurin noutaminen

JDBC-ajurit ovat käytännössä Java-kielellä kirjoitettuja ohjelmia, joita tietokannanhallintajärjestelmän toteuttajat tarjoavat ohjelmoijien käyttöön. Kurssin toisessa osassa ajurit on lisätty valmiiksi tehtäväpohjien lib-kansioon, jonka lisäksi niiden käyttö on valmiiksi määritelty tehtäväpohjissa.

Myöhemmissä osissa tutustumme kirjastojen käyttöönottoon ja hakemiseen Maven-apuvälineen avulla.

Ohjelmallinen tietokantakysely kokonaisuudessaan

Oletetaan, että käytössämme on seuraava tietokantataulu Opiskelija:

opiskelijanumero (integer) nimi (varchar) syntymävuosi (integer) pääaine (varchar)
9999999 Pihla 1997 Tietojenkäsittelytiede
9999998 Joni 1993 Tietojenkäsittelytiede
...

JDBCn avulla kyselyn tekeminen tietokantatauluun tapahtuu seuraavasti -- olettaen, että käytössämme on sekä tietokanta, että tietokannan ajuri.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class Main {
    public static void main(String[] args) throws Exception {
        // luodaan yhteys jdbc:n yli sqlite-tietokantaan nimeltä "tietokanta.db"
        Connection connection = DriverManager.getConnection("jdbc:sqlite:tietokanta.db");

        // luodaan kyely "SELECT * FROM Opiskelija", jolla haetaan
        // kaikki tiedot Opiskelija-taulusta
        PreparedStatement statement = connection.prepareStatement("SELECT * FROM Opiskelija");

        // suoritetaan kysely -- tuloksena resultSet-olio
        ResultSet resultSet = statement.executeQuery();

        // käydään tuloksena saadussa oliossa olevat rivit läpi -- next-komento hakee
        // aina seuraavan rivin, ja palauttaa true jos rivi löytyi
        while(resultSet.next()) {
            // haetaan nykyiseltä riviltä opiskelijanumero int-muodossa
            Integer opNro = resultSet.getInt("opiskelijanumero");
            // haetaan nykyiseltä riviltä nimi String-muodossa
            String nimi = resultSet.getString("nimi");
            // haetaan nykyiseltä riviltä syntymävuosi int-muodossa
            Integer syntVuosi = resultSet.getInt("syntymävuosi");
            // haetaan nykyiseltä riviltä pääaine String-muodossa
            String paaAine = resultSet.getString("pääaine");

            // tulostetaan tiedot
            System.out.println(opNro + "\t" + nimi + "\t" + syntVuosi + "\t" + paaAine);
        }

        // suljetaan lopulta yhteys tietokantaan
        connection.close();
    }
}

Ohjelman suoritus tuottaa (esimerkiksi) seuraavanlaisen tulostuksen:

999999	Pihla	1997	Tietojenkäsittelytiede
999998	Joni	1993	Tietojenkäsittelytiede
999997	Anna	1991	Matematiikka
999996	Krista	1990	Tietojenkäsittelytiede
...

Ohjelman rakentaminen osissa

Tässä oletetaan, että projektiin on lisätty tarvittava JDBC-ajuri.

Avaa projektiin liittyvä Source Packages ja valitse (tai tarvittaessa luo) sopiva pakkaus. Oletetaan tässä, että käytössä on pakkaus tikape. Valitse tämän jälkeen New -> Java Class, jonka jälkeen avautuu valikko, missä voit antaa luokalle nimen. Anna luokan nimeksi Main.

Avaa tiedosto tuplaklikkaamalla sitä. Muokkaa tiedostoa vielä siten, että se on seuraavan näköinen:

package tikape;

public class Main {

    public static void main(String[] args) throws Exception {

    }
}

Tietokantayhteyden luominen

Lisää projektiin import-komento import java.sql.*;, joka hakee kaikki SQL-kyselyihin liittyvät Javan kirjastot.

package tikape;

import java.sql.*;

public class Main {

    public static void main(String[] args) throws Exception {

    }
}

Avataan seuraavaksi tietokantayhteys tietokantatiedostoon nimeltä testi.db ja tehdään kysely "SELECT 1", jolla pyydetään tietokantaa palauttamaan luku 1 -- käytämme tätä yhteyden testaamiseksi. Jos yhteyden luominen onnistuu, tulostetaan "Hei tietokantamaailma!", muulloin "Yhteyden muodostaminen epäonnistui".

package tikape;

import java.sql.*;

public class Main {

    public static void main(String[] args) throws Exception {
        Connection connection = DriverManager.getConnection("jdbc:sqlite:testi.db");

        PreparedStatement statement = connection.prepareStatement("SELECT 1");

        ResultSet resultSet = statement.executeQuery();

        if(resultSet.next()) {
            System.out.println("Hei tietokantamaailma!");
        } else {
            System.out.println("Yhteyden muodostaminen epäonnistui.");
        }
    }
}
Hei tietokantamaailma!

Kun suoritamme ohjelman ensimmäistä kertaa valitsemalla Run -> Run Project, puuttuvan tietokannan paikalle luodaan tietokanta (ainakin SQLiteä käyttäessä). Projektin kansiossa on nyt tiedosto testi.db, joka on tietokantamme.

Kun ohjelma on suoritettu ensimmäistä kertaa, tiedosto testi.db luodaan projektiin.
Tietokantatiedosto testi.db löytyy projektin kansiosta. Tiedostot löytyvät Files-välilehdeltä.

Tietokantakyselyiden tekeminen

Osoitteessa vuokraamo.db löytyy kuvitteellisen moottoripyörävuokraamon tietokanta. Lataa se edellä tehdyn projektin juureen ja kokeile kyselyn tekemistä kyseiseen tietokantaan.

Tietokannassa on tietokantataulu Pyora, jolla on sarakkeet rekisterinumero ja merkki. Jokaisen pyörän rekisterinumeron ja merkin tulostaminen tapahtuu seuraavasti -- huomaa myös, että olemme vaihtaneet käytössä olevaa tietokantaa.

Connection connection = DriverManager.getConnection("jdbc:sqlite:vuokraamo.db");

PreparedStatement stmt = connection.prepareStatement("SELECT * FROM Pyora");
ResultSet rs = stmt.executeQuery();

while (rs.next()) {
    String rekisterinumero = rs.getString("rekisterinumero");
    String merkki = rs.getString("merkki");

    System.out.println(rekisterinumero + " " + merkki);
}

stmt.close();
rs.close();

connection.close();

Käydään ylläoleva ohjelmakoodi läpi askeleittain.

  1. Luomme ensin JDBC-yhteyden tietokantaan vuokraamo.db.

    Connection connection = DriverManager.getConnection("jdbc:sqlite:vuokraamo.db");
    

  2. Kysely luodaan antamalla yhteydelle merkkijono, jossa on kysely. Yhteys palauttaa PreparedStatement-olion, jota käytetään kyselyn suorittamiseen ja tulosten pyytämiseen. Metodi executeQuery suorittaa SQL-kyselyn ja palauttaa tulokset sisältävän ResultSet-olion.

    PreparedStatement statement = connection.prepareStatement("SELECT * FROM Pyora");
    ResultSet resultSet = statement.executeQuery();
    

  3. Tämän jälkeen ResultSet-oliossa olevat tulokset käydään läpi. Metodia next() kutsumalla siirrytään kyselyn palauttamissa tulosriveissä eteenpäin. Kultakin riviltä voi kysyä sarakeotsikon perusteella solun arvoa. Esimerkiksi kutsu getString("rekisterinumero") palauttaa kyseisellä rivillä olevan sarakkeen "rekisterinumero" arvon String-tyyppisenä.

    while(resultSet.next()) {
        String rekisterinumero = rs.getString("rekisterinumero");
        String merkki = rs.getString("merkki");
    
        System.out.println(rekisterinumero + " " + merkki);
    }
    

  4. Kun kyselyn vastauksena saadut rivit on käyty läpi, eikä niitä enää tarvita, vapautetaan niihin liittyvät resurssit.

    stmt.close();
    rs.close();
    

  5. Lopulta tietokantayhteys suljetaan.

    connection.close();
    

Parametrien lisääminen kyselyyn

Kyselyihin halutaan usein antaa rajausehtoja. Ohjelmallisesti tämä tapahtuu lisäämällä kyselyä muodostaessa rajausehtoihin kohtia, joihin asetetaan arvot. Alla olevassa esimerkissä kyselyyn lisätään merkkijono.

PreparedStatement statement = connection.prepareStatement("SELECT * FROM Pyora WHERE merkki = ?");
statement.setString(1, "Royal Enfield");

ResultSet resultSet = statement.executeQuery();

Kyselyiden paikat indeksoidaan kohdasta 1 alkaen. Alla olevassa esimerkissä haetaan Henkilo-taulusta henkilöitä, joiden syntymävuosi on 1952.

PreparedStatement statement = connection.prepareStatement("SELECT * FROM Henkilo WHERE syntymavuosi  = ?");
statement.setInt(1, 1952);

ResultSet resultSet = statement.executeQuery();

Ohjelma voi toimia myös siten, että rajausehdot kysytään ohjelman käyttäjältä.

Scanner lukija = new Scanner(System.in);
System.out.println("Minkä vuoden opiskelijat tulostetaan?");
int vuosi = Integer.parseInt(lukija.nextLine());

// ...

PreparedStatement statement = connection.prepareStatement("SELECT * FROM Henkilo WHERE syntymavuosi  = ?");
statement.setInt(1, vuosi);

ResultSet resultSet = statement.executeQuery();

// ...
PreparedStatement ja setArvo-metodit

Kun kyselyt luodaan tietokantayhteyteen liittyvän olion prepareStatement oliolla, kyselyihin merkitään kysymysmerkeillä ne kohdat, joihin käyttäjän syöttämiä arvoja voidaan lisätä. Kun metodilla setArvo -- esim setInt -- asetetaan parametrin arvo kyselyyn, Java tarkastaa (1) että arvo on varmasti halutun kaltainen ja (2) että arvossa ei ole esimerkiksi hipsuja, joiden kautta parametri voisi vaikuttaa kyselyn tavoitteisiin.

Palaamme tähän kohta SQL-injektioihin liittyvässä aliluvussa.

Päivityskyselyiden tekeminen

Myös päivityskyselyiden kuten rivien lisäämisten ja rivien poistamisten tekeminen onnistuu ohjelmallisesti. Tällöin tuloksessa ei ole erillistä ResultSet-oliota, vaan luku, joka kertoo muuttuneiden rivien määrän. Allaoleva ohjelmakoodi lisää pyöriä sisältävään tietokantaan uuden pyörän.

Connection connection = DriverManager.getConnection("jdbc:sqlite:vuokraamo.db");

PreparedStatement stmt = connection.prepareStatement("INSERT INTO Pyora (rekisterinumero, merkki) VALUES (?, ?)");
stmt.setString(1, "RIP-34");
stmt.setString(2, "Jopo");

int changes = stmt.executeUpdate();

System.out.println("Kyselyn vaikuttamia rivejä: " + changes);
stmt.close();

connection.close();

SQL-injektiot

Suurin osa olemassaolevista sovelluksista käyttää tietokannanhallintajärjestelmiä jollain tavalla: tietoa haetaan tietokannasta, tietokannassa olevaa tietoa muokataan, ja tietokantaan tallennetaan tietoa. Tyypillisesti sovelluksiin on myös käyttöliittymä, minkä kautta sovelluksen käyttäjät pääsevät vaikuttamaan kyselyiden sisältöön.

SQL-injektioiden tekeminen onnistuu jos ohjelmoija jättää tietokantaa käsitteleviin kyselyihin ns. käyttäjän mentävän aukon. Tämä onnistuu Javalla siten, että kyselyt luodaan niin, että kyselyihin lisättävät parametrit syötetään kyselyyn suoraan merkkijonona.

Alla on esimerkki, missä käyttäjältä kysytään haettavan pyörän merkkiä.

Scanner lukija = new Scanner(System.in);
System.out.println("Minkä merkkiset pyörät tulostetaan?");
String merkki = lukija.nextLine();

// ...

PreparedStatement statement = connection.prepareStatement("SELECT * FROM Pyora WHERE merkki = ?");
statement.setString(1, merkki);

// ...

ResultSet resultSet = statement.executeQuery();

Kysely on turvallinen, sillä merkki asetetaan metodin setString avulla. Metodi tarkastaa myös, ettei kyselyssä ole ylimääräistä sisältöä.

Kyselystä saa helposti erittäin turvattoman. Seuraavassa esimerkissä on mahdollisuus SQL-injektioon.

Scanner lukija = new Scanner(System.in);
System.out.println("Minkä merkkiset pyörät tulostetaan?");
String merkki = lukija.nextLine();

// ...

PreparedStatement statement = connection.prepareStatement("SELECT * FROM Pyora WHERE merkki = '" + merkki + "'");

// ...

ResultSet resultSet = statement.executeQuery();

Kun käyttäjän syöttämä merkkijono lisätään suoraan osaksi kyselyä, voi käyttäjä syöttää SQL-lauseita komentoonsa. Jos käyttäjä syöttää ohjelmaan esimerkiksi merkkijono a' OR 'a'='a, on suoritettava SQL-lause lopulta muotoa:

SELECT * FROM Pyora WHERE merkki = 'a' OR 'a'='a'

Edellinen lause on aina totta, sillä tarkastus 'a'='a' on totta.

Myös muunlaisten SQL-lauseiden suoritus olisi em. tapauksessa mahdollista. Web-sarjakuva xkcd kuvastaa tätä ilmiötä hyvin Exploits of a Mom-sarjakuvallaan.

School: Hi, this is your son's school. We're having some computer trouble.
								    
									Mom: Oh, dear -- Did he break something?
								    
								    School: In a way. Did you really name your son Robert'); DROP TABLE Students;--?
								    
								    Mom: Oh. Yes. Little Bobby Tables we call him.
								    
								    School: Well, we've lost this year's student records. I hope you're happy.
								    
								    Mom: And I hope you've learned to sanitize your database inputs.
http://xkcd.com/327/ -- Exploits of a Mom.

Luo tekstikäyttöliittymää käyttävä interaktiivinen sovellus, jossa voi listata ja lisätä kappaleita. Käytä tässä ensimmäisessä tehtävässä luomaasi tietokantaa (tai ainakin sen rakennetta). Ohjelman tulee käynnistyä kun luokassa ListaaminenJaLisaaminen oleva main-metodi suoritetaan. Ohjelman toiminnan tulee olla seuraavanlainen. Alla punaisella merkityt rivit ovat käyttäjän syöttämiä.

Tervetuloa!
Komennot:
1: Listaa kappaleet
2: Lisää kappale
3: Sulje
    
> 1
(ei kappaleita)

> 2
Nimi: The Days you Didn't Notice
Artisti: Rendezvous Park
Levytysvuosi: 2010
Pituus: 605

> 2
Nimi: Closer To Being Here - Reprise
Artisti: Rendezvous Park
Levytysvuosi: 2012
Pituus: 363

> 1
Rendezvous Park, Closer to Being Here - Reprise (363 s), 2012
Rendezvous Park, The Days you DIdn't Notice (605 s), 2010

> 3

Sovelluksen tulee käyttää tietokantaa tietojen tallentamiseen ja hakemiseen. Huomaa, että sovelluksen toiminnan tulee jatkua siitä mihin se jäi. Seuraavalla käynnistyskerralla edellisellä kerralla lisättyjen kappaleiden tulee siis olla olemassa tietokannassa.

Tervetuloa!
Komennot:
1: Listaa kappaleet
2: Lisää kappale
3: Sulje
    
> 1
Rendezvous Park, Closer to Being Here - Reprise (363 s), 2012
Rendezvous Park, The Days you DIdn't Notice (605 s), 2010

> 3

Sovelluksessa ei saa olla mahdollisuutta SQL-injektion tekemiseen.

Toteuta sama sovellus kuin edellä, mutta lisää sovellukseen SQL-injektiomahdollisuus. Käyttäjän tulee siis pystyä kirjoittamaan syötteenä -- ainakin jossain kohtaa sovellusta -- SQL-koodia, joka suoritetaan tietokannassa.

Luennolla 15.9. huomattiin, että JDBC ei hyväksy useampaa lausetta samassa kyselyssä. Jos haluat sallia useamman kyselyn tekemisen, lisää tietokantayhteyden muodostamiseen parametri allowMultiQueries=true, esim. DriverManager.getConnection("jdbc:sqlite:vuokraamo.db?allowMultiQueries=true").

Tähän löytyy muutamia muitakin tapoja, kuten useamman rivin lisääminen "vahingossa" (kts. https://stackoverflow.com/questions/452859/inserting-multiple-rows-in-a-single-sql-query) sekä -- todennäköisesti -- harmin aiheuttaminen alikyselyn kautta (kts. https://stackoverflow.com/questions/12467354/nesting-queries-in-sql).

Sisällysluettelo