Spring Data JPA 整合

目录

前言

正文 

1.Spring Data JPA 导入项目和配置 

2.Spring Data JPA 的 CRUD 实例 

3.Spring Data JPA 扩展封装 

总结


前言

JPA 全称是 Java Persistence API ,意思是 Java 持久层 API 接口。而 Spring Data JPA 是一套 ORM(对象关系映射)标准,关于这套标准的实现 ORM 框架很多,如大名鼎鼎的 Hibernate 。实际上是先有 Hibernate ,再有的 JPA 标准 。而这里推荐的是 Spring Boot 的实现,也就是 Spring Data。


正文 

Spring Data 算是 Spring 官配的项目,用于简化数据库相关操作,通过封装来减少冗余的代码,让数据库访问和操作变得非常简单。

1.Spring Data JPA 导入项目和配置 

首先创建用于测试的数据库,SQL 如下。

create database  `test_jpa` default character set utf8;

添加 MySQL 和 Spring Data JPA 相关依赖包,pom.xml 代码如下。

        
        
            com.alibaba
            druid
            1.1.23
        
        
        
            mysql
            mysql-connector-java
            8.0.28
        

然后设置好数据库的配置,配置 application.yml 代码。 

server:
  port: 8080
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    jedis:
      pool:
        max-active: 50
        max-idle: 20
        max-wait: 3000
        min-idle: 2
    timeout: 5000
  datasource:
    url: jdbc:mysql://localhost:3306/test_jpa?useTimezone=true&serverTimezone=UTC
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      # 初始化时建立物理连接的个数
      initial-size: 5
      # 最小连接数量
      min-idle: 5
      # 最大活跃连接池数量
      max-active: 20
      # 请求连接池在分配连接时,是否先检查该连接是否有效,建议设置成true。
      test-while-idle: true
      # 程序申请连接的时候,是否进行连接有效性检查,建议设置成false。
      test-on-borrow: false
      # 程序返还连接的时候,是否进行连接有效性检查,建议设置成false。
      test-on-return: false
      # 是否使用PSCache,对于mysql的性能提升不明显,对oracle效果更好。
      pool-prepared-statements: false
      # 获取连接的最大等待时间,单位是毫秒。
      max-wait: 60000
      # 检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查
      time-between-eviction-runs-millis: 60000
      # 池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将回收该链接,然后设置要小于防火墙的超时设置
      min-evictable-idle-time-millis: 30000
      # 设置插件
      filters: stat
      # 是否异步初始化
      async-init: true
      # 连接属性设置
      connection-properties: druid.stat.mergeSql=true;druid.stat.SlowSqlMills=5000



  # 设置实体类中的字段自动变成对应的数据库中下划线字段
  jpa:
    database: mysql
    hibernate:
      naming:
        # 需要安装hibernate相关依赖   开启驼峰命名
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
      # 是否自动创建表,设置为update则说明如果表存在则更新
      ddl-auto: none
    #physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl



2.Spring Data JPA 的 CRUD 实例 

为了后续演示,创建一个英雄表 ,建表代码如下。

create TABLE `tm_heros`
(
    `id` int(10) unsigned NOT NULL  auto_increment comment '主键ID',
    `name` varchar(80) NOT NULL comment '英雄名称',
    `role_type` varchar(60) NOT NULL comment '类型,如:射手、法师、游走',
    `attack_value` int(11) NOT NULL comment '攻击力,单位 1HP,最大值为1000',
    `version` varchar(50) NOT NULL comment '版本,如:v1.2.0',
    `created` int(11) not null comment '创建时间,时间戳',
    primary key (`id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

基于该表,开始编写对应的实体类,命名为 Hero.java 。 

package org.example.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity(name = "tm_heros")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Hero {
    //主键 ID
    @Id
    private int id;
    //英雄名称
    private String name;
    //角色类型,如:射手、法师
    @Column(name = "role_type")
    private String roleType;
    //攻击力 0 - 1000 的整数
    @Column(name = "attack_value")
    private int attackValue;
    //版本 如v1.0.2
    private String version;
    //创建时间
    private int created;
}

编写 HeroDao 接口,继承自 JpaRepository 接口。

import org.example.entity.Hero;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface HeroDao extends JpaRepository {
    @Override
    List findAll();
    //sql like : select * from tm_hero where name like "%name%"
    List getHeroByNameContaining(String name);//根据方法名自动实现sql
    //获取最新的英雄,根据创建时间(自定义)
    @Query(value = "select * from tm_heros where created = (select max(created) from tm_heros)",nativeQuery = true)
    Hero getLateHero();

}

POM依赖 

        
            org.springframework.data
            spring-data-jpa
        

JpaRepository 中提供了不少封装好的基础方法,例如 CURD 和一些分页排序功能。findAll() 就是一个获取所有数据的方法,只需重写该方法即可。 

而一些方法的命名也是有规律可以遵循的。

  1.  And 系列,findByNameAndRoleType 方法对应的 SQL 是 "where name =? and role_type=?"。
  2. Or 系列,findByNameOrRoletype 方法对应的 SQL 是  "where name =? or role_type=?"。
  3. Equals 系列,findByVersionEquals 对应的 SQL 是 "where Version=?"。
  4. Is 系列,findByNames 对应的是 SQL 是 "where name=?"。
  5. Between --- 等价于 SQL 中的 between 关键字,比如 findBySalaryBetween(int max, int min);
  6. LessThan --- 等价于 SQL 中的 "<",比如 findBySalaryLessThan(int max);
  7. GreaterThan --- 等价于 SQL 中的">",比如 findBySalaryGreaterThan(int min);
  8. IsNull --- 等价于 SQL 中的 "is null",比如 findByUsernameIsNull();
  9. IsNotNull --- 等价于 SQL 中的 "is not null",比如 findByUsernameIsNotNull();
  10. NotNull --- 与 IsNotNull 等价;
  11. Like --- 等价于 SQL 中的 "like",比如 findByUsernameLike(String user);
  12. NotLike --- 等价于 SQL 中的 "not like",比如 findByUsernameNotLike(String user);
  13. OrderBy --- 等价于 SQL 中的 "order by",比如 findByUsernameOrderBySalaryAsc(String user);
  14. Not --- 等价于 SQL 中的 "! =",比如 findByUsernameNot(String user);
  15. In --- 等价于 SQL 中的 "in",比如 findByUsernameIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;
  16. NotIn --- 等价于 SQL 中的 "not in",比如 findByUsernameNotIn(Collection userList) ,方法的参数可以是 Collection 类型,也可以是数组或者不定长参数;

还有更多既定的命名规则可以参考官方文档。由于这些既定的方法比较局限,不一定能满足所有的开发需求。所以 Spring Data JPA 也支持原生 SQL 自定义,如之前编写的获取最新英雄的查询。

如果涉及修改数据时,则要使用 @Modifying 注解。

继续编写 Service 。 

package org.example.service;

import org.example.dao.HeroDao;
import org.example.entity.Hero;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class HeroService {
    @Autowired
    HeroDao heroDao;
    //添加英雄
    public void addHero(Hero hero){
        heroDao.save(hero);
    }

    //分页获取英雄列表
    public Page getHeroByPage(Pageable pageable){
        return heroDao.findAll(pageable);
    }

    //根据名称查询英雄数据
    public List getHerosByNameStartingWith(String name){
        return heroDao.getHeroByNameContaining(name);
    }

    //获取最新被创建的英雄
    public Hero getLatestHero(){
        return heroDao.getLateHero();
    }
}

其中 save 方法是 JpaRespository 自带的,用于存储数据。而分页时传递 Pageable 对象就可以实现。该对象包含了总记录数、总页数、每页记录数等。 

创建 Controller 文件,命名为 HeroController.java ,具体实现如下。

package org.example.controller;

import org.example.entity.Hero;
import org.example.service.HeroService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class HeroController {
    @Autowired
    HeroService heroService;
    @GetMapping("/findAllHeros")
    public Page findAll(){
        // 第二页 2页(从0开始计数) 每页大小最大为 3
        PageRequest pageable = PageRequest.of(2,3);
        Page page = heroService.getHeroByPage(pageable);
        return page;
    }

    //查询
    @GetMapping("/findHeroesByName")
    public List searchHerosByName(@PathVariable String name){
        List heroes = heroService.getHerosByNameStartingWith(name);
        return heroes;
    }

    @GetMapping("/getLatestHero")
    public Hero getLatesHero(){
        return heroService.getLatestHero();
    }
}

别忘了 hibernate 依赖,不然可能无法启动,例如:dao that could not be found。

        
            org.hibernate
            hibernate-core
        

成功运行。 

Spring Data JPA 整合_第1张图片

Spring Data JPA 整合_第2张图片

3.Spring Data JPA 扩展封装 

Spring Data JPA 的扩展,其实就是对于 dao 层代码的扩展,因为所有 dao 层接口都是继承自 JpaRespository 接口,再不添加更多代码就能继承默认的方法。然后很多时候,由于业务需要,开发者需要编写很多自定义的数据库操作方法,这就是扩展封装。 

这种封装一般有以下两种:

  • 一是封装成一个自定义的扩展实现类,去实现之前编写好的 Dao 层接口。命名方面是 EntityNameRespositoryImpl 格式,并把相应的方法实现。
  • 二是直接在 Dao 层接口中添加自定义的方法,针对业务需求自行定义方法名和参数,同时在相应的 Mapper 文件中编写 SQL 语句来实现该方法。这种封装方式更加灵活,但也需要开发者对 SQL 语句的编写有一定的熟练度。其中,命名方式可以是在原先的方法名上添加一些前缀、后缀或其他修饰词。

由于编写了 HeroDao.java ,那么基于 HeroDao 进行封装。

package org.example.dao.impl;

import org.example.dao.HeroDao;
import org.example.entity.Hero;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import java.util.List;
import java.util.Optional;

public class HeroRespositoryImpl implements HeroDao {

    @Override
    public List findAll() {
        // other codes
        return findAll();
    }

    @Override
    public List findAll(Sort sort) {
        return null;
    }

    @Override
    public Page findAll(Pageable pageable) {
        return null;
    }

    @Override
    public List findAllById(Iterable iterable) {
        return null;
    }

    @Override
    public long count() {
        return 0;
    }

    @Override
    public void deleteById(Integer integer) {

    }

    @Override
    public void delete(Hero hero) {

    }

    @Override
    public void deleteAll(Iterable iterable) {

    }

    @Override
    public void deleteAll() {

    }

    @Override
    public  S save(S s) {
        return null;
    }

    @Override
    public  List saveAll(Iterable iterable) {
        return null;
    }

    @Override
    public Optional findById(Integer integer) {
        return Optional.empty();
    }

    @Override
    public boolean existsById(Integer integer) {
        return false;
    }

    @Override
    public void flush() {

    }

    @Override
    public  S saveAndFlush(S s) {
        return null;
    }

    @Override
    public void deleteInBatch(Iterable iterable) {

    }

    @Override
    public void deleteAllInBatch() {

    }

    @Override
    public Hero getOne(Integer integer) {
        return null;
    }

    @Override
    public  Optional findOne(Example example) {
        return Optional.empty();
    }

    @Override
    public  List findAll(Example example) {
        return null;
    }

    @Override
    public  List findAll(Example example, Sort sort) {
        return null;
    }

    @Override
    public  Page findAll(Example example, Pageable pageable) {
        return null;
    }

    @Override
    public  long count(Example example) {
        return 0;
    }

    @Override
    public  boolean exists(Example example) {
        return false;
    }

    @Override
    public List getHeroByNameContaining(String name) {
        if (name == "test") {
            return null;
        }
        return getHeroByNameContaining(name);
    }

    @Override
    public Hero getLateHero() {
        return null;
    }
}

根据业务封装基类接口,如一个公共开发使用的接口,命名为 BaseRespository.java 。

package org.example.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;

import java.io.Serializable;
import java.util.List;

@NoRepositoryBean
public interface BaseRespository extends JpaRepository, JpaSpecificationExecutor {
    /**
     * 查询多个属性
     * 返回 List 对象形式的 List
     *
     * @param sql 原生 SQL 语句
     * @param obj Class 格式对象
     * @return
     */
    List sqlObjectList(String sql,Object obj);

    /**
     * 查询多个属性
     * 返回 List 数组形式的 List
     * 
     * @param sql 原生 sql语句
     * @return
     */
    List sqlArrayList(String sql);

    /**
     * 查询单个个数
     * 返回 List 对象形式的List
     * @param sql 原生 SQL 语句
     * @return
     */
    List sqlStringleList(String sql);
}

其中,@NoRepositoryBean 注解说明这个接口不是一个标准的 Repository ,Spring 不会把它当作一个 Repository 处理,使其不要被 Spring Data JPA 自动扫描并创建相应的仓库实现。这个接口也继承了  JpaSpecificationExecutor ,可以使用 Specification 的复杂查询方式。

进一步可以编写实现 BaseRespository 的类,命名为:MyBaseJpaRepository.java 。

package org.example.dao.impl;

import org.example.dao.BaseRespository;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.io.Serializable;
import java.util.List;

/**
 * 用于公共的 Jpa 的接口实现类
 *
 * @param 
 * @param 
 */
public class MyBaseJpaRepository extends SimpleJpaRepository implements BaseRespository {
    private final EntityManager entityManager;
    private Class myclass;

    public MyBaseJpaRepository(Class domainClass, EntityManager entityManager) {
        super(domainClass, entityManager);
        this.entityManager = entityManager;
        this.myclass = domainClass;
    }

    @Override
    public List sqlObjectList(String sql, Object obj) {
        Query query = entityManager.createNativeQuery(sql, obj.getClass());
        List list = query.getResultList();
        return list;
    }

    //略
    @Override
    public List sqlArrayList(String sql) {
        return null;
    }

    //略
    @Override
    public List sqlStringleList(String sql) {
        return null;
    }
}

在实际工作中只需要编写 JPA 接口去阶乘这个 MyBaseJpaRepository 类就可以实现业务的需要。

Spring Data JPI 封装了许多接口,可以提供默认的 CURD 和数据库操作方法。如果在业务不复杂的 RESTful API 下,完全可以使用原生的 JpaRepository 或者 CrudJpaRepository 接口来编写接口即可。

如果业务更加复杂的情况下,可以考虑通过编写扩展接口来实现个性化的需求。Spring Data JPA 作为一款封装良好的 JPA 实现框架,可以很好地减少冗余代码,让开发者不必过多地去关注数据库持久层的底层操作和管理。

常用接口的继承关系

Spring Data JPA 整合_第3张图片

补充 

JpaSpecificationExecutor 是 Spring Data JPA 提供的一个扩展接口,用于支持基于 Criteria API 的动态查询。

JpaSpecificationExecutor 接口中定义了一个 Specification 接口和一组相关的查询方法。Specification 接口表示一个查询规范,可以用于构建动态查询条件。你可以在查询方法中传入 Specification 对象来指定查询规范,从而查询符合要求的数据。

以下是一个使用 JpaSpecificationExecutor 的示例:

public interface CarRepository extends JpaRepository, JpaSpecificationExecutor {
}

public class CarSpecification {
    public static Specification withModel(String model) {
        return (root, query, cb) -> cb.equal(root.get("model"), model);
    }

    public static Specification withColor(String color) {
        return (root, query, cb) -> cb.equal(root.get("color"), color);
    }
}

public class CarService {

    @Autowired
    private CarRepository carRepository;

    public List findCarsByCriteria(String model, String color) {
        Specification spec = Specification.where(null);
        if (model != null && !model.isEmpty()) {
            spec = spec.and(CarSpecification.withModel(model));
        }
        if (color != null && !color.isEmpty()) {
            spec = spec.and(CarSpecification.withColor(color));
        }
        return carRepository.findAll(spec);
    }
}

在这个例子中,CarRepository 继承了 JpaRepository 和 JpaSpecificationExecutor 接口,就可以使用 findAll(Specification) 方法进行动态查询。

CarSpecification 类定义了两个 Specification 对象:withModel 用于匹配指定车型,withColor 用于匹配指定颜色。这些 Specification 对象可以组合起来,用于构建复杂的查询条件。

在 CarService 类中,我们可以通过传入 model 和 color 参数来动态构建查询条件,并通过 findAll(spec) 方法查询符合条件的数据。

总之,JpaSpecificationExecutor 接口是 Spring Data JPA 提供的一个扩展接口,用于支持基于 Criteria API 的动态查询。通过使用 Specification 接口,你可以组合多个查询条件,构建高度动态的查询语句。 

总的来说,JpaSpecificationExecutor 接口的动态查询机制确实与 builder 模式有些类似,都是通过组合多个条件构建细粒度的对象。 


总结

通过简单的介绍,我们已经简单的对 JPA 有了基本的了解。 

你可能感兴趣的:(spring-boot,spring,boot,java,restful,后端)