В Spring JDBC есть улучшенная поддержка встраиваемых баз данных, подходящая для тех случаев, когда база данных нужна только на время работы приложения и её содержимым можно пренебречь. Spring JDBC может сам создавать такие базы и наполнять их содержимым.
Подготовка
За основу я взял код из статьи Hello, Spring JDBC и добавил в него артефакты встраиваемых баз данных:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<h2.version>1.4.190</h2.version>
<hsqldb.version>2.3.3</hsqldb.version>
<derby.version>10.12.1.1</derby.version>
..
</properties>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<version>${hsqldb.version}</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>${derby.version}</version>
</dependency>
</dependencies>
|
Кроме того, в каталог src/main/resources/ я добавил несколько SQL скриптов:
1
2
3
4
|
CREATE TABLE EXAMPLE (
GREETING VARCHAR(6),
TARGET VARCHAR(12)
);
|
1
|
INSERT INTO EXAMPLE VALUES('Hello', 'H2');
|
1
|
INSERT INTO EXAMPLE VALUES('Hello', 'Apache Derby');
|
1
|
INSERT INTO EXAMPLE VALUES('Hello', 'HSQLDB');
|
Создание баз данных
Создание встраиваемой базы данных сводится к паре строк в файле описания контекста:
1
2
3
4
|
<jdbc:embedded-database id="h2Source" type="H2">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:h2-data.sql"/>
</jdbc:embedded-database>
|
Базе данных присваивается id и Spring’у сообщается, какую именно базу данных мы хотим использовать. Поддерживаются H2, HSQLDB, и Apache Derby. При описании базы можно дополнительно перечислить файлы SQL скриптов, которые будут выполнены в порядке перечисления. Скрипты выполняются при создании базы (по умолчанию) или при удалении (если указан аттрибут «execution=DESTROY»).
Использование скриптов для создания структуры базы данных и наполнения её данными делает жизнь гораздо приятнее — писать скрипты с sql запросами проще, чем писать те же самые запросы в коде приложения и самому заниматься инициализацией базы.
Создавать встраиваемые базы можно и с использованием Java config:
1
2
3
4
5
6
7
8
9
|
@Bean
public DataSource hsqlDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.setScriptEncoding("UTF-8")
.addScript("schema.sql")
.addScript("hsql-data.sql")
.build();
}
|
Как видите, параметры и возможности в обоих случаях те же самые. Разумеется EmbeddedDataBaseBuilder можно использовать в любой удобный момент, а не только для создания Spring beans.
Тесты
Поддержку встраиваемых баз данных удобно использовать в тестах: они быстро работают, не требуют подготовки окружения и их нетрудно привести к требуемому состоянию. Например, для интеграционного теста моего уровня данных я подготовил отдельный скрипт для данных и отдельный контекст:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Service
public class GreeterImpl implements Greeter {
/**
* Our data layer.
*/
@Inject
private GreeterDao dao;
@Override
public final String greet() {
return dao
.getGreetings()
.stream()
.collect(
Collectors.mapping(r -> r.get("GREETING") + ", " + r.get("TARGET"),
Collectors.joining("\n")));
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<?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"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd">
<context:component-scan base-package="ru.easyjava"/>
<jdbc:embedded-database id="h2Source" type="H2">
<jdbc:script location="classpath:schema.sql"/>
<jdbc:script location="classpath:h2-test-data.sql"/>
</jdbc:embedded-database>
<bean id="h2Template"
class="org.springframework.jdbc.core.JdbcTemplate">
<constructor-arg name="dataSource" ref="h2Source"/>
</bean>
</beans>
|
1
|
INSERT INTO EXAMPLE VALUES('TEST', 'TEST');
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class GreeterImplTest extends EasyMockSupport {
@Rule
public EasyMockRule em = new EasyMockRule(this);
@Mock
private GreeterDao dao;
@TestSubject
private GreeterImpl testedObject = new GreeterImpl();
@Test
public void testGreets() {
Map<String, Object> expected = new HashMap<>();
expected.put("GREETING", "TEST");
expected.put("TARGET", "TEST");
expect(dao.getGreetings()).andReturn(Collections.singletonList(expected));
replayAll();
assertThat(testedObject.greet(), is("TEST, TEST"));
}
}
|
Используя отдельный контекст и отдельный SQL скрипт я наполняю базу тестовыми данными, которые ожидает мой тест.
Использование
Использование очевидно — data source оборачивается в JdbcTemplate и используется как обычно. Так как базы данных у меня три, то и JdbcTemplate тоже будет три:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
@Repository
public class GreeterDaoImpl implements GreeterDao {
/**
* Query that extracts data from table.
*/
private static final String GREET_QUERY =
"SELECT GREETING, TARGET FROM EXAMPLE";
/**
* H2 database.
*/
@Inject
private JdbcTemplate h2Template;
/**
* Derby database.
*/
@Inject
private JdbcTemplate derbyTemplate;
/**
* HSQLDB database.
*/
@Inject
private JdbcTemplate hsqlTemplate;
@Override
public final List<Map<String, Object>> getGreetings() {
List<Map<String, Object>> result = h2Template.queryForList(GREET_QUERY);
result.addAll(derbyTemplate.queryForList(GREET_QUERY));
result.addAll(hsqlTemplate.queryForList(GREET_QUERY));
return result;
}
}
|
Полученный результат обрабатывается и выводится:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Service
public class GreeterImpl implements Greeter {
/**
* Our data layer.
*/
@Inject
private GreeterDao dao;
@Override
public final String greet() {
return dao
.getGreetings()
.stream()
.collect(
Collectors.mapping(r -> r.get("GREETING") + ", " + r.get("TARGET"),
Collectors.joining("\n")));
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public final class App {
/**
* Entry point.
*
* @param args Command line args. Not used.
*/
public static void main(final String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("/applicationContext.xml");
Greeter greeter = context.getBean(Greeter.class);
System.out.println(greeter.greet());
}
}
|
1
2
3
|
Hello, H2
Hello, Apache Derby
Hello, HSQLDB
|
Поскольку queryForList() возвращает не ResultSet а обычный список, то можно использовать и Stream API, в отличие от сырого JDBC.
Бонус
Использование анализатора кода checkstyle и Spring Java config сопряжено с некоторыми неудобствами. Checkstyle настаивает, чтобы все методы в классе ContextConfiguration были final, в то время как Spring желает обратного. Чтобы сделать проект собирающимся, я добавил этот класс в исключения. Для этого в корне проекта я завёл файл checkstyle-suppressions.xml в котором перечислил, что надо игнорировать:
1
2
3
4
5
6
7
8
9
10
|
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Puppy Crawl//DTD Suppressions 1.0//EN"
"http://www.puppycrawl.com/dtds/suppressions_1_0.dtd">
<suppressions>
<suppress checks="DesignForExtension"
files="ContextConfiguration.java"/>
</suppressions>
|
Этот же файл я добавил к конфигурации maven плагина:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>2.15</version>
<executions>
<execution>
<id>validate</id>
<phase>validate</phase>
<configuration>
<encoding>UTF-8</encoding>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
<suppressionsLocation>checkstyle-suppressions.xml</suppressionsLocation>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
|
Код пример доступен на github.