Spring mit JPA und Hibernate (Tutorial)

Klassendiagramm dieses Beispiels

Dieser Beitrag ist Teil einer (Tutorial-) Serie über die Einführung in das Spring Framework und beschreibt den Einsatz von JPA (Datenbankzugriff) und Hibernate (OR-Mapper) mit dem Spring Framework.

Die Struktur des Projektes

Dieses Beispiel baut auf dem vorherigem Beispiel Spring an einem einfachem Beispiel in der Version 2.0.x auf. Die verwendeten Frameworks und Werkzeuge sind hier beschrieben. Dieses Beispiel ist eine Konsolenanwendung, die eine Fahrerverwaltung umsetzt. In diesem Beispiel werden die folgenden Technologien des Spring Frameworks vorgestellt:

  • Die Definition und die Verwendung von abstrakten und generischen Datenzugriffsobjekten (DAO).
  • Die Spezialisierung von Datenzugriffsobjekten für JPA.
  • Die Definition von Entitäten mit Relationen, Named Queries und Sequenzen.
  • Die Berücksichtigen von Besonderheiten bei Entitäten.
  • Das Erstellen einer XML Konfiguration für JPA.
  • Das Deklarieren eines Datenzugriffsobjekt im Spring Framework.
  • Ein Service im Spring Framework mit Transaktionen versehen.
  • Die Datenbank-spezifische Konfiguration für das Spring Framework erzeugen.
  • Einen JUnit-Test für Datenbankoperationen mit dem Spring Framework erstellen.

Die folgende Bibliotheken werden benötigt:

Die benötigten Bibliotheken (Dependencies)

Die benötigten Bibliotheken - Dependencies (© Frank Rahn)

Die Literaturempfehlungen für dieses Beispiel

Das abstrakte und generische Datenzugriffsobjekt

Ein DAO (Data Access Object) dient zum Abstrahieren von der konkreten Persistenz-Technologie und entkoppelt die Geschäftslogik vom Datenbankzugriff. Die folgende Schnittstelle eines Datenzugriffsobjektes (DAO) ist noch unabhängig von einer konkreten Zugriffstechnologie.

Diese Schnittstelle definiert die elementarsten Methoden (CRUD) für den Datenzugriff. Als generische Parametern wird die Klasse der Entität und des primären Schlüssels (Primary Key) definiert.

/**
 * Ein generische DAO-Schnittstelle für eine Entität mit einem primären Key.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @param <PrimaryKey> Die Klasse des primären Key
 */
public interface GenericDAO<Entity, PrimaryKey extends Serializable> {

    /**
     * Liefere den primären Key für das angegebene Objekt.
     * @param persistentObject das persistente Objekt
     * @return den primären Key
     */
    PrimaryKey getPrimaryKey(Entity persistentObject);

    /**
     * Speichere das neue Objekt in der Datenbank.
     * @param newPersistentObject das neue persistente Objekt
     * @return den primären Key
     */
    PrimaryKey create(Entity newPersistentObject);

    /**
     * Aktualisiere das geänderte Objekt in der Datenbank.
     * @param persistentObject das persistente Objekt
     */
    void save(Entity persistentObject);

    /**
     * Lösche das persistente Objekt aus der Datenbank.
     * @param persistentObject das persistente Objekt
     */
    void remove(Entity persistentObject);

    /**
     * Lösche das persistente Objekt aus der Datenbank.
     * @param key der primäre Key
     */
    void remove(PrimaryKey key);

    /**
     * Finde das persistente Objekt an Hand seines primären Keys.
     * @param key der primäre Key
     * @return das persistente Objekt
     */
    Entity findByPrimaryKey(PrimaryKey key);

}

Nun folgt eine abstrakte Implementierung dieser Schnittstelle. In dieser Implementierung wurde …

  • in Zeile 21 der Logger definiert und
  • in Zeile 30 bis 41 im Object-Initializer die Ermittlung der konkreten Klassen der Entität und des primären Schlüssels durchgeführt.
/**
 * Eine abstrakte Implementierung der Schnittstelle {@link GenericDAO}.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @param <PrimaryKey> Die Klasse des primären Key
 */
public abstract class AbstractGenericDAO<Entity, PrimaryKey extends Serializable>
    implements GenericDAO<Entity, PrimaryKey> {

    /** Der zentrale Logger für die DAO's. */
    protected final static Logger logger = getLogger(GenericDAO.class);

    /** Die Klasse der Entität. */
    protected final Class<Entity> entityClass;

    /** Die Klasse des primären Key. */
    protected final Class<PrimaryKey> primaryKeyClass;

    {
        ParameterizedType type =
            (ParameterizedType) getClass().getGenericSuperclass();
        Type[] actualTypeArguments = type.getActualTypeArguments();

        @SuppressWarnings("unchecked")
        Class<Entity> entityClass = (Class<Entity>) actualTypeArguments[0];
        this.entityClass = entityClass;

        @SuppressWarnings("unchecked")
        Class<PrimaryKey> primaryKey =
            (Class<PrimaryKey>) actualTypeArguments[1];
        this.primaryKeyClass = primaryKey;
    }

    /**
     * {@inheritDoc}
     * @see GenericDAO#remove(java.io.Serializable)
     */
    @Override
    public void remove(PrimaryKey key) {
        remove(findByPrimaryKey(key));
    }

}

Die Spezialisierung des Data Access Objects für JPA

Die grundlegende Schnittstelle eines Datenzugriffsobjektes diesmal abhängig von der Zugriffstechnologie Java Persistence API (JPA).

/**
 * Eine Erweiterung der Schnittstelle {@link GenericDAO} für JPA.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @param <PrimaryKey> Die Klasse des primären Key
 * @see de.rahn.db.dao.GenericDAO
 */
public interface GenericJpaDAO<Entity, PrimaryKey extends Serializable> extends
    GenericDAO<Entity, PrimaryKey> {

    // Noch keine spezielle Definitionen.

}

In der folgenden abstrakten Implementierung dieser Schnittstelle wird die JPA Annotation @PersistenceContext verwendet. Der EntityManager wird dadurch durch das Spring Framework injiziert.

/**
 * Eine Implementierung der Schnittstelle {@link GenericJpaDAO} für JPA.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @param <PrimaryKey> Die Klasse des primären Key
 * @see de.rahn.db.dao.AbstractGenericDAO
 */
public abstract class AbstractGenericJpaDAO<Entity, PrimaryKey extends Serializable>
    extends AbstractGenericDAO<Entity, PrimaryKey>
    implements GenericJpaDAO<Entity, PrimaryKey> {

    @PersistenceContext
    private EntityManager entityManager;

    /**
     * @return Liefert den {@link #entityManager}
     */
    protected final EntityManager getEntityManager() {
        return entityManager;
    }

    /**
     * {@inheritDoc}
     * @see GenericDAO#create(java.lang.Object)
     */
    @Override
    public PrimaryKey create(Entity newPersistentObject) {
        entityManager.persist(newPersistentObject);
        return getPrimaryKey(newPersistentObject);
    }

    /**
     * {@inheritDoc}
     * @see GenericDAO#save(java.lang.Object)
     */
    @Override
    public void save(Entity persistentObject) {
        entityManager.merge(persistentObject);
    }

    /**
     * {@inheritDoc}
     * @see GenericDAO#remove(java.lang.Object)
     */
    @Override
    public void remove(Entity persistentObject) {
        entityManager.remove(persistentObject);
    }

    /**
     * {@inheritDoc}
     * @see GenericDAO#findByPrimaryKey(java.io.Serializable)
     */
    @Override
    public Entity findByPrimaryKey(PrimaryKey key) {
        return entityManager.find(entityClass, key);
    }

}

Im folgendem Klassendiagramm ist der Sachverhalt der abstrakten generischen JPA Datenzugriffsobjekte grafisch per UML dargestellt.

Abstrakte generische Datenzugriffsobjekte (DAO) für JPA

Abstrakte generische Datenzugriffsobjekte für JPA (© Frank Rahn)

Die Definition des Service der Fahrerverwaltung

Zunächst wird die Schnittstelle dieses Services definiert. Auch dieses Beispiel ist relativ einfach gehalten.

/**
 * Das Interface zum Service Drivers.
 * @author Frank W. Rahn
 */
public interface Drivers {

    /**
     * Hole alle Fahrer.
     * @return die Liste der Fahrer
     */
    List<Driver> getDrivers();

    /**
     * Hole einen Fahrer.
     * @param id die Id eines Fahrers
     * @return der Fahrer
     */
    Driver getDriver(Long id);

    /**
     * Lege einen Fahrer an.
     * @param name der Name des Fahrers
     * @param firstname der Vorname des Fahrers
     * @return die Id des Fahrers
     */
    Long createDriver(String name, String firstname);

    /**
     * Lege einen Fahrer an.
     * @param driver der Fahrer
     * @return die Id des Fahrers
     */
    Long create(Driver driver);

    /**
     * Speichere den Fahrer.
     * @param driver der Fahrer
     */
    Driver save(Driver driver);

    /**
     * Füge ein Auto zum Fahrer hinzu.
     * @param id die Id des Fahrers
     * @param car das Auto
     * @return der komplette Fahrer
     */
    Driver addCarToDriver(Long id, Car car);

}

Die XML Konfiguration für dieses Modul.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für den Service Drivers.
    </description>

    <!-- Scanne das Package nach Spring Beans -->
    <context:component-scan base-package="de.rahn.services.drivers" />

</beans>

Die Entitäten des Services Fahrerverwaltung

Unsere Fahrerverwaltung besteht aus Fahrern mit ihren Autos. Hier werden nun eine Reihe von Annotationen von JPA verwendet, um das Datenmodell zu definieren.

  1. In den Zeilen 27 bis 29 wird diese Klasse als Entität mit Zugriff auf Attribut-Ebene definiert. Zusätzlich wird das Datenbankschema angegeben. Der Tabellenname rahn.Driver wird aus dem Klassennamen generiert.
  2. In der Zeile 30 wird eine statische und benannte Abfrage mit JPQL definiert. Mit dieser Abfrage können alle Fahrer selektiert werden. Damit wird sogenanntes SQL-Injection ausgeschlossen, da für diese Abfragen ausschließlich PreparedStatement verwendet werden.
  3. In den Zeilen 37 bis 41 wird der primäre Schlüssel für diese Klasse definiert. Dabei wird dieser Identifizierer über eine DB-Sequenz automatisch vergeben.
  4. In der Zeile 54 wird eine gerichtete Relation (1-zu-n) mit weiterreichen der CRUD-Operationen (Cascade) zum Auto definiert.
  5. In der Zeile 55 wird die Spalte driver-id in der Tabelle des Autos definiert. Dieses ist notwendig, da die Klasse Auto den Fahrer nicht kennt – also kein Attribut des Fahrers besitzt. Wenn in der Klasse Auto das Attribut public Driver driver; definiert worden wäre, hatte folgende Anweisung für die Relation ausgereicht:
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "driver").
/**
 * Die Klasse eines Fahrers.
 * @author Frank W. Rahn
 */
@Entity
@Table(schema = "rahn")
@Access(AccessType.FIELD)
@NamedQueries(@NamedQuery(name = Driver.FIND_ALL, query = "from Driver d"))
public class Driver {

    /** Konstante für die NamedQuery. */
    public static final String FIND_ALL = "Driver.findAll";

    /** Der Identifizierer des Fahrers. */
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "DriverSEQ")
    @SequenceGenerator(name = "DriverSEQ", sequenceName = "DriverSEQ",
        schema = "rahn")
    @Basic(optional = false)
    private Long id;

    /** Der Name des Fahrers. */
    @Basic(optional = false)
    @Column(nullable = false)
    private String name;

    /** Der Vorname des Fahrers. */
    @Basic
    private String firstname;

    /** Die Autos die der Fahrer fährt. */
    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "driver_id", nullable = false)
    private Set<Car> cars = new HashSet<>();
...

Im folgendem Abschnitt werden die von Eclipse IDE generierten Methoden für hashCode() und equals() dargestellt. Diese Methoden müssen überladen werden, da eine Entität eine eindeutige Identität besitzt, die über die beiden Methoden bestimmt werden muss.

Zwei eigentlich gleiche Entitäten werden als ungleich bewertet, wenn …

  • die Entitäten von unterschiedlichen EntityManager gelesen wurden oder
  • die eine Entität per Eager Loading (direkt beim Ausführen des Selects) und die andere Entität per Lazy Loading (durch einen Proxy beim Zugriff auf das Feld) geladen wurden.

Der automatische erzeugte Schlüssel (id) wird erst beim Speichern der Entitäten gesetzt. Die beiden Methoden hashCode() und equals() müssen von denselben signifikanten Attributen abhängen.

  • In der Zeile 67 und 99 wird mit Persistence.getPersistenceUtil().isLoaded(cars) geprüft, ob die Autos schon geladen wurden. Wenn nicht würden die Entitäten des Autos geladen oder einen Fehler auslösen, wenn der EntityManager schon geschlossen wurde.
    /**
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        if (cars != null && Persistence.getPersistenceUtil().isLoaded(cars)) {
            result = prime * result + cars.hashCode();
        } else {
            result = prime * result;
        }
        result =
            prime * result + (firstname == null ? 0 : firstname.hashCode());
        result = prime * result + (id == null ? 0 : id.hashCode());
        result = prime * result + (name == null ? 0 : name.hashCode());
        return result;
    }

    /**
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof Driver)) {
            return false;
        }
        Driver other = (Driver) obj;
        if (cars == null) {
            if (other.cars != null) {
                return false;
            }
        } else {
            if (Persistence.getPersistenceUtil().isLoaded(cars)
                && Persistence.getPersistenceUtil().isLoaded(other.cars)) {
                if (!cars.equals(other.cars)) {
                    return false;
                }
            }
        }
        if (firstname == null) {
            if (other.firstname != null) {
                return false;
            }
        } else if (!firstname.equals(other.firstname)) {
            return false;
        }
        if (id == null) {
            if (other.id != null) {
                return false;
            }
        } else if (!id.equals(other.id)) {
            return false;
        }
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        } else if (!name.equals(other.name)) {
            return false;
        }
        return true;
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return new StringBuilder().append("Driver [id=").append(id)
            .append(", name=").append(name).append(", firstname=")
            .append(firstname).append("]").toString();
    }
...

Als nächstes folgt die Klasse für die Autos. Das Auto kennt seinen Fahrer nicht, daher gibt es auch kein Attribut für den Fahrer in der Klasse.

  • Der primäre Schlüssel in Zeile 22 wird von Außen vorgegeben (Kennzeichen).
/**
 * Die Klasse eines Autos.
 * @author Frank W. Rahn
 */
@Entity
@Table(schema = "rahn")
@Access(AccessType.FIELD)
public class Car {

    /** Die Identität eines angemeldeten Autos. */
    @Id
    @Basic(optional = false)
    private String id;

    /** Der Typ des Autos. */
    @Basic(optional = false)
    private String type;
...

Es fehlt noch die XML Konfiguration für die Persistenz, die nicht sehr umfangreich ist.

  • In der Zeile 11 wird angegeben, dass die Anwendung die Transaktionen der Datenbank verwenden soll:
    EntityTransaction etx = entityManager.getTransaction();
    Im Gegensatz zu der Angabe JTA, wo der Transaktionsmanager des Java EE Containers verwendet würde:
    UserTransaction utx = (UserTransaction) (new InitialContext()).lookup("java:comp/UserTransaction");
<?xml version="1.0" encoding="UTF-8"?>

<persistence version="2.0" 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
    ">

    <persistence-unit
        name="test-spring-drivers" transaction-type="RESOURCE_LOCAL" />

</persistence>

Das Data Access Object für die Fahrerverwaltung

Die Definition des konkreten Datenzugriffsobjekt für den Service der Fahrerverwaltung.

  • In Zeile 16 wird die Annotation @Repository verwendet. Durch diese Annotation wird diese Klasse zu einem Spring Bean mit einer Behandlung für Ausnahmen der Persistenzschicht. Diese Ausnahmen werden in eine Unterklasse von org.springframework.dao.DataAccessException übersetzt.
  • Ab der Zeile 32 wird eine Methode findAll() implementiert, die per @NamedQuery definierten Abfrage alle Instanzen dieser Klasse lädt.
/**
 * Ein DAO für den Fahrer.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @param <PrimaryKey> Die Klasse des primären Key
 */
@Repository
public class DriverDAO extends AbstractGenericJpaDAO<Driver, Long> {

    /**
     * {@inheritDoc}
     * @see GenericDAO#getPrimaryKey(java.lang.Object)
     */
    @Override
    public Long getPrimaryKey(Driver persistentObject) {
        return persistentObject.getId();
    }

    /**
     * Suche alle Fahrer.
     * @return alle Fahrer
     */
    public List<Driver> findAll() {
        return getEntityManager().createNamedQuery(Driver.FIND_ALL,
            Driver.class).getResultList();
    }

}

Die Implementierung des Services der Fahrerverwaltung

Die Standard-Implementierung der Fahrerverwaltung.

  • Durch die Annotation @Service("drivers") in Zeile 19 wird diese Implementierung als Spring Bean mit dem Namen drivers definiert.
  • In der Zeile 24 wird das Datenzugriffsobjekt DriverDAO verwendet.
  • In der Zeile 20 wird definiert, dass alle Methoden in einer Transaktion ausgeführt werden.
  • In Zeile 31 und 41 wird durch die Annotation angegeben, dass diese Methoden eine existierende Transaktion unterstützen, aber keine eigene Transaktion benötigen.
/**
 * Die Standard-Implementierung des Services {@link Drivers}.
 * @author Frank W. Rahn
 */
@Service("drivers")
@Transactional
public class StandardDrivers implements Drivers {

    @Autowired
    private DriverDAO driverDAO;

    /**
     * {@inheritDoc}
     * @see Drivers#getDrivers()
     */
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public List<Driver> getDrivers() {
        return driverDAO.findAll();
    }

    /**
     * {@inheritDoc}
     * @see Drivers#getDriver(Long)
     */
    @Override
    @Transactional(propagation = Propagation.SUPPORTS)
    public Driver getDriver(Long id) {
        return driverDAO.findByPrimaryKey(id);
    }

    /**
     * {@inheritDoc}
     * @see Drivers#createDriver(String, String)
     */
    @Override
    public Long createDriver(String name, String firstname) {
        Driver driver = new Driver();
        driver.setName(name);
        driver.setFirstname(firstname);

        return create(driver);
    }

    /**
     * {@inheritDoc}
     * @see Drivers#create(Driver)
     */
    @Override
    public Long create(Driver driver) {
        return driverDAO.create(driver);
    }

    /**
     * {@inheritDoc}
     * @see Drivers#save(Driver)
     */
    @Override
    public Driver save(Driver driver) {
        driverDAO.save(driver);
        return driver;
    }

    /**
     * {@inheritDoc}
     * @see Drivers#addCarToDriver(Long, Car)
     */
    @Override
    public Driver addCarToDriver(Long id, Car car) {
        Driver driver = driverDAO.findByPrimaryKey(id);
        driver.getCars().add(car);

        return save(driver);
    }

}

Im folgenden Bild wird der vollständige Service in einem UML-Klassendiagramm dargestellt.

Klassendiagramm dieses Beispiels

Spring mit JPA und Hibernate (© Frank Rahn)

Die Anwendung mit Logging

Sie ist wie in dem Beispiel Spring an einem einfachem Beispiel aufgebaut. Es ändert sich nur die Anwendung, die XML Konfiguration de/rahn/app/application.xml und der Starter bleiben gleich. Die Änderung an der Application sind hier dargestellt.

/**
 * Die Anwendung zum Aufrufen der Fahrerverwaltung.
 * @author Frank W. Rahn
 */
@Component
public class Application implements Runnable {

    private static final Logger logger = getLogger(Application.class);

    @Autowired(required = true)
    private Drivers drivers;

    /**
     * {@inheritDoc}
     * @see Runnable#run()
     */
    @Override
    public void run() {
        // Lege einen Fahrer an
        Long id = drivers.createDriver("Rahn", "Frank");
        logger.info("Einen Fahrer mit der Id '{}' angelegt", id);

        // Hole den Fahrer wieder
        Driver driver = drivers.getDriver(id);
        logger.info("Den Fahrer mit der Id '{}' geholt: {}", id, driver);

        // Lege ein Auto an
        Car car = new Car();
        car.setId("K-XX 4711");
        car.setType("Audi A6");
        driver = drivers.addCarToDriver(id, car);
        logger.info("Den Fahrer mit der Id '{}' geändert: {}", id, driver);

        // Alle Fahrer selektieren
        List<Driver> listDrivers = drivers.getDrivers();
        for (Driver driver2 : listDrivers) {
            logger.info(
                "Fahrer: Id '{}' Name: {} {}",
                new Object[] { driver2.getId(), driver2.getFirstname(),
                    driver2.getName() });
        }
    }

}

Die XML Konfiguration zum Einstieg in die Anwendung muss um die Datenbank- und die Transaktionsdefinitionen erweitert werden.

  • In der Zeile 27 wird auf die Konfiguration mit den Datenbank- und die Transaktionsdefinitionen im gleichen Verzeichnis verwiesen.
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die Anwendungen.
    </description>

    <!-- Enabling des AspectJ Support -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <!-- Das Verwenden von allgemeinen Annotationen ermöglichen -->
    <context:annotation-config />

    <!-- Die projektspezifischen Konfigurationen laden -->
    <import resource="db.xml" />
    <import resource="classpath:/de/rahn/services/drivers/drivers.xml" />
    <import resource="classpath:/de/rahn/app/application.xml" />

</beans>

In der folgenden XML Konfiguration werden die Datenbank- und die Transaktionsdefinitionen vorgenommen. Hier wird erstmalig der p-Namesraum für das Setzen von Properties verwendet.

Die Syntax lautet dabei:
p:<Property-Name>="<Property-Wert>" oder p:<Property-Name>-ref="<Name der referenzierten Bean>"

  • Die Definitionen für den Datenbankzugriff (EntityManagerFactory) in den Zeilen 27 bis 34 und die Behandlung der Transaktionen in den Zeilen 37 bis 42.
  • In den Zeilen 22 bis 24 wird eine Datenbank gestartet und durch das angegebene Skript init.sql initialisiert.
    <!-- Starte die HSQL-Datenbank im Memory -->
    <jdbc:embedded-database id="dataSource" type="HSQL">
        <jdbc:script location="classpath:/META-INF/spring/init.sql" />
    </jdbc:embedded-database>

    <!-- Erzeuge die Persitence-Unit -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
        p:dataSource-ref="dataSource" p:persistenceUnitName="test-spring-drivers">
        <property name="jpaVendorAdapter">
            <1bean p:generateDdl="true"
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
    </bean>

    <!-- Einen Transaktionmanager erzeugen -->
    <bean id="transactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager"
        p:entityManagerFactory-ref="entityManagerFactory" />

    <!-- Das Verwenden von Annotationen für die Transaktionen ermöglichen -->
    <tx:annotation-driven proxy-target-class="true" />

</beans>

Im folgendem Kasten wird das Skript init.sql dargestellt. Der SQL Dialekt ist HSQL (HyperSQL Database). HSQLDB ist eine vollständig in Java geschrieben relationale Datenbank.

create SCHEMA rahn;
create sequence DriverSEQ;
create table rahn.Driver (
    id bigint not null,
    firstname varchar(255),
    name varchar(255) not null,
    primary key (id));
create table rahn.Car (
    id varchar(255) not null,
    type varchar(255) not null,
    driver_id bigint not null,
    primary key (id));
alter table rahn.Car
    add constraint FK107B45ADEE2FE foreign key (driver_id)
        references rahn.Driver;
insert into rahn.Driver (firstname, name, id)
    values ('Martin', 'Rahn', next value for DriverSEQ);

Der Unit Test

Zunächst erstellen wir einen Test für die Klasse DriverDAO.

  • In der Zeile 25 wird auf die benötigten XML Konfigurationen von Spring verwiesen. Das ist eine spezielle XML Konfiguration für den Test /META-INF/spring/context-test.xml, sie beinhaltet die Konfigurationen für den Datenbankzugriff, und die Konfiguration für die Fahrerverwaltung ../drivers.xml.
/**
 * Die Testklasse für {@link DriverDAO}.
 * @author Frank W. Rahn
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/META-INF/spring/context-test.xml",
    "../drivers.xml" })
@Transactional
public class DriverDAOTest {

    @Autowired
    private DriverDAO driverDAO;

    /**
     * Test method for {@link DriverDAO#getPrimaryKey(Driver)}.
     */
    @Test
    public void testGetPrimaryKey() {
        Driver driver = new Driver();
        driver.setId(new Long(4711));

        Long id = driverDAO.getPrimaryKey(driver);
        assertThat("Primärer Key unterschiedlich", driver.getId(), is(id));
    }

    /**
     * Test method for {@link DriverDAO#findAll()}.
     */
    @Test
    public void testFindAll() {
        List<Driver> drivers = driverDAO.findAll();
        assertThat("kein Ergebnis geliefert", drivers, notNullValue());
        assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1));

        Driver driver = drivers.get(0);
        assertThat("id ungleich", driver.getId(), is(0L));
        assertThat("firstname ungleich", driver.getFirstname(), is("Martin"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
    }

    /**
     * Test method for 
     * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#create(Object)}.
     */
    @Test
    public void testCreate() {
        Driver driver = new Driver();
        driver.setName("Rahn");
        driver.setFirstname("Frank");

        Long id = driverDAO.create(driver);
        assertThat("keine id geliefert", id, notNullValue());
        assertThat("ungleiche id", driver.getId(), is(id));

        Driver driver2 = driverDAO.findByPrimaryKey(id);
        assertThat("doch nicht gespeichert", driver2, notNullValue());
        assertThat("ungleicher Fahrer", driver2, sameInstance(driver));
    }

    /**
     * Test method for
     * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#save(Object)}.
     */
    @Test
    public void testSave() {
        Driver driver = driverDAO.findByPrimaryKey(0L);
        assertThat("kein Fahrer gefunden", driver, notNullValue());

        driver.setFirstname("Peter");
        driverDAO.save(driver);

        Driver driver2 = driverDAO.findByPrimaryKey(0L);
        assertThat("kein Fahrer gefunden", driver2, notNullValue());
        assertThat("ungleicher Fahrer", driver2, sameInstance(driver));
    }

    /**
     * Test method for
     * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#remove(Object)}.
     */
    @Test
    public void testRemoveEntity() {
        List<Driver> drivers = driverDAO.findAll();
        assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1));
        Driver driver = drivers.get(0);

        driverDAO.remove(driver);

        drivers = driverDAO.findAll();
        assertThat("Anzahl der Treffer ungleich", drivers.size(), is(0));
    }

    /**
     * Test method for
     * {@link de.rahn.db.jpa.dao.AbstractGenericJpaDAO#findByPrimaryKey(Serializable)}
     * .
     */
    @Test
    public void testFindByPrimaryKey() {
        Driver driver = driverDAO.findByPrimaryKey(0L);
        assertThat("kein Ergebnis geliefert", driver, notNullValue());
        assertThat("id ungleich", driver.getId(), is(0L));
        assertThat("firstname ungleich", driver.getFirstname(), is("Martin"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
    }

    /**
     * Test method for
     * {@link de.rahn.db.dao.AbstractGenericDAO#remove(Serializable)}.
     */
    @Test
    public void testRemovePrimaryKey() {
        driverDAO.remove(0L);

        List<Driver> drivers = driverDAO.findAll();
        assertThat("kein Ergebnis geliefert", drivers, notNullValue());
        assertThat("Anzahl der Treffer ungleich", drivers.size(), is(0));
    }

}

Für die allgemeinen Spring Beans wurde eine spezielle XML Konfiguration /META-INF/spring/context-test.xml erstellt. Sie lädt nur die Konfigurationen für den Datenbankzugriff.

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
            http://www.springframework.org/schema/context/spring-context.xsd
    ">

    <description>
        Dieses ist die zentrale Konfiguration für die Tests.
    </description>

    <!-- Das Verwenden von allgemeinen Annotationen ermöglichen -->
    <context:annotation-config />

    <!-- Die projektspezifischen Konfigurationen laden -->
    <import resource="db.xml" />

</beans>

Jetzt fehlt nur noch der Unit Test für den Service der Fahrerverwaltung StandardDriver.

/**
 * Die Testklasse für {@link StandardDrivers}.
 * @author Frank W. Rahn
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/META-INF/spring/context-test.xml",
    "../drivers.xml" })
@Transactional
public class StandardDriversTest {

    /** CAR_TYPE */
    private static final String CAR_TYPE = "Audi A6";

    /** CAR_ID */
    private static final String CAR_ID = "K-XX 4711";

    @Autowired
    private Drivers drivers;

    /**
     * Test method for {@link StandardDrivers#getDriver(Long)}.
     */
    @Test
    public void testGetDriver() {
        Driver driver = drivers.getDriver(0L);
        assertThat("kein Fahrer geliefert", driver, notNullValue());
        assertThat("id ungleich", driver.getId(), is(0L));
        assertThat("firstname ungleich", driver.getFirstname(), is("Martin"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
    }

    /**
     * Test method for {@link StandardDrivers#createDriver(String, String)}.
     */
    @Test
    public void testCreateDriver() {
        Long id = drivers.createDriver("Rahn", "Frank");
        assertThat("keine id geliefert", id, notNullValue());

        Driver driver = drivers.getDriver(id);
        assertThat("doch nicht gespeichert", driver, notNullValue());
        assertThat("ungleiche id", driver.getId(), is(id));
        assertThat("firstname ungleich", driver.getFirstname(), is("Frank"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
    }

    /**
     * Test method for {@link StandardDrivers#addCarToDriver(Long, Car)}.
     */
    @Test
    public void testAddCarToDriver() {
        Car car = new Car();
        car.setId(CAR_ID);
        car.setType(CAR_TYPE);
        Driver driver = drivers.addCarToDriver(0L, car);
        assertThat("doch nicht gespeichert", driver, notNullValue());
        assertThat("id ungleich", driver.getId(), is(0L));
        assertThat("firstname ungleich", driver.getFirstname(), is("Martin"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
        assertThat("anzahl Autos ungleich", driver.getCars().isEmpty(),
            not(true));
        car = driver.getCars().iterator().next();
        assertThat("id ungleich", car.getId(), is(CAR_ID));
        assertThat("type ungleich", car.getType(), is(CAR_TYPE));
    }

}

Hier noch die entsprechende Erfolgsmeldung.

Die Erfolgsmeldung von JUnit

Die Erfolgsmeldung von JUnit (© Frank Rahn)

Der Quellcode und Download des Beispiels

Quellcode ansehen bei GitHub:
Spring mit JPA und Hibernate

Download einer ZIP-Datei von GitHub:
Spring mit JPA und Hibernate

Die Maven Befehl

Eclipse Konfiguration neu erzeugen: $ mvn eclipse:clean eclipse:eclipse

Anwendung bauen: $ mvn clean install

Anwendung ausführen: $ mvn initialize exec:exec

Update: Criteria API

Es wurde in der pom.xml ein Maven-Plugin für die Generierung des Metamodels der Criteria API aufgenommen. Dieses Metamodel wird in das Verzeichnis target/generated-sources/apt generiert. Mit der Criteria API können Datenbankabfragen objektorientiert im Quellcode programmiert werden.

Nachfolgenden ist die Maven Konfiguration für die Erzeugung der Criteria API mit dem JPA 2 Metamodel Generator von Hibernate dargestellt.

    <plugin>
        <groupId>org.bsc.maven</groupId>
        <artifactId>maven-processor-plugin</artifactId>
        <version>22.2.1</version>
        <executions>
            <execution>
                <goals>
                    <goal>process</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <optionMap>
                <addGeneratedAnnotation>true</addGeneratedAnnotation>
                <addSuppressWarningsAnnotation>true</addSuppressWarningsAnnotation>
                <debug>true</debug>
            </optionMap>
            <processors>
                <processor>org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor</processor>
            </processors>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>org.hibernate</groupId>
                <artifactId>hibernate-jpamodelgen</artifactId>
                <version>${hibernate-jpamodelgen-version}</version>
            </dependency>
        </dependencies>
    </plugin>
...

Zusätzlich wurde ein Template, nach dem Muster von Spring, erzeugt. Dadurch kann das abstrakte und generische JPA-Datenzugriffsobjekt die Erzeugung der Factory-Klassen übernehmen.

/**
 * Ein typisches Spring-Template, dass die Erzeugung einiger notwendigen Factory
 * Klassen zur Erzeugung von Queries auf Basis der Criteria API an das
 * {@link AbstractGenericJpaDAO} deligiert.
 * @author Frank W. Rahn
 * @param <Entity> Die Klasse der Entität
 * @see AbstractGenericJpaDAO
 */
public interface CriteriaQueryTemplate<Entity> {

    /**
     * Diese Methode wird durch das {@link AbstractGenericJpaDAO} ausgeführt und
     * stellt einige Standardkomponenten der Criteria API zu Verfügung.
     * @param builder der {@link CriteriaBuilder}
     * @param query die an die Entität gebundene Abfrage
     * @param rootEntity die Projektionsvariable der FROM-Klausel
     */
     void doBuild(CriteriaBuilder builder, CriteriaQuery<Entity> query,
        Root<Entity> rootEntity);

}

Das abstrakte und generische JPA-Datenzugriffsobjekt AbstractGenericJpaDAO, wird um die Methode buildQuery() erweitert.

    /**
     * Erzeuge über die Criteria API eine JPA Query.
     * @param template der Builder
     * @return eine ausführbare JPA Query
     */
    protected TypedQuery<Entity> buildQuery(
        CriteriaQueryTemplate<Entity> template) {

        // Erzeuge eine Builder
        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

        // Erzeuge die Query
        CriteriaQuery<Entity> criteriaQuery =
            criteriaBuilder.createQuery(entityClass);

        // Erzeuge die Referenz zur Hauptentität
        Root<Entity> rootEntity = criteriaQuery.from(entityClass);

        // Führe den Build durch
        template.doBuild(criteriaBuilder, criteriaQuery, rootEntity);

        // Erzeuge eine ausführbare JPA Query
        return entityManager.createQuery(criteriaQuery);
    }
...

Das Datenzugriffsobjekt der Fahrer stellt die Methode findByName() bereit.

    /**
     * Suche alle Fahrer mit dem Namen.
     * @param name der Name des Fahrers
     * @return die gefundenen Fahrer
     */
    public List<Driver> findByName(final String name) {
        return buildQuery(new CriteriaQueryTemplate<Driver>() {

            @Override
            public void doBuild(CriteriaBuilder builder,
                CriteriaQuery<Driver> query, Root<Driver> rootEntity) {
                // Erzeuge eine logische Ausdruck
                Predicate predicate =
                    builder.equal(rootEntity.get(Driver_.name), name);

                // Definiere die Abfrage
                query.select(rootEntity).where(predicate).distinct(true);
            }
        }).getResultList();
    }
...

Zum Abschluss darf natürlich der Test nicht fehlen.

    /**
     * Test der Criteria API.
     */
    @Test
    public void testCriteriaAPI() {
        List<Driver> drivers = driverDAO.findByName("Rahn");
        assertThat("keine Ergebnis geliefert", drivers, notNullValue());
        assertThat("Anzahl der Treffer ungleich", drivers.size(), is(1));

        Driver driver = drivers.get(0);
        assertThat("id ungleich", driver.getId(), is(0L));
        assertThat("firstname ungleich", driver.getFirstname(), is("Martin"));
        assertThat("name ungleich", driver.getName(), is("Rahn"));
    }
...

Update am 28.09.2012

Es wurden folgende Änderungen an allen Projekten vorgenommen.

  1. Die Projekte wurden aus meinem lokalen Apache Subversion Repository in meine öffentlichen GitHub Repositories verschoben.
  2. Die folgenden typischen Anpassungen an Git wurden an allen Projekten durchgeführt.
    • .directory gelöscht
    • .gitignore hinzugefügt
    • README.md hinzugefügt
    • COPYRIGHT.md hinzugefügt
    • In jedem Repository wurde für jeden Beitrag der Serie ein Branch angelegt.
      • develop-spring-an-einem-einfachen-beispiel
      • develop-spring-mit-aop
      • develop-spring-mit-jpa-und-hibernate
      • develop-spring-mit-einer-einfachen-webanwendung
      • develop-spring-mit-einer-webanwendung-mit-jpa-und-validierung
      • develop-spring-security-mit-einer-webanwendung
      • develop-spring-mit-restful-webservice

Updates von 06.10.2012 bis zum 14.10.2012

Es wurden folgende Änderungen an allen Projekten vorgenommen.

  1. Aktualisierung des OpenJDK auf die Version 1.7.0.
  2. Aktualisierung der Entwicklungsumgebung Eclipse auf die Version 4.2.1.
    Die benötigten Plugins aus dem Eclipse Marketplace:
  3. Aktualisierung der Datei pom.xml:
    • Anpassungen an die OpenJDK Version
    • GitHub Einträge (SCM und URL) hinzugefügt
    • Aktualisierung der Libraries auf aktuellerer Versionen (z. B. Spring Version 3.1.2.RELEASE, Hibernate Version 4.1.7.Final, JUnit Version 4.10, mockito Version 1.9.0, …)
      Die genaueren Versionen bitte aus den jeweiligen pom.xml auf GitHub entnehmen.

Frank Rahn
Letzte Artikel von Frank Rahn (Alle anzeigen)
0 Kommentare

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Ihre E-Mail-Adresse wird nicht veröffentlicht. Ihr Kommentar wird verschlüsselt an meinen Server gesendet. Erforderliche Felder sind mit * markiert.

Weitere Informationen und Widerrufshinweise finden Sie in meiner Datenschutzerklärung.