起底spring data JPA全部增删改查(CRUD)方式
前言
本文会按照封装的等级的高低,介绍spring data JPA为数据库基本操作(增删改查,CRUD)提供的所有方式。如果读者需要手撸一遍,手边又没有现成项目能够上手,参考spring data JPA的简单入门可以帮助你搭建一个简单的demo。本文同时也是spring data JPA原理解析的前奏,后续作者会陆续对本文提到的各种查询方式进行代码原理分析,感兴趣的话可以后续关注。
Repository方式
一个基本的实体(对应数据库表)需要一个对应的Repository。在spring data JPA的简单入门中构造了一个接口SongRepository,继承自JpaRepository。
标准方法
父接口JpaRepository提供了一系列默认的CRUD方法,即使子接口不声明任何其他方法,也可以使用父接口的基本方法完成查询。
![起底spring data JPA全部增删改查(CRUD)方式-JpaRepository.png](https://i-blog.csdnimg.cn/blog_migrate/7df590e363621d168cbeb7c6207f024e.png)
JpaRepository提供了如下方法:
//查询所有记录实体
List<T> findAll();
//查询所有记录实体,按sort排序
List<T> findAll(Sort sort);
//查询ids的记录实体
List<T> findAllById(Iterable<ID> ids);
//写入或者更新entities中包含的所有实体
<S extends T> List<S> saveAll(Iterable<S> entities);
//刷新
void flush();
//写入或更新并刷新
<S extends T> S saveAndFlush(S entity);
//删除entities包含的所有实体
void deleteInBatch(Iterable<T> entities);
//删除所有实体
void deleteAllInBatch();
//查询该id的实体记录
T getOne(ID id);
//查询所有符合example条件的实体
@Override
<S extends T> List<S> findAll(Example<S> example);
//查询所有符合example条件的实体,并排序
@Override
<S extends T> List<S> findAll(Example<S> example, Sort sort);
PagingAndSortingRepository主要处理分页查询,主要提供了:
Page<T> findAll(Pageable pageable);
方法返回分页查询的结果。
QueryByExampleExecutor提供了条件查询的功能,可以构造Example作为查询条件,从而支持更丰富的条件查询策略。
<S extends T> Optional<S> findOne(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example);
<S extends T> Iterable<S> findAll(Example<S> example, Sort sort);
<S extends T> Page<S> findAll(Example<S> example, Pageable pageable);
<S extends T> long count(Example<S> example);
<S extends T> boolean exists(Example<S> example);
example的使用分为三步,1. 构造示例, 2.构造匹配器,3.再基于匹配器创建实例:
1. 构造示例
Song song = new Song();
song.setName("MyHeart");
2.构造匹配器。这里取name中包含"MyHeart"
ExampleMatcher exampleMatcher = ExampleMatcher.matching()
.withMatcher("name", ExampleMatcher.GenericPropertyMatchers.contains());
3.创建example
Example example = Example.of(song,exampleMatcher);
List<Song> songs = songRepository.findAll(example);
CrudRepository正如其名所示,主要包含比较全的Crud操作方法:
//写入或者更新实体
<S extends T> S save(S entity);
//写入或者更新实体
<S extends T> Iterable<S> saveAll(Iterable<S> entities);
//通过主键id查询实体
Optional<T> findById(ID id);
//通过主键id判断实体
boolean existsById(ID id);
//查询所有实体记录
Iterable<T> findAll();
//查询ids的所有实体记录
Iterable<T> findAllById(Iterable<ID> ids);
//查询所有记录数
long count();
//通过id删除实体记录
void deleteById(ID id);
//删除实体
void delete(T entity);
//删除entities包含的所有实体
void deleteAll(Iterable<? extends T> entities);
//删除所有
void deleteAll();
这里提前埋个伏笔。Repository的save方式同时应插入和更新两种操作,即框架内部会先查询是否已经存在该实体,如果存在则更新,如果不存在则插入(这是很多人遇到的save性能问题的第一个原因)。同时,save方法和delete方法一样,spring data jpa内部执行时,并不会立即操作数据库,至于为什么,可以参考下文“Entity管理方式”中的介绍。
自定义方法
除了使用父接口中提供的查询方法,还可以遵循spring data jpa的Repository方法定义规则,在接口中自定义方法。这种自定义方法类似于上文中findById形式,By后的参数限制查询条件,通过方法传参传入查询条件。
Song queryByIdAndYear(Long id, Long year);
List<Song> getByNameLikeOrYear(String name, Long year);
List<Song> findByYearAndLength(Long year, Long length);
void deleteByIdOrNameLike(Long id,String name);
上面示例中使用的query、get、find关键词同等代表sql下“select”的意思。后面的Id、Year、Length等关键词,是jpa通过分析实例中Song这个Entity实体得到的。还有其他条件参数关键词,当使用 spring data JPA的简单入门中的方式配置idea,可以在代码编写时自动提示,还是挺方便的。
关键词 | 示例 | sql解释 |
---|---|---|
And | findByLastnameAndFirstname | where x.lastname=?1 and x.firstname=?2 |
Or | findByLastnameOrFirstname | where x.lastname=?1 or x.firstname=?2 |
Between | findByStartDateBetween | where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | where x.startDate < ?1 |
GreaterThan | findByAgeGreaterThan | where x.startDate >?1 |
After | findByStartDateAfter | where x.startDate >n ?1 |
Before | findByStartDateBefore | where x.startDate < ?1 |
IsNull | findByAgeIsNull | where x.age is null |
(Is)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 | findByFirstnameStartingWithXXX | where x.firstname like ?1(parameter bound with appended %) |
EndingWith | findByFirstnameEndingWithXXX | where x.firstname like ?1(parameter bound with appended %) |
Containing | findByFirstnameContaining | where x.firstname like ?1(parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastname | where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | where x.lastname <> ?1 |
NotIn | findByAgeNotIn(Collection age ) | where x.age not in ?1 |
True | findByActiveTrue() | where x.active = true |
False | findByActiveFalse() | where x.active = false |
上表中列出了大部分自定义Repository方法的关键词。如果仔细分析,会发现和sql的条件关键词大差不差,spring data jpa是把关键词糅合在了方法中,从而替代sql的书写。
spring data jpa的Repository这种形式存在两个问题:
- 复杂的sql难以通过这种方式实现
- 不同情景下的查询,Repository方法难以复用。一般来说每次都需要构造一个新的方法。
CriteriaQuery方式
spring data jpa也提供CriteriaQuery的方式构造查询语句,然后使用EntityManager处理后,可以获取查询的返回结果。CriteriaQuery的查询主要需要四步:
- 使用EntityManager构造CriteriaBuilder;
- 使用CriteriaBuilder构造CriteriaQuery;
- 使用CriteriaBuilder和CriteriaQuery构造查询条件.
- 使用EntityManager处理,获取查询结果。
示例如下:
//构造CriteriaBuilder
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
//构造CriteriaQuery,绑定到Song实体
CriteriaQuery<Song> query= builder.createQuery(Song.class);
//构造查询条件
Root<Song> root = query.from(Song.class);
query.select(root);
query.where(builder.like(root.get("name"),"MyHeart"));
//使用EntityManager处理,获取查询结果
TypedQuery<Song> typedQuery = entityManager.createQuery(query);
List<Song> songs = typedQuery.getResultList();
其实在我看来,持久层框架的作用在于方便开发者使用数据库查询,屏蔽数据库使用的大多数细节,这样就要求在设计上抽象出更加简便的模型,取代原有数据库sql的模式,比如Repository就是一种尝试,这样无疑会导致丢失一部分原有的数据库查询的灵活性,但可以通过补丁来弥补。
Criteria模式不想放弃原有数据库查询的灵活性,在细节屏蔽上就无法做到足够好。因此在查询时,构造查询条件复杂繁琐,本质上是使用一种复杂的查询取代了另一种复杂繁琐的查询,学习成本也较高。当然Criteria模式也有其优点,实事求是来说,它确实屏蔽了数据库的sql,起到了代理的作用,从而使应用层和具体数据库无关。另一方面,这种方式能够全面的描述查询中的要素,所以spring data jpa的Repository查询内部实现会使用Criteria作为语义的中转层。
Entity管理方式
我在java数据库持久层框架基础:为什么不是JPA里介绍JPA的Entity思想,这里为了说明EntityManager中为Entity管理提供的方法,还是要把之前的图搬过来,详细解释可以参考上文实体相关部分。
图中的em,即EntityManager,在上文Criteria中出现过,后面还会频繁出现。顾名思义,EntityManager主要任务是作实体管理的,因此上图中的所有实体管理方法都在spring data jpa实现了,它们的功能也像java数据库持久层框架基础:为什么不是JPA里介绍的一样。我这里主要以使用最多的两个方法为例。
- persist()
//构造新的实体
Song song = new Song();
song.setName("Journey");
song.setLength(230);
song.setYear(2020);
entityManager.persist(song); //持久化实体,相当于插入操作
- merge()
//获取已有实体
Song song = songRepository.findById(20L);
song.setName("Journey");
song.setLength(230);
song.setYear(2020);
//持久化游离实体,相当于更新操作
entityManager.merge(song);
注:配置好spring boot环境的EntityManager可以通过注入获取
需要注意的是,persist()操作和merge()虽然表面上对应数据库查询过程中的插入和更新操作,但实体操作和数据库操作还是存在差别。主要原因是,spring data jpa在内部实现insert、update、delete操作时,为了提升性能,使用了缓存机制。应用在使用save、delete、persist、merge这些操作时,并不会立即在数据库层面执行sql,而是当框架自动调用flush或者因为查询而调用flush时,数据实际才被写入或者更改、删除。关于这部分,spring data jpa的具体实现逻辑,是属于更深层的原理实现问题,会在后续文章讨论到。
JPQL(HQL)方式
JPQL是类SQL的一种概念,设计之初的基本指导思想,是将SQL的关系数据转换为POJO,这样从数据模型完成了关系型数据到java类对象的转换。其他部分的基本语法和sql比较相像。因为spring data jpa底层使用的Hibernate核心代码,我这里看JPQL和HQL基本没有什么差别。
//JPQL查询语句示例
select s as ss from Song s;
//JPQL更新语句示例
update Song s set s.length = 120 where s.id = 1;
//JPQL删除语句示例
delete from Song s where s.id = 1;
以上是JPQL的使用示例。整体来看,所有数据相关的都可以使用实体类取代表、实体类字段取代具体表字段,通过内部实现java类对象和表的映射,完成了应用层数据和数据库底层数据的隔离。
spring data jpa 使用JPQL有两种方式,一种是使用EntityManager构造Query:
Query query = entityManager.createQuery("select s as ss from Song s");
List<Song> songs = query.getResultList();
另一种是在Repository的接口方法上使用@Query注释:
@Query("select s from Song s where s.name like (?1)")
List<Song> getSongs(String name);
这样定义的结果是,接口方法的参数name取代占位符"?1",完成整个JPQL的构造,查询结果以getSongs方法返回结果的形式返回。对于update和delete操作,必须要定义事务@Transactional和更改标识@Modifying。
@Transactional
@Modifying
@Query("delete from Song s where s.id = ?1")
void deleteBysongId(Long id);
@Transactional
@Modifying
@Query("update Song s set s.length = :length where s.id =:id")
void updateSong(@Param("length") int length, @Param("id") Long id);
@Transactional可以注解在Repository的接口方法上,也可以注解在其他使用了spring data jpa查询的普通方法上,代表整个方法中执行的数据库操作是一个事务整体。
原生SQL方式
使用原生SQL的查询方式和使用JPQL的方式比较相像,也是有用EntityManager构造Query的方法或者使用@Query注解的方法。
EntityManager的createNativeQuery方法用来实现原生sql Query语句,本质只是将原生sql存储,最后使用jdbc连接数据库执行。
Query nativeQuery = entityManager.createNativeQuery("select * from song s where name LIKE 'MyHeart'");
List<Song> songs = nativeQuery.getResultList();
@Query注解方式同样是在Repository接口方法上使用,只是需要添加nativeQuery = true参数。这里以JPQL不能支持的insert语句为例。
@Transactional
@Modifying
@Query(value = "insert into song value ( ?1, ?2, ?3, ?4, ?5)",nativeQuery = true)
void insertSong( Long id,String name, Integer year, Integer length,String type);
总结
对于数据库的操作,spring data jpa提供了多个层次、多种形式的方式,从Repository、CriteriaQuery 到JPQL、原生SQL。这样顺序实际是按照封装和抽象的从高级到低级的逻辑介绍的,在spring data jpa data内部的实现中,也是基本按照这样的顺序实现转化,最终到数据库的连接和操作执行。对于使用方式的深入了解,也有助于对于内部实现的学习和理解。反过来,对内部实现深入理解,也能进一步帮助开发者使用和开发工具。