Синопсис
В прошлых постах посвященных хибернэйту Hibernate (конфигурация в стиле аннотации) и Hibernate (конфигурация spring) мы рассмотрели как поднять простое приложение которое обращается к базе данных через хибернэйт. В этом посте речь пойдет о доступе к данным через JPA. JPA можно рассматривать как следующий этап эволюции, сначала к базе данных обращались через JDBC, потом через ORM-фрэймоворки и вот теперь через JPA. В этом посте мы шаг за шагом создадим простое JPA приложение, подобное тем, которые мы создавали в предыдущих постах когда мы рассматривали хибернейт.
Похожие посты
Весь проект можно взять с гитхаба: https://github.com/dev-blogs/jpa под проект simpleJPA
База данных
База данных в этом примере такая же как в посте Работа с базой данных через JDBC. Там приводится ее структура и как ее поднять в MySQL. Для экономии места в этом посте описание самой базы я не привожу. Кто собирается все это дело компилировать и запускать, то убедитесь, что у вас установлена MySQL и засетаплена соответствующая база из того поста. Чтобы поднять базу данных перейдите в каталог src/main/resources/scripts и выполните из этого каталога соответствующие скрипты:
mysql -u databaseUser -p < create-data-model.sql mysql -u databaseUser -p < fill-database.sql
Что такое JPA и зачем он нужен если и так хорошо живется с hibernate
Говоря академически JPA это спецификация описанная в документе JSR-317 в котором определен общий набор интерфейсов, аннотаций и прочих служб, которые поставщики постоянства JPA (такие как hibernate) должны реализовать. Говоря человеческим языком есть интерфейсы, перечисленные в пакете javax.persistence, удовлетворяющие спецификации JPA, которые должны быть реализованы конкретным поставщиком постоянства, таким как hibernate.
Для нас же JPA это просто стандартный API-интерфейс доступа к данным. В ранних версиях джавы (даже не джавы, а спецификации JEE) когда общего стандарта доступа к данным как такового не было, приходилось обращаться к базе данных через JDBC, но JDBC все таки не стандартный метод доступа к данным с точки зрения API, так как у каждой базы данных свой драйвер, синтаксис и пр. и тогда придумали ORM-фреймворк, который уже предоставлял свой интерфейс доступа к данным абстрагируясь от вендора базы данных. Но и тут были проблемы, например если в проекте по каким-либо причинам надо было заменить ORM-фреймворк на другой у которого другой API интерфейс, то приходилось менять всю DAO-часть на API другого фрэимворка. То есть все упиралось в API, на этот раз не самой базы данных, а ORM-фреимворка, но пагромистам от этого легче не становилось, так как приходилось переписывать код поддерживающий API другого ORM-фреймворка. Вот тогда было решено сделать общий интерфейс для всех ORM-фреимворков, то есть абстрагироваться пришлось еще на один лэвэл выше, уже не от поставщика базы данных, а от поставщика ORM-фреимворка, а сам ORM-фреимворк выступает в роли лежащего в основе поставщика службы постоянства JPA, который для пограмиста становился прозрачным. В итоге этот интерфейс получил называние Java Persistence API - JPA который является частью стэка технологий JEE.
Возьмем конкретный пример. Вот представим такую ситуацию, допустим у нас есть проект где используется hibernate для доступа к данным. Потом по какой-нибудь причине было решено заменить в проекте hibernate на eclipseLink. Это будет сделать не так и просто, так как придется переделать почти весь проект, выкинуть классы Session, добавить EntityManager и в результате приложение будет делать все то же самое, вытаскивать те же данные, только делать это будет через другой фреймворк. JPA позволяет избежать этих больших изменений в проекте и абстрагироваться от используемого фреймворка. С JPA нам нужно только прописать логику доступа к данным не заботясь какой будет поставщик постоянства, то есть мы работаем с базой данных всегда через EntityManager, а поставщик постоянства может быть как hibernate так и eclipseLink так и OpenJPA или какой-нибудь другой, так как стандарт JPA позволяет переключаться между разными поставщиками постоянства JPA.
Подобно тому как в основе hibernate лежит интерфейс Session в основе JPA лежит интерфейс EntityManager. Интерфейс EntityManager поступает из фабрики EntityManagerFactory. Таким образом интерфейс EntityManagerFactory это аналог интерфейса SessionFactory в hibernate, а интерфейс EntityManager в JPA это аналог интерфейса Session в hibernate.
Структура проекта
simple-jpa ├──src │ ├─main │ │ └─java │ │ └─com │ │ └─devblogs │ │ ├─service │ │ │ ├─ItemSerivce.java │ │ │ ├─ProviderService.java │ │ │ ├─WarehouseService.java │ │ │ └─jpa │ │ │ ├─ItemSerivceImpl.java │ │ │ ├─ProviderSerivceImpl.java │ │ │ └─WarehouseSerivceImpl.java │ │ └─model │ │ ├─Item.java │ │ ├─Provider.java │ │ └─Warehouse.java │ ├─resources │ │ ├─META-INF │ │ │ └─persistence.xml │ │ ├─scripts │ │ │ ├─create-data-model.sql │ │ │ └─fill-database.sql │ │ └─log4j.properties │ └─test │ └─java │ └─com │ └─devblogs │ ├─TestItem.java │ ├─TestProvider.java │ └─TestWarehouse.java └──pom.xml
Объекты предметной области
Объекты предметной области ни чем не отличаются от тех, что используются в хибернэйте.
Item.java
package com.devblogs.model; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.Table; @Entity @Table(name = "items") public class Item { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Column(name = "name") private String name; @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}) @JoinTable(name = "items_providers", joinColumns={@JoinColumn(name = "item_id")}, inverseJoinColumns={@JoinColumn(name = "provider_id")}) private Set<Provider> providers = new HashSet<Provider>(); @ManyToOne @JoinColumn(name = "warehouse_id") private Warehouse warehouse; public Item() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<Provider> getProviders() { return providers; } public void setProviders(Set<Provider> providers) { this.providers = providers; } public Warehouse getWarehouse() { return warehouse; } public void setWarehouse(Warehouse warehouse) { this.warehouse = warehouse; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Item that = (Item) obj; if (!name.equals(that.name)) return false; return true; } @Override public String toString() { return "[id=" + this.id + ", name=" + this.name + "]"; } }
В классе Provider обратим внимание на строчки 22 - 25. Это мы определяем именованные запросы (NamedQuery) которые мы назвали Provider.findAll и Provider.findById. Сам запрос мы определяем с помощью языка запросов HQL. Именованный запрос может быть вынесен в XML файл или объявлен в сущностном классе с применением аннотации @NamedQueries. Позже, в сервисном классе, мы обратимся к одному из именованных запросов по его имени.
Provider.java
package com.devblogs.model; import java.util.HashSet; import java.util.Set; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; @Entity @Table(name = "providers") @NamedQueries({ @NamedQuery(name = "Provider.findAll", query = "select p from Provider p"), @NamedQuery(name = "Provider.findById", query = "select distinct p from Provider p where p.id = :id") }) public class Provider { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Column(name = "name") private String name; @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL}) @JoinTable(name = "items_providers", joinColumns={@JoinColumn(name = "provider_id")}, inverseJoinColumns={@JoinColumn(name = "item_id")}) private Set<Item> items = new HashSet<Item>(); public Provider() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set<Item> getItems() { return items; } public void setItems(Set<Item> items) { this.items = items; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Provider that = (Provider) obj; if (!name.equals(that.name)) return false; return true; } @Override public String toString() { return "[id=" + this.id + ", name=" + this.name + "]"; } }
Warehouse.java
package com.devblogs.model; import java.util.HashSet; import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; @Entity @Table(name = "warehouses") public class Warehouse { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Column(name = "address") private String address; @OneToMany(fetch = FetchType.LAZY, mappedBy = "warehouse") private Set<Item> items = new HashSet<Item>(); public Warehouse() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public Set<Item> getItems() { return items; } public void setItems(Set<Item> items) { this.items = items; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Warehouse that = (Warehouse) obj; if (!address.equals(that.address)) return false; return true; } @Override public String toString() { return "[id=" + this.id + ", address=" + this.address + "]"; } }
Сервисный слой. Интерфейс
ProviderService.java
package com.devblogs.service; import java.util.List; import com.devblogs.model.Provider; public interface ProviderService { public List<Provider> findAll(); public Provider findById(Long id); public Provider save(Provider provier); public void delete(Provider provider); }
Сервисный слой. Имплементация
В строчках 18 и 25 мы обращаемся к именованным запросам, которые мы определили раннее в сущностном классе.
ProviderServiceImpl.java
package com.devblogs.service.jpa; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Persistence; import javax.persistence.TypedQuery; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.devblogs.model.Provider; import com.devblogs.service.ProviderService; public class ProviderServiceImpl implements ProviderService { private Log log = LogFactory.getLog(ProviderServiceImpl.class); private EntityManager em = Persistence.createEntityManagerFactory("warehouse").createEntityManager(); public List<Provider> findAll() { em.getTransaction().begin(); List<Provider> providers = em.createNamedQuery("Provider.findAll", Provider.class).getResultList(); em.getTransaction().commit(); return providers; } public Provider findById(Long id) { em.getTransaction().begin(); TypedQuery<Provider> query = em.createNamedQuery("Provider.findById", Provider.class); query.setParameter("id", id); Provider provider = query.getSingleResult(); em.getTransaction().commit(); return provider; } public Provider save(Provider provider) { em.getTransaction().begin(); if (provider.getId() == null) { log.info("Inserting new provider"); em.persist(provider); } else { em.merge(provider); log.info("Updating existing provider"); } em.getTransaction().commit(); log.info("Provider saved with id: " + provider.getId()); return provider; } public void delete(Provider provider) { em.getTransaction().begin(); Provider mergedProvider = em.merge(provider); em.remove(mergedProvider); em.getTransaction().commit(); log.info("Provider with id: " + provider.getId() + " deleted successfully"); } }
Тесты
TestProvider.java
package com.devblogs; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.util.List; import org.junit.Before; import org.junit.Test; import com.devblogs.model.Provider; import com.devblogs.service.ProviderService; import com.devblogs.service.jpa.ProviderServiceImpl; public class TestProvider { private ProviderService providerService; @Before public void init() { providerService = new ProviderServiceImpl(); } @Test public void testFindAll() { List<Provider> providers = providerService.findAll(); Provider firstElement = providers.get(0); assertNotNull(firstElement); } @Test public void testFindById() { Provider provider = providerService.findById(1l); assertNotNull(provider); } @Test public void testSave() { Provider provider = new Provider(); provider.setName("testName"); Provider savedProvider = providerService.save(provider); Provider providerFromDb = providerService.findById(savedProvider.getId()); assertEquals(savedProvider.getName(), providerFromDb.getName()); providerService.delete(providerFromDb); } @Test(expected = javax.persistence.NoResultException.class) public void testDelete() { Provider provider = new Provider(); provider.setName("testName"); Provider savedProvider = providerService.save(provider); Provider providerFromDb = providerService.findById(savedProvider.getId()); providerService.delete(providerFromDb); providerService.findById(providerFromDb.getId()); } }
Конфигурация проекта
В файле persistence.xml мы определяем единицы постоянства. В данном случае у нас только одна единица постоянства, которую мы назовем warehouse. Файл persistence.xml находится в каталоге META-INF.
persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <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_1_0.xsd" version="1.0"> <persistence-unit name="warehouse" transaction-type="RESOURCE_LOCAL"> <provider>org.hibernate.ejb.HibernatePersistence</provider> <properties> <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver"/> <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/warehouse"/> <property name="hibernate.connection.username" value="user"/> <property name="hibernate.connection.password" value="password"/> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect"/> <property name="hibernate.hbm2ddl.auto" value="update"/> </properties> </persistence-unit> </persistence>
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>com.devblogs</groupId> <artifactId>simple-jpa</artifactId> <version>1.0.0.CI-SNAPSHOT</version> <packaging>jar</packaging> <name>Spring JPA Utility</name> <url>http://www.springframework.org</url> <properties> <maven.test.failure.ignore>true</maven.test.failure.ignore> </properties> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.6.8.Final</version> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> <version>1.0.1.Final</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-jdk14</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.35</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.7</version> <scope>test</scope> </dependency> </dependencies> </project>
Компиляция и запуск
mvn test ... ...прочий аутпут... ... ------------------------------------------------------- T E S T S ------------------------------------------------------- Running com.devblogs.TestProvider ... ...прочий аутпут... ... Results : Tests run: 4, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.107 s [INFO] Finished at: 2016-06-06T16:33:29+03:00 [INFO] Final Memory: 12M/244M [INFO] ------------------------------------------------------------------------
Линки
JPA работа с базой данных
Spring embedded database examples
http://www.vogella.com/tutorials/JavaPersistenceAPI/article.html
http://monstersandwich.blogspot.com/2009/04/jee-web-development-shopping-cart.html
Ссылки
Шпаргалка Java программиста 1: JPA и Hibernate в вопросах и ответах
JPA 2.0 with EclipseLink - Tutorial