PV168/Spring JDBC v IntelliJ IDEA

Z FI WIKI
Přejít na: navigace, hledání


Ukážeme si, jak využít Spring JDBC a Maven výhodně v IntelliJ IDEA.

Databáze

Mějme z minulého semináře PV168/JDBC v IntelliJ IDEA vytvořenu databázi s tabulkou BOOKS, a implementaci příslušného manažeru.

Přidejte tabulky pro druhou entitu a pro jejich vztah, tj. například

 CREATE TABLE customers (
   id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   fullname VARCHAR(50),
   address VARCHAR(150),
   phone VARCHAR(20),
   email VARCHAR(50));

 CREATE TABLE leases (
   id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   bookId INT REFERENCES books(id) ON DELETE CASCADE,
   customerId INT REFERENCES customers(id) ON DELETE CASCADE,
   startDate DATE,
   expectedEnd DATE,
   realEnd TIMESTAMP);

kde tabulka CUSTOMERS představuje druhou entitu, a tabulka LEASES vztah mezi BOOKS a CUSTOMER pomocí cizích klíčů (foreign key) bookId a customerId. Omezení REFERENCES tabulka(sloupec) označuje cizí klíč odkazující určitý primární klíč. Klauzule ON DELETE CASCADE označuje, že při smazání odkazovaného záznamu se má smazat i tento záznam.

Konfigurace projektu

Na začátku mějme projekt pro Maven, v něm už z minula knihovny pro JUnit, SLF4J (logování) a JavaDB. Nyní do něj přidáme knihovny pro


Měli bychom tedy mít soubor pom.xml s následujícím obsahem:

 
<?xml version="1.0" encoding="UTF-8"?>
<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>spring-jdbc-1</groupId>
    <artifactId>spring-jdbc-1</artifactId>
    <version>1.0</version>
 
    <properties>7
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>
 
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.6</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derbyclient</artifactId>
            <version>10.10.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-framework-bom</artifactId>
                <version>4.0.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
</project>


Třídy

Mějme třídy pro entitu Customer a Lease, a příslušné interfacy CustomerManager a LeaseManager:

 
public class Customer {
 
    private Long id;
    private String fullname;
    private String address;
    private String phone;
    private String email;
 
   //settery, gettery, konstruktory, toString(), equals()
}
 
public class Lease {
 
    private Long id;
    private Book book;
    private Customer customer;
    private LocalDate startdate;
    private LocalDate expectedend;
    private LocalDateTime realend;
 
   //settery, gettery, konstruktory, toString()
}

Implementace pomocí Spring JDBC

Nyní vytvoříme implementaci interface CustomerManager, nazvanou CustomerManagerImpl, pomocí Spring JDBC:

 
package cz.muni.fi.pv168.books;
 
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.transaction.annotation.Transactional;
 
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class CustomerManagerImpl implements CustomerManager {
 
    private JdbcTemplate jdbc;
 
    public CustomerManagerImpl(DataSource dataSource) {
        this.jdbc = new JdbcTemplate(dataSource);
    }
 
    @Override
    public void deleteCustomer(long id) {
        jdbc.update("DELETE FROM customers WHERE id=?", id);
    }
 
    @Override
    public void updateCustomer(Customer c) {
        jdbc.update("UPDATE customers set fullname=?,address=?,phone=?,email=? where id=?",
                c.getFullname(), c.getAddress(), c.getPhone(), c.getEmail(), c.getId());
    }
 
    private RowMapper<Customer> customerMapper = (rs, rowNum) ->
            new Customer(rs.getLong("id"), rs.getString("fullname"), rs.getString("address"), rs.getString("phone"), rs.getString("email"));
 
    @Transactional
    @Override
    public List<Customer> getAllCustomers() {
        return jdbc.query("SELECT * FROM customers", customerMapper);
    }
 
    @Override
    public Customer getCustomerById(long id) {
        return jdbc.queryForObject("SELECT * FROM customers WHERE id=?", customerMapper, id);
    }
 
    @Override
    public void createCustomer(Customer c) {
        SimpleJdbcInsert insertCustomer = new SimpleJdbcInsert(jdbc)
                .withTableName("customers").usingGeneratedKeyColumns("id");
 
        SqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("fullname", c.getFullname())
                .addValue("address", c.getAddress())
                .addValue("phone", c.getPhone())
                .addValue("email", c.getEmail());
 
        Number id = insertCustomer.executeAndReturnKey(parameters);
        c.setId(id.longValue());
    }
 
}

Všimněte si, že

  • konstruktor přebírá DataSource jako parametr
  • deleteCustomer() a updateCustomer() jsou velice jednoduché metody
  • createCustomer() používá SimpleJdbcInsert pro vložení záznamu, je potřeba jen specifikovat názvy tabulky a sloupců
  • customerMapper je použit v getCarById() i findAllCars(), rozdíl je jen ve volání queryForObject() místo query() když víme, že výsledkem je jen jeden objekt
  • findAllCustomers() je označena anotací @Transactional, což není v tomto případě potřeba, ale je to ukázka, jak deklarovat použití transakcí ve Spring JDBC

Použítí v programu

Při použití CustomerManagerImpl v programu potřebujeme získat jeho instanci a té poskytnout implementaci DataSource.

Mohli bychom to udělat přímo, tj.

 
  DataSource dataSource = ...;
  CustomerManager cm = new CustomerManagerImpl(dataSource);

ale pak bychom neměli vyřešeno řízení databázových transakcí. Proto to uděláme trošičku složitěji, pomocí Spring IoC containeru, který nám transakce ošetří. IoC Container znamená Inversion-of-Control Container, který využívá tzv. dependency injection. V zkratce dojde k invertování závislostí - třída LeaseManagerImpl (níž) nenastavuje spojení s databází, ale bere přímo DataSource.

 
package cz.muni.fi.pv168.books;
 
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import javax.sql.DataSource;
import java.io.IOException;
 
public class Main {
 
    public static void main(String[] args) throws BookException, IOException {
 
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookManager bookManager = ctx.getBean(BookManager.class);
        CustomerManager customerManager = ctx.getBean(CustomerManager.class);
 
        bookManager.getAllBooks().forEach(System.out::println);
 
        customerManager.getAllCustomers().forEach(System.out::println);
 
        Customer customer = new Customer(null, "Jan Novák", "Brno", "602123456", "novak@email.cz");
        customerManager.createCustomer(customer);
 
    }
 
    @Configuration  //je to konfigurace pro Spring
    @EnableTransactionManagement //bude řídit transakce u metod označených @Transactional
    @PropertySource("classpath:myconf.properties") //načte konfiguraci z myconf.properties
    public static class SpringConfig {
 
        @Autowired
        Environment env;
 
        @Bean
        public DataSource dataSource() {
            BasicDataSource bds = new BasicDataSource(); //Apache DBCP connection pooling DataSource
            bds.setDriverClassName(env.getProperty("jdbc.driver"));
            bds.setUrl(env.getProperty("jdbc.url"));
            bds.setUsername(env.getProperty("jdbc.user"));
            bds.setPassword(env.getProperty("jdbc.password"));
            return bds;
        }
 
        @Bean //potřeba pro @EnableTransactionManagement
        public PlatformTransactionManager transactionManager() {
            return new DataSourceTransactionManager(dataSource());
        }
 
        @Bean //náš manager, bude automaticky obalen řízením transakcí
        public CustomerManager customerManager() {
            return new CustomerManagerImpl(dataSource());
        }
 
        @Bean
        public BookManager bookManager() {
            // BookManagerImpl nepoužívá Spring JDBC, musíme mu vnutit spolupráci se Spring transakcemi
            return new BookManagerImpl(new TransactionAwareDataSourceProxy(dataSource()));
        }
 
        @Bean
        public LeaseManager leaseManager() {
            LeaseManagerImpl leaseManager = new LeaseManagerImpl(dataSource());
            leaseManager.setBookManager(bookManager());
            leaseManager.setCustomerManager(customerManager());
            return leaseManager;
        }
    }
}
 

Třída SpringConfig představuje konfiguraci pro Spring IoC (Inversion-of-Control) container, její anotace @Configuration říká že jde o konfiguraci pro Spring, anotace @EnableTransactionManagement aktivuje řízení transakcí, a metody označené anotací @Bean poskytují instance objektů, které budou v naší aplikaci potřeba, totiž DataSource, TransactionManager, CustomerManager a BookManager.

Výhoda Spring IoC je, že činí třídy nezávislé na tom, odkud se jejich závislosti berou. Všimněte si, že třídu BookManagerImpl jsme nemuseli měnit, pro řízení transakcí ji stačilo injektovat DataSource obalený třídou TransactionAwareDataSourceProxy, která řízení transakcí zajistí.

Více o konfiguraci Springu pomocí anotací je v Using the @Configuration annotation.

Konfiguraci spojení na databázi opět dáme do samostatného souboru myconf.properties v adresáři src/main/resources, jeho obsah bude lehce odlišný kvůli tomu, že použijeme jinou implementaci DataSource:

# myconf.properties
jdbc.driver=org.apache.derby.jdbc.ClientDriver
jdbc.url=jdbc:derby://localhost:1527/MojeDB
jdbc.user=makub
jdbc.password=heslo

LeaseManager

 
package cz.muni.fi.pv168.books;
 
import java.util.List;
 
public interface LeaseManager {
    
    List<Lease> getLeasesForCustomer(Customer c);
 
    void createLease(Lease lease);
    
    //další metody
}
 
package cz.muni.fi.pv168.books;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
 
import javax.sql.DataSource;
import java.sql.Date;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.List;
 
public class LeaseManagerImpl implements LeaseManager {
 
    final static Logger log = LoggerFactory.getLogger(LeaseManagerImpl.class);
    private JdbcTemplate jdbc;
    private BookManager bookManager;
    private CustomerManager customerManager;
 
    public LeaseManagerImpl(DataSource dataSource) {
        jdbc = new JdbcTemplate(dataSource);
    }
 
    public void setBookManager(BookManager bookManager) {
        this.bookManager = bookManager;
    }
 
    public void setCustomerManager(CustomerManager customerManager) {
        this.customerManager = customerManager;
    }
 
    @Override
    public List<Lease> getLeasesForCustomer(final Customer c) {
        return jdbc.query("SELECT * FROM leases WHERE customerId=?",
                (rs, rowNum) -> {
                    long bookId = rs.getLong("bookId");
                    Book book = null;
                    try {
                        book = bookManager.getBookById(bookId);
                    } catch (BookException e) {
                        log.error("cannot find book", e);
                    }
                    LocalDate startdate = rs.getDate("startdate").toLocalDate();
                    LocalDate expectedend = rs.getDate("expectedend").toLocalDate();
                    Timestamp ts = rs.getTimestamp("realend");
                    LocalDateTime realend = ts == null ? null : ts.toLocalDateTime();
                    return new Lease(rs.getLong("id"), book, c, startdate, expectedend, realend);
                },
                c.getId());
    }
 
    @Override
    public void createLease(Lease lease) {
        SimpleJdbcInsert insertLease = new SimpleJdbcInsert(jdbc).withTableName("leases").usingGeneratedKeyColumns("id");
        SqlParameterSource parameters = new MapSqlParameterSource()
                .addValue("bookId", lease.getBook().getId())
                .addValue("customerId", lease.getCustomer().getId())
                .addValue("startdate", toSQLDate(lease.getStartdate()))
                .addValue("expectedend", toSQLDate(lease.getExpectedend()))
                .addValue("realend", toSQLTimestamp(lease.getRealend()));
        Number id = insertLease.executeAndReturnKey(parameters);
        lease.setId(id.longValue());
    }
 
    private Date toSQLDate(LocalDate localDate) {
        if (localDate == null) return null;
        return new Date(ZonedDateTime.of(localDate.atStartOfDay(), ZoneId.systemDefault()).toInstant().toEpochMilli());
    }
 
    private Timestamp toSQLTimestamp(LocalDateTime localDateTime) {
        if (localDateTime == null) return null;
        return new Timestamp(ZonedDateTime.of(localDateTime, ZoneId.systemDefault()).toInstant().toEpochMilli());
    }
}

S hodnotami typu LocalDate a LocalDateTime je v současnosti (březen 2014) ta potíž, že žádný JDBC ovladač zatím nepodporuje JDBC 4.2, proto musíme hodnoty převést na typ java.sql.Date a java.util.Timestamp. V budoucnosti by mělo jít použít přímo instance tříd LocalDate a LocalDateTime.

Alternativně můžeme použít vlastní formátování do řetězce a specifikovat cílový typ parametru:

 
@Override
    public void createLease(Lease lease) {
        KeyHolder keyHolder = new GeneratedKeyHolder();
        jdbc.update(con -> {
            PreparedStatement ps = con.prepareStatement("insert into leases (BOOKID, CUSTOMERID, STARTDATE, EXPECTEDEND, REALEND) VALUES (?,?,?,?,?)", new String[]{"id"});
            ps.setLong(1, lease.getBook().getId());
            ps.setLong(2, lease.getCustomer().getId());
            ps.setObject(3, lease.getStartdate().format(DateTimeFormatter.ISO_LOCAL_DATE), Types.DATE);
            ps.setObject(4, lease.getExpectedend().format(DateTimeFormatter.ISO_LOCAL_DATE), Types.DATE);
            ps.setObject(5, lease.getRealend().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")), Types.TIMESTAMP);
            return ps;
        }, keyHolder);
        lease.setId(keyHolder.getKey().longValue());
    }

Do SpringConfig ve třídě Main pak přidáme

 
        @Bean
        public LeaseManager leaseManager() {
            LeaseManagerImpl leaseManager = new LeaseManagerImpl(dataSource());
            leaseManager.setBookManager(bookManager());
            leaseManager.setCustomerManager(customerManager());
            return leaseManager;
        }

a můžeme použít v metodě main():

 
        Book book = allBooks.get(0);
        Customer customer = allCustomers.get(0);
        Lease lease = new Lease(null, book, customer, LocalDate.now(),LocalDate.now().plusDays(30),null);
        LeaseManager leaseManager = ctx.getBean(LeaseManager.class);
        leaseManager.createLease(lease);
 
        List<Lease> leasesForCustomer = leaseManager.getLeasesForCustomer(customer);
        System.out.println("leasesForCustomer = " + leasesForCustomer);

Pokročilé použití

použití XML místo Java kódu pro konfiguraci Springu

Alternativně můžeme pro konfiguraci Springu místo třídy označené anotací @Configuration použít popis v XML, který by vypadal takto:

 
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:tx="http://www.springframework.org/schema/tx"
       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/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
 
    <context:property-placeholder location="classpath:myconf.properties"/>
 
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
 
    <tx:annotation-driven/>
 
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
 
    <bean id="customerManager" class="cz.muni.fi.pv168.books.CustomerManagerImpl" scope="singleton">
        <constructor-arg ref="dataSource"/>
    </bean>
 
    <bean id="bookManager" class="cz.muni.fi.pv168.books.BookManagerImpl" scope="singleton">
        <constructor-arg ref="dataSource"/>
    </bean>
    
</beans>

a byl by umístěn v souboru třeba "src/main/resources/spring-context.xml". Ve metodě main() bychom pak získali ApplicationContext takto:

 
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-context.xml");

Transakce

Někdy je potřeba zajistit, že několik SQL příkazů se buď provede jako celek, nebo se neprovede vůbec. Klasickým příkladem je přepis peněz v bance z účtu na účet - buď musí peníze z jednoho účtu ubýt a na druhém přibýt, nebo se nesmí stát nic. Nelze připustit stav, kdy peníze z jednoho účtu odejdou, ale nikam se nepřipíšou, protože třeba zrovna vypnuli přívod elektřiny k bankovnímu počítači.

Řešením jsou transakce. Transakce se zahájí, SQL příkazy se dělají a pokud všechny skončí úspěšně, provede se commit, který všechny změny udělá najednou. Pokud ne, provede se rollback a jako kdyby se nic nestalo.

V čistém JDBC se transakce zahajují voláním metody setAutoCommit(false) a končí voláním commit() případně rollback() a opětovným nastavením setAutoCommit(true).

Ve Springu je to mnohem jednodušší. Stačí příslušnou metodu označit anotací:

 
    @Transactional
    public void slozitaOperace() {
           jdbc.update("...");
           jdbc.update("...");
           jdbc.update("...");
           //...
    }

Aby anotace zafungovala, je potřeba mít v konfiguraci Springu nastavený transakční manager a aktivované používání anotací. Podrobnosti jsou popsány v Spring Framework: Chapter 11. Transaction management.

Pokud nechceme použít konfiguraci Springu a anotaci @Transactional, lze transakce řídit i pomocí kódu takto:

 
    private final JdbcTemplate jdbc;
    private final TransactionTemplate transaction;
 
    public MyManagerImpl(DataSource dataSource) {
        this.jdbc = new JdbcTemplate(dataSource);
        this.transaction = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
    }
 
    
    public void slozitaOperace() {
        transaction.execute(transactionStatus -> {
            jdbc.update("...");
            jdbc.update("...");
            jdbc.update("...");
            //...
        });
    }

Logování

Pokud chcete vidět, co Spring JDBC dělá, můžete povolit logování na podprobnějších úrovních. V pom.xml byste měli mít

  • SLF4J API
  • JCL-over-SLF4J bridge, protože Spring loguje pomocí JCL.
  • Logback-classic jakožto implementaci logování
 
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.6</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.1.1</version>
        </dependency>

V adresáři src/main/resources pak je potřeba soubor logback.xml s následujícím obsahem:

 
<configuration>
    <appender name="APP" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- naše vlastní třídy -->
    <logger name="cz.muni.fi.pv168.books" level="debug"/>
    <!-- transakce -->
    <logger name="corg.springframework.transaction" level="debug"/>
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="debug" />
    <!-- SQL příkazy -->
    <logger name="org.springframework.jdbc.core" level="debug"/>
    <!-- nastavování hodnot v PreparedStatementech -->
    <logger name="org.springframework.jdbc.core.StatementCreatorUtils" level="trace"/>
    
    <root level="info">
        <appender-ref ref="APP"/>
    </root>
</configuration>

Unit testy

Pomocí stejného ApplicationContext

Nejjednodušší na nastavení, ale nejsložitější na psaní samotných testů, je použít při testech databázi se stejným nastavením jako ve třídě Main, tj.:

 
public class CustomerManagerImplTest {
 
    static ApplicationContext ctx;
    private CustomerManager customerManager;
 
    @BeforeClass
    public static void beforeClass() throws Exception {
        ctx = new AnnotationConfigApplicationContext(Main.SpringConfig.class);
    }
 
    @Before
    public void setUp() throws Exception {
        customerManager = ctx.getBean("customerManager", CustomerManager.class);
    }
 
    //implementace testů ...
}

Problém s tímto nastavením je, že všechny testy vlastně provádíme na stejné databázi, která působením testů mění svůj obsah, a pořadí testů není zaručeno. Také tím nemáme zajištěn výchozí stav databáze a spuštění databázového serveru před spuštěním unit testů.

Pomocí znovuvytvářené embedded databáze

Pro unit testy je výhodné použít místo síťového databázového serveru embedded databázi, která běží jen v paměti, a je před každým testem vždy znovu vytvořena a uvedena do známého stavu.


Do pom.xml je proto potřeba přidat balík s embedded driverem pro Derby se scope nastaveným na test, tedy použije se jen při testech:

 
        <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>10.8.2.2</version>
            <scope>test</scope>
        </dependency>

Dále je třeba vytvořit (v adresáři src/main/resources)

  • soubor obsahující DDL příkazy pro vytvoření struktury databáze (třeba my-schema.sql)
-- my-schema.sql

CREATE TABLE books (
  id     INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  name   VARCHAR(70),
  author VARCHAR(45),
  releasedate DATE);

CREATE TABLE customers (
  id       INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  fullname VARCHAR(50),
  address  VARCHAR(150),
  phone    VARCHAR(20),
  email    VARCHAR(50));

CREATE TABLE leases (
  id          INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  bookId      INT REFERENCES books (id) ON DELETE CASCADE,
  customerId  INT REFERENCES customers (id) ON DELETE CASCADE,
  startDate   DATE,
  expectedEnd DATE,
  realEnd     TIMESTAMP);

  • soubor obsahující SQL příkazy pro naplnění tabulek testovacími daty (třeba my-test-data.sql)
-- my-test-data.sql
INSERT INTO BOOKS (NAME, AUTHOR,RELEASEDATE) VALUES ('Babička','Božena Němcová',DATE('1855-05-01'));

INSERT INTO CUSTOMERS (FULLNAME, ADDRESS, PHONE, EMAIL) VALUES ('Karel Čtvrtý','Karlštejn 1','','karel4@gmail.com');

INSERT INTO LEASES (BOOKID, CUSTOMERID, STARTDATE, EXPECTEDEND, REALEND) VALUES (1,1,'2013-04-01','2013-05-20',NULL);

Celkově byste tedy měli mít tyto soubory:

Idea junit jdbc spring books.png

Pak využijeme jako DataSource třídu EmbeddedDatabase poskytnutou Springem. Podstatné je, že před každým testem se v metodě označené @Before vytvoří nová embedded databáze, a po skončení každého testu se v metodě označené @After zase zruší voláním db.shutdown(), takže každá metoda označená @Test má vlastní novou databázi ve výchozím stavu:

 
package cz.muni.fi.pv168.books;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
 
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.matchers.JUnitMatchers.hasItem;
import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.DERBY;
 
public class CustomerManagerImplTest {
 
    private EmbeddedDatabase db;
    private CustomerManager customerManager;
 
    @Before
    public void setUp() throws Exception {
        db = new EmbeddedDatabaseBuilder().setType(DERBY).addScript("my-schema.sql").addScript("my-test-data.sql").build();
        customerManager = new CustomerManagerImpl(db);
    }
 
    @After
    public void tearDown() throws Exception {
        db.shutdown();
    }
 
    @Test
    public void testGetAllCustomers() throws Exception {
        assertThat(customerManager.getAllCustomers(), hasItem(customerManager.getCustomerById(1)));
    }
 
    // další testy ...
}

Nevýhoda tohoto přístupu je pomalost, protože před každým testem je databáze zrušena a znovu vytvořena.

Pomocí rollbackování transakcí na libovolné databázi

Nejefektivnější je využít podporu Springu pro testování databází, viz Spring Core Technologies - 11. Testing.

Můžeme ji využít s libovolnou databází ve známém stavu, protože každý test je prováděn v transakci, která je po provedení testu rollbackována, a stav databáze se tedy nemění.

Pro toto musíme přidat do pom.xml závislost na knihovně Spring Test:

 
       <dependency>
            <groupId>org.apache.derby</groupId>
            <artifactId>derby</artifactId>
            <version>10.10.1.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>

Mějme příkazy pro vytvoření tabulek v souboru src/main/resources/my-schema.sql a příkazy pro naplnění tabulek testovacími daty v souboru src/main/resourecs/my-test-data.sql tak jako v předešlém případě.

Vytvoříme konfiguraci Springu, která zajistí vytvoření databáze v definovaném stavu. Můžeme využít embedded databázi nebo se připojit na síťový databázový server:

 
package cz.muni.fi.pv168.books;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.test.jdbc.JdbcTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
 
import javax.sql.DataSource;
 
import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.DERBY;
 
@Configuration
@EnableTransactionManagement
public class MySpringTestConfig {
 
    @Bean
    public DataSource dataSource() {
        //embedded databáze
        return new EmbeddedDatabaseBuilder()
                .setType(DERBY)
                .addScript("classpath:my-schema.sql")
                .addScript("classpath:my-test-data.sql")
                .build();
    }
 
//    @Bean
//    public DataSource dataSource() {
//        //sítová databáze
//        BasicDataSource bds = new BasicDataSource();
//        bds.setDriverClassName("org.apache.derby.jdbc.ClientDriver");
//        bds.setUrl("jdbc:derby://localhost:1527/MojeDB");
//        bds.setUsername("makub");
//        bds.setPassword("heslo");
//        JdbcTemplate template = new JdbcTemplate(bds);
//        JdbcTestUtils.dropTables(template, "leases", "books", "customers");
//        ScriptUtils.executeSqlScript(bds.getConnection(),new ClassPathResource("my-schema.sql"));
//        ScriptUtils.executeSqlScript(bds.getConnection(),new ClassPathResource("my-test-data.sql"));
//        return bds;
//    }
 
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
 
    @Bean
    public CustomerManager customerManager() {
        return new CustomerManagerImpl(dataSource());
    }
 
    @Bean
    public BookManager bookManager() {
        // BookManagerImpl nepoužívá Spring JDBC, musíme mu vnutit spolupráci se Spring transakcemi !
        return new BookManagerImpl(new TransactionAwareDataSourceProxy(dataSource()));
    }
 
    @Bean
    public LeaseManager leaseManager() {
        LeaseManagerImpl leaseManager = new LeaseManagerImpl(dataSource());
        leaseManager.setBookManager(bookManager());
        leaseManager.setCustomerManager(customerManager());
        return leaseManager;
    }
}

Všimněte si, že v definici metody bookmanager() je použita třída TransactionAwareDataSourceProxy, která umožňuje použít Springové řízení transakcí i u tříd, které byly napsány v čistém JDBC bez použití Springu !


Testovací třída pak musí využít anotace @RunWith, @ContextConfiguration a @Transactional k tomu, aby se při spuštění unit testů pomocí JUnit použila konfigurace Springu a jeho řízení transakcí. Instanci BookManageru můžeme injektovat pomocí anotace @Autowired, viz níže:

 
package cz.muni.fi.pv168.books;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
 
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.junit.matchers.JUnitMatchers.hasItem;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {MySpringTestConfig.class})
@Transactional
public class CustomerManagerImplTest {
 
    @Autowired
    private CustomerManager customerManager;
 
    @Test
    public void testGetCustomerById() throws Exception {
        Customer c1 = customerManager.getCustomerById(1);
        assertThat(c1, is(notNullValue()));
        assertThat(c1.getFullname(), is(equalTo("Karel Čtvrtý")));
    }
 
    @Test
    public void testCreateCustomer() throws Exception {
        Customer c2 = new Customer(null, "Jan Novák", "Dlouhá 1", "603123456", "novak@gmail.com");
        customerManager.createCustomer(c2);
        assertThat("customer id", c2.getId(), notNullValue());
        Customer c3 = customerManager.getCustomerById(c2.getId());
        assertThat(c3, is(equalTo(c2)));
 
    }
 
    @Test
    public void testDeleteCustomer() throws Exception {
        customerManager.deleteCustomer(1);
        try {
            customerManager.getCustomerById(1);
            fail("customer 1 not deleted");
        } catch (EmptyResultDataAccessException e) {
            //no code
        }
    }
 
    @Test
    public void testUpdateCustomer() throws Exception {
        Customer c1 = customerManager.getCustomerById(1);
        c1.setAddress("Krátká 3");
        c1.setPhone("2222");
        c1.setEmail("a@b.com");
        c1.setFullname("Pepa První");
        customerManager.updateCustomer(c1);
        Customer c2 = customerManager.getCustomerById(1);
        assertThat(c2, is(equalTo(c1)));
    }
 
    @Test
    public void testGetAllCustomers() throws Exception {
        assertThat(customerManager.getAllCustomers(), hasItem(customerManager.getCustomerById(1)));
    }
}

Výhody tohoto přístupu jsou:

  • databáze se vytvoří jen jednou
  • přesto má každý test databázi vždy ve výchozím stavu
  • lze využít i jinou než embedded databázi, například pokud máme hodně testovacích dat


Nevýhodnou je mírně komplikovanější konfigurace.

Vytvoření spustitelného JARu

Toto je pokročilé téma pro zájemce, ostatní nechť ho vynechají.

Pokud použijeme Spring verze 3 a vyšší, jehož knihovny jsou rozděleny do více JAR souborů, a chceme nakonec vyprodukovat jeden spustitelný JAR soubor, musíme použít plugin pro Maven, který umí správně spojit všechny použité knihovny (SLF4J, JDBC ovladač, Spring) do jednoho JARu.

To uděláme v souboru pom.xml přidáním následujícího textu do části build/plugins:

 
<!-- zabalit vsechno do jednoho JARu -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>1.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <!-- co se ma spustit pri spusteni celeho JARu -->
                                    <mainClass>cz.muni.fi.pv168.books.Main</mainClass>
                                </transformer>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.handlers</resource>
                                </transformer>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/spring.schemas</resource>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

Potíž je totiž s konfiguračními soubory META-INF/spring.handlers a META-INF/spring.schemas v JARech různých částí Springu, ty musí být spojeny dohromady.

Pokud používáme Spring Platform jako rodičkovský projekt, je Shade plugin již nakonfigurován a je nutné mu mainClass předat přes property ${start-class}:

 
 
    <parent>
        <groupId>io.spring.platform</groupId>
        <artifactId>platform-bom</artifactId>
        <version>2.0.5.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <!-- for shade plugin as configured in spring-boot -->
        <start-class>cz.muni.fi.Main</start-class>
    </properties>
 
   <build>
        <defaultGoal>package</defaultGoal>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <!-- repack everything into a single jar -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
            </plugin>
        </plugins>
    </build>