SpringData笔记
SpringData相关概念:
SpringData是Spring基于ORM框架、JPA规范封装的一套JPA应用框架,它提供了包括增删改查在内的常用功能,且易于扩展,可使开发者用极简的代码实现对数据库的访问和操作。
什么是JPA呢?
JPA全称Java Persistence API,是sun提出的一个对象持久化规范,各JavaEE应用服务器自主选择具体实现。JPA仅仅只是一个规范,而不是产品;使用JPA本身是不能做到持久化的。所以,JPA只是一系列定义好的持久化操作的接口,在系统中使用时,需要真正的实现者。
JPA的设计者是Hibernate框架的作者,因此Hibernate EntityManager作为Jboss服务器中JPA的默认实现;Oracle的Weblogic使用EclipseLink(以前叫TopLink)作为默认的JPA实现;IBM的Websphere和Sun的Glassfish默认使用OpenJPA(Apache的一个开源项目)作为其默认的JPA实现。
JPA的底层实现是一些流行的开源ORM(对象关系映射)框架,因此JPA其实也就是java实体对象和关系型数据库建立起映射关系,通过面向对象编程的思想操作关系型数据库的规范。
什么是ORM呢?
ORM,即Object-Relational Mapping(对象关系映射),它的作用是在关系型数据库和业务实体对象之间作一个映射,这样,我们在具体的操作业务对象的时候,就不需要再去和复杂的SQL语句打交道,只需简单的操作对象的属性和方法。
只要提供了持久化类与表的映射关系,ORM框架在运行时就能参照映射文件的信息,把对象持久化到数据库中。当前ORM框架主要有三种:Hibernate,iBATIS,EclipseLink。
SpringData提供的编程接口:
Repository:最顶层接口,是一个空接口,目的是为了统一所有的Repository的类型,且能让组件扫描的时候自动识别;
CrudRepository:提供基础的增删改查操作;
PagingAndSortingRepository:提供分页和排序的操作;
JpaRepository:增加了批量操作的功能;
JpaSpecificationExecutor :组合查询条件,提供原生SQL查询。
接口关系图如下:
下面我们讲一下SpringDataJPA与Hibernate的集成:
需要导入的包:
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.10.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.3.1.RELEASE</version><!--Spring核心代码--> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.2.6.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.9</version><!--SpringAOP要用到的包--> </dependency>
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>5.1.0.Final</version> </dependency>
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version><!--dbcp连接池--> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> |
Spring-jap.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:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:task="http://www.springframework.org/schema/task" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<context:component-scan base-package="com.firstMaven.springData"/> <context:property-placeholder location="JDBC.properties"/><!--导入JDBC.properties文件,或者下面的代码也能导入properties文件-->
<!-- spring启动时扫描项目路径下的properties文件 <bean id="propertyPlaceholderConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:db.properties</value> </list> </property> </bean>-->
<!--连接池配置--> <bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource"><!--要注意检查这里的类的路径对不对--> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean>
<!-- 定义实体管理器工厂,就跟配置sessionFactory一样--> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <!-- 指定数据源 --> <property name="dataSource" ref="dataSource"/> <!-- 指定Jpa持久化实现厂商类,这里以Hibernate为例 --> <property name="jpaVendorAdapter" ref="hibernateJpaVendorAdapter"/> <property name="packagesToScan" ><!--设置Spring去哪些包中查找@Entity的实体类 --> <array> <value>com.spring.jpa</value> </array> </property> <property name="jpaProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> </props> </property> </bean>
<!-- Hibernate对Jpa的实现 --> <bean id="hibernateJpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
<!--重要配置:启用扫描并自动创建代理的功能,base-package是定义你的DAO放在哪里--> <jpa:repositories base-package="com.firstMaven.springData.DAO" transaction-manager-ref="TxManager" entity-manager-factory-ref="entityManagerFactory"/>
<!-- Jpa事务管理器 --> <bean id="TxManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean>
<!--如下定义切面和通知--> <aop:config> <!--为Controller包下所有的类的所有方法匹配下面的通知配置--> <aop:pointcut id="Methods" expression="execution(* com.firstMaven.springData.Controller.*.*(..))"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="Methods"/> </aop:config> <tx:advice id="txAdvice" transaction-manager="TxManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="load*" propagation="REQUIRED" read-only="true"/> <tx:method name="list*" propagation="REQUIRED" read-only="true"/> </tx:attributes> </tx:advice> <!--除了以上写一大堆的配置来决定哪些类的哪些方法添加事务通知外,我们还可以用下面的代码开启注解事务: <tx:annotation-driven transaction-manager="TxManager" proxy-target-class="true" /> 这要写上这句配置,我们就能直接在方法上添加@Transaction注解来为增删改类型的方法添加事务,用@Transac tional(readOnly=true)为查询类型的方法添加事务。 --> </beans> |
DAO类的编写:
@Repository public interface UserRepositoryextends JpaRepository<User, Integer>{ /*分页查询,用的时候这么用Page<User> us = uc.find1("%狗%", new PageRequest(0, 5, new Sort(Direction.ASC, "birthday"))); * 上面的就表示将所有数据按birthday从小到大排序,再按每5条一页分隔,取出第一页的数据。如果不想排序也可以直接传入new PageRequest(0, 5) * */ Page<User> findByUsernameLike(String search, Pageablepageable); List<User> findByUsernameAndNickname(String username, String nickname); /*相当于select u from User u where u.username like ?1 and u.nickname = ?2*/ List<User> findByUsernameLikeAndNickname(String search, String nickname); /*对数据库的操作只有两类,查询数据库和修改数据库,而Modifying注解的作用就是说明下面的Query中的JPQL是用于修改数据库的,这样框架会生成一个更新操作而不是查询操作*/ @Modifying @Query("delete from User u where u.id=?1") void delete(int id); @Modifying @Query("update User u set u.username=:username where id=:id") void update(@Param("username")Stringusername, @Param("id")int id); } |
上面的UserRepository继承了JpaRepository<T, ID>,UserRepository中就自动有了基本的增删查该方法。我们还使用SpringData的查询关键字自定义了几个查询方法,SpringData的查询关键字如下:
Keyword | Sample | JPQL snippet |
And | findByLastnameAndFirstname | where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | where x.firstname = 1? |
Between | findByStartDateBetween | where x.startDate between 1? and ?2 |
LessThan | findByAgeLessThan | where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | where x.age >= ?1 |
After | findByStartDateAfter | where x.startDate > ?1 |
Before | findByStartDateBefore | where x.startDate < ?1 |
IsNull | findByAgeIsNull | where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | where x.age not null |
Like | findByFirstnameLike | where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | where x.firstname like ?1 (参数前面加%) |
EndingWith | findByFirstnameEndingWith | where x.firstname like ?1 (参数后面加%) |
Containing | findByFirstnameContaining | where x.firstname like ?1 (参数两边加%) |
OrderBy | findByAgeOrderByLastnameDesc | where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | where x.lastname <> ?1 |
In | findByAgeIn(Collection<Age> ages) | where x.age in ?1 |
NotIn | findByAgeNotIn(Collection<Age> age) | where x.age not in ?1 |
True | findByActiveTrue() | where x.active = true |
False | findByActiveFalse() | where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | where UPPER(x.firstame) = UPPER(?1) |
Specification查询:
JPA提供了基于准则的查询方式,即Criteria查询。而Spring Data JPA提供了一个Specification(规范)接口让我们可以更方便地构造准则查询,Specification接口定义了一个toPredicate方法来构造查询条件。
要想使用Specification查询,我们的UserRepository必须要继承于JpaSpecificationExecutor<User>接口,然后定义我们的Criteria查询:
public class MySpecification { public static Specification<User> FindUserByUserGroup(){ return new Specification<User>(){ @Override public Predicate toPredicate(Root<User>root, CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.equal(root.get("group"), 2);//注意,这里不能写t_user的外键字段名,只能写User类的关联属性名,不过这里的第二个参数可以直接传入int,这智能得很令人费解啊 } }; } } |
上面的MySpecification类就定义了查询“group_id为2的用户”,用的时候这么用
List<User> us = ur.findAll(MySpecification.FindUserByUserGroup()); |
我们使用Root来获得需要查询的属性,通过CriteriaBuilder构造查询条件,CriteriaBuilder、CriteriaQuery、Predicate、Root都是来自JPA接口。
CriteriaBuilder包含的条件构造有:exists,and,or,not,conjunction,disjunction,isTrue,isFalse,isNull,isNotNull,equal,notEqual,greaterThan,greaterThanOrEqualTo,lessThan,lessThanOrEqualTo,between等,详细可以查看CriteriaBuilder的API。
Spring Data JPA的各种缓存
一级缓存:
@Resource private EntityManagerFactoryemf; //这里就是注入spring-data.xml中配置的entityManagerFactory @Test public void testCache(){ EntityManager em = emf.createEntityManager(); User u = em.find(User.class, 1); User u1 = em.find(User.class, 1);em.close();//一个session里就发一条sql EntityManager em2 = emf.createEntityManager(); User u2 = em2.find(User.class, 1);em2.close(); EntityManager em3 = emf.createEntityManager(); User u3 = em3.find(User.class, 1);em3.close();//两个session就发两条sql } |
二级缓存:
要想用二级缓存,我们首先得在spring-data.xml文件的entityManagerFactory中的jpaProperties配置中添加下面的配置:
<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory </prop> <prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop> |
然后我们直接在实体类上面加@Cacheable就能实现二级缓存:
@Entity @Table(name="t_user") @Cacheable public class User |
我们之前在为Hibernate开启二级缓存的时候用到了一个配置:<property name="hibernate.cache.use_second_level_cache">true</property>,这个配置文档中是这么解释的:
Can be used to completely disable the second level cache, which is enabled by default for classes which specify a <cache> mapping.
也就是说这句配置主要不是用来开启二级缓存的,而是用来关闭二级缓存的,只要实体类打上@cache或者@cacheable注解,就自动开启二级缓存了。
上面的<prop key="javax.persistence.sharedCache.mode">ENABLE_SELECTIVE</prop>配置有以下这么几个值,文档里是这么介绍的:
The javax.persistence.sharedCache.mode property can be set to one of the following values:
ENABLE_SELECTIVE (Default and recommended value): entities are not cached unless explicitly marked as cacheable;
DISABLE_SELECTIVE:entities are cached unless explicitly marked as not cacheable;
NONE:no entity are cached even if marked as cacheable. This option can make sense to disable second-level cache altogether;
ALL:all entities are always cached even if marked as non cacheable(如果用all的话,连实体上的@cacheable都不用打,直接默认全部开启二级缓存)。
其实不加这句配置也行,如果不加,那么我们就得用@Cache注解标注需要缓存的实体类:
@Entity @Table(name="t_user") @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) public class User |
测试一下:
@Test public void testSecondCache(){ EntityManager em = emf.createEntityManager(); User u = em.find(User.class, 1); em.close(); EntityManager em2 = emf.createEntityManager(); User u2 = em2.find(User.class, 1); em2.close();//只发一条sql } |
查询缓存:
要想用查询缓存,我们必须在jpaProperties中加入如下配置语句:
<prop key="hibernate.cache.use_query_cache">true</prop> |
然后在想要开启查询缓存的方法上打上@QueryHints注解就行了:
@QueryHints({@QueryHint(name="org.hibernate.cacheable", value="true")}) List<User> findAll();
@QueryHints({@QueryHint(name="org.hibernate.cacheable", value="true")}) List<User> findByUsernameLike(String search);
@Query("select u from User u where u.id=?1") @QueryHints({@QueryHint(name="org.hibernate.cacheable", value="true")}) List<User> findByMyQuery(int id); |
测试一下:
@Test public void testQueryCache(){//就发了三条sql uc.findAll(); uc.findAll(); String search = "%狗%"; uc.findByUsernameLike(search); uc.findByUsernameLike(search); int id = 1; uc.findByMyQuery(id); uc.findByMyQuery(id); } |
缓存是针对查询操作所设置的,而查询分为两种,一种是框架自带的查询方法,例如hibernate中我们学过的session.get(User.class,1)或者session.load(User.class,1)、上面写的em.find(User.class,1)或者CrudRepository的findOne方法等,这一类方法做查询的时候会先去缓存中找目标对象,找不到再去发sql去数据库中找。一级缓存和二级缓存就是为这类方法所设置的,前者是session级别的缓存,后者是sessionFactory级别的缓存。
还有一种查询就是我们自己定义sql语句查询,例如Hibernate中的HQL查询以及上面讲过的SpringDataJPA查询关键字查询以及SpringDataJPA中的@Query查询都属于这样的自定义查询。这类查询是直接发命令给数据库,只有同时打开查询缓存和二级缓存,才能避免多次发同样的sql的情况。
框架自带的查询方法查询出来的实体能存入二级缓存,自定义Query只要查询的是实体查询结果也能存入二级缓存。查询结果存入二级缓存之后再使用框架自带的查询方法查询已存在于二级缓存中的实体就不会发sql,而再使用自定义Query查询某个实体,即使这个实体已存在于二级缓存,系统依然还会再次发sql。这时我们就可以开启查询缓存,查询缓存会储存之前通过自定义Query查出来已经存入二级缓存中的实体的id,这就相当于一个索引,此时再用自定义Query查询已查过的实体,就不会再发sql了。
针对查询缓存和二级缓存的区别,我做了如下实验:
@Test public void testQueryCache(){ uc.findAll(); int id = 1; uc.findByMyQuery(id); } | @Test public void testQueryCache(){ uc.findAll(); EntityManager em = emf.createEntityManager(); User u = em.find(User.class, 1); em.close(); } |
在查询缓存和二级缓存同时打开的情况下,左边依然要发两条sql,而右边只发了一条sql。二级缓存关闭、查询缓存打开的情况下,两边都是要发两条sql。