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, стандартный шаблон проектирования, описывающий объект, который предоставляет интерфейс к какому-либо типу базы данных или механизму хранения.