欢迎来到代码驿站!

JAVA代码

当前位置:首页 > 软件编程 > JAVA代码

SpringData JPA基本/高级/多数据源的使用详解

时间:2022-10-04 10:51:32|栏目:JAVA代码|点击:

一、Spring Data JPA基本用法

Spring Data JPA 是 Spring Boot 体系中约定优于配置的最佳实现,大大简化了项目中数据库的操作

1、概念

JPA由来

ORM 框架能够将 Java 对象映射到关系数据库中,能够直接持久化复杂的 Java 对象。ORM 框架的出现,可以让开发者从数据库编程中解脱出来,把更多的精力放在了业务模型与业务逻辑上。目前比较流行的 ORM 框架有 Hibernate、MyBatis、TopLink、Spring JDBC 等。

在 JPA 规范之前,由于没有官方的标准,使得各 ORM 框架之间的 API 差别很大,使用了某种 ORM 框架的系统会严重受制于该 ORM 的标准。基于此,Sun 引入新的 JPA ORM,主要的原因有:其一,简化现有 Java EE 和 Java SE 应用开发工作;其二,Sun 希望整合 ORM 技术,实现统一的 API 调用接口。

JPA是什么

JPA(Java Persistence API)是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供了一种对象 / 关联映射工具来管理 Java 应用中的关系数据。它的出现主要是为了简化现有的持久化开发工作和整合 ORM 技术,结束现在 Hibernate、TopLink、JDO 等 ORM 框架各自为营的局面。

注意: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 都可以依赖于它实现。

2、快速上手

1.添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
 <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

2.添加配置文件

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 参数的作用主要用于:自动创建、更新、验证数据库表结构,有四个值。

  • create:每次加载 Hibernate 时都会删除上一次生成的表,然后根据 model 类再重新来生成新表,哪怕两次没有任何改变也要这样执行,这就是导致数据库表数据丢失的一个重要原因。
  • create-drop:每次加载 Hibernate 时根据 model 类生成表,但是 sessionFactory 一关闭,表就自动删除。
  • update:最常用的属性,第一次加载 Hibernate 时根据 model 类会自动建立起表的结构(前提是先建立好数据库),以后加载 Hibernate 时根据 model 类自动更新表结构,即使表结构改变了,但表中的行仍然存在,不会删除以前的行。要注意的是当部署到服务器后,表结构是不会被马上建立起来的,是要等应用第一次运行起来后才会。
  • validate:每次加载 Hibernate 时,验证创建数据库表结构,只会和数据库中的表进行比较,不会创建新表,但是会插入新值。

其他

  • dialect 主要是指定生成表名的存储引擎为 InnoDB
  • show-sql 是否在日志中打印出自动生成的 SQL,方便调试的时候查看

3.实体类

@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) 注解。

4.Repository构建

创建的 Repository 只要继承 JpaRepository 即可,就会帮我们自动生成很多内置方法。另外还有一个功能非常实用,可以根据方法名自动生产 SQL,比如 findByUserName 会自动生产一个以 userName 为参数的查询方法,比如 findAll 会自动查询表里面的所有数据等。

public interface UserRepository extends JpaRepository<User,Long> {
    User findByUserName(String userName);
    User findByUserNameOrEmail(String username,String email);
}

我们只需要在对应的 Repository 中创建好方法,使用的时候直接将接口注入到类中调用即可。在 IDEA 中打开类 UserRepository,在这个类的大括号内的区域右键单击,选择 Diagrams | Show Diagram 选项,即可打开类图,如下:

通过上图我们发现 JpaRepository 继承 PagingAndSortingRepository 和 QueryByExampleExecutor,PagingAndSortingRepository 类主要负责排序和分页内容,QueryByExampleExecutor 提供了很多示例的查询方法,如下:

public interface QueryByExampleExecutor<T> { 
    <S extends T> 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);  //根据“实例”判断是否有符合条件的对象
}

因此,继承 JpaRepository 的会自动拥有上述这些方法和排序、分页功能。查看源码我们发现 PagingAndSortingRepository 又继承了 CrudRepository。CrudRepository 的源码如下:

@NoRepositoryBean
public interface CrudRepository<T,ID> extends Repository<T,ID> {
    <S extends T> S save(S entity);
    <S extends T> Iterable<S> saveAll(Iterable<S> entities);
    Optional<T> findById(ID id);
    boolean existsById(ID id);
    Iterable<T> findAll();
    Iterable<T> findAllById(Iterable<ID> 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,就会拥有了很多常用的数据库操作方法。

5.测试

创建好 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","aa@126.com","aa","aa123456",formattedDate));
        userRepository.save(new User("bb","bb@126.com","bb","bb123456",formattedDate));
        userRepository.save(new User("cc","cc@126.com","cc","cc123456",formattedDate));
        Assert.assertEquals(9,userRepository.findAll().size());
        Assert.assertEquals("bb",userRepository.findByUserNameOrEmail("bb","cc@126.com").getNickName());
        userRepository.delete(userRepository.findByUserName("aa1"));
    }
}

6.基本查询

我们可以将 Spring Data JPA 查询分为两种,一种是 Spring Data JPA 默认实现的,另一种是需要根据查询的情况来自行构建。

预生成方法

预生成方法就是我们上面看到的那些方法,因为继承了 JpaRepository 而拥有了父类的这些内容。

(1)继承 JpaRepository

public interface UserRepository extends JpaRepository<User,Long> {
}

(2)使用默认方法

//所有父类拥有的方法都可以直接调用,根据方法名也可以看出它的含义。
@Test
public void testBaseQuery() {
userRepository.findAll();
userRepository.findById(1l);
userRepository.save(user);
userRepository.delete(user);
userRepository.count();
userRepository.existsById(1l);
// ...
    }

7.自定义查询

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<User> findByEmailLike(String email);
User findByUserNameIgnoreCase(String userName);
List<User> findByUserNameOrderByEmailDesc(String email);

可以根据查询的条件不断地添加和拼接,Spring Boot 都可以正确解析和执行,其他使用示例可以参考下表。

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(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<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)
Count countByFirstName select count(*) from ... where x.firstName = ?1
Exists existsByFirstName like the dao.exists(Example),judge by attribution of firstName:select keyindex0_.id as col_0_0_ from key_index keyindex0_ where keyindex0_.name=? limit ?

3、小结

第一部分讲解了使用 JPA 大大解放了我们对数据库的操作,经常使用的 SQL 大部分都已经被预生成,直接使用即可。另外 JPA 还有一个特点,那就是再也不用关心数据库的表结构了,需要更改的时候只需要修改对应 Model 的属性即可。在微服务架构中,因为服务拆分得越来越小,微服务内部只关心自己的业务,需要复杂查询的场景会越来越少,在微服务架构中更推荐使用 JPA 技术。

二、Spring Data JPA高级用法

第一部分介绍了 Spring Data JPA 的使用方式和基本查询,常用的增、删、改、查需求 Spring Data JPA 已经实现了。但对于复杂的数据库场景,动态生成方法不能满足,对此 Spring Data JPA 提供了其他的解决方案。

1、自定义 SQL 查询

使用 Spring Data 大部分的 SQL 都可以根据方法名定义的方式来实现,但是由于某些原因必须使用自定义的 SQL 来查询,Spring Data 也可以完美支持。

在 SQL 的查询方法上面使用 @Query 注解,在注解内写 Hql 来查询内容。

@Query("select u from User u")
Page<User> findALL(Pageable pageable);

当然如果感觉使用原生 SQL 更习惯,它也是支持的,需要再添加一个参数 nativeQuery = true。

@Query("select * from user u where u.nick_name = ?1", nativeQuery = true)
Page<User> findByNickName(String nickName, Pageable pageable);

@Query 上面的 1 代表的是方法参数里面的顺序,如果有多个参数也可以按照这个方式添加 1、2、3....。除了按照这种方式传参外,还可以使用 @Param 来支持。

@Query("select u from User u where u.nickName = :nickName")
Page<User> 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<User> findByPassWord(String passWord);
List<User> findByNickName(String nickName);

Query 查找策略

到此,我们有了三种方法来定义 Query:(1)通过方法名自动创建 Query,(2)通过 @Query 注解实现自定义 Query,(3)通过 @NamedQuery 注解来定义 Query。那么,Spring Data JPA 如何来查找这些 Query 呢?

通过配置 @EnableJpaRepositories 的 queryLookupStrategy 属性来配置 Query 查找策略,有如下定义。

  • CREATE:尝试从查询方法名构造特定于存储的查询。一般的方法是从方法名中删除一组已知的前缀,并解析方法的其余部分。
  • USE_DECLARED_QUERY:尝试查找已声明的查询,如果找不到,则抛出异常。查询可以通过某个地方的注释定义,也可以通过其他方式声明。
  • CREATE_IF_NOT_FOUND(默认):CREATE 和 USE_DECLARED_QUERY 的组合,它首先查找一个已声明的查询,如果没有找到已声明的查询,它将创建一个自定义方法基于名称的查询。它允许通过方法名进行快速查询定义,还可以根据需要引入声明的查询来定制这些查询调优。

2、分页查询Spring Data JPA 已经帮我们内置了分页功能

在查询的方法中,需要传入参数 Pageable,当查询中有多个参数的时候 Pageable 建议作为最后一个参数传入。

@Query("select u from User u")
Page<User> findALL(Pageable pageable);
Page<User> findByNickName(String nickName, Pageable pageable);

Pageable 是 Spring 封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则,Page 是 Spring 封装的分页对象,封装了总页数、分页数据等。返回对象除使用 Page 外,还可以使用 Slice 作为返回值。

Slice<User> findByNickNameAndEmail(String nickName, String email,Pageable pageable);

Page 和 Slice 的区别如下:

Page 接口继承自 Slice 接口,而 Slice 继承自 Iterable 接口。

Page 接口扩展了 Slice 接口,添加了获取总页数和元素总数量的方法,因此,返回 Page 接口时,必须执行两条 SQL,一条复杂查询分页数据,另一条负责统计数据数量。

返回 Slice 结果时,查询的 SQL 只会有查询分页数据这一条,不统计数据数量。

用途不一样:Slice 不需要知道总页数、总数据量,只需要知道是否有下一页、上一页,是否是首页、尾页等,比如前端滑动加载一页可用;而 Page 知道总页数、总数据量,可以用于展示具体的页数信息,比如后台分页查询。

@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);
    }
  • Sort,控制分页数据的排序,可以选择升序和降序。
  • PageRequest,控制分页的辅助类,可以设置页码、每页的数据条数、排序等。

限制查询:有时候我们只需要查询前 N 个元素,或者只取前一个实体。

User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);

3、复杂查询

我们可以通过 AND 或者 OR 等连接词来不断拼接属性来构建多条件查询,但如果参数大于 6 个时,方法名就会变得非常的长,并且还不能解决动态多条件查询的场景。到这里就需要给大家介绍另外一个利器 JpaSpecificationExecutor 了。

JpaSpecificationExecutor 是 JPA 2.0 提供的 Criteria API 的使用封装,可以用于动态生成 Query 来满足我们业务中的各种复杂场景。Spring Data JPA 为我们提供了 JpaSpecificationExecutor 接口,只要简单实现 toPredicate 方法就可以实现复杂的查询。

我们来看一下 JpaSpecificationExecutor 的源码:

public interface JpaSpecificationExecutor<T> {
   //根据 Specification 条件查询单个对象,注意的是,如果条件能查出来多个会报错
   T findOne(@Nullable Specification<T> spec);
   //根据 Specification 条件查询 List 结果
   List<T> findAll(@Nullable Specification<T> spec);
   //根据 Specification 条件,分页查询
   Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);
   //根据 Specification 条件,带排序的查询结果
   List<T> findAll(@Nullable Specification<T> spec, Sort sort);
   //根据 Specification 条件,查询数量
   long count(@Nullable Specification<T> spec);
}

JpaSpecificationExecutor 的源码很简单,根据 Specification 的查询条件返回 List、Page 或者 count 数据。在使用 JpaSpecificationExecutor 构建复杂查询场景之前,我们需要了解几个概念:

  • Root root,代表了可以查询和操作的实体对象的根,开一个通过 get("属性名") 来获取对应的值。
  • CriteriaQuery query,代表一个 specific 的顶层查询对象,它包含着查询的各个部分,比如 select 、from、where、group by、order by 等。
  • CriteriaBuilder cb,来构建 CritiaQuery 的构建器对象,其实就相当于条件或者是条件组合,并以 Predicate 的形式返回。

使用案例: 首先定义一个 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<UserDetail>,JpaRepository<UserDetail, Long>  {
}

定义一个查询 Page<UserDetail> 的接口:

public interface UserDetailService {
    public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable);
}

在 UserDetailServiceImpl 中,我们来演示 JpaSpecificationExecutor 的具体使用。

@Service
public class UserDetailServiceImpl implements  UserDetailService{
    @Resource
    private UserDetailRepository userDetailRepository;
    @Override
    public Page<UserDetail> findByCondition(UserDetailParam detailParam, Pageable pageable){
        return userDetailRepository.findAll((root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<Predicate>();
            //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 查询。

@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<UserDetail> page1=userDetailService.findByCondition(param,pageable);
        for (UserDetail userDetail:page1){
            System.out.println("userDetail: "+userDetail.toString());
        }
    }
}

4、多表查询

多表查询在 Spring Data JPA 中有两种实现方式,第一种是利用 Hibernate 的级联查询来实现,第二种是创建一个结果集的接口来接收连表查询后的结果,这里主要介绍第二种方式。

我们还是使用上面的 UserDetail 作为数据模型来使用,定义一个结果集的接口类,接口类的内容来自于用户表和用户详情表。

public interface UserInfo {
    String getUserName();
    String getEmail();
    String getAddress();
    String getHobby();
}

在运行中 Spring 会给接口(UserInfo)自动生产一个代理类来接收返回的结果,代码中使用 getXX 的形式来获取。

在 UserDetailRepository 中添加查询的方法,返回类型设置为 UserInfo,特别注意这里的 SQL 是 HQL,需要写类的名和属性,这块很容易出错

@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<UserInfo> findUserInfo(String hobby);

测试验证

@Test
public void testUserInfo()  {
    List<UserInfo> userInfos=userDetailRepository.findUserInfo("钓鱼");
    for (UserInfo userInfo:userInfos){
        System.out.println("userInfo: "+userInfo.getUserName()+"-"+userInfo.getEmail()+"-"+userInfo.getHobby()+"-"+userInfo.getIntroduction());
    }
}

测试后返回

userInfo: aa-aa@126.com-钓鱼-程序员

5、小结

Spring Data JPA 使用动态注入的原理,根据方法名动态生成方法的实现,因此根据方法名实现数据查询,即可满足日常绝大部分使用场景。除了这种查询方式之外,Spring Data JPA 还支持多种自定义查询来满足更多复杂场景的使用,两种方式相结合可以灵活满足项目对 Orm 层的需求。

通过学习 Spring Data JPA 也可以看出 Spring Boot 的设计思想,80% 的需求通过默认、简单的方式实现,满足大部分使用场景,对于另外 20% 复杂的场景,提供另外的技术手段来解决。Spring Data JPA 中根据方法名动态实现 SQL,组件环境自动配置等细节,都是将 Spring Boot 约定优于配置的思想体现的淋淋尽致。

三、Spring Data JPA多数据源使用

1、前言

项目中使用多个数据源在以往工作中比较常见,微服务架构中不建议一个项目使用多个数据源。在微服务架构下,一个微服务拥有自己独立的一个数据库,如果此微服务要使用其他数据库的数据,需要调用对应库的微服务接口来调用,而不是在一个项目中连接使用多个数据库,这样微服务更独立、更容易水平扩展。

虽然在微服务架构下,不提倡一个项目拥有多个数据源,但在 Spring Boot 体系中,项目实现多数据源调用却是一件很容易的事情,本节课将介绍 Spring Data JPA 多数据源的使用。

Spring Data JPA 使用多数据源的整体思路是,配置不同的数据源,在启动时分别加载多个数据源配置,并且注入到不同的 repository 中。这样不同的 repository 包就有不同的数据源,使用时注入对应包下的 repository,就会使用对应数据源的操作。

项目结构如下:

onfig 启动时加载、配置多数据源;

model 存放数据操作的实体类;

repository 目录下有两个包路径 test1 和 test2 ,分别代表两个不同数据源下的仓库,这两个包下的 repository 可以相同也可以不同。

2、多数据源的支持

配置 Spring Data JPA 对多数据源的使用,一般分为以下几步:

  • 创建数据库 test1 和 test2
  • 配置多数据源
  • 不同源的 repository 放入不同包路径
  • 声明不同的包路径下使用不同的数据源、事务支持
  • 不同的包路径下创建对应的 repository
  • 测试使用

上面的一些步骤我们在前面两课中已经讲过了,这里只补充不同的内容。

spring.datasource.primary.jdbc-url=jdbc:mysql://localhost:3306/test1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.primary.username=root
spring.datasource.primary.password=root
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.secondary.jdbc-url=jdbc:mysql://localhost:3306/test2?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
spring.datasource.secondary.username=root
spring.datasource.secondary.password=root
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
# 设置将项目中的 SQL 格式化后打印出来,方便在开发过程中调试跟踪。
#SQL 输出
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.hbm2ddl.auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
#format 一下 SQL 进行输出
spring.jpa.properties.hibernate.format_sql=true

创建 DataSourceConfig 添加 @Configuration 注解,在项目启动时运行初始化数据库资源。

@Configuration
public class DataSourceConfig {
}

在 DataSourceConfig 类中加载配置文件,利用 ConfigurationProperties 自动装配的特性加载两个数据源。

加载第一个数据源,数据源配置以 spring.datasource.primary 开头,注意当有多个数据源时,需要将其中一个标注为 @Primary,作为默认的数据源使用。

@Bean(name = "primaryDataSource")
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSource firstDataSource() {
    return DataSourceBuilder.create().build();
}

加载第二个数据源,数据源配置以 spring.datasource.secondary 为开头。

@Bean(name = "secondaryDataSource")
@ConfigurationProperties("spring.datasource.secondary")
public DataSource secondDataSource() {
    return DataSourceBuilder.create().build();
}

加载 JPA 的相关配置信息,JpaProperties 是 JPA 的一些属性配置信息,构建 LocalEntityManagerFactoryBean 需要参数信息注入到方法中。

@Autowired
private JpaProperties jpaProperties;
@Autowired
private HibernateProperties hibernateProperties;
@Bean(name = "vendorProperties")
public Map<String, Object> getVendorProperties() {
    return hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings());
}

第一个数据源的加载配置过程

首先来看第一个数据源的加载配置过程,创建 PrimaryConfig 类,将上面创建好的第一个数据源注入到类中,添加 @Configuration 和 @EnableTransactionManagement 注解,第一个代表启动时加载,第二个注

解表示启用事务,同时将第一个数据源和 JPA 配置信息注入到类中。

@Configuration
@EnableTransactionManagement
public class PrimaryConfig {
    @Autowired
    @Qualifier("primaryDataSource")
    private DataSource primaryDataSource;
    @Autowired
    @Qualifier("vendorProperties")
    private Map<String, Object> vendorProperties;
}

LocalEntityManagerFactoryBean 负责创建一个适合于仅使用 JPA 进行数据访问的环境的 EntityManager,构建的时候需要指明提示实体类的包路径、数据源和 JPA 配置信息。

@Bean(name = "entityManagerFactoryPrimary")
@Primary
public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary (EntityManagerFactoryBuilder builder) {
    return builder
            .dataSource(primaryDataSource)
            .properties(vendorProperties)
            .packages("com.neo.model") //设置实体类所在位置
            .persistenceUnit("primaryPersistenceUnit")
            .build();
}

利用上面的 entityManagerFactoryPrimary() 方法构建好最终的 EntityManager。

@Bean(name = "entityManagerPrimary")
@Primary
public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
    return entityManagerFactoryPrimary(builder).getObject().createEntityManager();
}

EntityManager 是 JPA 中用于增、删、改、查的接口,它的作用相当于一座桥梁,连接内存中的 Java 对象和数据库的数据存储。使用 EntityManager 中的相关接口对数据库实体进行操作的时候, EntityManager

会跟踪实体对象的状态,并决定在特定时刻将对实体的操作映射到数据库操作上面,同时给数据源添加上 JPA 事务。

@Bean(name = "transactionManagerPrimary")
@Primary
PlatformTransactionManager transactionManagerPrimary(EntityManagerFactoryBuilder builder) {
    return new JpaTransactionManager(entityManagerFactoryPrimary(builder).getObject());
}

最后一步最为关键,将我们在类中配置好的 EntityManager 和事务信息注入到对应数据源的 repository 目录下,这样此目录下的 repository 就会拥有对应数据源和事务的信息。

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactoryPrimary",
        transactionManagerRef="transactionManagerPrimary",
        basePackages= { "com.neo.repository.test1" })//设置dao(repo)所在位置
public class PrimaryConfig {}

其中,basePackages 支持设置多个包路径,例如,basePackages= { "com.neo.repository.test1","com.neo.repository.test3" }

第二个数据源的加载配置过程

第二个数据源配置和第一个数据源配置类似,只是方法上去掉了注解:@Primary,第二个数据源数据源加载配置类 SecondaryConfig 完整代码如下: 

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef="entityManagerFactorySecondary",
        transactionManagerRef="transactionManagerSecondary",
        basePackages= { "com.neo.repository.test2" })
public class SecondaryConfig {
    @Autowired
    @Qualifier("secondaryDataSource")
    private DataSource secondaryDataSource;
    @Autowired
    @Qualifier("vendorProperties")
    private Map<String, Object> vendorProperties;
    @Bean(name = "entityManagerFactorySecondary")
    public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary (EntityManagerFactoryBuilder builder) {
        return builder
                .dataSource(secondaryDataSource)
                .properties(vendorProperties)
                .packages("com.neo.model")
                .persistenceUnit("secondaryPersistenceUnit")
                .build();
    }
    @Bean(name = "entityManagerSecondary")
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactorySecondary(builder).getObject().createEntityManager();
    }
    @Bean(name = "transactionManagerSecondary")
    PlatformTransactionManager transactionManagerSecondary(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactorySecondary(builder).getObject());
    }
}

到此多数据源的配置就完成了,项目中使用哪个数据源的操作,就注入对应包下的 repository 进行操作即可,接下来我们对上面配置好的数据源进行测试。

创建 UserRepositoryTests 测试类,将两个包下的 repository 都注入到测试类中:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRepositoryTests {
    @Resource
    private UserTest1Repository userTest1Repository;
    @Resource
    private UserTest2Repository userTest2Repository;
}

首先测试两个数据库中都存入数据,数据源1插入 2 条用户信息,数据源2插入 1 条用户信息。

@Test
public void testSave() throws Exception {
    Date date = new Date();
    DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG);
    String formattedDate = dateFormat.format(date);
    userTest1Repository.save(new User("aa", "aa123456","aa@126.com", "aa",  formattedDate));
    userTest1Repository.save(new User("bb", "bb123456","bb@126.com", "bb",  formattedDate));
    userTest2Repository.save(new User("cc", "cc123456","cc@126.com", "cc",  formattedDate));
}

执行完测试用例后查看数据库,发现 test1 库有两条数据,test2 有一条,证明两个数据源均保存数据正常。下面继续测试删除功能,使用两个数据源的 repository 将用户信息全部删除。

@Test
public void testDelete() throws Exception {
    userTest1Repository.deleteAll();
    userTest2Repository.deleteAll();
}

执行完测试用例后,发现 test1 库和 test2 库用户表的信息已经被清空,证明多数据源删除成功。

3、小结

Spring Data JPA 通过在启动时加载不同的数据源,并将不同的数据源注入到不同的 repository 包下,从而实现项目多数据源操作,在项目中使用多数据源时,需要用到哪个数据源,只需要将对应包下的 repository 注入操作即可。本课示例中以两个数据源作为演示,但其实三个或者更多数据源配置、操作,都可以按照上面方法进行配置使用。

上一篇:深入浅出讲解Spring框架中AOP及动态代理的应用

栏    目:JAVA代码

下一篇:Java实现多选批量删除功能(vue+Element)

本文标题:SpringData JPA基本/高级/多数据源的使用详解

本文地址:http://www.codeinn.net/misctech/215441.html

推荐教程

广告投放 | 联系我们 | 版权申明

重要申明:本站所有的文章、图片、评论等,均由网友发表或上传并维护或收集自网络,属个人行为,与本站立场无关。

如果侵犯了您的权利,请与我们联系,我们将在24小时内进行处理、任何非本站因素导致的法律后果,本站均不负任何责任。

联系QQ:914707363 | 邮箱:codeinn#126.com(#换成@)

Copyright © 2020 代码驿站 版权所有