Мечта официантки — чтобы клиенты обедали у себя дома, а чаевые присылали ей по почте. Я думаю разработчики тоже не отказались бы, чтобы программы писали себя сами, хотя бы малую часть. Именно это делает Spring Data JPA, который является частью большего проекта, Spring Data.
Spring Data JPA позволяет заменить ручную реализацию JPA запросов к базе данных на её декларативное объявление. Говоря более предметно, Spring Data JPA позволяет заменить классы, подобные тому, что показан ниже, на однострочный интерфейс.
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 RoleRepository {
@Inject
private SessionFactory sessionFactory;
public void store(Role r) {
sessionFactory.getCurrentSession().merge(r);
}
public Role get(Long id) {
return getSession()
.createCriteria(Role.class)
.setCacheable(true)
.add(Restrictions.eq("id", id))
.uniqueResult();
}
public Collection<Role> getRoles() {
return getSession()
.createCriteria(Role.class)
.setCacheable(true)
.addOrder(Order.asc("role"))
.list();
}
}
|
Подготовка
Помимо обычных реализации JPA и, собственно, Spring, необходимо добавить в maven проект артефакт Spring Data JPA:
1
2
3
4
5
6
7
8
9
10
11
12
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<org.springframework.data.version>1.11.0.RELEASE</org.springframework.data.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>${org.springframework.data.version}</version>
</dependency>
</dependencies>
|
Настройка JPA и Spring Context
Описания сущностей, с которыми мы будем работать, я взял из примера JPA entity mapping, и не хочу на них подробно останавливаться сейчас. Конфигурация JPA достаточно проста. Поскольку в качестве реализации JPA я использую Hibernate, то всё сводится к включению создания таблиц и настройке соединения с базой. Напоминаю, что конфигурация JPA по соглашению размещается в файле /META-INF/persistence.xml
1
2
3
4
5
6
7
8
9
10
11
12
|
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="ru.easyjava.spring.data.jpa">
<properties>
<property name="hibernate.hbm2ddl.auto" value="create"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.connection.url" value="jdbc:h2:mem:test"/>
</properties>
</persistence-unit>
</persistence>
|
Главное, что нам понадобится из настроек JPA, это имя persistence unit, которое передаётся в конфигурацию Spring. В этот раз мы будем настраивать Spring context используя Java config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@Configuration
@ComponentScan("ru.easyjava.spring.data.jpa")
@EnableJpaRepositories
@EnableTransactionManagement
public class ContextConfiguration {
@Bean
public LocalEntityManagerFactoryBean entityManagerFactory() {
LocalEntityManagerFactoryBean result =
new LocalEntityManagerFactoryBean();
result.setPersistenceUnitName("ru.easyjava.spring.data.jpa");
return result;
}
@Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager result = new JpaTransactionManager();
result.setEntityManagerFactory(entityManagerFactory().getObject());
return result;
}
}
|
Контекст будет маленький, только создание JPA бина и менеджера транзакций. Самое интересное делает аннотация @EnableJpaRepositories. Эта аннотация включает поддержку Spring Data JPA и автогенерацию кода для доступа к данным. Наличие этой аннотации подразумевает, что в Spring context будет два бина, один с именем entityManagerFactory, другой с именем transactionManager. Думаю типы этих бинов понятны из названия 🙂 При желании имена бинов можно задать явно в параметрах аннотации.
Уровень DAO
В проекте у нас четыре сущности, но, дабы не загромождать пример, работать будем только с одной:
1
|
public interface PersonRepository extends CrudRepository<Person, Long> {}
|
И нет, я не забыл написать реализацию интерфейса. За меня это сделает Spring Data JPA.
Интерфейс CrudRepository определяет следующие методы:
- count() — возвращает число сущностей этого класса в базе
- delete(id)/delete(Iterable) — удаляет сущность с заданным(и) id
- exists(id) — проверяет, есть ли в базе сущность с заданным id
- findAll() — возвращает все сущности этого класса
- find(id) — возвращает сущность с заданным id (или null, если таковой нет)
- save(entity)/save(Iterable) — сохраняет сущность (сущности) в базу
Таким образом, в каждом интерфейсе, заданном выше, мы автоматически получаем реализации вышеописанных методов. Совершенно бесплатно, то есть даром.
Но что если этих методов недостаточно? Spring Data JPA позволяет декларативно задавать описания запросов прямо в интерфейсе:
1
2
3
4
5
6
7
8
|
public interface PersonRepository extends CrudRepository<Person, Long> {
/**
* Returns list of persons filtered by name
* @param name value of name filter.
* @return list of matching persons.
*/
Collection<Person> findByFirstName(String name);
}
|
Метод findByFirstName() автоматически будет сгенерирован и будет делать ровно то, что говорит его название — искать по полю firstName в сущности Person.
Использование в приложении
Просто добавляете зависимость от вашего интерфейса в класс и используете, как если бы вы сами его писали.
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
|
@ContextConfiguration(classes = ru.easyjava.spring.data.jpa.ContextConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class DalIT {
@Inject
private PersonRepository personRepository;
@Before
@Transactional
public void setUp() throws Exception {
Passport p = new Passport();
p.setSeries("AS");
p.setNo("123456");
p.setIssueDate(LocalDate.now());
p.setValidity(Period.ofYears(20));
Address a = new Address();
a.setCity("Kickapoo");
a.setStreet("Main street");
a.setBuilding("1");
Person person = new Person();
person.setFirstName("Test");
person.setLastName("Testoff");
person.setDob(LocalDate.now());
person.setPrimaryAddress(a);
person.setPassport(p);
Company c = new Company();
c.setName("Acme Ltd");
p.setOwner(person);
person.setWorkingPlaces(Collections.singletonList(c));
personRepository.save(person);
}
@Test
@Transactional
public void testGreeter() {
personRepository.findAll()
.forEach(System.out::println);
personRepository.findByFirstName("Test")
.forEach(System.out::println);
assertTrue(personRepository.findByFirstName("Fail").isEmpty());
}
}
|
Код примера доступен на github.