Java Webprogrammierung mit Spring und Maven (Teil 2) (Permalink)

Ausgehend vom ersten Teil der Serie soll nun noch Persistenz, also die dauerhafte Speicherung von Daten, hinzukommen.

Das Spring-Framework bietet hierzu ein Modul (spring-orm) an. Spring-orm bietet die Integration von verschiedenen ORM Produkten, wie Hibernate, JPA, JDO und iBATIS SQL Maps, an. Dieses Beispiel verwendet Hibernate. Die Auswahl der unterstützten Datenbanksystemen ist bei Hibernate durchaus ansehnlich. Hier wird die weit verbreitete  MySQL in Version 5.5 eingesetzt.

Voraussetzung

Wie oben angesprochen, setzt dieses Beispiel eine existierende Installation von MySQL voraus. Wichtig ist ein leeres Schema, welches sonst ungenutzt ist, ein Nutzer samt Passwort, welcher in diesem Schema alle Berechtigungen hat. Dies ist wichtig, um während der Entwicklung die Struktur der Tabellen anpassen zu können.

pom.xml

In der pom.xml werden die Abhängigkeiten für die neuen Bibliotheken hinzugefügt.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>3.1.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>4.1.6.Final</version>
</dependency>
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.21</version>
</dependency>

Weitere Änderungen sind nicht erforderlich.

Java Entity Klasse Counter

In Entitäten werden die zu persistierenden Daten auf Java-Seite abgelegt. Es handelt sich dabei um POJOs, die über Annotations zu Entities werden.

package de.lusiardi.testprojekt.entity;

import java.io.Serializable;
import javax.persistence.*;

@Entity
@NamedQueries({
    @NamedQuery(name="counter.findByPage",
        query="from Counter c where c.page=:page")
})
public class Counter implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    @Column(nullable = false, unique = true)
    private String page;

    @Column(nullable = false, name = "counter")
    private int count;

    // notwendige Getter und Setter
}

Hier werden folgende Annotations verwendet:

  • @Entity: Legt fest, das diese Klasse eine Entität ist.
  • @Id: Legt den Primärschlüssel einer Entität fest, dieser dient auch für Zugriffe über die Id.
  • @GeneratedValue: Legt für die Id fest, dass sie automatisch erzeugt wird. Dies entspricht dem Zusatz AUTO_INCREMENT in SQL.
  • @Column: Erlaubt die verfeinerte Definition der Spalten in der resultierenden Datenbanktabelle. Hierbei legt nullable fest, ob zwingend ein Wert angeben sein muss oder nicht. unique legt fest, dass jeder Wert in dieser Spalte eindeutig sein muss. Über name lassen sich die Spalten umbenennen, beispielsweise wenn der Java-Name des Feldes mit einem HQL/SQL-Keyword kollidiert. Dies ist bei count der Fall.
  • @NamedQueries und @NamedQuery: Diese Annotationen erlauben es, benannte Queries direkt bei der Entität anzugeben. Dies hat den Vorteil, dass Daten und Abfragen in einer Datei zusammen liegen und nicht wild über den Code verteilt sind. Die Namen der Queries sind global eindeutig zu halten.

Weitere Informationen zu den einzelnen Annotationen findet man in den verlinkten APIs und unter der Dokumentation zu javax.persistence.

Java Dao Klasse CounterDao

Data Access Objects oder kurz DAOs übernehmen die eigentliche Kommunikation mit der Datenquelle. In diesem Fall über Hibernate. Auch hier werden mit Annotationen zusätzliche Informationen transportiert.

package de.lusiardi.testprojekt.dao;

import de.lusiardi.testprojekt.entity.Counter;
import org.hibernate.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class CounterDao {

    @Autowired
    SessionFactory sessionFactory;

    @Transactional(readOnly = true)
    public Counter findByPage(String page) {
        Query q = getCurrentSession().
            getNamedQuery("counter.findByPage");
        q.setString("page", page);
        return (Counter) q.uniqueResult();
    }

    @Transactional
    public void save(final Counter entity) {
        getCurrentSession().persist(entity);
    }

    @Transactional
    public void update(final Counter entity) {
        getCurrentSession().merge(entity);
    }

    @Transactional
    public void delete(final Counter entity) {
        getCurrentSession().delete(entity);
    }

    private Session getCurrentSession() throws HibernateException {
        return sessionFactory.getCurrentSession();
    }
}

Der Zugriff auf Datenbanken geschieht in Hibernate über Session-Objekte. Diese ermöglichen das Speichern (persist), Aktualisieren (merge) und Löschen (delete) von Entitäten. Über getNamedQuery können dieses abgerufen und eingesetzt werden.

Wichtigste Annotation in dieser Klasse ist @Autowired. Autowired dient der Dependeny Injection, einer wichtigen Technik im Spring Framework. Das Feld sessionFactory ist deshalb auch protected um Spring die Möglichkeit zu geben, die in der spring-servlet.xml definierte SessionFactory einzufügen. Dies geschieht in diesem Fall anhand des Typs des Feldes.

@Repository markiert das DAO für Spring als solches und legt fest, das es sich um eine Bean handelt, die über ihren Typ ansprechbar ist.

@Transactional legt fest, welche Methoden innerhalb einer Datenbanktransaktion ablaufen. Das Attribute readOnly gibt hier Hilfestellungen bei Optimierungen für rein lesenende Transaktionen.

Änderungen an der Controller Klasse

Nun wird der Controller aus Teil 1 so erweitert, das er für die angezeigte View zählt, wie oft diese aufgerufen wurde.

package de.lusiardi.testprojekt.controller;

import de.lusiardi.testprojekt.dao.CounterDao;
import de.lusiardi.testprojekt.entity.Counter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MainController {

    @Autowired
    CounterDao counterDao;

    @RequestMapping("view")
    public ModelAndView getTest() {
        Counter c = counterDao.findByPage("test");
        if (c == null) {
            c = new Counter();
            c.setPage("test");
            c.setCount(0);
            counterDao.save(c);
        }
        int count = c.getCount() + 1;
        c.setCount(count);
        counterDao.update(c);

        return new ModelAndView("view", "key", "Page view number: "
            + count);
    }
}

Auch hier wird über @Autowired eine Abhängigkeit, hier das DAO, automatisch eingefügt. In der Methode getTest wird nun dieses DAO verwendet um die Anzahl der Pageviews zu zählen. Dabei muss der Fall abgedeckt sein, dass für die gewünschte Seite noch kein Datenbankeintrag besteht.

spring-servlet.xml

Damit Instanzen der SessionFactory und des CounterDao jeweils automatisch eingefügt werden können, muss die spring-servlet.xml entsprechend angepasst werden. Dazu fügt man folgende Definition hinzu:

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="location" 
        value="/WEB-INF/config/jdbc/jdbc.properties"></property>
</bean>

<bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" 
        value="${jdbc.driverClassName}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>  
</bean>

<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="myDataSource"></property>
    <property name="packagesToScan" 
         value="de.lusiardi.testprojekt" ></property>
    <property name="hibernateProperties">
        <props>
            <prop key="hibernate.dialect">${jdbc.dialect}</prop>
            <prop key="hibernate.show_sql">true</prop>
            <prop key="hibernate.hbm2ddl.auto">create-drop</prop> 
        </props>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="dataSource" ref="myDataSource" />
    <property name="sessionFactory" ref="sessionFactory" />
</bean>

<tx:annotation-driven transaction-manager="txManager" />

Zunächst wird eine Instanz von PropertyPlaceholderConfigurer definiert, die es ermöglicht Konfigurationsdaten für die Datenbank in einer Property-Datei abzulegen. Die Bean mit der Id myDataSource ist eine Instanz von DriverManagerDataSource und wird über Werte aus der Property-Datei passend konfiguriert. Die SessionFactory, welche  in das DOA injiziert wurde, wird ebenfalls hier definiert. Sie bekommt selbst die Bean namens myDataSource eigefügt und wird ebenfalls passend konfiguriert. Wichtig hier bei ist die Eigentschaft hibernate.hbm2ddl.auto und der Wert create-drop. Mit diesen Einstellungen wird bei jedem Start der Applikation die Datenbank neu angelegt und ist anschließend leer!  Diese Einstellung ist nur für die Entwicklung sinnvoll und sollte im Produktivbetrieb auf validate umgestellt werden.

web-inf/config/jdbc.properties

In dieser Properties-Datei werden die ensprechenden Schlüssel-Wert-Paare zur Konfiguration der Datenbank abgelegt.

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=URL wie jdbc:mysql://server:3306/schema eintragen
jdbc.username=Nutzernamen eintragen
jdbc.password=Passwort eintragen
jdbc.dialect=org.hibernate.dialect.MySQL5Dialect

Die Konfiguration ist für MySQL 5 ausgelegt und muss noch mit entsprechenden Werten für Verbindungs-URL, Nutzername und Passwort versehen werden.

Übersetzen und Deployen

Dieser Schritt verläuft analog zu Teil 1, nur wird die entstehende war-Datei durch die zusätzlichen Abhängigkeiten größer.

Résumé

Die Integration einer Datenbankanbindung in ein Demoprojekt setzt bereits einiges an Hintergrundwissen voraus und macht deutlich, das man mit anderen Techniken sicher schneller ans Ziel kommt. Allerdings sorgt die deutliche Trennung der Schichten und das Verwenden etablierter Techniken für wartbaren und übersichtlichen Code.

Update 6. Juni 2018

Die Quellen zu diesem Artikel findet man jetzt unter https://gitea.lusiardi.de/jlusiardi/webbasics2. Maven 3.3 kann in einem Docker-Container mit folgendem Kommando zum Build benutzt werden:

docker run -it --rm -v "$(pwd)":/usr/src/mymaven   
 -w /usr/src/mymaven maven:3.3-jdk-8 mvn clean install