亲宝软件园·资讯

展开

JPA基本/高级/多数据源使用

纯洁的微笑 人气:0

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

其他

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 方法、构造方法
}

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 都可以正确解析和执行,其他使用示例可以参考下表。

KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is,EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1(parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1(parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1(parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstame) = UPPER(?1)
CountcountByFirstNameselect count(*) from ... where x.firstName = ?1
ExistsexistsByFirstNamelike 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 查找策略,有如下定义。

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);
    }

限制查询:有时候我们只需要查询前 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 构建复杂查询场景之前,我们需要了解几个概念:

使用案例: 首先定义一个 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 对多数据源的使用,一般分为以下几步:

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

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 注入操作即可。本课示例中以两个数据源作为演示,但其实三个或者更多数据源配置、操作,都可以按照上面方法进行配置使用。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持。

加载全部内容

相关教程
猜你喜欢
用户评论