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

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解释
AndfindByLastnameAndFirstnamewhere x.lastname=?1 and x.firstname=?2
OrfindByLastnameOrFirstnamewhere x.lastname=?1 or x.firstname=?2
BetweenfindByStartDateBetweenwhere x.startDate between ?1 and ?2
LessThanfindByAgeLessThanwhere x.startDate < ?1
GreaterThanfindByAgeGreaterThanwhere x.startDate >?1
AfterfindByStartDateAfterwhere x.startDate >n ?1
BeforefindByStartDateBeforewhere x.startDate < ?1
IsNullfindByAgeIsNullwhere x.age is null
(Is)NotNullfindByAge(Is)NotNullwhere x.age not null
LikefindByFirstnameLikewhere x.firstname like ?1
notLikefindByFirstnameNotLikewhere x.firstname not like ?1
StartingWithfindByFirstnameStartingWithXXXwhere x.firstname like ?1(parameter bound with appended %)
EndingWithfindByFirstnameEndingWithXXXwhere x.firstname like ?1(parameter bound with appended %)
ContainingfindByFirstnameContainingwhere x.firstname like ?1(parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnamewhere x.age = ?1 order by x.lastname desc
NotfindByLastnameNotwhere x.lastname <> ?1
NotInfindByAgeNotIn(Collection age )where x.age not in ?1
TruefindByActiveTrue()where x.active = true
FalsefindByActiveFalse()where x.active = false

    上表中列出了大部分自定义Repository方法的关键词。如果仔细分析,会发现和sql的条件关键词大差不差,spring data jpa是把关键词糅合在了方法中,从而替代sql的书写。

    spring data jpa的Repository这种形式存在两个问题:

  1. 复杂的sql难以通过这种方式实现
  2. 不同情景下的查询,Repository方法难以复用。一般来说每次都需要构造一个新的方法。

CriteriaQuery方式

    spring data jpa也提供CriteriaQuery的方式构造查询语句,然后使用EntityManager处理后,可以获取查询的返回结果。CriteriaQuery的查询主要需要四步:

  1. 使用EntityManager构造CriteriaBuilder;
  2. 使用CriteriaBuilder构造CriteriaQuery;
  3. 使用CriteriaBuilder和CriteriaQuery构造查询条件.
  4. 使用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管理提供的方法,还是要把之前的图搬过来,详细解释可以参考上文实体相关部分。
java数据库持久层框架基础:为什么不是JPA?-entity-state.png

    图中的em,即EntityManager,在上文Criteria中出现过,后面还会频繁出现。顾名思义,EntityManager主要任务是作实体管理的,因此上图中的所有实体管理方法都在spring data jpa实现了,它们的功能也像java数据库持久层框架基础:为什么不是JPA里介绍的一样。我这里主要以使用最多的两个方法为例。

  1. persist()
//构造新的实体
Song song = new Song();
song.setName("Journey");
song.setLength(230);
song.setYear(2020);
entityManager.persist(song); //持久化实体,相当于插入操作
  1. 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内部的实现中,也是基本按照这样的顺序实现转化,最终到数据库的连接和操作执行。对于使用方式的深入了解,也有助于对于内部实现的学习和理解。反过来,对内部实现深入理解,也能进一步帮助开发者使用和开发工具。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值