Spring, как всегда, упрощает использование сторонних API и делает жизнь разработчика проще. Поддержка JDBC в Spring framework берёт на себя часть реализации обязательного кода для работы с JDBC: поддержку соединений, транзакции, выполнение запросов, закрытие ресурсов и т.д.
Подготовка
Нам понадобится пустой maven проект с Spring, Spring JDBC, H2 и библиотеками тестирования:
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <javaee.version>7.0</javaee.version> <org.springframework.version>4.1.7.RELEASE</org.springframework.version> <h2.version>1.4.190</h2.version> <junit.version>4.12</junit.version> <hamcrest.version>1.3</hamcrest.version> <easymock.version>3.3.1</easymock.version> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${h2.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>${hamcrest.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>${easymock.version}</version> </dependency> </dependencies> |
Соединение
Я использую XML конфигурацию для настройки параметров соединения:
1 2 3 4 5 6 | <jdbc:embedded-database id="dataSource" type="H2"/> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <constructor-arg name="dataSource" ref="dataSource"/> </bean> |
Настройка Spring JDBC в общем случае требует двух действий — настройку data source, то есть конкретного соединения с базой, и связывание его с JdbcTemplate, классом, который и реализует интерфейс к Spring JDBC.
Выполнение запросов
Чтоб к базе обращаться, в ней надо создать какую-нибудь таблицу:
1 2 3 | public interface DBInitService { void initializeDatabase(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Service public class DBInitServiceImpl implements DBInitService { /** * Query that create table. */ private static final String CREATE_QUERY = "CREATE TABLE EXAMPLE (GREETING VARCHAR(6), TARGET VARCHAR(6))"; @Inject private JdbcTemplate jdbcTemplate; @Override public void initializeDatabase() { jdbcTemplate.execute(CREATE_QUERY); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @ContextConfiguration("/applicationContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class DBInitServiceImplTest { @Inject private JdbcTemplate jdbcTemplate; @Inject private DBInitService testedObject; @Test public void testInitialize() { testedObject.initializeDatabase(); jdbcTemplate.execute("SELECT GREETING FROM EXAMPLE"); } } |
Bean jdbcTemplate просто добавляется в сервис, используя стандартные механизмы Spring. Метод execute() выполняет любой запрос к базе и не возвращает ничего. В случае ошибки метод кидает DataAccessException.
Запросы
execute() — очень удобный метод, если надо создать таблицу. Но метод, который не возвращает ничего, малополезен при работе с данными, да и связывание параметров гораздо удобнее формирования запросов склеиванием строк.В Spring JDBC есть два основных семейства методов: update() и query*(), которые служат для выполнения запросов изменяющих данные и возвращающих данные.
1 2 3 4 | public interface GreeterDao { void addGreet(String greeting, String target); List<Map<String, Object>> getGreetings(); } |
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 | @Repository public class GreeterDaoImpl implements GreeterDao { /** * Query that populates table with data. */ private static final String DATA_QUERY = "INSERT INTO EXAMPLE VALUES(?, ?)"; /** * Query that extracts data from table. */ private static final String GREET_QUERY = "SELECT GREETING, TARGET FROM EXAMPLE"; @Inject private JdbcTemplate jdbcTemplate; @Override public void addGreet(String greeting, String target) { jdbcTemplate.update(DATA_QUERY, greeting, target); } @Override public List<Map<String, Object>> getGreetings() { return jdbcTemplate.queryForList(GREET_QUERY); } } |
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 | @ContextConfiguration("/applicationContext.xml") @RunWith(SpringJUnit4ClassRunner.class) public class GreeterDaoImplTest { @Inject private JdbcTemplate jdbcTemplate; @Inject private GreeterDao testedObject; @Inject private DBInitService dbInitService; @Test public void testInitialize() { dbInitService.initializeDatabase(); testedObject.addGreet("TEST", "TEST"); List<Map<String, Object>> actual = testedObject.getGreetings(); Iterator<Map<String,Object>> it = actual.iterator(); Map<String, Object> row =it.next(); assertThat(row.get("GREETING"), is("TEST")); assertThat(row.get("TARGET"), is("TEST")); } } |
Метод update(), из примера выше, принимает запрос и параметры запроса. Метод queryForList() возвращает список key-value объектов. Каждый элемент списка соответствует одной строке в result set, который возвращает запрос, а Map содержит значения столбцов строки, ключами к которым являются имена столбцов.
Уровень DAO
Хорошим тоном разработки является разделение кода, который работает непосредственно с базами данных (уровень DAO), от кода, который обрабатывает данные (уровень сервисов). Это позволяет абстрагировать сервисы от конкретной реализации DAO и, при необходимости, менять эти реализации без изменения кода сервисов. В Spring даже предусмотрены отдельные аннотации, которые имеют в настоящий момент одинаковый эффект, но позволяют разработчику сразу понять, что делает тот или иной класс. Так GreeterServiceimpl имеет аннотацию @Service, а GreeterDaoImpl аннотацию @Repository.
1 2 3 | public interface Greeter { String greet(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @Service public class GreeterImpl implements Greeter { @Inject private GreeterDao dao; @Override public String greet() { List<Map<String, Object>> greets = dao.getGreetings(); Iterator<Map<String, Object>> it = greets.iterator(); if (!it.hasNext()) { return "No greets"; } Map<String, Object> row = it.next(); return row.get("GREETING")+", "+row.get("TARGET"); } } |
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 | 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 testNoGreets() { expect(dao.getGreetings()).andReturn(Collections.EMPTY_LIST); replayAll(); assertThat(testedObject.greet(), is("No greets")); } @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")); } } |
Обратите внимание, что тест сервиса проверяет только сервис, заменяя GreeterDao его подделкой. В то время как тесты DAO являются интеграционными и работают непосредственно с базой.
Поскольку я писал код сразу с модульными и интеграционными тестами, то у меня есть некоторая уверенность, что он работает, но, тем не менее, попробуем его запустить:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public static void main(final String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("/applicationContext.xml"); Greeter greeter = context.getBean(Greeter.class); GreeterDao dao = context.getBean(GreeterDao.class); DBInitService init = context.getBean(DBInitService.class); init.initializeDatabase(); dao.addGreet("Hello", "World"); System.out.println(greeter.greet()); } |
1 2 3 4 5 6 7 8 9 | дек 22, 2015 11:48:19 AM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@48140564: startup date [Tue Dec 22 11:48:19 EET 2015]; root of context hierarchy дек 22, 2015 11:48:19 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions INFO: Loading XML bean definitions from class path resource [applicationContext.xml] дек 22, 2015 11:48:19 AM org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor <init> INFO: JSR-330 'javax.inject.Inject' annotation found and supported for autowiring дек 22, 2015 11:48:19 AM org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory initDatabase INFO: Creating embedded database 'dataSource' Hello, World |
Код примера доступен на github.
P.S. DAO — Data Access Object, стандартный шаблон проектирования, описывающий объект, который предоставляет интерфейс к какому-либо типу базы данных или механизму хранения.