Opas tietokantasovellusten tekemisen aloittamiseen Java-kielellä, osa 6

Edellisessä osassa kytkettiin uimaseura uimari-olioon siten, että uimari tiesi aina oman uimaseuransa.

Tässä osassa mahdollistetaan uimarien listaus uimaseuran kautta.

Liitostaulu

Pystyäksemme listaamaan kaikki uimaseuraan liittyvät uimarit, tietokantamme tarvitsee liitostaulun (join table). Liitostaulu kytkee kahden tai useamman taulun tietoja yhteen. Omassa tapauksessamme liitostaulussa on sarakkeet "uimari_id" ja "uimaseura_id", joita käytetään tallentamaan kytkökset seurojen ja uimareiden välillä.

Kun tietokantaa käytetään ohjelmallisesti, liitostaulun avulla kytkettyjä olioita voidaan käyttää kuten liitoskolumnin (join column) avulla kytkettyjä olioita. Lisätään Uimaseura-luokalle attribuutti uimarit.

@Entity
public class Uimaseura implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column
    private String nimi;
    
    private List<Uimari> uimarit;

    // getterit ja setterit
}

Liitostaulu määritellään annotaatiolla @JoinTable, joka saa parametreikseen kolumnit mitä käytetään kenttien kytkemisessä. Attribuutilla joinColumns kerrotaan kentän nimi, jolla tämä taulu, eli uimaseura identifioidaan liitostaulussa. Attribuutti inverseJoinColumns kertoo kentän nimen, jolla toinen taulu, eli uimari, kytketään liitostauluun. Alla olevassa esimerkissä liitostaulussa käytetään kenttien niminä "uimaseura_id" ja "uimari_id".

@Entity
public class Uimaseura implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column
    private String nimi;

    @JoinTable(joinColumns = {
        @JoinColumn(name = "uimaseura_id")},
    inverseJoinColumns = {
        @JoinColumn(name = "uimari_id")})
    private List<Uimari> uimarit;

    // getterit ja setterit
}

Lisätään lopuksi JPA:lle vinkki kytköksen tyypistä. Yhdestä uimaseurasta on linkki useaan uimariin, eli kytkös on tyyppiä yksi useaan (one to many). Annotaatio @OneToMany ajaa asian.

@Entity
public class Uimaseura implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column
    private String nimi;

    @OneToMany
    @JoinTable(joinColumns = {
        @JoinColumn(name = "uimaseura_id")},
    inverseJoinColumns = {
        @JoinColumn(name = "uimari_id")})
    private List<Uimari> uimarit;

    // getterit ja setterit
}

Muutetaan seuraavaksi Uimari-luokkaa siten, että myös se vinkkaa kytköstyypistä. Monta uimaria voi kuulua samaan uimaseuraan, eli kytkös on tyyppiä monta yhteen (many to one) -- tähän löytyy annotaatio @ManyToOne. Palaamme annotaatioon liitettyyn cascade=CascadeType.MERGE-parametriin myöhemmin.

@Entity
public class Uimari implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column
    private String nimi;
    @Column
    private int syntymaVuosi;
    @ManyToOne(cascade = CascadeType.MERGE)
    @JoinColumn
    private Uimaseura uimaseura;

    // getterit ja setterit
}

Vaikka uimarilla on viite uimaseuraan, ei uimaria tallennettaessa automaattisesti tallenneta muutosta uimaseuraan. Uimaria tallennettaessa on oleellista että myös uimaseuraan liittyvä uimari-lista päivittyy. Muutetaan Uimari-luokan setUimaseura-metodia siten, että siinä asetetaan myös uimari uimaseuraan.

public void setUimaseura(Uimaseura uimaseura) {
    this.uimaseura = uimaseura;
    if (!uimaseura.getUimarit().contains(this)) {
        uimaseura.getUimarit().add(this);
    }
}

Edes tämä muutos ei takaa sitä, että uimaseuran tiedot tallennettaisiin automaattisesti uimaria tallennettaessa.

Merge ja cascade

EntityManager-luokan komento persist luo oliosta uuden ilmentymän tietokantaan, mutta ei osaa käsitellä tilannetta jossa olio on jo tietokannassa. Lisätessämme uimari-oliota uimaseuraan, uimaseura on jo olemassa, ja sitä ei tule luoda uudestaan. Operaatio merge päivittää jo olemassaolevan olion tilan. Jos oliota ei ole vielä tietokannassa, tallentaa merge-operaatio sen sinne.

Muutetaan metodia lisaaUimari siten, että kutsumme merge-metodia metodin persist-sijaan. Xebian blogiposti kertoo merge- ja persist-operaatioiden eroista tarkemmin.

public void lisaaUimari(Uimari uimari) {
    EntityManager em = getEntityManager();

    em.getTransaction().begin();
    em.merge(uimari);
    em.getTransaction().commit();
}

Hetkinen, me kutsumme merge-operaatiota vain uimari-oliolle?

Määrittelimme Uimari-luokassa olevaan kytkösvinkkiin (@ManyToOne) parametrin (cascade = CascadeType.MERGE). Cascade-parametri kertoo millaisissa tilanteissa olioiden tallennukset ym. jatketaan myös viitattuihin olioihin. Yllä oleva merkintä, @ManyToOne(cascade = CascadeType.MERGE), kertoo että kutsuessamme merge-operaatiota uimarille, merge-operaatiota kutsutaan myös uimariin liittyvään uimaseuraan. Tallentaessamme uimaria, tallennamme siis samalla myös muutokset siihen liittyvään uimaseuraan.

lista.jsp

Alla olevaa lista.jsp:tä voi käyttää testaamiseen. Huomaa, että uimaseuralla täytyy olla viite uimareihin.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
    "http://www.w3.org/TR/html4/loose.dtd">

<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>JSP Page</title>
    </head>
    <body>
        <h1>Hello ${viesti}</h1>

        <c:if test="${not empty lista}">
            <h2>Uimarit</h2>

            <c:forEach var="uimari" items="${lista}">
                ${uimari.nimi}, seura: ${uimari.uimaseura.nimi} <br/>
            </c:forEach>
        </c:if>

        <!-- testataan onko attribuutti "seurat" tyhjä //-->
        <c:if test="${not empty seurat}">
            <h2>Lisää uimari</h2>
            <!-- jos seurat ei ole tyhjä, annetaan mahdollisuus luoda uusia uimareita //-->
            <form name="uusiUimari"
                  action="${pageContext.request.contextPath}/LisaaUimari"
                  method="post">
                Nimi: <input type="text" name="nimi"/> <br/>
                Syntymävuosi: <input type="text" name="syntymaVuosi"/> <br/>
                Uimaseura: <!-- uimarille pitää valita myös uimaseura //-->
                <select name="seuraId">
                    <c:forEach var="seura" items="${seurat}">
                        <option value="${seura.id}">${seura.nimi}</option>
                    </c:forEach>
                </select><br/>

                <input type="submit" value="Lähetä"/>
            </form>

            <h2>Seurat</h2>
            <ul>
                <c:forEach var="seura" items="${seurat}">
                    <li>${seura.nimi}</li>
                    <c:forEach var="uimari" items="${seura.uimarit}">
                        ${uimari.nimi}<br/>
                    </c:forEach>
                </c:forEach>
            </ul>
        </c:if>

        <h2>Lisää seura</h2>

        <form name="uusiSeura"
              action="${pageContext.request.contextPath}/LisaaUimaseura"
              method="post">
            Nimi: <input type="text" name="nimi"/> <br/>
            <input type="submit" value="Lähetä"/>
        </form>
    </body>
</html>

Seuraavaksi viedään sovellus users.cs.helsinki.fi-koneelle.