Spring облегчает использование JPA в приложениях, избавляя разработчика от необходимости вручную создавать объекты JPA и отделяя логику приложения от конкретной реализации JPA. Spring обеспечивает унифицированный интерфейс для различных реализаций ORM, тем самым упрощая переход от одной реализации к другой.
Подготовка
Нам понадобится пустой maven проект с Spring, Spring ORM, H2, Hibernate в качестве реализации 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
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
|
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javaee.version>7.0</javaee.version>
<lombok.version>1.16.12</lombok.version>
<org.springframework.version>4.3.4.RELEASE</org.springframework.version>
<hibernate.version>5.2.5.Final</hibernate.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.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</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>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.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>
|
Настройка JPA
Конфигурация JPA располагается в файле META-INF/persistence.xml В моём примере я использую Hibernate в качестве реализации JPA и встраиваемую базу H2 в качестве базы данных.
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="update"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.connection.url" value="jdbc:h2:mem:test"/>
</properties>
</persistence-unit>
</persistence>
|
Разумеется, в настройках Hibernate как JPA реализации можно использовать любые базы данных и пулы соединений.
Настройка Spring
Для включения поддержки JPA в Spring необходимо добавить в контекст особый бин. Можно сделать это программно, а можно с использованием xml конфигурации:
1
2
3
|
<bean id="emf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
<property name="persistenceUnitName" value="ru.easyjava.spring.data.jpa"/>
</bean>
|
Свойство persistensUnitName связывает конкретный бин с конкретным persistence unit, сконфигурированным в persistence.xml
Схема данных
ORM отображает объектную модель данных на реляционную модель данных. Чтобы не загораживать пример, используем как можно более простую модель:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Entity
public class Greeter {
@Id
@GeneratedValue
@Getter
@Setter
private Integer id;
@Getter
@Setter
private String greeting;
@Getter
@Setter
private String target;
}
|
Аннотация @Entity говорит JPA, что это класс сущности, а @Getter и @Setter генерируют код для доступа к полям.
Уровень DAO
Хорошим тоном разработки является разделение кода, который работает непосредственно с базами данных (уровень DAO), от кода, который обрабатывает данные (уровень сервисов). Это позволяет абстрагировать сервисы от конкретной реализации DAO и, при необходимости, менять эти реализации без изменения кода сервисов.
1
2
3
4
5
|
public interface GreeterDao {
void addGreet(Greeter g);
List<Greeter> getGreetings();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Repository
public class GreeterDaoImpl implements GreeterDao {
@PersistenceUnit
private EntityManagerFactory emf;
@Override
public final void addGreet(final Greeter g) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(g);
em.getTransaction().commit();
}
@Override
public final List<Greeter> getGreetings() {
return emf.createEntityManager()
.createQuery("from Greeter", Greeter.class)
.getResultList();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
@ContextConfiguration("/applicationContext.xml")
@RunWith(SpringJUnit4ClassRunner.class)
public class GreeterDaoImplIT {
@Inject
private GreeterDao testedObject;
@DirtiesContext
@Test
public void testRetrieve() {
Greeter expected = new Greeter();
expected.setGreeting("TEST");
expected.setTarget("TEST");
testedObject.addGreet(expected);
List<Greeter> actual = testedObject.getGreetings();
Iterator<Greeter> it = actual.iterator();
Greeter actualGreet =it.next();
assertThat(actualGreet.getGreeting(), is("TEST"));
assertThat(actualGreet.getTarget(), is("TEST"));
}
}
|
Ключевой частью нашего DAO класса является объект EntityManagerFactory, который Spring самостоятельно внедряет в класс, увидев аннотацию @PersistenceUnit. Аннотация поддерживает параметр name, который позволяет указать, какой-же именно из persistence units надо внедрить, если их создано несколько.
Использование JPA в приложении
Наконец, напишем сервис который будет использовать DAO, определённый выше, и делать что-нибудь с этими данными.
1
2
3
|
public interface GreeterService {
String greet();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Service
public class GreeterServiceImpl implements GreeterService {
@Inject
private GreeterDao dao;
@Override
public final String greet() {
List<Greeter> greets = dao.getGreetings();
Iterator<Greeter> it = greets.iterator();
if (!it.hasNext()) {
return "No greets";
}
Greeter greeter = it.next();
return greeter.getGreeting() + ", " + greeter.getTarget();
}
}
|
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 GreeterServiceImplTest extends EasyMockSupport {
@Rule
public EasyMockRule em = new EasyMockRule(this);
@Mock
private GreeterDao dao;
@TestSubject
private GreeterServiceImpl testedObject = new GreeterServiceImpl();
@Test
public void testNoGreets() {
expect(dao.getGreetings()).andReturn(Collections.EMPTY_LIST);
replayAll();
assertThat(testedObject.greet(), is("No greets"));
}
@Test
public void testGreets() {
Greeter expected = new Greeter();
expected.setGreeting("TEST");
expected.setTarget("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
14
|
public static void main(final String[] args) {
ApplicationContext context =
new ClassPathXmlApplicationContext("/applicationContext.xml");
GreeterService greeterService = context.getBean(GreeterService.class);
GreeterDao dao = context.getBean(GreeterDao.class);
Greeter greeter = new Greeter();
greeter.setGreeting("Hello");
greeter.setTarget("World");
dao.addGreet(greeter);
System.out.println(greeterService.greet());
}
|
1
2
3
4
5
6
|
INFO: HHH10001501: Connection obtained from JdbcConnectionAccess [org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess@6cc0bcf6] for (non-JTA) DDL execution was not in auto-commit mode; the Connection 'local transaction' will be committed and the Connection will be set into auto-commit mode.
дек 18, 2016 7:09:03 PM org.springframework.orm.jpa.LocalEntityManagerFactoryBean buildNativeEntityManagerFactory
INFO: Initialized JPA EntityManagerFactory for persistence unit 'ru.easyjava.spring.data.jpa'
дек 18, 2016 7:09:03 PM org.hibernate.hql.internal.QueryTranslatorFactoryInitiator initiateService
INFO: HHH000397: Using ASTQueryTranslatorFactory
Hello, World
|
Код примера доступен на github