本篇博文详细记录Spring Data JPA查询中的那些事,尤其是复杂的动态查询。
【1】规范方法查询
① 只要符号命名规范的接口都可以被正常解析使用
- 查询方法以
find|read|get
开头; - 涉及条件查询时,条件的属性用关键字连接;
- 条件属性首字母大写;
- 支持级联属性。若当前类有符合条件的属性时,优先使用而不使用级联属性。若想使用级联属性,则属性之间用
_
进行连接。
如下所示:
//根据 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> getByEmailInAndBirthLessThan(List<String> emails, Date birth);
② 支持级联查询
如User类中有属性为Address类。
User类如下:
@Entity //告诉JPA这是一个实体类(和数据表映射的类) @Table(name = "tb_user") //@Table来指定和哪个数据表对应;如果省略默认表名就是user; public class User implements Serializable{ @Id //这是一个主键 @GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键 private Integer id; @Column(name = "last_name",length = 50) //这是和数据表对应的一个列 private String lastName; @Column //省略默认列名就是属性名 private String email; @ManyToOne @JoinColumn(name = "address_id") private Address address; //... }
UserRepository中添加方法如下:
List<User> getByAddressIdGreaterThan(Integer id);
进行测试,查看控制台打印SQL:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_4_1_, user0_.email AS email2_1_, user0_.last_name AS last_nam3_1_ FROM tb_user user0_ LEFT OUTER JOIN tb_address address1_ ON user0_.address_id = address1_.id WHERE address1_.id >?
其默认使用左外连接对tb_address表进行级联查询,根据tb_address表的id进行判断。
③ 如果User中有个自身属性为addressId,怎么处理?
User如下所示:
@Entity //告诉JPA这是一个实体类(和数据表映射的类) @Table(name = "tb_user") //@Table来指定和哪个数据表对应;如果省略默认表名就是user; public class User implements Serializable{ @Id //这是一个主键 @GeneratedValue(strategy = GenerationType.IDENTITY)//自增主键 private Integer id; @Column(name = "last_name",length = 50) //这是和数据表对应的一个列 private String lastName; @Column //省略默认列名就是属性名 private String email; @ManyToOne @JoinColumn(name = "address_id") private Address address; @Column(name = "add_id") private int addressId; //... }
此时再次测试接口方法:
List<User> getByAddressIdGreaterThan(Integer id);
查看控制台打印SQL:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.add_id >?
默认直接使用tb_user表的add_id(即User的私有addressId属性)进行查询!
那么此时还想根据Address.id进行查询怎么办?
若当前类有符合条件的属性时,优先使用当前类自身属性而不使用级联属性。若想使用级联属性,则属性之间用 _
进行连接。
如下所示:
//WHERE a.id > ? List<User> getByAddress_IdGreaterThan(Integer id);
查看控制台打印SQL:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ LEFT OUTER JOIN tb_address address1_ ON user0_.address_id = address1_.id WHERE address1_.id >?
使用左外连接对tb_address表进行级联查询,根据tb_address表的id进行判断。
【2】@Query注解
如果查询接口不符合命名规范呢,如果想使用自定义查询,比如子查询呢?
上面所讲述的方法将失效,此时就要用到@Query注解,注解里面使用JPQL语言或者普通SQL查询。
① 使用JPQL
如下所示,查询id最大的用户:
@Query("select u from User u where u.id=(select max(u2.id) from User u2)") User getMaxIdPerson(Integer id);
查看控制台打印SQL如下:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.id = ( SELECT max(user1_.id) FROM tb_user user1_ )
② JPQL参数传递
怎么往@Query注解中的JPQL中传递参数呢?两种方式:索引参数和命名参数。
① 索引参数
索引参数如下所示,索引值从1开始,查询中 ”?X”
个数需要与方法定义的参数个数相一致,并且顺序也要一致。
实例如下:
@Query("select u from User u where u.lastName=?1 and u.email=?2") User testQueryAnnotationParams1(String lastName,String email);
查看控制台打印SQL如下:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.last_name =? AND user0_.email =?
② 命名参数
可以定义好参数名,赋值时采用@Param(“参数名”),而不用管顺序。推荐使用这种方式。
实例如下:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.last_name =? AND user0_.email =?
命名参数对比索引参数,其使用起来并没有什么大的差别。但是还是推荐在自定义使用JPQL查询时,使用命名参数,参数名一一对应,不容易混淆。
注意:如果使用命名参数,方法参数处必须使用@Param指定参数名!
③ Query中有like关键字
如果是 @Query 中有 LIKE 关键字,后面的参数需要前面或者后面加 %,这样在传递参数值的时候就可以不加 %:
//参数后面添加% @Query("select u from User u where u.lastName like ?1%") public List<User> findBylastName (String lastName ); //参数前面添加% @Query("select u from User u where u.lastName like %?1") public List<User> findBylastName (String lastName ); //参数前后添加% @Query("select u from User u where u.lastName like %?1%") public List<User> findBylastName (String lastName );
这里以参数后面添加%为例,查询控制台打印SQL如下所示:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.last_name LIKE ?
其实也可以不在@Query中写%,而是传参过来。但是我想,你不会喜欢在参数中添加%的!
④ Native Query
就是想使用原生SQL查询怎么做?SpringData同样支持!
可以使用@Query来指定本地查询,只要设置nativeQuery为true。
示例如下:
@Query(nativeQuery = true,value = "select count(1) from tb_user") long getTotalCount();
控制台打印SQL如下:
Hibernate: select count(1) from tb_user
【3】@Modifying 注解和事务
可以通过自定义的JPQL完成update和delete操作,JPQL不支持insert操作。
在@Query中编写JPQL语句进行update或者delete时,必须使用@Modifying注解,以通知SpringData这是一个update或者delete操作。
在update或者delete操作时,需要使用事务;此时需要在Service实现类的方法上声明事务@Transactional。
① @Query 与 @Modifying 执行更新操作
@Query 与 @Modifying 这两个 annotation一起声明,可定义个性化更新操作,例如只涉及某些字段更新时最为常用。
① 不用@Modifying执行更新
接口方法如下:
@Query("update User u set u.email = :email where u.id = :id") int updateEmailById(@Param("id") Integer id,@Param("email") String email);
尝试进行操作抛异常:
org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations [update com.jane.model.User u set u.email = :email where u.id = :id]
意思是说不支持的数据库操作,关于DML科普如下:
DML(data manipulation language)数据操纵语言:就是我们最经常用到的 SELECT、UPDATE、INSERT、DELETE。 主要用来对数据库的数据进行一些操作
.
DDL(data definition language)数据库定义语言:其实就是我们在创建表的时候用到的一些sql,比如说:CREATE、ALTER、DROP等。DDL主要是用在定义或改变表的结构,数据类型,表之间的链接和约束等初始化工作上。
.
DCL(Data Control Language)数据库控制语言:是用来设置或更改数据库用户或角色权限的语句,包括(grant,deny,revoke等)语句。
② 用@Modifying执行更新
@Modifying @Query("update User u set u.email = :email where u.id = :id") int updateEmailById(@Param("id") Integer id,@Param("email") String email);
再次测试如下:
javax.persistence.TransactionRequiredException: Executing an update/delete query
意思是说执行update或者delete操作时,必须显示声明事务!
② 事务
Spring Data 提供了默认的事务处理方式,即所有的查询均声明为只读事务。
对于自定义的方法,如需改变 Spring Data 提供的事务默认方式,可以在方法上注解 @Transactional 声明 。
进行多个 Repository 操作时,也应该使它们在同一个事务中处理,按照分层架构的思想,这部分属于业务逻辑层,因此,需要在 Service 层实现对多个 Repository 的调用,并在相应的方法上声明事务。
Service实现类如下:
@Service public class UserServiceImpl implements UserServcie{ @Autowired UserRepository userRepository; @Transactional//这里声明事务 public int updateEmailById(Integer id, String email) { System.out.println("进入Service方法。。。"); int i = userRepository.updateEmailById(id, email); return i; } }
查询控制台打印SQL如下:
Hibernate: update tb_user set email=? where id=?
在以前项目中 @Transactional一般是放在Service接口中的,并非实现类中。但是在这里放在接口中不行仍然抛出上面那个需要事务的异常。
【4】通用分页排序查询
PagingAndSortingRepository接口继承自CrudRepository,在此基础上添加了两个方法:
【4】通用分页排序查询 PagingAndSortingRepository接口继承自CrudRepository,在此基础上添加了两个方法: //根
通常我们使用Page<T> findAll(Pageable pageable);
来进行分页。
① Pageable是什么
这货是一个接口,封装了分页的相关操作,源码如下:
public interface Pageable { /** * Returns a {@link Pageable} instance representing no pagination setup. */ static Pageable unpaged() { return Unpaged.INSTANCE; } /** * Returns whether the current {@link Pageable} contains pagination information. */ default boolean isPaged() { return true; } /** * Returns whether the current {@link Pageable} does not contain pagination information. */ default boolean isUnpaged() { return !isPaged(); } //pageNumber int getPageNumber(); //pageSize int getPageSize(); /** * Returns the offset to be taken according to the underlying page and page size. */ long getOffset(); /** * Returns the sorting parameters. */ Sort getSort(); /** * Returns the current {@link Sort} or the given one if the current one is unsorted. * * @param sort must not be {@literal null}. */ default Sort getSortOr(Sort sort) { Assert.notNull(sort, "Fallback Sort must not be null!"); return getSort().isSorted() ? getSort() : sort; } /** * Returns the {@link Pageable} requesting the next {@link Page}. */ Pageable next(); /** * Returns the previous {@link Pageable} or the first {@link Pageable} if the current one already is the first one. */ Pageable previousOrFirst(); /** * Returns the {@link Pageable} requesting the first page. */ Pageable first(); /** * Returns whether there's a previous {@link Pageable} we can access from the current one. Will return * {@literal false} in case the current {@link Pageable} already refers to the first page. * * @return */ boolean hasPrevious(); /** * Returns an {@link Optional} so that it can easily be mapped on. * * @return */ default Optional<Pageable> toOptional() { return isUnpaged() ? Optional.empty() : Optional.of(this); } }
通常我们并不会实现该接口来进行分页,而是使用其实现类PageRequest来进行分页操作。
其构造函数如下:
//最简单的分页 public PageRequest(int page, int size) {} // 分页+排序 public PageRequest(int page, int size, Sort sort) {}; //分页+排序=》不使用sort,直接提供direction和properties public PageRequest(int page, int size, Direction direction, String... properties) {};
在SpringBoot2.0下,这些构造方法已经过时,SpringBoot2.0建议我们使用其静态方法创建对象:
// 最简单的分页 public static PageRequest of(int page, int size) { return of(page, size, Sort.unsorted()); } // 分页+排序 public static PageRequest of(int page, int size, Sort sort) { return new PageRequest(page, size, sort); } //分页+排序=》不使用sort,直接提供direction和properties public static PageRequest of(int page, int size, Direction direction, String... properties) { return of(page, size, Sort.by(direction, properties)); }
其实关于第三种方法,可以查看源码得知其和第二种方法并无差异!唯一的区别是使用第二种方式创建sort时可以不用指定ASC|DESC,但是使用第三种方法Direction不能为null !!
② 分页实例
Controller测试如下:
@GetMapping("/test12") public Page<User> test12(){ int page = 1;//当前页,从 0 开始。 int pageSize = 5; Pageable pageable = PageRequest.of(page,pageSize); Page<User> userPage = userRepository.findAll(pageable); return userPage; }
返回结果如下:
content里面的数据如图右侧所示。分页查询结果可谓是很详细了!!
查看控制台SQL打印:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ LIMIT ?, ? //...
底层SQL还是我们熟悉的limit !
③ 分页排序实例
功能:如按照id倒序分页。
Controller实例如下:
@GetMapping("/test13") public Page<User> test13(){ Sort sort = new Sort(Sort.Direction.DESC,"id"); int page = 1; int pageSize = 5; Pageable pageable = PageRequest.of(page,pageSize,sort); Page<User> userPage = userRepository.findAll(pageable); return userPage; }
Sort是为查询进行排序服务的,至少应该提供一个排序熟悉,默认排序为ASC,可以通过其静态内部枚举类Direction进行制定。Sort源码如下:
/** * Sort option for queries. You have to provide at least a list of properties to sort for that must not include * {@literal null} or empty strings. The direction defaults to {@link Sort#DEFAULT_DIRECTION}. * * @author Oliver Gierke * @author Thomas Darimont * @author Mark Paluch */ public class Sort implements Streamable<org.springframework.data.domain.Sort.Order>, Serializable { private static final long serialVersionUID = 5737186511678863905L; private static final Sort UNSORTED = Sort.by(new Order[0]); public static final Direction DEFAULT_DIRECTION = Direction.ASC; //... }
查看控制台SQL打印:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ ORDER BY user0_.id DESC LIMIT ?, ?
查看页面返回分页排序结果:
【5】JpaSpecificationExecutor实现带查询条件的分页排序
此时让UserRepository继承自JpaSpecificationExecutor,如下:
public interface UserRepository extends JpaRepository<User,Integer>,JpaSpecificationExecutor<User> { //... }
额外注意一点,Java中类是单继承,接口是可以多继承的。
① 实例代码
Controller实例
/** * 目标: 实现带查询条件的分页. id > 5 的条件 * * 调用 JpaSpecificationExecutor 的 Page<T> findAll(Specification<T> spec, Pageable pageable); * Specification: 封装了 JPA Criteria 查询的查询条件 * Pageable: 封装了请求分页的信息: 例如 pageNo, pageSize, Sort */ @GetMapping("/test15") public Page<User> test15(){ Sort sort = new Sort(Sort.Direction.DESC,"id"); int page = 1; int pageSize = 5; Pageable pageable = PageRequest.of(page,pageSize,sort); //通常使用 Specification 的匿名内部类 Specification<User> specification = new Specification<User>() { /** * @param *root: 代表查询的实体类. * @param query: 可以从中得到 Root 对象, * 即告知 JPA Criteria 查询要查询哪一个实体类. * 还可以来添加查询条件, 还可以结合 EntityManager 对象得到最终查询的 TypedQuery 对象. * @param *cb: CriteriaBuilder 对象. * 用于创建 Criteria 相关对象的工厂. * 当然可以从中获取到 Predicate 对象 * @return: *Predicate 类型, 代表一个查询条件. */ @Override public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) { Path path = root.get("id"); Predicate predicate = cb.gt(path, 5); return predicate; } }; Page<User> userPage = userRepository.findAll(specification, pageable); return userPage; }
页面返回结果如下所示:
其中totalElements:22;totalPages:5。说明已经过滤了id小于等于5的实体!
查看控制台打印SQL如下:
SELECT user0_.id AS id1_1_, user0_.address_id AS address_5_1_, user0_.add_id AS add_id2_1_, user0_.email AS email3_1_, user0_.last_name AS last_nam4_1_ FROM tb_user user0_ WHERE user0_.id > 5 ORDER BY user0_.id DESC LIMIT ?, ?
支持,使用JpaSpecificationExecutor完成了一个动态查询。下面详细研究一下相关联的几个接口和类。
② 实现机制
① 首先传参为Specification,其内部封装了Predicate
/** * Creates a WHERE clause for a query of the referenced entity in form of a {@link Predicate} for the given * {@link Root} and {@link CriteriaQuery}. * * @param root must not be {@literal null}. * @param query must not be {@literal null}. * @param criteriaBuilder must not be {@literal null}. * @return a {@link Predicate}, may be {@literal null}. */ @Nullable Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder); //使用给定的root,query&criteriaBuilder返回一个where子句---即一个查询条件
通常使用匿名类的方式并实现toPredicate
方法来达到实现复杂动态查询的目的。
② Root接口
/** * A root type in the from clause. * // From字句中的根类型 * Query roots always reference entities. * //查询根总是引用实体 * * @param <X> the entity type referenced by the root//X为根引用的实体类型 * @since Java Persistence 2.0 */ public interface Root<X> extends From<X, X> { /** * // 返回与根(ROOT)对应的元模型实体 * @return metamodel entity corresponding to the root */ EntityType<X> getModel(); }
可以简单理解Root为代表查询的实体类。其接口继承示意图如下:
ROOT接口实现类继承示意图如下:
③ TreatedRoot接口
TreatedRoot是RootImpl静态内部子类,如下所示;
public static class TreatedRoot<T> extends RootImpl<T> { private final RootImpl<? super T> original; private final Class<T> treatAsType; public TreatedRoot(RootImpl<? super T> original, Class<T> treatAsType) { super( original.criteriaBuilder(), original.criteriaBuilder().getEntityManagerFactory().getMetamodel().entity( treatAsType ) ); this.original = original; this.treatAsType = treatAsType; } //... }
本例中ROOT具体如下:
④ CriteriaQuery
该接口定义了一些具体的、功能性的方法(对于那些顶层查询来讲)。代表一个specific的顶层查询对象,它包含着查询的各个部分,比如:select 、from、where、group by、order by。
可以从中可到 Root 对象, 即告知 JPA Criteria 查询要查询哪一个实体类;
可以用来添加查询条件;
可以结合 EntityManager 对象得到最终查询的 TypedQuery 对象;
其主要方法如下:
本实例中CriteriaQuery如下所示:
⑤ CriteriaBuilder接口
该接口用于构造criteria queries, compound selections,expressions, predicates, orderings。
用于构造标准查询、复合条件、表达式、排序等 ;
创建 Criteria 相关对象的工厂.
可以从中获取到 Predicate 对象;
可以通过createQuery的方式获取CriteriaQuery实例
其下面有很多方法:
⑥ Predicate接口
Predicate接口(代表一个简单或复杂的查询条件)
代表Criteria查询的根对象,定义了实体类型,能为将来导航获得想要的结果。
源码如下:
/** * The type of a simple or compound predicate: a conjunction or * disjunction of restrictions.--And || OR * A simple predicate is considered to be a conjunction with a * single conjunct. * * @since Java Persistence 2.0 */ public interface Predicate extends Expression<Boolean> { public static enum BooleanOperator { AND, OR } /** * Return the boolean operator for the predicate. * If the predicate is simple, this is <code>AND</code>. * * @return boolean operator for the predicate */ BooleanOperator getOperator(); /** * Whether the predicate has been created from another * predicate by applying the <code>Predicate.not()</code> method * or the <code>CriteriaBuilder.not()</code> method. * * @return boolean indicating if the predicate is * a negated predicate */ boolean isNegated(); /** * Return the top-level conjuncts or disjuncts of the predicate. * Returns empty list if there are no top-level conjuncts or * disjuncts of the predicate. * Modifications to the list do not affect the query. * * @return list of boolean expressions forming the predicate */ List<Expression<Boolean>> getExpressions(); // 创建一个否定的(反面的)Predicate Predicate not(); }
接口继承示意图如下:
本实例中Predicate具体如下:
即 左边&操作符&右边==》leftHandSide&comparisonOperator&rightHandSide,可以把它当做一个Expression。
【6】自定义 Repository 方法
两个层次:为某一个 Repository 上添加自定义方法;为所有的 Repository 都添加自实现的方法。
① 为某一个 Repository 上添加自定义方法
步骤如下:
- 定义一个接口: 声明要添加的, 并自实现的方法
- 提供该接口的实现类: 类名需在要声明的 Repository 后添加 Impl, 并实现方法
- 声明 Repository 接口, 并继承 第一步中声明的接口
即如下图所示:
注意: 默认情况下, Spring Data 会在 base-package 中查找 “接口名Impl” 作为实现类。也可以通过repository-impl-postfix
声明后缀。
示例如下:
// 自定义接口方法 public interface UserDao { User getUserById(Integer id); } //实现类 public class UserRepositoryImpl implements UserDao { @PersistenceContext private EntityManager entityManager; public User getUserById(Integer id) { User user = entityManager.find(User.class, id); System.out.println("UserRepositoryImpl.test()"); return user; } } //声明的通用接口 public interface UserRepository extends UserDao,JpaRepository<User,Integer>, JpaSpecificationExecutor<User> { //... } //测试Controller @GetMapping("/test16") public User test16(Integer id){ User user = userRepository.getUserById(id); return user; }
查看控制台打印SQL如下:
SELECT user0_.id AS id1_1_0_, user0_.address_id AS address_5_1_0_, user0_.add_id AS add_id2_1_0_, user0_.email AS email3_1_0_, user0_.last_name AS last_nam4_1_0_, address1_.id AS id1_0_1_, address1_.city AS city2_0_1_, address1_.province AS province3_0_1_ FROM tb_user user0_ LEFT OUTER JOIN tb_address address1_ ON user0_.address_id = address1_.id WHERE user0_.id =?
正常查询id为1的User对象并返回 !
② 为所有的repository添加自定义方法
基础接口如下:
import java.io.Serializable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.NoRepositoryBean; import javax.persistence.EntityManager; import com.jane.model.Address; @NoRepositoryBean public interface CommonMethodTest<T, ID extends Serializable> extends JpaRepository<T, ID>{ Address method(); }
实现类如下:
import java.io.Serializable; import javax.persistence.EntityManager; import com.jane.model.*; import org.springframework.data.jpa.repository.support.JpaEntityInformation; import org.springframework.data.jpa.repository.support.SimpleJpaRepository; import org.springframework.data.repository.NoRepositoryBean; @NoRepositoryBean public class CommonMethodTestImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CommonMethodTest<T, ID> { private EntityManager entityManager; public CommonMethodTestImpl(Class<T> domainClass, EntityManager em) { super(domainClass, em); } public CommonMethodTestImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager em) { super(entityInformation, em); this.entityManager = em; } @Override public Address method() { Address address = entityManager.find(Address.class, 1); System.out.println("...BASE METHOD TEST..."); return address; } }
RepositoryFactoryBean如下:
import java.io.Serializable; import javax.persistence.EntityManager; import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation; import org.springframework.data.jpa.repository.support.JpaRepositoryFactory; import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; public class CommonJpaRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends JpaRepositoryFactoryBean<T, S, ID> { public CommonJpaRepositoryFactoryBean(Class<? extends T> repositoryInterface) { super(repositoryInterface); } protected RepositoryFactorySupport createRepositoryFactory( EntityManager entityManager) { return new CommonRepositoryFactory(entityManager); } private static class CommonRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory { private EntityManager entityManager; public CommonRepositoryFactory(EntityManager entityManager) { super(entityManager); this.entityManager = entityManager; } protected Object getTargetRepository(JpaMetamodelEntityInformation information) { return new CommonMethodTestImpl<T, I>(information,entityManager); } // protected Object getTargetRepository(RepositoryMetadata metadata) { // // return new CommonMethodTestImpl<T, I>( // (Class<T>) metadata.getDomainType(), entityManager); // } protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) { return CommonMethodTestImpl.class; } // protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) { // return CommonMethodTest.class; // } } }
创建一个自定义的RepositoryFactoryBean来代替默认的RepositoryFactoryBean。
RepositoryFactoryBean负责返回一个RepositoryFactory,Spring Data Jpa 将使用RepositoryFactory来创建Repository具体实现。
这里我们用CommonMethodTestImpl代替SimpleJpaRepository作为Repository接口的实现。这样我们就能够达到为所有Repository添加自定义方法的目的。
测试接口如下:
public interface AddressRepository extends CommonMethodTest<Address, Integer>{ }
主程序修改如下:
@SpringBootApplication @EntityScan(basePackages={"com.jane.model"}) @EnableJpaRepositories(basePackages = {"com.jane.dao"},repositoryFactoryBeanClass = CommonJpaRepositoryFactoryBean.class) public class JpaApplication { public static void main(String[] args) { SpringApplication.run(JpaApplication.class, args); } }
我们需要配置Jpa使用我们自定义的CommonJpaRepositoryFactoryBean。
Controller测试如下:
@RestController public class AddressController { @Autowired AddressRepository addressRepository; @GetMapping("/test17") public Address test17(){ Address address = addressRepository.method(); return address; } }
测试结果如下:
【7】懒加载异常与处理
① 传统项目下如何处理
如下示例,JPA默认使用@ManyToOne(fetch=FetchType.EAGER)
在获取User的时候会同时获取管理的Address:
@ManyToOne @JoinColumn(name = "address_id") private Address address;
当然这会带来性能上的影响,可以使用如下方式开启懒加载:
@ManyToOne(fetch=FetchType.LAZY) @JoinColumn(name = "address_id") private Address address;
此时返回结果中Address是为null,如果页面想要使用Address,将会抛出异常。
org.hibernate.LazyInitializationException:could not initialize proxy - no Session
(懒加载异常在默认情况下,hibernate为懒加载),这意味着在读取数据的时候,Session已经关闭。
这里推荐配置spring提供的OpenSessionInViewFilter的过滤器:
<filter> <!-- 配置seeion作用时间不足而导页面需要查询数据session已经关闭问题,扩大作用时间 --> <filter-name>OpenSessionInViewFilter</filter-name> <filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class> </filter> <filter-mapping> <filter-name>OpenSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
在SpringDataJPA中更推荐使用OpenEntityManagerInViewFilter来替代OpenSessionInViewFilter:
<filter> <filter-name>Spring OpenEntityManagerInViewFilter</filter-name> <filter-class>org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter</filter-class> <init-param> <!-- 指定org.springframework.orm.jpa.LocalEntityManagerFactoryBean在spring配置文件中的名称,默认值为entityManagerFactory 如果LocalEntityManagerFactoryBean在spring中的名称不是entityManagerFactory,该参数一定要指定,否则会出现找不到entityManagerFactory的例外 --> <param-name>entityManagerFactoryBeanName</param-name> <param-value>entityManagerFactory</param-value> </init-param> </filter> <filter-mapping> <filter-name>Spring OpenEntityManagerInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
② SpringBoot如何处理
SpringBoot做了一系列的自动配置,在项目启动后会自动进行初始化,如DispatcherServletAutoConfiguration、HttpEncodingAutoConfiguration等。
如果依赖中加入了其它功能的依赖,SpringBoot还会实现这些功能的自动适配,比如我们增加数据库的JPA的功能,就会启用对JpaRepositoriesAutoConfiguration的自动配置功能。
JpaRepositoriesAutoConfiguration如下:
/** * {@link EnableAutoConfiguration Auto-configuration} for Spring Data's JPA Repositories. * <p> * Activates when there is a bean of type {@link javax.sql.DataSource} configured in the * context, the Spring Data JPA * {@link org.springframework.data.jpa.repository.JpaRepository} type is on the classpath, * and there is no other, existing * {@link org.springframework.data.jpa.repository.JpaRepository} configured. * <p> * Once in effect, the auto-configuration is the equivalent of enabling JPA repositories * using the {@link org.springframework.data.jpa.repository.config.EnableJpaRepositories} * annotation. * <p> * This configuration class will activate <em>after</em> the Hibernate auto-configuration. * * @author Phillip Webb * @author Josh Long * @see EnableJpaRepositories */ @Configuration @ConditionalOnBean(DataSource.class) @ConditionalOnClass(JpaRepository.class) @ConditionalOnMissingBean({ JpaRepositoryFactoryBean.class, JpaRepositoryConfigExtension.class }) @ConditionalOnProperty(prefix = "spring.data.jpa.repositories", name = "enabled", havingValue = "true", matchIfMissing = true) @Import(JpaRepositoriesAutoConfigureRegistrar.class) @AutoConfigureAfter(HibernateJpaAutoConfiguration.class) public class JpaRepositoriesAutoConfiguration { }
具体的可以自行跟踪该类上面的注解标签,即可明白。
application.properties配置:
spring.jpa.open-in-view java.lang.Boolean Default: true Register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the thread for the entire processing of the request.
该配置会注册一个OpenEntityManagerInViewInterceptor。在处理请求时,将EntityManager 绑定到整个处理流程中(model->dao->service->controller),开启和关闭session。这样一来,就不会出现 no Session 的错误了(可以尝试将该配置的值置为 false, 就会出现懒加载的错误了。)