Adatbázis kezelés Javában

Kategória: Java külső könyvtárak.

Adatok keletkeznek, és szeretnénk azokat tartósan eltárolni. Az adatokat adatbázisokban tároljuk, melyről az oldalamon is olvashatunk az Adatbázisok oldalon. Most azt nézzük meg, hogy mindezt hogyan tudjuk kezelni Java-ban. Nem véletlenül került ez a fejezet is ide: de facto szabványok kialakultak, de ezek nem részei az alap Java-nak. A példában MySQL adatbázist fogunk használni; az Adatbázisok oldalon leírtak szerint telepítsük fel, állítsuk be, hozzuk létre a teszt adatbázist a megadott táblákkal, és töltsük fel adatokkal.

JDBC

A JDBC a Java Database Connectivity (magyarul kb. Java adatbázis kapcsolat) rövidítése, így ebből már sejthető, hogy a Java-ból történő adatbázis elérés alapvető eleméről van szó. A JDBC gyártó specifikus. Nézzünk egy MySQL példát! Ebben az esetben a Maven repository-ból le tudjuk tölteni a JDBC meghajtót (ügyeljünk a megfelelő verzióra):

pom.xml:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>dbexample</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
    </dependencies>
</project>

Az egyes adatbázis rendszerek esetén a következő JDBC-t kell használni:

Lássunk egy példát, ami tartalmaz beszúrást, lekérdezést és törlést!

src/main/java/dbexample/JdbcExample.java:

package dbexample;

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

public class JdbcExample {
    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb?useSSL=false", "csaba", "farago")) {
            // insert
            PreparedStatement insertStatement = connection.prepareStatement("INSERT INTO person(id, name, age, addressid) VALUES (default, ?, ?, null)");
            insertStatement.setString(1, "Gyuri");
            insertStatement.setInt(2, 40);
            insertStatement.executeUpdate();

            // select
            Statement selectStatement = connection.createStatement();
            ResultSet resultSet = selectStatement.executeQuery("SELECT name, age FROM person");
            while (resultSet.next()) {
                String name = resultSet.getString("name");
                int age = resultSet.getInt("age");
                System.out.println(name + " (" + age + ")");
            }

            // delete
            PreparedStatement deleteStatement = connection.prepareStatement("DELETE FROM person WHERE name = ?");
            deleteStatement.setString(1, "Gyuri");
            deleteStatement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

A fenti példához működő adatbázisra van szükség. Tesztelési céllal viszont használhatunk memóriában tárolt adatbázis is, ami a programmal együtt indul és áll le. (Az adatok tehát elvesznek.) Példaként a H2-t nézzük meg. Csak az eltéréseket mutatom meg. A pom.xml-ben nem a mysql függőséget kell betölteni, hanem a h2-t:

<project...>
    ....
    <dependencies>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.200</version>
        </dependency>
    </dependencies>
</project>

Egységteszteléshez kiválóan alkalmas ez a módszer; ebben az esetben célszerű odaírni ezt is: <scope>test</scope>.

A kódban a H2 adatbázis meghajtóját kell betölteni, és magát a sémát is létre kell hozni, mivel az nem létezik. Minden más változatlan:

        ...
        try (Connection connection = DriverManager.getConnection("jdbc:h2:mem:", "", "")) {
            // create
            PreparedStatement createPreparedStatement = connection.prepareStatement("CREATE TABLE PERSON(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200), age INT, addressid INT)");
            createPreparedStatement.executeUpdate();

            // insert
            ...

A JDBC megoldással több probléma is van:

  • Kivétel kivétel hátán! A fenti példában a try-with-resources módszert alkalmazva ez nem látszódik, de a kapcsolat zárása is kivételt dob, még azt is le kell kezelni.
  • Az SQL parancsok szövegként "be vannak drótozva": egy esetleges elgépelés csak futási időben derül ki.
  • Nincs megoldva a relációs adattábla és az objektumorientált osztályszerkezet közötti átjárás. Igazából ebben a formában eléggé macerás az adatok kezelése.
  • A fenti példán ugyan nem látszik, de bonyolultabb esetekben lehetnek különbségek két adatbázis gyártó lekérdezései között.
  • Korábban be kellett tölteni a meghajtót a következő módon: Class.forName("com.mysql.jdbc.Driver");. Néhány leírás még tartalmazza ezt az utasítást. Benne lehet, ártani nem árt (max. annyit, hogy ez is kiválthat ellenőrzött kivételt, amit kezelni kell), de ma már nincs erre szükség.
  • A csatlakozást leíró szöveg tartalmazza ezt: ?useSSL=false. A tapasztalatom szerint enélkül rendben lefutott, a végén viszont kivételt dobott a lecsatlakozáskor.

E problémák kezelésére szintén szabványnak tekinthető megoldások születtek.

JPA és ORM

A JPA a Java Persistence API rövidítése, az ORM pedig a Object-Relational Mapping-é (objektum-relációs leképezés). Fent láthattuk, hogy a "gyalog" módszerrel eléggé nehézkes kezelni az adatokat. Az ORM létrehozza a kapcsolatot az objektumorientált programozási nyelvben (jelen esetben a Java-ban) jelen levő osztályok ill. objektumok, valamint a relációs adatmodellben levő adattáblák ill. azok tartalma között a kapcsolatot. Ez többek között a következő megfeleltetéseket jelenti (relációs adatbázis ↔ objektumorientált nyelv):

  • adattábla ↔ osztály
  • oszlop ↔ attribútum
  • sor ↔ objektum
  • a másik tábla kulcsa (foreign key) ↔ referencia
  • 1-1 kapcsolat ↔ egymásra hivatkozás
  • 1-n kapcsolat ↔ az 1 oldalról valamilyen gyűjteményt használhatunk
  • m-n kapcsolat ↔ mindkettő egy-egy gyűjteményben hivatkozik a másikra, így nem kell külön osztályként kezelni a kapcsolótáblát

A JPA maga a szabvány. A Java világban a legnépszerűbb megvalósítás a Hibernate, azt fogjuk most megnézni. Ez egy adatbázis független felületet nyújt a programozó felé, azaz elvileg ugyanúgy kell megvalósítani a dolgokat minden esetben, a mögöttes adatbázisttól függetlenül.

Feltételezzük, hogy az Adatbázisok oldalon leírtak szerint van beállítva a person és address. A pom.xml-ben benne kell hagyni a JDBC meghajtót, és hozzá kell tenni a hibernate-core megfelelő változatát:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>hu.faragocsaba</groupId>
    <artifactId>ormexample</artifactId>
    <version>1.0</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.15</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>5.4.8.Final</version>
        </dependency>
    </dependencies>
</project>

Létre kell hozni egy perzisztencia leíró fájlt, melyben beállítjuk az adatbázis kapcsolatot (src/main/resources/META-INF/persistence.xml):

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
    <persistence-unit name="hu.faragocsaba.ormexample">
        <description>Hibernate ORM Example</description>
        <class>ormexample.entity.Person</class> 
        <class>ormexample.entity.Address</class> 
        <exclude-unlisted-classes>true</exclude-unlisted-classes>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="validate"/>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/testdb"/>
            <property name="javax.persistence.jdbc.user" value="csaba"/>
            <property name="javax.persistence.jdbc.password" value="farago"/>
        </properties>
    </persistence-unit>
</persistence>

Némi magyarázat:

  • A hu.faragocsaba.ormexample az a név, amivel hivatkozunk erre a kódból.
  • A <class> rész adja meg, hogy hol találhatóak az entitások megvalósításai; ezekről lesz szó később.
  • A properties részben megadjuk az adatkapcsolat részleteit: azt, hogy milyen adatbázist használunk (MySQL), hol található, hogyan érhetjük el.
  • A hibernate.hbm2ddl.auto azt mondja meg, hogy mit kezdjen a Hibernate az adatmodellel. A validate azt jelenti, hogy ellenőrzi, megfelelő-e, de nem módosítja. Egyéb lehetőségek: update: módosítja a sémát; create: létrehozza azt (törli a korábbit); create-drop: az elején létrehozza, a végén törli. A legtöbb esetben a sémát a programtól függetlenül hozzuk létre, így legtöbbször célszerű a validate-et használni.

A következő lépésben megvalósítjuk a két adattáblához tartozó osztályokat.

src/main/java/ormexample/entity/Person.java:

package ormexample.entity;

import javax.persistence.*;

@Entity
@Table(name="person")
public class Person {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    private String name;

    private Integer age;

    @ManyToOne
    @JoinColumn(name="addressid")
    private Address address;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
    }
}

Ill. src/main/java/ormexample/entity/Address.java:

package ormexample.entity;

import java.util.List;
import javax.persistence.*;

@Entity
@Table(name="address")
public class Address {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Integer id;

    private String country;

    private String town;

    private String street;

    @OneToMany(mappedBy="address")
    private List<Person> persons;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

    public String getTown() {
        return town;
    }

    public void setTown(String town) {
        this.town = town;
    }

    public String getStreet() {
        return street;
    }

    public void setStreet(String street) {
        this.street = street;
    }

    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }

    public void addPerson(Person person) {
        this.persons.add(person);
    }

    @Override
    public String toString() {
        return "Address [id=" + id + ", country=" + country + ", town=" + town + ", street=" + street + "]";
    }
}

Néhány gondolat a példáról:

  • A fenti osztályokat entitásoknak (entity) hívjuk, és @Entity annotációval látjuk el.
  • A @Table annotáció opcionális. Ezzel adjuk meg a tábla nevet. Ha a táblanév és az entitás név megegyezik, akkor elhagyható.
  • A legtöbb esetben a fordító kitalálja a megfelelő leképezést. Bizonyos esetekben annotációk segítségével tudjuk ezt vezérelni.
  • A @Id annotáció az elsődleges kulcsot jelöli. Célszerű ezt az értéket generálni, és ennek megfelelően beállítani az adatbázisban (AUTO_INCREMENT) és itt a kódban is (@GeneratedValue(strategy=GenerationType.IDENTITY)).
  • Kell, hogy legyen paraméter nélküli publikus konstruktora az entitásnak.
  • A @OneToMany, és a párja, a @ManyToOne vezérli az 1-n kapcsolatot, mint a példában azt, hogy egy embernek egy címe lehet, de ugyanaz a címe több embernek is lehet.

További szabályok:

  • @Version: ezt használjuk az optimista lockoláshoz.
  • @Column(name="columnname"): megadhatjuk az oszlop nevét, ha eltér az adatbázisban.
  • A many-to-many kapcsolatot a @ManyToMany és a @JoinTable annotációkkal tudjuk vezérelni.
  • Ha a tábla néhány oszlopát külön osztályként szeretnénk megvalósítani, akkor az @Embeddable ill. @Embedded annotációkat használhatjuk.
  • Nagy méretű objektumok tárolása: @Lob.
  • Tranziens mezők: @Transient, transient, static, final.

Végül lássunk egy programot néhány alapművelettel!

src/main/java/ormexample/OrmExample.java:

package ormexample;

import javax.persistence.*;
import ormexample.entity.*;

public class OrmExample {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hu.faragocsaba.ormexample");
        EntityManager em = emf.createEntityManager();

        Address address = em.find(Address.class, 1);
        System.out.println(address);

        em.getTransaction().begin();
        Person gyuri = new Person();
        gyuri.setName("Gyuri");
        gyuri.setAge(40);
        gyuri.setAddress(address);
        em.persist(gyuri);
        em.getTransaction().commit();

        for (Person person : address.getPersons()) {
            System.out.println(person);
        }

        em.getTransaction().begin();
        em.remove(gyuri);
        em.getTransaction().commit();

        List<Address> budapestAddresses = em.createQuery("SELECT a FROM Address a WHERE a.town = :town").setParameter("town", "Szeged").getResultList();
        for (Address budapestAddress: budapestAddresses) {
            System.out.println(budapestAddress);
        }
    }
}

Néhány információ a programról:

  • Az EntityManager alapvető fontosságú, ez kezeli az entitások adatbázisba mentését, ill. onnan történő kiolvasását.
  • Az EntityManager kétféle lehet:
    • alkalmazás vezérelt (application managed), mint a fenti példában (ez esetben az EntityManagerFactory osztályt használjuk a példányosításhoz)
    • konténer vezérelt (container managed), alkalmazás szerverek esetén (@PersistenceContext private EntityManager entityManager;)
  • A Persistence.createEntityManagerFactory("hu.faragocsaba.ormexample") paramétere az, amit a persistence.xml-ben megadtunk.
  • Az alkalmazás vezérelt megoldásban explicit meg kell adnunk a tranzakció elejét (em.getTransaction().begin();) és végét (em.getTransaction().commit();). A fenti példában nem foglalkozunk a hibákkal, a valóságban hiba esetén a rollback() függvényt kell meghívnunk.
  • Az EntityManager find() metódusával tudunk lekérni kulcs alapján egy entitást.
  • A persist() függvény segítségével tudunk lementeni egy entitást.
  • A remove() törli az adatbázisból az entitást.
  • A példa nem illusztrálja, de fontos tudnunk, hogy az entitásoknak alapvetően két állapotuk van: csatlakoztatott (attached) és lekapcsolt (detached). Csatlakoztatott állapotban lehet segítségével adatbázis műveletet végrehajtani: ez esetben minden módosítás (tehát egy egyszerű attribútum beállítás is) lementődik (tranzakció commit esetén) az adatbázisba. Csatlakoztatni az em.merge(entity) hívással lehet, explicit lecsatlakoztatni pedig az em.detach(entity) hívással.
  • Az em.refresh(entity) beolvassa az adatbázisból az aktuális értéket, és beállítástól függően ez rekurzívan történik (cascade).
  • Az em.createQuery() segítségével tudunk az SQL-hez nagyon hasonló JPQL (Java Persistence Query Language) lekérdezéseket végrehajtani. Névvel ellátott, valamint natív lekérdezéseket is végre tudunk hajtani.
  • A fenti példa feltételezi azt, hogy az adatbázis olyan állapotban van, ahogy az Adatbázisok oldalon le van írva. A módosítást visszacsinálja, így hibamentes lefutás esetén ugyanabba az állapotba kerül, mint volt eredetileg. Érdemes megfigyelnünk viszont azt, hogy újabb és újabb lefutáskor az új elem azonosítója egyre nagyobb.

Előzetes az Enterprise Java lehetőségeiből

Szó volt a konténer vezérelt EntityManager lehetőségéről: ha alkalmazás szervert használunk, akkor ezt a lehetőséget választva nem kell a programból törődnünk a tranzakció kezeléssel, azt megoldja a konténer.

A Spring ezt is tovább gondolta; például a fenti SELECT utasítást lecserélhetjük erre: a függvény fejlécre egy megfelelő interfészben: List<Address> findByTown(String town);, és a keretrendszer maga legenerálja a szükséges lekérdezést pusztán a függvény nevéből.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License