Osa 3

Tietokantatransaktiot

Kurssilla tietokantojen perusteet todettiin seuraavaa: Tietokantatransaktio sisältää yhden tai useamman tietokantaan kohdistuvan operaation, jotka suoritetaan (järjestyksessä) kokonaisuutena. Jos yksikin operaatio epäonnistuu, kaikki operaatiot perutaan, ja tietokanta palautetaan tilaan, missä se oli ennen transaktion aloitusta.

Käytännössä transaktioiden avulla pidetään yllä tietokannan eheyttä ja varmistetaan, ettei käyttäjälle näytetä epätoivottua tilaa.

Tietokantatransaktiot määritellään Spring-sovelluskehyksen avulla toteutetuissa sovelluksissa metodi- tai luokkatasolla annotaation @Transactional avulla. Annotaatiolla @Transactional merkittyä metodia suoritettaessa metodin alussa aloitetaan tietokantatransaktio, jossa tehdyt muutokset viedään tietokantaan metodin lopussa. Jos annotaatio @Transactional määritellään luokkatasolla, se koskee jokaista luokan metodia.

Rajapinnalle JpaRepository on valmiina määriteltynä transaktiot luokkatasolle. Tämä tarkoittaa sitä, että yksittäiset tallennusoperaatiot toimivat myös ilman @Transactional-annotaatiota.

Alla on kuvattuna tilisiirto, joka on ehkäpä klassisin transaktiota vaativa tietokantaesimerkki. Jos ohjelmakoodin suoritus epäonnistuu (esim. päätyy poikkeukseen) sen jälkeen kun toiselta tililtä on otettu rahaa, mutta toiselle sitä ei vielä ole lisätty, rahaa katoaa.

@PostMapping("/tilit/{mistaId}/siirra")
public String siirra(@PathVariable Long mistaId,
        @RequestParam Long minneId,
        @RequestParam BigDecimal summa) {
    Tili mista = tiliRepository.getOne(mistaId);
    Tili minne = tiliRepository.getOne(minneId);

    mista.setSaldo(mista.getSaldo().subtract(summa));
    minne.setSaldo(minne.getSaldo().add(summa));

    tiliRepository.save(mista);

    // jos täällä tapahtuu poikkeus,
    // tietokannasta katoaa rahaa

    tiliRepository.save(minne);

    return "redirect:/tilit/" + mistaId;
}

Jos metodille määritellään annotaatio @Transactional, rahat eivät katoa vaan poikkeuksen yhteydessä koko operaatio peruuntuu.

@Transactional
@PostMapping("/tilit/{mistaId}/siirra")
public String siirra(@PathVariable Long mistaId,
        @RequestParam Long minneId,
        @RequestParam BigDecimal summa) {
    Tili mista = tiliRepository.getOne(mistaId);
    Tili minne = tiliRepository.getOne(minneId);

    mista.setSaldo(mista.getSaldo().subtract(summa));
    minne.setSaldo(minne.getSaldo().add(summa));

    tiliRepository.save(mista);

    // jos täällä tapahtuu poikkeus,
    // metodissa tehtyjä muutoksia
    // ei viedä tietokantaan

    tiliRepository.save(minne);

    return "redirect:/tilit/" + mistaId;
}

Vain lukemiseen tarkoitetut transaktiot

Annotaatiolle @Transactional voidaan määritellä parametri readOnly, jonka avulla määritellään kirjoitetaanko muutokset tietokantaan. Jos parametrin readOnly arvo on true, metodiin liittyvä transaktio perutaan metodin lopussa (rollback) eikä metodissa mahdollisesti tehtyjä muutoksia viedä tietokantaan.

Transaktiot ja entiteettien automaattinen hallinta

Kun metodille määritellään annotaatio @Transactional, tietokannasta ladatuista entiteeteistä pidetään kirjaa ja muutokset tallennetaan tietokantaan automaattisesti metodin suorituksen jälkeen.

Tämä tarkoittaa sitä, että tilisiirron määrittevä metodi voidaan kirjoittaa suoraviivaisemmin. Yllä kuvattu metodi toimii samalla tavalla kuin seuraava metdi:

@Transactional
@PostMapping("/tilit/{mistaId}/siirra")
public String siirra(@PathVariable Long mistaId,
        @RequestParam Long minneId,
        @RequestParam BigDecimal summa) {
    Tili mista = tiliRepository.getOne(mistaId);
    Tili minne = tiliRepository.getOne(minneId);

    mista.setSaldo(mista.getSaldo().subtract(summa));
    minne.setSaldo(mista.getSaldo().add(summa));

    return "redirect:/tilit/" + mistaId;
}

Tarkastellaan toista esimerkkiä entiteettien automaattisesta hallinnasta.

Edellisessä osassa toteutettu tilin omistajan lisäämiseen käytetty metodi oli seuraavanlainen.

@PostMapping("/tilit/{tiliId}/omistajat/{henkiloId}")
public String addOmistaja(@PathVariable Long tiliId, @PathVariable Long henkiloId) {
    Tili tili = tiliRepository.getOne(tiliId);
    Henkilo henkilo = henkiloRepository.getOne(henkiloId);

    henkilo.getTilit().add(tili);
    henkiloRepository.save(henkilo);

    return "redirect:/tilit/" + tiliId;
}

Kun metodille lisätään annotaatio @Transactional, ei henkilöä tarvitse metodin suorituksen yhteydessä enää erikseen tallentaa.

@Transactional
@PostMapping("/tilit/{tiliId}/omistajat/{henkiloId}")
public String addOmistaja(@PathVariable Long tiliId, @PathVariable Long henkiloId) {
    Tili tili = tiliRepository.getOne(tiliId);
    Henkilo henkilo = henkiloRepository.getOne(henkiloId);

    henkilo.getTilit().add(tili);

    return "redirect:/tilit/" + tiliId;
}

Metodia voi suoraviivaistaa vielä edellisestä. Alla oleva metodi tekee saman asian kuin edellinen. Metodin luettavuus riippuu toki kontekstista ja on lukijan vastuulla päättää kumpi — yllä vai alla oleva — vaihtoehto on parempi.

@Transactional
@PostMapping("/tilit/{tiliId}/omistajat/{henkiloId}")
public String addOmistaja(@PathVariable Long tiliId, @PathVariable Long henkiloId) {
    henkiloRepository
            .getOne(henkiloId)
            .getTilit().add(tiliRepository.getOne(tiliId));

    return "redirect:/tilit/" + tiliId;
}
Loading
Pääsit aliluvun loppuun! Jatka tästä seuraavaan osaan:

Muistathan tarkistaa pistetilanteesi materiaalin oikeassa alareunassa olevasta pallosta!