SpringData入门

一、SpringData概述

SpringData:Spring的一个子项目,用于简化数据库访问,支持NoSql和关系型数据库,其主要目标是使数据库的访问变得方便快捷。

SpringData项目所支持的NoSql存储:

  1. MongoDB(文档数据库)
  2. Neo4J(图形数据库)
  3. Redis(键值对存储)
  4. HBase(列族存储)

Spring项目所支持的关系型数据库:

  1. JDBC
  2. JPA

二、SpringData JPA概述

SpringData JPA致力于减少数据访问层(DAO)的开发量,开发者唯一要做的,就是声明持久层接口,其他都交给SpringData JPA来帮你完成。
框架怎么可能代替开发者实现业务逻辑呢?比如:当一个UserDao.findUserById()这样一个方法声明,大致应该能判断出这是根据给定条件的ID查询出满足条件的User对象,SpringData JPA做的便是规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。

Spring Data 包含多个子项目:

  • Commons - 提供共享的基础框架,适合各个子项目使用,支持跨数据库持久化
  • Hadoop - 基于 Spring 的 Hadoop 作业配置和一个 POJO 编程模型的 MapReduce 作业
  • Key-Value - 集成了 Redis 和 Riak ,提供多个常用场景下的简单封装
  • Document - 集成文档数据库:CouchDB 和 MongoDB 并提供基本的配置映射和资料库支持
  • Graph - 集成 Neo4j 提供强大的基于 POJO 的编程模型
  • Graph Roo AddOn - Roo support for Neo4j
  • JDBC Extensions - 支持 Oracle RAD、高级队列和高级数据类型
  • JPA - 简化创建 JPA 数据访问层和跨存储的持久层功能
  • Mapping - 基于 Grails 的提供对象映射框架,支持不同的数据库
  • Examples - 示例程序、文档和图数据库
  • Guidance - 高级文档

三、SpringData的HelloWorld。

使用SpringData JPA进行持久层开发需要的四个步骤:

  1. 配置Spring整合JPA。
  2. 在Spring配置文件中配置SpringData。让Spring为声明的接口创建代理对象。配置jpa:repositories后,Spring初始化容器时将会扫描base-package指定的包目录及其子目录,为继承Repository或其子接口的接口创建代理对象,并将代理对象注册为SpringBean,业务层便可以通过Spring自动封装的特性来直接使用该对象。
  3. 声明持久层的接口,该接口继承Repository。Repository是一个标记型接口,它不包含任何方法,如必要,SpringData可实现Repository其他子接口,其中定义了一些常用的增删改查,以及分页相关的方法。
  4. 在接口中声明需要的方法。SpringData将根据给定的策略(具体策略稍后讲解)来为其声称实现代码。

四、实现过程

1、添加Spring、Hibernate、JPA、C3P0、MySql的jar包。

2、添加Spring的配置文件。命名空间添加Beans,context,tx。

a、添加外部jdbc配置文件db.properties

jdbc.user=root
jdbc.password=root
jdbc.driverClass=com.mysql.jdbc.Driver
jdbc.jdbcUrl=jdbc:mysql:///test

b、添加Spring的配置文件。添加Spring对数据源和事务。

<?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:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
	http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context 
		http://www.springframework.org/schema/context/spring-context.xsd
		http://www.springframework.org/schema/tx 
		http://www.springframework.org/schema/tx/spring-tx.xsd">

	<!-- 1、配置数据源 -->
	<context:property-placeholder location="classpath:db.properties"/>
	
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="user" value="${jdbc.user}"></property>
		<property name="password" value="${jdbc.password}"></property>
		<property name="driverClass" value="${jdbc.driverClass}"></property>
		<property name="jdbcUrl" value="${jdbc.jdbcUrl}"></property>
	</bean>
	
	<!-- 2、配置JPA的EntityManagerFactory -->
	<bean id="entityManagerFactory" 
		class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" >
		<!-- 设置JPA的数据源 -->
		<property name="dataSource" ref="dataSource"></property>
		<!-- 设置JPA的实现类 -->
		<property name="jpaVendorAdapter">
			<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"></bean>
		</property>
		<!-- 配置jpa所要扫描的包 -->
		<property name="packagesToScan" value="com.lxit.springdata"></property>
		<!-- 配置jpa相关的属性 -->
		<property name="jpaProperties">
			<props>
				<!-- 二级缓存相关 -->
				<!--  
				<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
				<prop key="net.sf.ehcache.configurationResourceName">ehcache-hibernate.xml</prop>
				-->
				<!-- 生成的数据表的列的映射策略 -->
				<prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
				<!-- hibernate 基本属性 -->
				<prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop>
				<prop key="hibernate.show_sql">true</prop>
				<prop key="hibernate.format_sql">true</prop>
				<prop key="hibernate.hbm2ddl.auto">update</prop>
			</props>
		</property>
		
	</bean>
	
	<!-- 3、配置事务管理器 -->
	<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
		<property name="entityManagerFactory" ref="entityManagerFactory"></property>
	</bean>
	
	<!-- 4、配置支持注解的事务 -->
	<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

c、添加springdata包,并添加SpringData的配置文件。

<jpa:repositories base-package=“com.lxit.springdata” entity-manager-factory-ref=“entityManagerFactory”></jpa:repositories>
d、添加PersonRepository接口,并继承Repository接口。Repository是一个泛型接口Repository<要处理的类型,主键类型>。

import org.springframework.data.repository.Repository;

import com.lxit.springdata.entities.Person;

public interface PersonRepository extends Repository<Person, Integer>{
	//根据ID来获取Person
	Person getById(Integer id);
}
添加测试代码。
@Test
public void testPerson(){
	PersonRepository personRepository = ctx.getBean(PersonRepository.class);
		
	Person person = personRepository.getById(1);
		
	System.out.println(person);
}

直接运行测试代码,发现会抛出异常。原因是springdata-jpa必须依赖slf4jjar包。添加后springdata-jpa运行成功。控制台打印内容如下。

总结:

使用SpringData JPA极大的简化了我们对数据库的操作,使之变得非常简单。只需要添加一个接口继承Repository接口即可。并不需要提供具体的实现代码,SpringData-JPA会自动帮我们实现。

五、Repository接口

通过前面的例子,我们可以看到我们只要实现了Repository接口,那么SpringData会自动帮我们来完成对应方法的实现。
我们来看下Repository接口的特征:

  1. Repository接口本身就是一个空的接口。里面不提供任何实现。
  2. 若我们定义了接口继承了Repository,则该接口会被IOC容器识别为一个Repository Bean,纳入到IOC容器中。进而可以在该接口中定义满足一定规范的方法。
  3. 除了继承Repository接口以外,还可以通过@RepositoryDefinition注解来实现IOC容器的管理。其中domainClass表示实现具体的实体类的类型,idClass表示主键的类型。
    @RepositoryDefinition(domainClass=Person.class,idClass=Integer.class)
  4. 从IOC容器中获取出来的Repository对象,实际上获取出来的是一个代理对象。

六、Repository的子接口

基础的Repository提供了最基本的数据访问的功能,其几个子接口则扩展了一些功能。它们的继承关系如下;
Repository:仅仅是一个标识,表明任何继承它的均为仓库接口类。
CrudRepository:继承REpository,实现了一组CRUD相关的方法。
PagingAndSortingRepository:继承CrudRepository,实现一组分页排序相关的方法。
JpaRepository:继承PagingAndSortingRepository,实现一组JPA规范相关的方法。
自定义XxxxRepository:需要继承JpaRepository,这样的XxxxRepository接口具备了通用的数据访问控制层的能力。
JpaSpecificationExecutor:不属于Repository体系,实现一组JPA Criteria查询相关的方法。

七、SpringData方法定义规范。

在Repository子接口中,我们可以定义方法,但是这些方法不是我们随意定义的,必须遵守一些规范。在子接口中定义的方法,必须符合一定的命名规范,不能随意定义名称。我们来看看Repository中定义方法有哪些规范:

  1. 简单条件查询,查询某一个实体类或者集合。查询方法以find|read|get开头,涉及查询条件时,条件的属性用条件关键字连接,要注意的是,条件属性以首字母大写。
    例如:定义一个Person类
    class Person{
    private String firstName;
    private String lastName;
    }
    如果使用And条件连接时,应该写成findByLastNameAndFirstName(String lastName,String firstName);条件的属性名称与个数要与参数的位置与个数一一对应。
    支持属性的级联查询。例如:Person中包含一个Address属性,可以使用getByAddressIdGreaterThan(Integer id),这个时候会映射到Address对象属性的id属性。
    若当前类中有符合条件的属性,则优先使用,则不使用级联属性。例如:假如在Person中添加一个AddressId的属性,这个时候级联就不会映射,而会直接映射到AddressId属性中。如果想访问级联属性,可以使用“_”线进行分割。

八、SpringData中所提供的关键字。

SpringData入门
SpringData入门
直接在接口中定义查询方法,如果是符合规范的,可以不用写实现,条件中的关键字如下。
关键字示例解析出的

示例:
//根据lastname,来获取对应的Person  
Person getByLastName(String lastName);  
      
//Where lastName like ?% and id<?  
List<Person> getByLastNameStartingWithAndIdLessThan(String lastName,Integer id);  
            
//Where lastName like ?% and id<?  
List<Person> getByLastNameEndingWithAndIdLessThan(String lastName,Integer id);  
      
//where email In(?,?,?) Or birth < ?  
List<Person> getByEmailInOrBirthLessThan(List<String> emails,Date birth);  
      
//where a.id>?  
List<Person> getByAddressIdGreaterThan(Integer id);  
      
//查询id值最大的那个person  
//使用@Query注解可以自定义JPQL语句,语句可以实现更灵活的查询  
@Query("SELECT p FROM Person p WHERE p.id=(SELECT max(p2.id) FROM Person p2)")  
Person getMaxIdPerson();  
      
//为@Query注解传递参数的方式1:使用占位符  
@Query("SELECT P FROM Person P where P.lastName=?1 AND P.email=?2")  
List<Person> testQueryAnnotationParams1(String lastName,String email);  

查询方法的解析流程:
加入创建如下查询:findByUserDepUuid(),框架在解析该方法时,首先剔除findBy,然后剩下对属性的解析,假设查询实体为Doc,先判断userDepUuid(根据POJO规范,首字母变为小写)是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性则继续第二步。

从右向左截取第一个大写字母开头的字符串(此处为Uuid),然后检查剩下的字符串是否为查询实体的一个属性,如果是,则表示根据该属性进行查询;如果没有该属性,则重复第二步,继续从右向左截取;最后假设user为查询实体的一个属性;

接着处理剩下部分(DepUuid),先判断user所哦对应的类型是否有depUuid属性,如果有则表示该方法最终是根据"Doc.user.depUuid"的取值进行查询;否则继续按照步骤2的规则从右往左截取,最终表示根据“Doc.user.dep.uuid"的值进行查询。

可能会存在一种特殊情况,比如Doc包含一个user的属性,也有一个userDep属性,此时会存在混淆。可以明确在属性之间加上"_“以显示表达意图,比如"findByUser_DepUuid()“或者"findByUserDep_uuid()”。

  • 特殊参数还可以直接在方法的参数上加入分页或者排序的参数。比如:
Page<UserModel> findByName(String name,Pageable pageable);
List<UserModel> findByName(String name,Sort sort);

由于这种写法对于不够灵活,而且非常容易写错,所以在实际开发中不是特别推荐使用这种写法。

九、@Query注解

查询方法不是万能的,再有些情况下,查询方法不能完全实现所有的SQL查询,例如子查询等。所以我们还是需要来编写JPQL语句,SpringData中给我们提供了@Query注解,可以帮助我们来编写JPQL语句。

1、不带参数的@Query注解:
示例:查找最大值得Person。
//获取id最大的Person信息
@Query("SELECT p FROM Person p WHERE p.id = (SELECT max(p2.id) from Person p2)")
Person getMaxIdPerson();

//测试代码
Person person = personRepository.getMaxIdPerson();
System.out.println(person);


2、带参数的@Query注解:
a.以占位符的方式,顺序必须和参数的顺序一致。
示例:查询用户名以a开头并且年龄大于18的Person。
@Query("SELECT p FROM Person p WHERE p.lastName like ?1 AND p.age > ?2")
List<Person> testQueryAnnotationParams1(String lastName,Integer age);

//测试代码
List<Person> list = personRepository.testQueryAnnotationParams1("a%", 18);


3、带参数的@Query注解:
a.以命名参数的方式,参数可以通过名称指定。
示例:查询用户名以a开头并且年龄大于18的Person。
@Query("SELECT p FROM Person p WHERE p.lastName like :lastName AND p.age > :age")
List<Person> testQueryAnnotationParams2(@Param("lastName")String lastName,@Param("age")Integer age);

//测试代码
List<Person> list = personRepository.testQueryAnnotationParams1("a%", 18);
@Query注解中,在进行Like模糊查询时,可以直接将%写在JPQL语句中。
@Query("SELECT p FROM Person p WHERE p.lastName like %?1%")
List<Person> testQueryAnnotationLikeParam(String lastName);

//测试代码
List<Person> list = personRepository.testQueryAnnotationLikeParam("a");


4、本地SQL查询。
@Query注解中也可以支持原生的SQL语句。只需要设置nativeQuery=true即可。
@Query(value="SELECT * from person",nativeQuery=true)
List<Person> testSql2();

//测试代码
List<Person> list = personRepository.testSql2();
for (Person person : list) {
	System.out.println(person);
}

十、@Modifying注解和事务

前面我们基本都是在执行查询操作,@Query也可以做修改和删除的操作,不支持增加。
使用@Modifying实现更新邮箱操作。

@Modifying
@Query("UPDATE Person p set p.email = :email WHERE p.id = :id")
public void updatePersonEmail(@Param("id")Integer id,@Param("email")String email);

注意:如果这里不加@Modifying会直接抛出异常QueryExecutionRequestException,表示@Query不支持DML操作。所以必须添加@Modifying注解。
添加了@Modifying后,还是会抛出异常TransactionRequiredException,表示没有开启事务。所以我们必须添加事务处理。
默认情况下SpringData的每个方法上都有事务,但都是一个只读事务。他们不能完成修改的操作。

这里添加PersonService层,用来处理事务。

@Service
public class PersonService {
	
	@Autowired
	private PersonRepository personRepository;
	
	@Transactional
	public void updatePersonEmail(Integer id,String email){
		personRepository.updatePersonEmail(id, email);
	}
}

在spring.xml中添加自动扫描的包。

<!-- 自动扫描的包 -->
<context:component-scan base-package="com.lxit.springdata"></context:component-scan>

调用的测试代码

PersonService personService = ctx.getBean(PersonService.class);
personService.updatePersonEmail(1, "[email protected]");