Для работы с реляционными базами данных в Spring Framework предусмотрен такой замечательный инструмент как JdbcTemplate, который является обвязкой вокруг стандартных средств JDBC. JdbcTemplate прост и гибок в применении, его возможностей вполне достаточно для реализации проектов малых и средних размеров. В JDBCTemplate не предусмотрены стандартные средства ORM в отличии от Hibernate или JPA, так что при его использовании нужно самостоятельно писать мапперы, которые будут преобразовывать данные полученные из БД в объекты классов сущностей.
Настройка проекта
Создадим новый Maven-проект и добавим в зависимости spring-jdbc и spring-context. Кстати, spring-context в нашем случае нужен только потому что я буду демонстрировать работу Spring JDBC в рамках Spring-приложения. Так же Spring JDBC можно использовать вне контекста Spring и даже в проекте не использующем Spring. Так же для тестов нам понадобятся junit и h2. При разработке приложения без Spring Boot список зависимостей будет следующий:
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 |
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> <version>1.3.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.191</version> <scope>test</scope> </dependency> </dependencies> |
spring-boot-starter-logging я добавил, чтобы не отвлекаться на настройку логгирования. Альтернативный набор зависимостей, если мы разрабатываем приложение с использованием Spring Boot, будет следующим:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency> </dependencies> |
Архитектура приложения, используемого в данном примере, подразумевает наличие класса-сущности и DAO-класса, который будет реализовывать работу с базой данных.
Исходные данные
В корневой директории ресурсов создадим файл schema.sql, из которого Spring JDBC будет создавать структуру базы данных перед тестированием:
1 |
create table person (id varchar(36) primary key, name varchar(255) not null, email varchar(255) not null unique); |
Так же рядом добавим файл data.sql, из которого будет заполняться база:
1 |
insert into person values ('jack-daniels', 'Jack Daniels', 'jackdaniels@example.com'), ('george-dickel', 'George Dickel', 'georgedickel@example.com'); |
Теперь опишем класс-сущность Person:
1 2 3 4 5 6 7 8 9 10 11 |
public class Person { private String id; private String name; private String email; // лишний код опущен } |
И наконец опишем DAO-интерфейс и класс, его реализующий:
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 |
public interface PersonRepository { // Маппер, превращающий строку из таблицы БД в объект класса Person RowMapper<Person> ROW_MAPPER = (ResultSet resultSet, int rowNum) -> { return new Person(resultSet.getString("id"), resultSet.getString("name"), resultSet.getString("email")); }; List<Person> findAll(); Person findOne(String id); Person save(Person person); int delete(String id); } @Component public final class PersonRepositoryImpl implements PersonRepository { private static final Logger LOGGER = LoggerFactory.getLogger(PersonRepository.class); private final JdbcTemplate jdbcTemplate; @Autowired public PersonRepositoryImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Person> findAll() { throw new UnsupportedOperationException("Not supported yet."); } @Override public Person findOne(String id) { throw new UnsupportedOperationException("Not supported yet."); } @Override public Person save(Person person) { throw new UnsupportedOperationException("Not supported yet."); } @Override public int delete(String id) { throw new UnsupportedOperationException("Not supported yet."); } } |
Если DAO-класс используется не в Spring-приложении, то аннтоацию @Component можно убрать. Так же обратите внимание, что объект класса JdbcTemplate внедряется через конструктор. Это даёт независимость от контекста Spring и возможность сделать свойство класса final.
Тестирование
Подготовим класс тестов:
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 |
public class TestPersonRepository { private EmbeddedDatabase embeddedDatabase; private JdbcTemplate jdbcTemplate; private PersonRepository personRepository; @Before public void setUp() { // Создадим базу данных для тестирования embeddedDatabase = new EmbeddedDatabaseBuilder() .addDefaultScripts()// Добавляем скрипты schema.sql и data.sql .setType(EmbeddedDatabaseType.H2)// Используем базу H2 .build(); // Создадим JdbcTemplate jdbcTemplate = new JdbcTemplate(embeddedDatabase); // Создадим PersonRepository personRepository = new PersonRepositoryImpl(jdbcTemplate); } @After public void tearDown() { embeddedDatabase.shutdown(); } } |
В наших тестах контекст Spring использоваться не будет, так как в этом нет никакой необходимости.
Напишем тесты:
- findAll() не должен возвращать null и должен возвращать список всех объектов, находящихся в таблице.
12345@Testpublic void testFindAll() {Assert.assertNotNull(personRepository.findAll());Assert.assertEquals(2, personRepository.findAll().size());} - findOne(String id) должен возвращать объект с указанным идентификатором, либо null, если объект не существует.
12345@Testpublic void testFindOne() {Assert.assertNotNull(personRepository.findOne("jack-daniels"));Assert.assertNull(personRepository.findOne("nonexistent-id"));} - save(Person person) должен сохранять объект и возвращать его сохранённую версию, либо выбрасывать исключение DataIntegrityViolationException в случае попытке сохранения объекта с невалидными данными.
123456789101112131415161718192021222324252627282930313233343536373839404142434445@Testpublic void testSave() {Person person = personRepository.save(new Person("Jim Beam", "jimbeam@example.com"));Assert.assertNotNull(person);Assert.assertNotNull(person.getId());Assert.assertEquals("Jim Beam", person.getName());}@Test(expected = DataIntegrityViolationException.class)public void testSaveInvalid() {personRepository.save(new Person());}@Test(expected = DataIntegrityViolationException.class)public void testSaveConflict() {personRepository.save(new Person("Jim Beam", "jackdaniels@example.com"));}@Testpublic void testUpdate() {Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER);person.setName("Johny Walker");person = personRepository.save(person);Assert.assertNotNull(person);Assert.assertNotNull(person.getId());Assert.assertEquals("Johny Walker", person.getName());}@Test(expected = DataIntegrityViolationException.class)public void testUpdateInvalid() {Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER);person.setName(null);personRepository.save(person);}@Test(expected = DataIntegrityViolationException.class)public void testUpdateConflict() {Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER);person.setEmail("georgedickel@example.com");personRepository.save(person);} - delete(String id) должен удалять запись с указанным идентификатором и возвращать количество затронутых записей.
12345@Testpublic void testDelete() {Assert.assertEquals(1, personRepository.delete("jack-daniels"));Assert.assertEquals(0, personRepository.delete("nonexistent-id"));}
Весь класс тестов целиком:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
package name.alexkosarev.sandbox.repositories; import name.alexkosarev.sandbox.entities.Person; import org.junit.*; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; public class TestPersonRepository { private EmbeddedDatabase embeddedDatabase; private JdbcTemplate jdbcTemplate; private PersonRepository personRepository; @Before public void setUp() { embeddedDatabase = new EmbeddedDatabaseBuilder() .addDefaultScripts() .setType(EmbeddedDatabaseType.H2) .build(); jdbcTemplate = new JdbcTemplate(embeddedDatabase); personRepository = new PersonRepositoryImpl(jdbcTemplate); } @Test public void testFindAll() { Assert.assertNotNull(personRepository.findAll()); Assert.assertEquals(2, personRepository.findAll().size()); } @Test public void testFindOne() { Assert.assertNull(personRepository.findOne("nonexistent-id")); Assert.assertNotNull(personRepository.findOne("jack-daniels")); } @Test public void testSave() { Person person = personRepository.save(new Person("Jim Beam", "jimbeam@example.com")); Assert.assertNotNull(person); Assert.assertNotNull(person.getId()); Assert.assertEquals("Jim Beam", person.getName()); } @Test(expected = DataIntegrityViolationException.class) public void testSaveInvalid() { personRepository.save(new Person()); } @Test(expected = DataIntegrityViolationException.class) public void testSaveConflict() { personRepository.save(new Person("Jim Beam", "jackdaniels@example.com")); } @Test public void testUpdate() { Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER); person.setName("Johny Walker"); person = personRepository.save(person); Assert.assertNotNull(person); Assert.assertNotNull(person.getId()); Assert.assertEquals("Johny Walker", person.getName()); } @Test(expected = DataIntegrityViolationException.class) public void testUpdateInvalid() { Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER); person.setName(null); personRepository.save(person); } @Test(expected = DataIntegrityViolationException.class) public void testUpdateConflict() { Person person = jdbcTemplate.queryForObject("select * from person where id = 'jack-daniels'", PersonRepository.ROW_MAPPER); person.setEmail("georgedickel@example.com"); personRepository.save(person); } @Test public void testDelete() { Assert.assertEquals(1, personRepository.delete("jack-daniels")); Assert.assertEquals(0, personRepository.delete("nonexistent-id")); } @After public void tearDown() { embeddedDatabase.shutdown(); } } |
Реализация
Для получения данных из БД у JdbcTemplate есть несколько методов, но для нашего примера достаточно самого простого варианта с использованием метода query. Аргументы этого метода: SQL-запрос, список параметров запроса в виде массива объектов, если таковые есть и маппер, который будет полученные данные превращать в объекты нужного нам класса. Параметры запроса указываются в запросе при помощи вопросительного знака и подставляются по порядку. Вызов
1 |
jdbcTemplate.query("select * from person where id = ?", new Object[]{"jack-daniels"}); |
будет приведён к виду
1 |
select * from person where id = 'jack-daniels'; |
Так же при необходимости можно задавать порядковый номер параметра запроса:
1 |
jdbcTemplate.query("select * from person where name = ?2 or email = ?1", new Object[]{"jackdaniels@example.com", "Jack Daniels"}); |
что равноценно
1 |
select * from person where name = 'Jack Daniels' or email = 'jackdaniels@example.com'; |
Стоит помнить, что порядковые номера параметров запроса начинаются с 1, а не с 0.
Для добавления, изменения или удаления записей в таблицах БД используются методы update и batchUpdate. В нашем случае достаточно update.
Реализация DAO-интерфейса, удовлятворяющая описанным тестам:
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 |
@Component public final class PersonRepositoryImpl implements PersonRepository { private static final Logger LOGGER = LoggerFactory.getLogger(PersonRepository.class); private final JdbcTemplate jdbcTemplate; @Autowired public PersonRepositoryImpl(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @Override public List<Person> findAll() { return jdbcTemplate.query("select * from person", ROW_MAPPER); } @Override public Person findOne(String id) { Person person = null; try { person = jdbcTemplate.queryForObject("select * from person where id = ?", new Object[]{id}, ROW_MAPPER); } catch (DataAccessException dataAccessException) { LOGGER.debug("Couldn't find entity of type Person with id {}", id); } return person; } @Override public Person save(Person person) { if (person.getId() == null) { person.setId(UUID.randomUUID().toString()); assert jdbcTemplate.update("insert into person values (?, ?, ?)", person.getId(), person.getName(), person.getEmail()) > 0; } else { assert jdbcTemplate.update("update person set name = ?2, email = ?3 where id = ?1", person.getId(), person.getName(), person.getEmail()) > 0; } return findOne(person.getId()); } @Override public int delete(String id) { return jdbcTemplate.update("delete from person where id = ?", id); } } |
На этом моменте тесты должны проходить успешно и покрывать 100% кода PersonRepository и PersonRepositoryImpl.
Более подробно о JdbcTemplate в официальной документации Spring.