Skip to content

Основы JPA

Синопсис

В прошлых постах посвященных хибернэйту Hibernate (конфигурация в стиле аннотации) и Hibernate (конфигурация spring) мы рассмотрели как поднять простое приложение которое обращается к базе данных через хибернэйт. В этом посте речь пойдет о доступе к данным через JPA. JPA можно рассматривать как следующий этап эволюции, сначала к базе данных обращались через JDBC, потом через ORM-фрэймоворки и вот теперь через JPA. В этом посте мы шаг за шагом создадим простое JPA приложение, подобное тем, которые мы создавали в предыдущих постах когда мы рассматривали хибернейт.

Похожие посты

  • Работа с базой данных через JDBC
  • Работа с базой данных через Spring
  • Hibernate (конфигурация в стиле xml)
  • Hibernate (конфигурация в стиле аннотации)
  • Hibernate (конфигурация spring)
  • JPA и Spring
  • Аудит и отслеживание версий для сущностных классов в Spring
  • Простой Spring MVC

  • Весь проект можно взять с гитхаба: 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.

    1
    Подобно тому как в основе 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

    Поделиться в социальных сетях

    Опубликовать в Google Plus
    Опубликовать в LiveJournal
    Опубликовать в Мой Мир
    Опубликовать в Одноклассники
    Опубликовать в Яндекс

    Добавить комментарий

    Ваш e-mail не будет опубликован. Обязательные поля помечены *