Tuntee luokkakaavioissa esiintyvät assosiaatiot yhden suhde yhteen, yhden suhde moneen ja monen suhde moneen. Osaa luoda annetusta luokkakaaviosta relaatiokaavion, ja osaa muuntaa luokkakaavion assosiaatiot tarvittaessa tietokannalle sopivampaan muotoon. Tuntee käsitteet pääavain ja viiteavain. Osaa luoda SQL-kielellä useampia tietokantatauluja sisältävän tietokannan, lisätä tietokantaan tietoa, ja tehdä tietokantaan kyselyitä. Osaa hakea useammasta tietokantataulusta tietoa JOIN-kyselyiden avulla. Osaa luoda ohjelman, joka hyödyntää useamman tietokantataulun sisältävää tietokantaa ja tuntee data access object (DAO) -suunnittelumallin.
Luokkakaavio ja olioiden väliset yhteydet
Luokkakaavio on kuvaus tarkasteltavan ongelman käsitteistä sekä käsitteiden välisistä yhteyksistä. Luokkakaavio voi sisältää myös käsitteisiin liittyvät attribuutit. Ohjelmistokehityksessä luokkakaavio kuvaa järjestelmän luokkarakennetta, eli mitä luokkia ohjelmassa on, minkälaisia luokat ovat ja miten luokat liittyvät toisiinsa.
Alla on kuvattuna erään tilausjärjestelmän luokkakaavio. Tilausjärjestelmässä on asiakkaita, jotka voivat tehdä tilauksia. Tilauksiin liittyy joukko annoksia, joihin taas liittyy raaka-aineet sekä ravintola. Sama annos voi olla useammassa tilauksessa, ja useampi tilaus voi sisältää saman annoksen -- annos on toisaalta aina tietyn ravintolan tarjoama. Jokaisen annoksen kohdalla listataan annokseen kuuluvat raaka-aineet -- sama raaka-aine voi esiintyä useammassa annoksessa, ja yhdessä annoksessa voi olla useampia raaka-aineita.
Jokaisen luokkakaaviossa olevan käsitteen kohdalle on merkitty käsitteeseen kuuluvat attribuutit sekä niiden tietotyypit. Esimerkiksi asiakkaalla on nimi, puhelinnumero, katuosoite, postinumero sekä postitoimipaikka. Edellisistä postinumero tallennetaan numerona, muut tallennetaan merkkijonona. Tilaukseen liittyy aika, kuljetustapa, tieto tilauksen vastaanottamisesta sekä tieto tilauksen toimittamisesta. Aika tallennetaan Date-tyyppisenä tietona, kuljetustapa on merkkijono (esim. "kuljetus" tai "nouto"), ja tieto tilauksen vastaanottamisesta sekä toimittamisesta on tallennettu totuusavoisena muuttujana.
Tarkastellaan yllä olevassa luokkakaaviossa olevia yhteystyyppejä eli käsitteiden välisiä suhteita hieman tarkemmin.
Yhden suhde moneen
Kahden käsitteen välillä on yhden suhde moneen (1-N
) -yhteys, jos ensimmäisen käsitteen ilmentymään voi liittyä monta toisen käsitteen ilmentymää, mutta yhteen toisen käsitteen ilmentymään voi liittyä vain yksi ensimmäisen käsitteen ilmentymä.
Yllä olevassa esimerkissä tällainen suhde on esimerkiksi asiakkaan ja tilauksen välillä. Käytännössä yksittäinen asiakas -- esimerkiksi Anna Asiakas -- voi tehdä monta tilausta, mutta jokainen tehty tilaus liittyy täsmälleen yhteen asiakkaaseen. Sama tilaus ei voi liittyä samaan aikaan Anna Asiakkaaseen sekä Essi Esimerkkiin.
Yhdestä moneen yhteystyyppi edellyttää sen käsitteen ilmentymän olemassaoloa, johon liittyy monta toisen käsitteen ilmentymää. Käytännössä tilauksen luominen edellyttää tilaukseen liittyvän asiakkaan olemassaoloa. Toisin sanoen, tilausta ei voi luoda ilman, että sille olisi asiakas.
Java-lähdekoodina luokka asiakas kuvattaisiin (esimerkiksi) seuraavasti.
public class Asiakas {
String nimi;
String puhelinnumero;
String katuosoite;
Integer postinumero;
String postitoimipaikka;
List<Tilaus> tilaukset;
public Asiakas(String nimi, String puh, String katu, Integer postiNro, String paikka) {
this.nimi = nimi;
this.puhelinnumero = puh;
this.katuosoite = katu;
this.postinumero = postiNro;
this.postitoimipaikka = paikka;
this.tilaukset = new ArrayList<>();
}
}
Luokalla olisi lisäksi tarvittavat importit sekä metodeja mm. tilausten lisäämiseen.
Vastaavasti luokka tilaus kuvattaisiin lähdekoodina (esimerkiksi) seuraavasti. Alla tilaukseen on lisätty alkuperäisessä laajemmassa luokkakaaviossa näkyneet tuotteet.
public class Tilaus {
Asiakas asiakas;
Date aika;
String kuljetustapa;
Boolean vastaanotettu;
Boolean toimitettu;
List<Tuote> tuotteet;
public Tilaus(Asiakas asiakas, String kuljetustapa, List<Tuote> tuotteet) {
this.asiakas = asiakas;
this.kuljetustapa = kuljetustapa;
this.aika = new Date();
this.vastaanotettu = false;
this.toimitettu = false;
}
}
Luokalla olisi -- taas -- lisäksi tarvittavat importit sekä metodeja tilauksen tilan muokkaamiseen. Jonkun vastuulla olisi esimerkiksi merkitä tilaus vastaanotetuksi kun tilauksen tekeminen aloitetaan, jonka lisäksi tilauksen toimituksen (tai noudon) yhteydessä tilaus tulisi merkitä toimitetuksi.
Yleisemmin ottaen voi todeta, että jos käsitteen ilmentymään liittyy monta toista käsitettä, merkitään se Java-luokassa listalla tai muulla tietorakenteella. Jos käsitteen ilmentymään liittyy taas vain yksi toinen käsite, merkitään se Java-luokassa yksittäisenä muuttujana joka viittaa toiseen luokkaan (käsitteeseen).
Monen suhde moneen
Kahden käsitteen välillä on monen suhde moneen (N-N
) -yhteys, jos ensimmäisen käsitteen ilmentymään voi liittyä monta toisen käsitteen ilmentymää, ja toisen käsitteen ilmentymään voi liittyä monta ensimmäisen käsitteen ilmentymää.
Yllä olevassa esimerkissä tällainen suhde on esimerkiksi annoksen ja raaka-aineen välillä. Käytännössä yksittäinen annos -- esimerkiksi Poronkäristys -- voi sisältää montaa eri raaka-ainetta kuten perunaa, puolukkaa ja poroa. Toisaalta, yksittäinen raaka-aine kuten peruna voi sisältyä moneen eri annokseen.
Yhteystyyppien yhden suhde moneen ja monen suhde moneen lisäksi on myös yhteystyyppi yhden suhde yhteen. Kahden käsitteen välillä on yhden suhde yhteen (1-1
) -yhteys, jos ensimmäisen käsitteen ilmentymään voi liittyä vain yksi toisen käsitteen ilmentymä, ja toisen käsitteen ilmentymään voi liittyä vain yksi ensimmäisen käsitteen ilmentymä.
Edellisessä tilausjärjestelmän luokkakaaviossa tätä yhteystyyppiä ei näkynyt. Eräs esimerkki tällaisesta yhteystyypistä voisi olla kuljettajien ja kuljetusvälineiden välinen -- ehkäpä hieman teennäinen -- yhteys. Yhdellä kuljettajalla voi olla käytössään vain yksi kuljetusväline, ja yksi kuljetusväline voi olla vain yhden kuljettajan käytössä. Vastaavan esimerkin voisi rakentaa myös lainaesineiden kautta -- esimerkiksi yhdellä opiskelijalla voi olla vain yksi laite lainassa ylläpidolta, ja ylläpito voi lainata tietyn laitteen vain yhdelle opiskelijalle.
Luokkakaaviosta relaatiokaavioksi
Relaatiokaavio on kuvaus tietokantatauluista sekä tietokantatauluihin liittyvistä tiedoista. Relaatiokaavion perusteella luodaan tietokantataulujen luomiseen tarvittavat kyselyt. Tutustutaan ensin termiin relaatio ja harjoitellaan sen jälkeen luokkakaavion muuntamista relaatiokaavioksi.
Relaatiomalli
Relaatiomallin perusajatus on tallennettavan tiedon jakaminen käsitteisiin sekä käsitteiden välisiin yhteyksiin. Jokaista käsitettä vastaa relaatiotietokannassa taulu, ja jokaiselle käsitteen ominaisuudelle eli attribuutille on taulussa oma sarake. Jokainen taulun rivi vastaa yhtä käsitteen ilmentymää, ja tietokantatauluja määriteltäessä taululle määritellään tyypillisesti myös avain, jonka perusteella kukin rivi -- eli käsitteen ilmentymä -- voidaan yksilöidä.
Relaatiomallille on myös hieman formaalimpi määritelmä, johon tutustutaan seuraavaksi. Noudatamme tässä Edgar Coddin vuonna 1970 julkaistun artikkelin "A Relational Model of Data for Large Shared Data Banks" esitysasua.
Relaatio
Olkoon S1, S2, ..., Sn arvojoukkoja, joiden sisältämät arvot eivät ole välttämättä täysin erillisiä. Relaatio R on joukko n alkion kokoisia monikkoja. Jokaisen relaatiossa R olevan monikon ensimmäisen arvon tulee kuulua joukkoon S1, toisen arvon kuulua joukkoon S2 jne.
Relaatio R on siis osajoukko joukkojen S1, S2, ..., Sn välisestä karteesisesta tulosta S1 ⨯ S2 ⨯ ... ⨯ Sn.
Relaatio esitetään tyypillisesti taulukkona, jolla on seuraavat ominaisuudet:
- Jokainen taulukon rivi kuvaa yhtä relaation R monikkoa.
- Taulukon rivien järjestyksellä ei ole väliä.
- Jokainen taulukon rivi on erilainen.
- Sarakkeiden järjestyksellä kuvataan relaation arvojoukkoja; ensimmäisen sarakkeen arvot tulevat arvojoukosta S1, toisen sarakkeen arvojoukosta S2 jne..
- Jokaiselle sarakkeelle annetaan nimi, joka kuvaa kunkin arvojoukon mahdollisia arvoja.
Pääavain, viittaaminen ja viiteavain
Jokaisella relaatiolla on tyypillisesti yksi arvojoukko tai arvojoukkojen yhdistelmä, joiden arvojen perusteella voidaan yksilöidä relaation monikko (eli taulukon rivi). Tällaista arvojoukkoa tai arvojoukkojen yhdistelmää kutsutaan pääavaimeksi. Oleellinen ominaisuus relaatioissa on myös saman tai toisen relaation arvoihin viittaaminen. Tämä tapahtuu viiteavaimen avulla. Relaatiossa R oleva arvojoukko tai arvojoukkojen yhdistelmä, joka ei ole relaation R pääavain, mutta sisältää jonkun relaation pääavaimia, on viiteavain.
Joukko-operaatiot ja kyselyt
Relaatiot ovat joukkoja, joten niitä voi käsitellä joukko-opin operaatioiden avulla. Tällä kurssilla näihin ei syvennytä tarkemmin, mutta teemasta kiinnostuneiden kannattanee lukea klassikkoteos "A Relational Model of Data for Large Shared Data Banks".
Muunnos luokkakaaviosta relaatiokaavioksi
Muunnos luokkakaaviosta relaatiokaavioksi tapahtuu seuraavia askeleita noudattaen:
- Askel 1: määrittele jokaiselle luokkakaavion käsitteelle käsitteen ilmentymän yksilöivä pääavain.
- Askel 2: monen suhden moneen -yhteyksien käsittely lisäämällä yhteyteen liitostaulu, mikä muuntaa yhteydet yhden suhde moneen -muotoisiksi
- Askel 3: yhden suhde moneen -yhteyksien käsittely lisäämällä "moneen"-yhteyden päädyssä olevaan tauluun viiteavain
Käsitteen yksilöivän pääavaimen määrittely
Ensimmäisessä askeleessa määrittelemme jokaiselle käsitteelle pääavaimen (primary key). Pääavain yksilöi käsitteen ilmentymän. Pääavaimeksi valitaan (nykyään) tyypillisesti juokseva numero. Käytössämme olevissa relaatiokaaviossa pääavain merkitään merkinnällä (pk).
Alla tilausjärjestelmän luokkakaavioon on merkitty pääavaimet.
Monen suhde moneen -yhteyksien käsittely
Askeleessa kaksi käsitellään monen suhde moneen -yhteydet.
Monen suhde moneen -yhteydet muunnetaan yhden suhde moneen -tyyppisiksi lisäämällä monen suhde moneen -yhteyksille niin kutsuttu liitostaulu. Liitostaulun avulla saadaan selville mikä käsitteen ilmentymä liittyy mihinkin toisen käsitteen ilmentymään.
Käytännössä muunnos tapahtuu seuraavasti. Alla olevassa kuvassa on monen suhde moneen -yhteys käsitteiden Annos ja Raaka-aine välillä. Käsitteille annos ja raaka-aine on jo määritelty pääavaimet.
Luodaan käsitteiden välille liitostaulu AnnosRaakaAine. Liitostaulusta AnnosRaakaAine on yhden suhde moneen -yhteys käsitteisiin Annos ja Raaka-aine. Käytännössä yksi AnnosRaakaAine-käsitteen ilmentymä yksilöi aina yksittäisen annos - raaka-aine -parin.
Jokainen monesta moneen suhde käsitellään yksitellen. Kun kaikki monen suhde moneen -yhteydet on käsitelty, kaavio on seuraavanlainen.
Yhden suhde moneen -yhteyksien käsittely
Yhden suhde moneen -yhteydet käsitellään lisäämällä yhteyden tähdellä merkittyyn käsitteeseen viiteavain, joka viittaa käsitteeseen, joka on merkitty yhteydessä ykkösellä. Viiteavain viittaa toisen käsitteen pääavaimeen. Viiteavain siis yksilöi (toisen) käsitteen ilmentymän, johon nykyinen käsite viittaa. Käytössämme olevissa relaatiokaaviossa pääavain merkitään merkinnällä (fk), jonka lisäksi viiteavaimen nimi ja tyyppi kertoo minkä käsitteen pääavaimeen viiteavain viittaa.
Yllä oleva yhden suhde moneen yhteys käsitellään lisäämällä Tilaus-käsitteeseen viiteavain, joka viittaa asiakkaaseen. Tämän viiteavaimen perusteella jokaisesta tilauksesta saa selville tilaukseen liittyvän asiakkaan.
Jokainen yhdestä moneen suhde käsitellään yksitellen. Kun kaikki yhdestä moneen -yhteydet on käsitelty, kaavio on seuraavanlainen.
Luokkakaavion muunnos relaatiokaavioksi tapahtuu kolmen askeleen kautta. Voit tarkastella relaatiokaavion toimivuutta poistamalla kaikki yhteyksiä kuvaavat viivat.
Piirrä tämän jälkeen viivat takaisin viiteavainten perusteella. Viiteavaimen sisältävän käsitteen päätyyn päätyyn tulee aina tähti ja viivan toiseen päätyyn numero 1. Jos viiteavaimia seuraamalla tapahtunut piirtäminen tuottaa saman kaavion kuin mikä askelia seuraamalla saatu lopputulos oli, on kaavio melko suurella todennäköisyydellä kunnossa.
Relaatiokaaviosta tietokannaksi
Edellisessä luvussa muunnettiin luokkakaavio relaatiokaavioksi kolmea askelta seuraamalla. Relaatiokaavio kuvaa tietokannan tauluja, ja taulut voidaan luoda SQL-kielisillä lauseilla. Relaatiokaavion voi kuvata tekstimuodossa seuraavasti.
Taulu ((pk) paaavaimen_nimi: PaaavaimenTyyppi, (fk) viiteavaimen_nimi: ViitattuTaulu, sarake:SarakeTyyppi, sarake:SarakeTyyppi, ...)
Edellisen relaatiokaavion voi kuvata tekstimuodossa seuraavasti.
- Asiakas((pk) id:Integer, nimi:String, puhelinnumero:String, katuosoite:String, postinumero:Integer, postitoimipaikka:String)
- Ravintola((pk) id:Integer, nimi:String, puhelinnumero:String, katuosoite:String, postinumero:Integer, postitoimipaikka:String)
- Annos((pk) id:Integer, (fk) ravintola_id -> Ravintola, nimi:String, koko:String, hinta:double)
- Tilaus((pk) id:Integer, (fk) asiakas_id -> Asiakas, aika:Date, kuljetustapa:String, vastaanotettu:Boolean, toimitettu:Boolean)
- RaakaAine((pk) id:Integer, nimi:String)
- AnnosRaakaAine((fk) annos_id - > Annos, (fk) raaka_aine_id -> RaakaAine)
- TilausAnnos((fk) tilaus_id - > Tilaus, (fk) annos_id -> Annos)
Kun relaatiokaavion perusteella luodaan tietokantataulut, tulee taulut luoda järjestyksessä joka varmistaa viitattavien taulujen olemassaolon. Ensin luodaan taulut, joissa on vain pääavaimia (ei viiteavaimia), ja vasta sitten taulut, joissa on viiteavaimia. Eräs sopiva luontijärjestys edellisille tauluille on seuraava.
- Asiakas((pk) id:Integer, nimi:String, puhelinnumero:String, katuosoite:String, postinumero:Integer, postitoimipaikka:String)
- Ravintola((pk) id:Integer, nimi:String, puhelinnumero:String, katuosoite:String, postinumero:Integer, postitoimipaikka:String)
- RaakaAine((pk) id:Integer, nimi:String)
- Annos((pk) id:Integer, (fk) ravintola_id -> Ravintola, nimi:String, koko:String, hinta:double)
- Tilaus((pk) id:Integer, (fk) asiakas_id -> Asiakas, aika:Date, kuljetustapa:String, vastaanotettu:Boolean, toimitettu:Boolean)
- AnnosRaakaAine((fk) annos_id - > Annos, (fk) raaka_aine_id -> RaakaAine)
- TilausAnnos((fk) tilaus_id - > Tilaus, (fk) annos_id -> Annos)
Pääavaimen määrittely CREATE TABLE -lauseessa
Pääavain on taulukohtainen tunniste, joka on uniikki jokaiselle taulun riville. Tämän lisäksi sen arvo ei saa olla tyhjä (null) millään rivillä. Pääavaimeksi valitaan joko olemassaoleva attribuutti (tällöin attribuutin tulee olla muuttumaton), tai sitä varten luodaan uusi attribuutti.
Pääavain määritellään tietokantataulun luonnin yhteydessä lisäämällä sarakkeen tyypin perään rajoite PRIMARY KEY
. Tämä tarkoittaa sitä, että rivin arvon tulee olla on uniikki, ja että se ei saa koskaan olla tyhjä. Luodaan tietokantataulut Asiakas ja RaakaAine siten, että niissä on määriteltynä pääavain.
CREATE TABLE Asiakas (
id integer PRIMARY KEY,
nimi varchar(200),
puhelinnumero varchar(20),
katuosoite varcar(50),
postinumero integer,
postitoimipaikka varchar(20)
)
CREATE TABLE RaakaAine (
id integer PRIMARY KEY,
nimi varchar(200)
)
Tietyt tietokannanhallintajärjestelmät tarjoavat tuen automaattiselle pääavaimen arvojen luomiselle. Esimerkiksi SQLite luo automaattisesti kokonaislukutyyppiselle sarakkeelle arvoja, jos sarake on määritelty pääavaimeksi, ja ohjelmoija ei tauluun tietoa lisätessään arvoja erikseen määrittele. Vastaava toiminnallisuus löytyy myös useista muista tietokannanhallintajärjestelmistä -- tutustu esimerkiksi MariaDB:n dokumentaatioon asiasta.
Automaattista pääavaimen arvojen luomista hyödynnetään SQLitessä automaattisesti, jos INSERT-kyselyissä ei määritellä pääavaimelle arvoa. Seuraavilla lisäyskyselyillä tauluun RaakaAine lisättäviin riveihin lisätään automaattisesti lisättävät rivit yksilöivät pääavaimet.
INSERT INTO RaakaAine (nimi) VALUES ('Banaani');
INSERT INTO RaakaAine (nimi) VALUES ('Bataatti');
INSERT INTO RaakaAine (nimi) VALUES ('Peruna');
INSERT INTO RaakaAine (nimi) VALUES ('Porkkana');
Viiteavaimen määrittely CREATE TABLE -lauseessa
Viiteavaimet ovat tietokantatauluissa sarakkeita, joissa olevat arvot viittaavat toisissa tauluissa oleviin pääavaimiin. Tietokantataulua määriteltäessä viiteavaimet listataan sarakkeiden määrittelyn jälkeen. Jokaisen viiteavaimen yhteydessä kerrotaan sekä luotavan taulun sarake -- eli sarake, joka on viiteavain -- että taulu ja sarake, johon viiteavaimella viitataan.
Viiteavaimen määrittely tapahtuu CREATE TABLE -lauseen loppuun asetettavan määäreen FOREIGN KEY(sarake) REFERENCES ViitattavaTaulu(viitattavaSarake) avulla. Viiteavaimia voidaan määritellä useampia.
.
Oletetaan, että käytössämme on edellisessä alikuvussa määriteltytietokantataulu Asiakas. Nyt voimme luoda taulun Tilaus, jossa on viittaus tauluun Asiakas.
CREATE TABLE Tilaus (
id integer PRIMARY KEY,
asiakas_id integer,
aika date,
kuljetustapa varchar(40),
vastaanotettu boolean,
toimitettu boolean,
FOREIGN KEY (asiakas_id) REFERENCES Asiakas(id)
);
Viiteavaimet ovat siis sarakkeita siinä missä muutkin sarakkeet, mutta niille määritellään erikseen tieto siitä, ette ne ovat viiteavaimia sekä tieto siitä, että mihin tauluun ja sarakkeeseen kukin viiteavain viittaa. Taulussa käytettävien viiteavainten määrä ei ole rajattu -- voi olla, että niitä ei ole yhtäkään, tai niitä voi olla useita.
Tiedon lisääminen tauluun Tilaus tapahtuu INSERT INTO -lauseen avulla. Nyt, koska taulussa Tilaus on viiteavain, tulee viitattavan asiakkaan tunnus olla tiedossa ennalta. Jos oletamme, että tilauksen tehnyt asiakkaan pääavaimen arvo on 7, onnistuu tilauksen lisääminen tietokantaan seuraavasti.
INSERT INTO Tilaus (asiakas_id, aika, kuljetustapa, vastaanotettu, toimitettu) VALUES (7, now(), 'nouto', false, false);
Yllä käytetty kutsu now()
hakee järjestelmän ajan ja asetttaa sen lisättävän rivin sarakkeen aika arvoksi.
Toistaiseksi käytössämme olevan SQLiten versiossa viiteavaimiin liittyvät tarkistukset -- eli tietokannanhallintajärjestelmän tekemä varmistus viitattavan rivin olemassaolosta -- ovat oletuksena poissa päältä. Tarkastukset saadaan päälle seuraavalla komennolla.
PRAGMA foreign_keys = ON;
Jokaisella taululla voi olla vain yksi määritelty pääavain. Joskus haluamme, että pääavain liittyy useampaan sarakkeeseen, jolloin sarakkeiden yhdistelmän tulee olla uniikki. Voimme esimerkiksi haluta rajoittaa annokseen määriteltäviä raaka-aineita siten, että kukin raaka-aine saa esiintyä kussakin annoksessa korkeintaan kerran. Tämä onnistuu määrittelemällä taululle AnnosRaakaAine pääavain, joka sisältää sekä annostunnuksen että raaka-aineen
CREATE TABLE AnnosRaakaAine (
annos_id integer,
raaka_aine_id integer,
FOREIGN KEY (annos_id) REFERENCES Annos(id),
FOREIGN KEY (raaka_aine_id) REFERENCES RaakaAine(id),
PRIMARY KEY (annos_id, raaka_aine_id)
)
Tilausjärjestelmämme tietokannan CREATE TABLE -lauseet kokonaisuudessaan.
CREATE TABLE Asiakas (
id integer PRIMARY KEY,
nimi varchar(200),
puhelinnumero varchar(20),
katuosoite varcar(50),
postinumero integer,
postitoimipaikka varchar(20)
);
CREATE TABLE Ravintola (
id integer PRIMARY KEY,
nimi varchar(200),
puhelinnumero varchar(20),
katuosoite varcar(50),
postinumero integer,
postitoimipaikka varchar(20)
);
CREATE TABLE RaakaAine (
id integer PRIMARY KEY,
nimi varchar(200)
);
CREATE TABLE Annos (
id integer PRIMARY KEY,
ravintola_id integer,
nimi varchar(200),
koko varchar(30),
hinta double,
FOREIGN KEY (ravintola_id) REFERENCES Ravintola(id)
);
CREATE TABLE Tilaus (
id integer PRIMARY KEY,
asiakas_id integer,
aika date,
kuljetustapa varchar(40),
vastaanotettu boolean,
toimitettu boolean,
FOREIGN KEY (asiakas_id) REFERENCES Asiakas(id)
);
CREATE TABLE AnnosRaakaAine (
annos_id integer,
raaka_aine_id integer,
FOREIGN KEY (annos_id) REFERENCES Annos(id),
FOREIGN KEY (raaka_aine_id) REFERENCES RaakaAine(id)
);
CREATE TABLE TilausAnnos (
tilaus_id integer,
annos_id integer,
FOREIGN KEY (tilaus_id) REFERENCES Tilaus(id),
FOREIGN KEY (annos_id) REFERENCES Annos(id)
);
Lisätyn rivin pääavaimen saa selville SQL-kyselyllä. Kun lisäämme uuden rivin, saa lisätyn rivin pääavaimen selville SELECT -lauseella. Alla esimerkki RaakaAine-tauluun liittyen.
INSERT INTO RaakaAine (nimi) VALUES ('Banaani');
SELECT id FROM RaakaAine WHERE nimi = 'Banaani';
Ensimmäinen lause lisää raaka-aineen, ja toisella selvitetään raaka-aineen tunnus.
Alla on erään kurssisuorituksia seuraavan järjestelmän luokkakaavio. Järjestelmä pitää kirjaa opiskelijoista, kurssisuorituksista, kurssista sekä tehtävistä. Opiskelijalla voi olla useampia kurssisuorituksia, joista jokainen liittyy aina tiettyyn kurssiin. Kurssiin liittyy useampia tehtäviä ja toisaalta sama tehtävä voi esiintyä useammalla kurssilla.
Tee yllä kuvatun luokkakaavion perusteella tietokantataulun luomiseen tarvittavat CREATE TABLE -lauseet. Määrittele CREATE TABLE -lauseisiin luokkakaaviosta pääteltävien attribuuttien lisäksi oleelliset pää- ja viiteavaimet. Huom! Kannattanee tehdä ensin muunnos relaatiokaavioksi, ja tehdä CREATE TABLE -lauseet relaatiokaavion perusteella.
Kun olet luonut CREATE TABLE -lauseet, etsi tehtäväpohjan sisältävä kansio ja mene sen alikansioon db
. Luo kansioon db
tietokanta kurssit.db
. Avaa tietokanta sqliten avulla, ja suorita edellä luomasi CREATE TABLE -lauseet tietokannassa.
Palauta tehtävä kun tiedosto kurssit.db -tietokanta sisältää em. tietokantataulut.
Tiedon hakeminen kahdesta tai useammasta taulusta
Edellisissä luvuissa loimme luokkakaavion pohjalta relaatiokaavion, jonka pohjalta loimme tietokantataulujen luomiseen tarvittavat kyselyt. Tutustuimme kurssin edellisessä osassa SQL-kyselyihin, joiden avulla mm. haimme tietoa yksittäisestä tietokantataulusta. Tutustutaan seuraavaksi tiedon hakemiseen useammasta taulusta.
Edellisen osan pohjalta muistamme, että SELECT-lauseen avainsanaa FROM
seuraa taulu, josta tietoa haetaan. Voimme määritellä haun kohteeksi useampia tauluja listaamalla ne FROM-avainsanan jälkeen pilkulla eroteltuna seuraavasti SELECT * FROM Asiakas, Tilaus
. Jos emme kerro miten taulujen rivit yhdistetään, on lopputuloksessa kaikki rivit yhdistettynä kaikkiin riveihin -- esimerkiksi jokainen taulun Asiakas rivi yhdistettynä jokaiseen taulun Tilaus riviin.
Tällainen lopputulos ei ole tyypillisesti tavoiteltu -- kyselyn tulostaulu listaa jokaiseen asiakkaaseen kytkettynä jokaisen tilauksen, jolloin todellista tietoa siitä, kenen mikäkin tilaus oli ei ole.
Taulujen yhdistäminen tapahtuu kyselyä rajaavan WHERE
-ehdon avulla siten, että taulun pääavainta verrataan siihen viittaavan taulun viiteavaimeen. Esimerkiksi, jos haluamme vain asiakkaat sekä asiakkaaseen liittyvät tilaukset, hyödynnämme Asiakas-taulun pääavainta id sekä Tilaus-taulun viiteavainta asiakas_id, joka viittaa Asiakas-taulun pääavaimeen. Käytännössä tämä tapahtuu ehdolla WHERE Asiakas.id = Tilaus.asiakas_id
.
Kokonaisuudessaan lause "Listaa jokaisen asiakkaan tekemät tilaukset" kirjoitetaan seuraavasti: SELECT * FROM Asiakas, Tilaus WHERE Asiakas.id = Tilaus.asiakas_id
.
Tulosten nimeäminen
Useamman taulun yhdistäminen onnistuu samalla tavalla. Kaikki taulut, jotka haluamme lisätä kyselyyn, tulevat FROM-avainsanan jälkeen. Jos tauluja on useampi, on hyvä varmistaa, että kaikki taulut yhdistetään avainkenttien perusteella.
Kun yhdistämme useampia tauluja, päädymme helposti tilanteeseen, missä tuloksessa on myös useampia samannimisiä kenttiä. Esimerkiksi tilaustietokannassa taulut Asiakas, Ravintola, Annos ja RaakaAine kukin sisältää attribuutin nimi. Voimme määritellä taulun, mihin haettava kenttä liittyy, pisteoperaattorin avulla. Kyselyn SELECT nimi FROM Asiakas
voi siis kirjoittaa myös muodossa SELECT Asiakas.nimi FROM Asiakas
.
Voimme toisaalta myös nimetä kentän tulostusmuodon seuraavasti SELECT Asiakas.nimi AS asiakas FROM Asiakas
. Edelläoleva kysely hakee Asiakas-taulusta asiakkaan nimen, mutta tulostaa nimet otsikolla 'asiakas'.
Alla oleva kysely listaa asiakkaan sekä kaikki asiakkaan tilauksissa olleet annokset. Huomaa, että jokainen annos tulee omalle rivilleen.
SELECT Asiakas.nimi AS asiakas, Annos.nimi AS annos
FROM Asiakas, Tilaus, TilausAnnos, Annos
WHERE Asiakas.id = Tilaus.asiakas_id
AND TilausAnnos.tilaus_id = Tilaus.id
AND Annos.id = TilausAnnos.annos_id;
Taulujen yhdistämisestä
Jotta tietokantakysely tulostaa oleelliset rivit, on jokainen kyselyyn lisättävä taulu kytkettävä toisiinsa. Eräs hyödyllinen tapa taulujen yhdistämiseen tarvittujen kyselyjen hahmottamiseen on tietokantakaavion katsominen. Jos tavoitteena olisi esimerkiksi etsiä kaikki raaka-aineet, mitä Leevi-niminen asiakas on saattanut syödä, ensimmäinen askel on etsiä polku taulusta Asiakas
tauluun RaakaAine
.
Aloitamme taulusta Asiakas
ja etsimme polkua tauluun RaakaAine
. Jotta pääsemme taulusta Asiakas tauluun RaakaAine, tulee meidän vierailla tauluissa TilausAnnos
, Annos
ja AnnosRaakaAine
.
-
Haemme aluksi asiakkaan nimeltä Leevi.
SELECT Asiakas.nimi AS asiakas FROM Asiakas WHERE Asiakas.nimi = 'Leevi';
-
Kytketään tähän seuraavaksi kaikki Leevin tekemät tilaukset.
SELECT Asiakas.nimi AS asiakas FROM Asiakas, Tilaus WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id;
-
Yhdistämme edelliseen kyselyyn taulun TilausAnnos.
SELECT Asiakas.nimi AS asiakas FROM Asiakas, Tilaus, TilausAnnos WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id AND TilausAnnos.tilaus_id = Tilaus.id;
-
Yhdistämme edelliseen kyselyyn taulun Annos.
SELECT Asiakas.nimi AS asiakas FROM Asiakas, Tilaus, TilausAnnos, Annos WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id AND TilausAnnos.tilaus_id = Tilaus.id AND Annos.id = TilausAnnos.annos_id;
-
Yhdistämme edelliseen kyselyyn taulun AnnosRaakaAine.
SELECT Asiakas.nimi AS asiakas FROM Asiakas, Tilaus, TilausAnnos, Annos, AnnosRaakaAine WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id AND TilausAnnos.tilaus_id = Tilaus.id AND Annos.id = TilausAnnos.annos_id AND AnnosRaakaAine.annos_id = Annos.id;
-
Yhdistämme edelliseen kyselyyn taulun RaakaAine.
SELECT Asiakas.nimi AS asiakas FROM Asiakas, Tilaus, TilausAnnos, Annos, AnnosRaakaAine, RaakaAine WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id AND TilausAnnos.tilaus_id = Tilaus.id AND Annos.id = TilausAnnos.annos_id AND AnnosRaakaAine.annos_id = Annos.id AND RaakaAine.id = AnnosRaakaAine.raaka_aine_id;
-
Lopulta lisäämme raaka-aineen nimien SELECT-komentoon.
SELECT Asiakas.nimi AS asiakas, RaakaAine.nimi AS raaka_aine FROM Asiakas, Tilaus, TilausAnnos, Annos, AnnosRaakaAine, RaakaAine WHERE Asiakas.nimi = 'Leevi' AND Tilaus.asiakas_id = Asiakas.id AND TilausAnnos.tilaus_id = Tilaus.id AND Annos.id = TilausAnnos.annos_id AND AnnosRaakaAine.annos_id = Annos.id AND RaakaAine.id = AnnosRaakaAine.raaka_aine_id;
Taulujen yhdistämisestä JOIN-kyselyillä
Kyselyssä, missä taulujen rivit yhdistetään WHERE-ehdon ja avainten perusteella, valitaan näytettäväksi vain ne rivit, jotka täyttävät annetun ehdon. Entä jos haluaisimme nähdä myös ne kurssit, joita kukaan ei ole suorittanut? Tämä ei ole suoraviivaista WHERE-ehdon kautta rakennetun kyselyn avulla.
Vuonna 1992 julkaistu SQL-standardin versio toi mukanaan JOIN-kyselyt, joiden avulla edellä määritelty ongelma ratkeaa -- pienen harjoittelun kautta. Tutustutaan seuraavaksi aiemmin oppimaamme taulujen yhdistämistapaa tukeviin erityyppisiin JOIN-kyselyihin.
INNER JOIN
Edellä tutuksi tullut kysely SELECT * FROM Asiakas, Tilaus WHERE Asiakas.id = Tilaus.asiakas_id
valitsee vastaukseen vain ne rivit, joiden kohdalla ehto Asiakas.id = Tilaus.asiakas_id pätee, eli missä Asiakkaan id-sarakkeen (pääavaimen) arvo on sama kuin Tilaus-taulun asiakas_id-sarakkeen (viiteavain).
Edellinen kysely voidaan kirjoittaa myös muodossa SELECT * FROM Asiakas INNER JOIN Tilaus ON Asiakas.id = Tilaus.asiakas_id
.
Jos haluamme kyselyyn useampia tauluja, lisätään ne INNER JOIN -komennon avulla kyselyn jatkoksi. Esimerkiksi kaksi seuraavaa kyselyä ovat toiminnallisuudeltaan samankaltaiset.
SELECT Asiakas.nimi AS asiakas, Tilaus.aika AS tilausaika, Annos.nimi AS annos FROM Asiakas, Tilaus, TilausAnnos, Annos WHERE Asiakas.id = Tilaus.asiakas_id AND TilausAnnos.tilaus_id = Tilaus.id AND Annos.id = TilausAnnos.annos_id;
SELECT Asiakas.nimi AS asiakas, Tilaus.aika AS tilausaika, Annos.nimi AS annos FROM Asiakas INNER JOIN Tilaus ON Asiakas.id = Tilaus.asiakas_id INNER JOIN TilausAnnos ON TilausAnnos.tilaus_id = Tilaus.id INNER JOIN Annos ON Annos.id = TilausAnnos.annos_id
Kyselyn INNER JOIN avulla voimme siis tehdä kutakuinkin saman työn kuin aiemman WHERE-ehdon avulla, eli valita mukaan vain ne rivit, joiden kohdalla ehto pätee.
LEFT JOIN
Mikä tekee taulujen liitoksesta JOIN-kyselyn avulla WHERE-ehtoa monipuolisemman, on se, että JOIN-kyselyn avulla voidaan määritellä kyselyehtoa täyttämättömille riveille toiminnallisuutta. Avainsanalla LEFT JOIN
voidaan määritellä kyselyn tulos sellaiseksi, että ehdon täyttävien rivien lisäksi vastaukseen sisällytetään kaikki FROM-avainsanaa seuraavan taulun rivit, joille liitosehto ei täyttynyt.
Allaoleva kysely listaa tilauksia tehneiden asiakkaiden lisäksi myös ne asiakkaat, joilla ei ole yhtäkään tilausta. Tällöin tilaukseen liittyvä vastauksen osa jää tyhjäksi.
SELECT Asiakas.nimi AS asiakas, Tilaus.aika AS tilausaika FROM Asiakas LEFT JOIN Tilaus ON Asiakas.id = Tilaus.asiakas_id
Liitostyypit lyhyesti
Kyselyn JOIN-tyypin voi muotoilla usealla eri tavalla:
-
INNER JOIN
-- palauta vain ne rivit, joihin valintaehto kohdistuu. -
LEFT JOIN
-- palauta kaikki FROM-komentoa seuraavan taulun rivit, ja liitä niihin LEFT JOIN-komentoa seuraavan taulun rivit niiltä kohdin, kuin se on ON-liitosehdossa määritellyn ehdon mukaan mahdollista -
RIGHT JOIN
-- palauta kaikki RIGHT JOIN-komentoa seuraavan taulun rivit, ja liitä niihin FROM-komentoa seuraavan taulun rivit niiltä kohdin, kuin se on ON-liitosehdossa määritellyn ehdon mukaan mahdollista -
FULL JOIN
-- palauta kaikki FROM-komentoa seuraavan taulun rivit sekä kaikki FULL JOIN-komentoa seuraavan taulun rivit, ja liitä ne toisiinsa niiltä kohdin, kuin se on ON-liitosehdossa määritellyn ehdon mukaan mahdollista
Valitettavasti SQLite ei tue RIGHT JOIN ja FULL JOIN -tyyppisiä kyselyitä.
C.L. Moffatt on kirjoittanut hyvän yhteenvedon erilaisista JOIN-tyypeistä. Tutustu yhteenvetoon osoitteessa http://www.codeproject.com/Articles/33052/Visual-Representation-of-SQL-Joins.
Tehtäväpohjan kansiossa db
tulee tiedosto nimeltä Chinook_Sqlite.sqlite
. Avaa tiedosto SQLiten avulla. Kun tietokanta on avattu oikein, komento .tables antaa seuraavanlaisen tulostuksen.
sqlite> .tables Album Employee InvoiceLine PlaylistTrack Artist Genre MediaType Track Customer Invoice Playlist
Kuten taulujen nimistä voi arvata, tietokanta kuvaa digitaalisen musiikin myyntipalvelua. Tietokannan relaatiokaavio löytyy osoitteesta http://chinookdatabase.codeplex.com/wikipage?title=Chinook_Schema&referringTitle=Documentation. Kirjoita SQLiten avulla kyselyt, joilla saa selville seuraavat tiedot.
- Kysely 1: Listaa artistit, jotka soittavat Blues-musiikkia.
- Kysely 2: Listaa soittolistat (playlist), joilla Eric Clapton esiintyy.
- Kysely 3: Listaa sähköpostiosoitteet niiltä asiakkailta, jotka ovat ostaneet Jazz-musiikkia.
Kun olet saanut kyselyt toimimaan, kopioi ne tehtäväpohjassa olevan luokan Kyselyja metodeihin kysely1, kysely2 ja kysely3.
Tietokantataulujen käsittely ohjelmallisesti
Käsittelimme edellisessä osassa tietokantakyselyiden tekemistä ohjelmallisesti. Tietokantakyselyiden tekeminen koostuu oleellisesti muutamasta osasta: (1) yhteyden muodostaminen tietokantaan, (2) kyselyn muodostaminen, (3) kyselyn suorittaminen, (4) vastausten läpikäynti, ja (5) resurssien vapauttaminen sekä yhteyden sulkeminen. Edellisessä osassa käsiteltiin Pyora-taulun sisältävää tietokantaa seuraavasti.
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();
Oliot ja tietokantataulu
Ohjelmoijan näkökulmasta on paljon mielekkäämpää jos tietoa pystyy käsittelemään olioiden avulla. Oletetaan, että käytössämme on luokka Asiakas sekä tietokantataulu Asiakas. Tietokantataulu on luotu seuraavalla CREATE TABLE -lauseella.
CREATE TABLE Asiakas (
id integer PRIMARY KEY,
nimi varchar(200),
puhelinnumero varchar(20),
katuosoite varcar(50),
postinumero integer,
postitoimipaikka varchar(20)
);
Alla on taulua vastaava luokka.
public class Asiakas {
Integer id;
String nimi;
String puhelinnumero;
String katuosoite;
Integer postinumero;
String postitoimipaikka;
public Asiakas(Integer id, String nimi, String puhelinnumero, String
katuosoite, Integer postinumero, String postitoimipaikka) {
this.id = id;
this.nimi = nimi;
this.puhelinnumero = puhelinnumero;
this.katuosoite = katuosoite;
this.postinumero = postinumero;
this.postitoimipaikka = postitoimipaikka;
}
// muita metodeja ym
}
Hakiessamme tietoa tietokantataulusta Asiakas voimme muuntaa kyselyn tulokset Asiakas-olioiksi.
Connection connection = DriverManager.getConnection("jdbc:sqlite:tietokanta.db");
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM Asiakas");
ResultSet rs = stmt.executeQuery();
List<Asiakas> asiakkaat = new ArrayList<>();
while (rs.next()) {
Asiakas a = new Asiakas(rs.getInt("id"), rs.getString("nimi"),
rs.getString("puhelinnumero"), rs.getString("katuosoite"),
rs.getInt("postinumero"), rs.getString("postitoimipaikka"));
asiakkaat.add(a);
}
stmt.close();
rs.close();
connection.close();
// nyt asiakkaat listassa
Myös uuden Asiakas-olion tallentaminen tietokantatauluun onnistuu.
Connection connection = DriverManager.getConnection("jdbc:sqlite:tietokanta.db");
PreparedStatement stmt = connection.prepareStatement("INSERT INTO Asiakas"
+ " (nimi, puhelinnumero, katuosoite, postinumero, postitoimipaikka)"
+ " VALUES (?, ?, ?, ?, ?)");
stmt.setString(1, asiakas.getNimi());
stmt.setString(2, asiakas.getPuhelinnumero());
stmt.setString(3, asiakas.getKatuosoite());
stmt.setInt(4, asiakas.getPostinumero());
stmt.setString(5, asiakas.getPostitoimipaikka());
stmt.executeUpdate();
stmt.close();
// voimme halutessamme tehdä myös toisen kyselyn, jonka avulla saadaan selville
// juuri tallennetun olion tunnus -- alla oletetaan, että asiakkaan voi
// yksilöidä nimen ja puhelinnumeron perusteella
stmt = connection.prepareStatement("SELECT * FROM Asiakas"
+ " WHERE nimi = ? AND puhelinnumero = ?");
stmt.setString(1, asiakas.getNimi());
stmt.setString(2, asiakas.getPuhelinnumero());
ResultSet rs = stmt.executeQuery();
rs.next(); // vain 1 tulos
Asiakas a = new Asiakas(rs.getInt("id"), rs.getString("nimi"),
rs.getString("puhelinnumero"), rs.getString("katuosoite"),
rs.getInt("postinumero"), rs.getString("postitoimipaikka"));
stmt.close();
rs.close();
connection.close();
DAO-suunnittelumalli
Edellisissä esimerkeissä tietokantakyselytoiminnallisuus ja suurin osa ohjelman omasta toiminnallisuudesta on ollut samassa, mikä johtaa helposti sekavaan koodiin. Esimerkiksi tilanteessa, missä alla oleva tietokantalogiikka muuttuisi, lähes koko ohjelmaa tulisi muuttaa.
Tietokantasovelluksia toteuttaessa on hyvin tyypillistä abstrahoida, eli piilottaa, konkreettinen tiedon hakemis- ja tallennustoiminnallisuus siten, että ohjelmoijan ei tarvitse nähdä sitä jatkuvasti. Ideana on, että sovelluskehittäjä käyttää DAO-rajapinnan toteuttamia olioita, ja se, että mistä tai miten tiedot konkreettisesti haetaan ei ole sovelluksen muiden osien tiedossa.
Wikipedia: In computer software, a data access object (DAO) is an object that provides an abstract interface to some type of database or other persistence mechanism. By mapping application calls to the persistence layer, DAO provide some specific data operations without exposing details of the database. This isolation supports the Single responsibility principle. It separates what data accesses the application needs, in terms of domain-specific objects and data types (the public interface of the DAO), from how these needs can be satisfied with a specific DBMS, database schema, etc. (the implementation of the DAO).
Although this design pattern is equally applicable to the following: (1- most programming languages; 2- most types of software with persistence needs; and 3- most types of databases) it is traditionally associated with Java EE applications and with relational databases (accessed via the JDBC API because of its origin in Sun Microsystems' best practice guidelines "Core J2EE Patterns" for that platform).
Hahmotellaan hakemiseen ja poistamiseen liittyvää rajapintaa, joka tarjoaa metodit findOne
, findAll
, saveOrUpdate
ja delete
, eli toiminnallisuudet hakemiseen, tallentamiseen ja poistamiseen. Tehdään rajapinnasta geneerinen, eli toteuttava luokka määrittelee palautettavien olioiden tyypin sekä avaimen.
import java.sql.*;
import java.util.*;
public interface Dao<T, K> {
T findOne(K key) throws SQLException;
List<T> findAll() throws SQLException;
T saveOrUpdate(T object) throws SQLException;
void delete(K key) throws SQLException;
}
Metodi findOne hakee tietyllä avaimella haettavan olion, jonka tyyppi voi olla mikä tahansa, ja metodi saveOrUpdate joko tallentaa olion tai päivittää tietokannassa olevaa oliota riippuen siitä, onko olion id-kentässä arvoa. Alustava hahmotelma konkreettisesta asiakkaiden käsittelyyn tarkoitetusta AsiakasDao
-luokasta on seuraavanlainen.
import java.util.*;
import java.sql.*;
public class AsiakasDao implements Dao<Asiakas, Integer> {
@Override
public Asiakas findOne(Integer key) throws SQLException {
// ei toteutettu
return null;
}
@Override
public List<Asiakas> findAll() throws SQLException {
// ei toteutettu
return null;
}
@Override
public Asiakas saveOrUpdate(Asiakas object) throws SQLException {
// ei toteutettu
return null;
}
@Override
public void delete(Integer key) throws SQLException {
// ei toteutettu
}
}
Käytännössä tyyppiparametrit annetaan rajapinnan toteuttamisesta kertovan avainsanan implements
-yhteyteen. Ylläolevassa esimerkissä haettavan olion tyyppi on Asiakas
, ja sen pääavain on tyyppiä Integer
.
Luodaan tietokanta-abstraktio, jolta voidaan pyytää tietokantayhteyttä tarvittaessa.
import java.sql.*;
public class Database {
private String databaseAddress;
public Database(String databaseAddress) throws ClassNotFoundException {
this.databaseAddress = databaseAddress;
}
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(databaseAddress);
}
}
Jatketaan luokan AsiakasDao
toteuttamista. Lisätään luokkaan tietokannan käyttö tietokanta-abstraktion avulla sekä asiakkaan poistaminen avaimen perusteella
import java.util.*;
import java.sql.*;
public class AsiakasDao implements Dao<Asiakas, Integer> {
private Database database;
public AsiakasDao(Database database) {
this.database = database;
}
@Override
public Asiakas findOne(Integer key) throws SQLException {
// ei toteutettu
return null;
}
@Override
public List<Asiakas> findAll() throws SQLException {
// ei toteutettu
return null;
}
@Override
public Asiakas saveOrUpdate(Asiakas object) throws SQLException {
// ei toteutettu
return null;
}
@Override
public void delete(Integer key) throws SQLException {
Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement("DELETE FROM Asiakas WHERE id = ?");
stmt.setInt(1, key);
stmt.executeUpdate();
stmt.close();
conn.close();
}
}
Vastaavasti yksittäisen asiakkaan noutaminen onnistuisi findOne-metodilla.
import java.util.*;
import java.sql.*;
public class AsiakasDao implements Dao<Asiakas, Integer> {
private Database database;
public AsiakasDao(Database database) {
this.database = database;
}
@Override
public Asiakas findOne(Integer key) throws SQLException {
Connection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM Asiakas WHERE id = ?");
stmt.setInt(1, key);
ResultSet rs = stmt.executeQuery();
boolean hasOne = rs.next();
if (!hasOne) {
return null;
}
Asiakas a = new Asiakas(rs.getInt("id"), rs.getString("nimi"),
rs.getString("puhelinnumero"), rs.getString("katuosoite"),
rs.getInt("postinumero"), rs.getString("postitoimipaikka"));
stmt.close();
rs.close();
conn.close();
return a;
}
@Override
public List<Asiakas> findAll() throws SQLException {
// ei toteutettu
return null;
}
@Override
public Asiakas saveOrUpdate(Asiakas object) throws SQLException {
// ei toteutettu
return null;
}
@Override
public void delete(Integer key) throws SQLException {
Collection conn = database.getConnection();
PreparedStatement stmt = conn.prepareStatement("DELETE FROM Asiakas WHERE id = ?");
stmt.setInt(1, key);
stmt.executeUpdate();
stmt.close();
conn.close();
}
}
Ja niin edelleen. Nyt asiakkaiden muokkaaminen on DAO-rajapintaa käyttävän ohjelman näkökulmasta hieman helpompaa.
Database database = new Database("jdbc:sqlite:kanta.db");
AsiakasDao asiakkaat = new AsiakasDao(database);
Scanner lukija = new Scanner(System.in);
System.out.println("Millä tunnuksella asiakasta haetaan?");
int tunnus = Integer.parseInt(lukija.nextLine());
Asiakas a = asiakkaat.findOne(tunnus);
System.out.println("Asiakas: " + a);
Viitteet olioiden välillä
Edellisessä esimerkissä käsittelimme yksittäistä oliota, josta ei ole viitteitä muihin käsitteisiin. Hahmotellaan seuraavaksi Tilaus-käsitteen käsittelyä ohjelmallisesti. Luodaan ensin Tilausta kuvaava luokka ja toteutetaan tämän jälkeen tilausten tallennuksesta ja käsittelystä vastaava DAO-luokka.
public class Tilaus {
Integer id;
Asiakas asiakas;
Date aika;
String kuljetustapa;
Boolean vastaanotettu;
Boolean toimitettu;
// konstruktorit sekä getterit ja setterit
}
Toteutetaan tilausten käsittelyyn tarkoitettu DAO-luokka siten, että se saa konstruktorissaan sekä viitteen tietokanta-olioon että viitteen asiakkaiden hakemiseen tarkoitettuun Dao-rajapintaan.
import java.util.*;
import java.sql.*;
public class TilausDao implements Dao<Tilaus, Integer> {
private Database database;
private Dao<Asiakas, Integer> asiakasDao;
public TilausDao(Database database, Dao<Asiakas, Integer> asiakasDao) {
this.database = database;
this.asiakasDao = asiakasDao;
}
@Override
public Tilaus findOne(Integer key) throws SQLException {
Connection connection = database.getConnection();
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM Tilaus WHERE id = ?");
stmt.setObject(1, key);
ResultSet rs = stmt.executeQuery();
boolean hasOne = rs.next();
if (!hasOne) {
return null;
}
Asiakas asiakas = asiakasDao.findOne(rs.getInt("asiakas_id"));
Tilaus t = new Tilaus(key, asiakas,
rs.getDate("aika"), rs.getString("kuljetustapa"),
rs.getBoolean("vastaanotettu"), rs.getBoolean("toimitettu"));
rs.close();
stmt.close();
connection.close();
return t;
}
@Override
public List<Tilaus> findAll() throws SQLException {
// ei toteutettu
return null;
}
@Override
public Tilaus saveOrUpdate(Tilaus object) throws SQLException {
// ei toteutettu
return null;
}
@Override
public void delete(Integer key) throws SQLException {
// ei toteutettu
}
}
Nyt yksittäisen tilauksen hakemisen yhteydessä palautetaan sekä tilaus, että siihen liittyvä asiakas. Rajapintaa käyttävän toteutuksen näkökulmasta tietokannan käyttäminen toimii seuraavasti:
Database database = new Database("jdbc:sqlite:kanta.db");
AsiakasDao asiakkaat = new AsiakasDao(database);
TilausDao tilaukset = new TilausDao(database, asiakkaat);
Tilaus t = tilaukset.findOne(4);
System.out.println("Tilauksen teki: " + t.getAsiakas().getNimi());
Kun jatkamme edellistä esimerkkiä, pitäisikö annosta haettaessa hakea aina siihen liittyvä ravintola? Entä pitääkö tilausta haettaessa oikeasti hakea myös tilaukseen liittyvä asiakas?
Hyvä kysymys. Kun tietokantataulujen välisten yhteyksien perusteella tehdään uusia kyselyitä tietokantaan, olemassa on oleellisesti kaksi vaihtoehtoa sekä niiden seuraukset: (1) haetaan liikaa tietoa, jolloin hakemisoperaatioon menee turhaan aikaa, tai (2) haetaan liian vähän tietoa, jolloin tieto tulee hakea myöhemmin.
Yksi tapa ratkaista ongelma on toimia siten, että tietoa haetaan vain silloin kun sitä tarvitaan. Tällöin esimerkiksi vasta Tilaus-olioon mahdollisesti liittyvää getAsiakas
-metodia kutsuttaessa asiakkaaseen liittyvät tiedot haettaisiin tietokannasta -- getAsiakas-metodi tekisi siis tietokantahaun. Tämäkään ei kuitenkaan ratkaise tilannetta, sillä jos tavoitteenamme olisi vaikkapa tulostaa kaikki tilaukset ja niihin liittyvät asiakkaat -- edellisellä lähestymistavalla kaksi tietokantakyselyä -- saattaisi toteutus lopulta tehdä jokaisen tilauksen ja asiakkaan kohdalla oman erillisen tietokantahaun.
Tähän ei ole suoraviivaista ratkaisua. Tyypillisesti Dao-rajapinnan määrittelemille metodeille kerrotaan, tuleeko haettaviin olioihin liittyvät viitteet hakea erikseen.
Tehtäväpohjassa on mukana edellisessä esimerkeissä luodut AsiakasDao ja TilausDao sekä niihin liittyvät luokat. Täydennä Dao-luokkien metodit siten, että jokainen metodi toimii toivotusti. Tehtäväpohjassa olevassa db
kansiossa on mukana myös on mukana myös tiedosto tilauskanta.db
, johon tietokannan taulut ovat luotuna. Lisää tietokantaan tarvitsemaasi testidataa.
Kun Dao-luokkien metodit toimivat oikein, palauta tehtävä TMC:lle.