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