Spring Data JPA 是 Spring Boot 体系中约定优于配置的最佳实现,大大简化了项目中数据库的操作
JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供了一种对象 / 关联映射工具来管理 Java 应用中的关系数据。它的出现主要是为了简化现有的持久化开发工作和整合 ORM 技术,结束现在 Hibernate、TopLink、JDO 等 ORM 框架各自为营的局面。
值得注意的是,JPA 是在充分吸收了现有的 Hibernate、TopLink、JDO 等 ORM 框架的基础上发展而来的,具有易于使用、伸缩性强等优点。从目前的开发社区的反应上看,JPA 受到了极大的支持和赞扬,其中就包括了 Spring 与 EJB 3.0 的开发团队。
注意:JPA 是一套规范,不是一套产品,那么像 Hibernate、TopLink、JDO 它们是一套产品,如果说这些产品实现了这个 JPA 规范,那么我们就可以称他们为 JPA 的实现产品。
Spring Data JPA
Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,可以让开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增、删、改、查等在内的常用功能,且易于扩展,学习并使用 Spring Data JPA 可以极大提高开发效率。Spring Data JPA 其实就是 Spring 基于 Hibernate 之上构建的 JPA 使用解决方案,方便在 Spring Boot 项目中使用 JPA 技术。
Spring Data JPA 让我们解脱了 DAO 层的操作,基本上所有 CRUD 都可以依赖于它实现。
添加依赖
org.Springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
添加配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#SQL 输出
spring.jpa.show-sql=true
#format 一下 SQL 进行输出
spring.jpa.properties.hibernate.format_sql=true
hibernate.hbm2ddl.auto 参数的作用主要用于:自动创建、更新、验证数据库表结构,有四个值。
其中:
实体类
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private String userName;
@Column(nullable = false)
private String passWord;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = true, unique = true)
private String nickName;
@Column(nullable = false)
private String regTime;
//省略 getter settet 方法、构造方法
}
下面对上面用的注解做一个解释。
@Entity(name="EntityName")
必须,用来标注一个数据库对应的实体,数据库中创建的表名默认和类名一致。其中,name 为可选,对应数据库中一个表,使用此注解标记 Pojo 是一个 JPA 实体。@Table(name="",catalog="",schema="")
可选,用来标注一个数据库对应的实体,数据库中创建的表名默认和类名一致。通常和 @Entity 配合使用,只能标注在实体的 class 定义处,表示实体对应的数据库表的信息。@Id
必须,@Id 定义了映射到数据库表的主键的属性,一个实体只能有一个属性被映射为主键。@GeneratedValue(strategy=GenerationType,generator="")
可选,strategy: 表示主键生成策略,有 AUTO、INDENTITY、SEQUENCE 和 TABLE 4 种,分别表示让 ORM 框架自动选择,generator: 表示主键生成器的名称。@Column(name = "user_code", nullable = false, length=32)
可选,@Column 描述了数据库表中该字段的详细定义,这对于根据 JPA 注解生成数据库表结构的工具。name: 表示数据库表中该字段的名称,默认情形属性名称一致;nullable: 表示该字段是否允许为 null,默认为 true;unique: 表示该字段是否是唯一标识,默认为 false;length: 表示该字段的大小,仅对 String 类型的字段有效。@Transient
可选,@Transient 表示该属性并非一个到数据库表的字段的映射,ORM 框架将忽略该属性。@Enumerated
可选,使用枚举的时候,我们希望数据库中存储的是枚举对应的 String 类型,而不是枚举的索引值,需要在属性上面添加 @Enumerated(EnumType.STRING) 注解。Repository 构建
创建的 Repository 只要继承 JpaRepository 即可,就会帮我们自动生成很多内置方法。另外还有一个功能非常实用,可以根据方法名自动生产 SQL,比如 findByUserName 会自动生产一个以 userName 为参数的查询方法,比如 findAll 会自动查询表里面的所有数据等。
public interface UserRepository extends JpaRepository {
User findByUserName(String userName);
User findByUserNameOrEmail(String username,String email);
}
我们只需要在对应的 Repository 中创建好方法,使用的时候直接将接口注入到类中调用即可。在 IDEA 中打开类 UserRepository,在这个类的大括号内的区域右键单击,选择 Diagrams | Show Diagram 选项,即可打开类图,如下:
通过上图我们发现 JpaRepository 继承 PagingAndSortingRepository 和 QueryByExampleExecutor,PagingAndSortingRepository 类主要负责排序和分页内容,QueryByExampleExecutor 提供了很多示例的查询方法,如下:
public interface QueryByExampleExecutor {
S findOne(Example example); //根据“实例”查找一个对象
Iterable findAll(Example example); //根据“实例”查找一批对象
Iterable findAll(Example example, Sort sort); //根据“实例”查找一批对象,且排序
Page findAll(Example example, Pageable pageable); //根据“实例”查找一批对象,且排序和分页
long count(Example example); //根据“实例”查找,返回符合条件的对象个数
boolean exists(Example example); //根据“实例”判断是否有符合条件的对象
}
因此,继承 JpaRepository 的会自动拥有上述这些方法和排序、分页功能。查看源码我们发现 PagingAndSortingRepository 又继承了 CrudRepository。CrudRepository 的源码如下:
@NoRepositoryBean
public interface CrudRepository extends Repository {
S save(S entity);
Iterable saveAll(Iterable entities);
Optional findById(ID id);
boolean existsById(ID id);
Iterable findAll();
Iterable findAllById(Iterable ids);
long count();
void deleteById(ID id);
void delete(T entity);
void deleteAll(Iterable extends T> entities);
void deleteAll();
}
从 CrudRepository 的源码可以看出 CrudRepository 内置了我们最常用的增、删、改、查的方法,方便我们去使用,因为 JpaRepository 继承了 PagingAndSortingRepository,PagingAndSortingRepository 继承了 CrudRepository,所以继承 JpaRepository 的类也默认拥有了上述方法。
因此使用 JPA 操作数据库时,只需要构建的 Repository 继承了 JpaRepository,就会拥有了很多常用的数据库操作方法。
测试
创建好 UserRepository 之后,当业务代码中需要使用时直接将此接口注入到对应的类中,在 Spring Boot 启动时,会自动根据注解内容创建实现类并注入到目标类中。
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTests {
@Resource
private UserRepository userRepository;
@Test
public void test() {
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
String formattedDate = dateFormat.format(date);
userRepository.save(new User("aa", "[email protected]", "aa", "aa123456",formattedDate));
userRepository.save(new User("bb", "[email protected]", "bb", "bb123456",formattedDate));
userRepository.save(new User("cc", "[email protected]", "cc", "cc123456",formattedDate));
Assert.assertEquals(9, userRepository.findAll().size());
Assert.assertEquals("bb", userRepository.findByUserNameOrEmail("bb", "[email protected]").getNickName());
userRepository.delete(userRepository.findByUserName("aa1"));
}
}
上述测试方法简单测试了 JPA 的报错和查询功能,测试用例执行成功表示 JPA 的增、删、改成功。
基本查询
我们可以将 Spring Data JPA 查询分为两种,一种是 Spring Data JPA 默认实现的,另一种是需要根据查询的情况来自行构建。
预生成方法
预生成方法就是我们上面看到的那些方法,因为继承了 JpaRepository 而拥有了父类的这些内容。
(1)继承 JpaRepository
public interface UserRepository extends JpaRepository {
}
(2)使用默认方法
@Test
public void testBaseQuery() {
userRepository.findAll();
userRepository.findById(1l);
userRepository.save(user);
userRepository.delete(user);
userRepository.count();
userRepository.existsById(1l);
// ...
}
所有父类拥有的方法都可以直接调用,根据方法名也可以看出它的含义。
自定义查询
Spring Data JPA 可以根据接口方法名来实现数据库操作,主要的语法是 findXXBy、readAXXBy、queryXXBy、countXXBy、getXXBy 后面跟属性名称,利用这个功能仅需要在定义的 Repository 中添加对应的方法名即可,使用时 Spring Boot 会自动帮我们实现,示例如下。
根据用户名查询用户:
User findByUserName(String userName);
也可以加一些关键字 And、or:
User findByUserNameOrEmail(String username, String email);
修改、删除、统计也是类似语法:
Long deleteById(Long id);
Long countByUserName(String userName)
基本上 SQL 体系中的关键词都可以使用,如 LIKE 、IgnoreCase、OrderBy:
List findByEmailLike(String email);
User findByUserNameIgnoreCase(String userName);
List findByUserNameOrderByEmailDesc(String email);
可以根据查询的条件不断地添加和拼接,Spring Boot 都可以正确解析和执行,其他使用示例可以参考下表。
具体的关键字,使用方法和生产成 SQL 如下表所示
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 | 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 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection |
… where x.age in ?1 |
NotIn | findByAgeNotIn(Collection |
… 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) |
使用 Spring Data 大部分的 SQL 都可以根据方法名定义的方式来实现,但是由于某些原因必须使用自定义的 SQL 来查询,Spring Data 也可以完美支持。
在 SQL 的查询方法上面使用 @Query 注解,在注解内写 Hql 来查询内容。
@Query("select u from User u")
Page findALL(Pageable pageable);
当然如果感觉使用原生 SQL 更习惯,它也是支持的,需要再添加一个参数 nativeQuery = true。
@Query("select * from user u where u.nick_name = ?1", nativeQuery = true)
Page findByNickName(String nickName, Pageable pageable);
@Query 上面的 1 代表的是方法参数里面的顺序,如果有多个参数也可以按照这个方式添加 1、2、3....。除了按照这种方式传参外,还可以使用 @Param 来支持。
@Query("select u from User u where u.nickName = :nickName")
Page findByNickName(@Param("nickName") String nickName, Pageable pageable);
如涉及到删除和修改需要加上 @Modifying,也可以根据需要添加 @Transactional 对事务的支持、操作超时设置等。
@Transactional(timeout = 10)
@Modifying
@Query("update User set userName = ?1 where id = ?2")
int modifyById(String userName, Long id);
@Transactional
@Modifying
@Query("delete from User where id = ?1")
void deleteById(Long id);
使用已命名的查询
除了使用 @Query 注解外,还可以预先定义好一些查询,并为其命名,然后再 Repository 中添加相同命名的方法。
定义命名的 Query:
@Entity
@NamedQueries({
@NamedQuery(name = "User.findByPassWord", query = "select u from User u where u.passWord = ?1"),
@NamedQuery(name = "User.findByNickName", query = "select u from User u where u.nickName = ?1"),
})
public class User {
……
}
通过 @NamedQueries 注解可以定义多个命名 Query,@NamedQuery 的 name 属性定义了 Query 的名称,注意加上 Entity 名称 . 作为前缀,query 属性定义查询语句。
定义对应的方法:
List findByPassWord(String passWord);
List findByNickName(String nickName);
Query 查找策略
到此,我们有了三种方法来定义 Query:(1)通过方法名自动创建 Query,(2)通过 @Query 注解实现自定义 Query,(3)通过 @NamedQuery 注解来定义 Query。那么,Spring Data JPA 如何来查找这些 Query 呢?
通过配置 @EnableJpaRepositories 的 queryLookupStrategy 属性来配置 Query 查找策略,有如下定义。
一般情况下使用默认配置即可,如果确定项目 Query 的具体定义方式,可以更改上述配置,例如,全部使用 @Query 来定义查询,又或者全部使用命名的查询。
Spring Data JPA 已经帮我们内置了分页功能,在查询的方法中,需要传入参数 Pageable,当查询中有多个参数的时候 Pageable 建议作为最后一个参数传入。
@Query("select u from User u")
Page findALL(Pageable pageable);
Page findByNickName(String nickName, Pageable pageable);
Pageable 是 Spring 封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则,Page 是 Spring 封装的分页对象,封装了总页数、分页数据等。返回对象除使用 Page 外,还可以使用 Slice 作为返回值。
Slice findByNickNameAndEmail(String nickName, String email,Pageable pageable);
Page 和 Slice 的区别如下。
@Test
public void testPageQuery() {
int page=1,size=2;
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size, sort);
userRepository.findALL(pageable);
userRepository.findByNickName("aa", pageable);
}
还有一些更简洁的方式来排序和分页查询,如下。
限制查询
有时候我们只需要查询前 N 个元素,或者只取前一个实体。
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page queryFirst10ByLastname(String lastname, Pageable pageable);
List findFirst10ByLastname(String lastname, Sort sort);
List findTop10ByLastname(String lastname, Pageable pageable);
我们可以通过 AND 或者 OR 等连接词来不断拼接属性来构建多条件查询,但如果参数大于 6 个时,方法名就会变得非常的长,并且还不能解决动态多条件查询的场景。到这里就需要给大家介绍另外一个利器 JpaSpecificationExecutor 了。
JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。
我们来看一下 JpaSpecificationExecutor 的源码:
public interface JpaSpecificationExecutor {
//根据 Specification 条件查询单个对象,注意的是,如果条件能查出来多个会报错
T findOne(@Nullable Specification spec);
//根据 Specification 条件查询 List 结果
List findAll(@Nullable Specification spec);
//根据 Specification 条件,分页查询
Page findAll(@Nullable Specification spec, Pageable pageable);
//根据 Specification 条件,带排序的查询结果
List findAll(@Nullable Specification spec, Sort sort);
//根据 Specification 条件,查询数量
long count(@Nullable Specification spec);
}
JpaSpecificationExecutor 的源码很简单,根据 Specification 的查询条件返回 List、Page 或者 count 数据。在使用 JpaSpecificationExecutor 构建复杂查询场景之前,我们需要了解几个概念:
root,代表了可以查询和操作的实体对象的根,开一个通过 get("属性名") 来获取对应的值。使用案例
下面的使用案例中会报错这几个对象的使用。
首先定义一个 UserDetail 对象,作为演示的数据模型。
@Entity
public class UserDetail {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false, unique = true)
private Long userId;
private Integer age;
private String realName;
private String status;
private String hobby;
private String introduction;
private String lastLoginIp;
}
创建 UserDetail 对应的 Repository:
public interface UserDetailRepository extends JpaSpecificationExecutor,JpaRepository {
}
定义一个查询 Page
的接口:
public interface UserDetailService {
public Page findByCondition(UserDetailParam detailParam, Pageable pageable);
}
在 UserDetailServiceImpl 中,我们来演示 JpaSpecificationExecutor 的具体使用。
@Service
public class UserDetailServiceImpl implements UserDetailService{
@Resource
private UserDetailRepository userDetailRepository;
@Override
public Page findByCondition(UserDetailParam detailParam, Pageable pageable){
return userDetailRepository.findAll((root, query, cb) -> {
List predicates = new ArrayList();
//equal 示例
if (!StringUtils.isNullOrEmpty(detailParam.getIntroduction())){
predicates.add(cb.equal(root.get("introduction"),detailParam.getIntroduction()));
}
//like 示例
if (!StringUtils.isNullOrEmpty(detailParam.getRealName())){
predicates.add(cb.like(root.get("realName"),"%"+detailParam.getRealName()+"%"));
}
//between 示例
if (detailParam.getMinAge()!=null && detailParam.getMaxAge()!=null) {
Predicate agePredicate = cb.between(root.get("age"), detailParam.getMinAge(), detailParam.getMaxAge());
predicates.add(agePredicate);
}
//greaterThan 大于等于示例
if (detailParam.getMinAge()!=null){
predicates.add(cb.greaterThan(root.get("age"),detailParam.getMinAge()));
}
return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
}, pageable);
}
}
上面的示例是根据不同条件来动态查询 UserDetail 分页数据,UserDetailParam 是参数的封装,示例中使用了常用的大于、like、等于等示例,根据这个思路我们可以不断扩展完成更复杂的动态 SQL 查询。
使用时只需要将 UserDetailService 注入调用相关方法即可:
@RunWith(SpringRunner.class)
@SpringBootTest
public class JpaSpecificationTests {
@Resource
private UserDetailService userDetailService;
@Test
public void testFindByCondition() {
int page=0,size=10;
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = PageRequest.of(page, size, sort);
UserDetailParam param=new UserDetailParam();
param.setIntroduction("程序员");
param.setMinAge(10);
param.setMaxAge(30);
Page page1=userDetailService.findByCondition(param,pageable);
for (UserDetail userDetail:page1){
System.out.println("userDetail: "+userDetail.toString());
}
}
}
多表查询在 Spring Data JPA 中有两种实现方式,第一种是利用 Hibernate 的级联查询来实现,第二种是创建一个结果集的接口来接收连表查询后的结果,这里主要介绍第二种方式。
我们还是使用上面的 UserDetail 作为数据模型来使用,定义一个结果集的接口类,接口类的内容来自于用户表和用户详情表。
public interface UserInfo {
String getUserName();
String getEmail();
String getAddress();
String getHobby();
}
在运行中 Spring 会给接口(UserInfo)自动生产一个代理类来接收返回的结果,代码中使用 getXX 的形式来获取。
在 UserDetailRepository 中添加查询的方法,返回类型设置为 UserInfo:
@Query("select u.userName as userName, u.email as email, d.introduction as introduction , d.hobby as hobby from User u , UserDetail d " +
"where u.id=d.userId and d.hobby = ?1 ")
List findUserInfo(String hobby);
特别注意这里的 SQL 是 HQL,需要写类的名和属性,这块很容易出错。
测试验证:
@Test
public void testUserInfo() {
List userInfos=userDetailRepository.findUserInfo("钓鱼");
for (UserInfo userInfo:userInfos){
System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEmail()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
}
}
运行测试方法后返回:
userInfo: [email protected]钓鱼-程序员
证明关联查询成功,最后的返回结果来自于两个表,按照这个思路可以进行三个或者更多表的关联查询。
项目中使用多个数据源在以往工作中比较常见,微服务架构中不建议一个项目使用多个数据源。在微服务架构下,一个微服务拥有自己独立的一个数据库,如果此微服务要使用其他数据库的数据,需要调用对应库的微服务接口来调用,而不是在一个项目中连接使用多个数据库,这样微服务更独立、更容易水平扩展。
虽然在微服务架构下,不提倡一个项目拥有多个数据源,但在 Spring Boot 体系中,项目实现多数据源调用却是一件很容易的事情
Spring Data JPA 使用动态注入的原理,根据方法名动态生成方法的实现,因此根据方法名实现数据查询,即可满足日常绝大部分使用场景。除了这种查询方式之外,Spring Data JPA 还支持多种自定义查询来满足更多复杂场景的使用,两种方式相结合可以灵活满足项目对 Orm 层的需求。
通过学习 Spring Data JPA 也可以看出 Spring Boot 的设计思想,80% 的需求通过默认、简单的方式实现,满足大部分使用场景,对于另外 20% 复杂的场景,提供另外的技术手段来解决。Spring Data JPA 中根据方法名动态实现 SQL,组件环境自动配置等细节,都是将 Spring Boot 约定优于配置的思想体现的淋淋尽致。