В 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.