Skip to content

JPA и Spring

Синопсис

В прошлом посте мы рассматривали доступ к данным через JPA. В этом посте мы будем делать все тоже самое, только JPA приложение будет подниматься на спринге. Подробно об JPA было описано в прошлом посте, в этом посте только укажу на отличие обычного JPA проекта от спрингового.

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

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

  • Весь проект можно взять с гитхаба: https://github.com/dev-blogs/jpa под проект simple-spring-jpa

    Основные отличия

    Отличий в спринговом приложении будет не так уж и много. Основное отличие это в том, что вся конфигурация единицы постоянства переносится из файла persistence.xml в спринговый конфигурационный файл, но такая возможность появилась только в Spring 3.1. Согласно спецификации JPA единица постоянства должна быть определена в конфигурационном файле META-INF/persistence.xml. В прошлый раз, мы так и делали, в иксэмэль файле META-INF/persistence.xml прописывали конфигурацию единицы постоянства:

    <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>
    

    затем в коде мы вытаскивали фабрику менеджера сущностей по имени определенном в файле META-INF/persistence.xml, а из фабрики уже доставали менеджер сущности:

    EntityManagerFactory entiryManagerFactory = Persistence.createEntityManagerFactory("warehouse");
    EntityManager em = entiryManagerFactory.createEntityManager();
    

    В этот раз вместо конфигурационного файла META-INF/persistence.xml мы используем спринговый класс org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean добавив его в конфигурационный класс спринга как спринговый бин и конфигурируем его для работы с базой данных, а так же указываем лежащий в основе поставщик постоянства и некоторые другие свойства:

    <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>       
        <property name="packagesToScan" value="com.devblogs.model"/>
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
                <prop key="hibernate.max_fetch_depth">3</prop>
                <prop key="hibernate.jdbc.fetch_size">50</prop>
                <prop key="hibernate.jdbc.batch_size">10</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>       
        </property>
    </bean>
    

    База данных

    Скрипты create-data-model.sql и fill-database.sql можно взять в посте Работа с базой данных через JDBC
    Так как в этом посте будет встроенная база данных H2, то из тех скриптов нужно будет выкинуть строчки:

    CREATE DATABASE warehouse CHARACTER SET utf8;
    USE warehouse;
    

    из скрипта create-data-model.sql и строчку:

    USE warehouse;
    

    из скрипта fill-database.sql

    Структура проекта

    simple-spring-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
        │   │   ├─spring
        │   │   │   └─context.xml
        │   │   ├─scripts
        │   │   │   ├─create-data-model.sql
        │   │   │   └─fill-database.sql
        │   │   └─log4j.properties
        │   └─test
        │       └─java
        │           └─com
        │               └─devblogs
        │                   ├─TestItem.java
        │                   ├─TestProvider.java
        │                   └─TestWarehouse.java
        └──pom.xml
    

    Объекты предметной области

    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 + "]";
        }
    }
    

    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 + "]";
        }
    }
    

    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 provider);
    	public void delete(Provider provider);
    }
    

    Сервисный слой. Имплементация

    ProviderServiceImpl.java

    package com.devblogs.service.impl;
    
    import java.util.List;
    import javax.persistence.EntityManager;
    import javax.persistence.PersistenceContext;
    import javax.persistence.TypedQuery;
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import com.devblogs.model.Provider;
    import com.devblogs.service.ProviderService;
    
    @Service("providerService")
    @Transactional
    public class ProviderServiceImpl implements ProviderService {
    	private Log log = LogFactory.getLog(ProviderServiceImpl.class);
    	
    	@PersistenceContext	
    	private EntityManager em;
    
    	@Transactional(readOnly = true)
    	public List<Provider> findAll() {
    		return em.createNamedQuery("Provider.findAll", Provider.class).getResultList();
    	}
    
    	@Transactional(readOnly = true)
    	public Provider findById(Long id) {
    		TypedQuery<Provider> query = em.createNamedQuery("Provider.findById", Provider.class);
    		query.setParameter("id", id);
    		return query.getSingleResult();
    	}
    
    	@Transactional(readOnly = false)
    	public Provider save(Provider provider) {
    		if (provider.getId() == null) {
    			log.info("Inserting new provider");
    			em.persist(provider);
    		} else {
    			em.merge(provider);
    			log.info("Updating existing provider");
    		}
    		log.info("Provider saved with id: " + provider.getId());
    		return provider;
    	}
    
    	@Transactional(readOnly = false)
    	public void delete(Provider provider) {
    		Provider mergedProvider = em.merge(provider);
    		em.remove(mergedProvider);
    		log.info("Provider with id: " + provider.getId() + " deleted successfully");
    	}
    }
    

    Поскольку в проекте транзакции управляются на основе аннотаций в классе ProviderServiceImpl аннотация @Transactional применяется на уровне класса, это значит что Spring гарантирует открытие транзакции перед каждым выполнением методом класса и фиксацию транзакции после завершения метода. Этим занимается экземпляр org.springframework.orm.jpa.JpaTransactionManager из библиотеки spring-orm.

    Так же можно заметить что у некоторых методов переопределяются атрибуты транзакций по умолчанию. Методы которые не должны изменять данные в базе переводятся в режим чтения атрибутом readOnly=true. Так переопределяется транзакция которая определена на уровне класса изменяя режим с чтения-записи на режим чтения, а все остальные атрибуты остаются неизменными. Это делается для того, чтобы поставщик постоянства проводил определенную оптимизацию транзакций, предназначенных только для чтения.

    У транзакций может быть несколько атрибутов изменяя которые транзакцию можно конфигурировать (настраивать). Можно управлять распространением транзакции, тайм-аутом а так же задавать уровень изоляции. Эти параметры можно получить из интерфейса TransactionDefinition который используется в ключевом интерфейсе поддержки транзакций PlatformTransactionManager реализации которого выполняются на конкретной платформе, такой как JDBC или JPA.

    Под изоляцией понимается уровень изолированности изменения данных от других транзакций. Так вот, этот уровень можно регулировать. Например можно настроить его так, чтобы уже модифицированные данные были видны другой транзакции до того, как они за фиксировались, но такой уровень изоляции транзакцией назвать сложно, то есть транзакция никак не изолирована. На этом подробно останавливаться здесь не буду.

    Тесты

    TestProvider.java

    package com.devblogs;
    
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.assertEquals;
    import java.util.List;
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
    import com.devblogs.model.Provider;
    import com.devblogs.service.ProviderService;
    
    @ContextConfiguration("classpath:spring/context.xml")
    public class TestProvider extends AbstractJUnit4SpringContextTests {
    	@Autowired
    	private ProviderService providerService;
    	
    	@Test
    	public void testFindAll() {
    		// Вытаскиваем все объекты из базы
    		List<Provider> providers = providerService.findAll();
    		// Вытаскиваем первый элемент из результирующего набора
    		Provider firstElement = providers.get(0);
    		// Проверяем объект на существование
    		assertNotNull(firstElement);
    	}
    	
    	@Test
    	public void testFindById() {
    		// Вытаскиваем провайдер по id 1l
    		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);
    		
    		// Проверяем удаленный объект на отсутствие в базе данных.
    		// Должен быть сгенерирован эксцепшен NoResultException
    		providerService.findById(providerFromDb.getId());
    	}
    }
    

    Конфигурация проекта

    context.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    	xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    	xmlns:tx="http://www.springframework.org/schema/tx"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
    		http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.1.xsd
    		http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd
    		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd">
    
        <jdbc:embedded-database id="dataSource" type="H2">
            <jdbc:script location="classpath:scripts/create-data-model.sql"/>
            <jdbc:script location="classpath:scripts/fill-database.sql"/>    
        </jdbc:embedded-database>
    
        <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
            <property name="entityManagerFactory" ref="emf"/>
        </bean>
    
        <tx:annotation-driven transaction-manager="transactionManager" />
    
        <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
            <property name="dataSource" ref="dataSource" />
            <property name="jpaVendorAdapter">
                <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
            </property>        
            <property name="packagesToScan" value="com.devblogs.model"/>
            <property name="jpaProperties">
                <props>
                    <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
                    <prop key="hibernate.max_fetch_depth">3</prop>
                    <prop key="hibernate.jdbc.fetch_size">50</prop>
                    <prop key="hibernate.jdbc.batch_size">10</prop>
                    <prop key="hibernate.show_sql">true</prop>
                </props>        
            </property>
        </bean>
    
        <context:annotation-config/>
    
        <context:component-scan base-package="com.devblogs"/>
    </beans>
    

    Разберем файл конфигурации подробней.
    Тег jdbc:embedded-database определяет встроенную базу данных
    Тег tx:annotation-driven включает в спринге поддержку аннотаций для управления транзакциями.
    Бин org.springframework.orm.jpa.JpaTransactionManager

    log4j.properties

    log4j.rootCategory=INFO, stdout
    
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n
    
    log4j.category.org.apache.activemq=ERROR
    log4j.category.org.springframework.batch=DEBUG
    log4j.category.org.springframework.transaction=INFO
    
    log4j.category.org.hibernate.SQL=DEBUG
    # for debugging datasource initialization
    # log4j.category.test.jdbc=DEBUG
    

    pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    
    	<groupId>com.devblogs</groupId>
    	<artifactId>simple-spring-jpa</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>jar</packaging>
    
    	<name>simple-spring-jpa</name>
    	<url>http://maven.apache.org</url>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<spring.framework.version>3.1.0.RELEASE</spring.framework.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-test</artifactId>
    			<version>${spring.framework.version}</version>
    			<scope>test</scope>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-context-support</artifactId>
    			<version>${spring.framework.version}</version>
    		</dependency>
    		<dependency>
    			<groupId>org.hibernate</groupId>
    			<artifactId>hibernate-entitymanager</artifactId>
    			<version>3.6.8.Final</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-orm</artifactId>
    			<version>${spring.framework.version}</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>com.h2database</groupId>
    			<artifactId>h2</artifactId>
    			<version>1.3.156</version>
    		</dependency>
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>4.11</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] ------------------------------------------------------------------------
    

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

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

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

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