Spring-Data-Jpa动态查询(Specification)

JPA允许基于Criteria对象进行按条件查询,而SpringDataJpa提供了一个Specification接口,Specification接口封装了JPA的Criteria查询条件,从而可以通过此接口更加方便地使用Criteria查询,Specification接口的源代码如下。

import static org.springframework.data.jpa.domain.SpecificationComposition.*;
import java.io.Serializable;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.lang.Nullable;

public interface Specification<T> extends Serializable {
	long serialVersionUID = 1L;
	static <T> Specification<T> not(@Nullable Specification<T> spec) {
		return spec == null //
				? (root, query, builder) -> null//
				: (root, query, builder) -> builder.not(spec.toPredicate(root, query, builder));
	}

	@Nullable
	static <T> Specification<T> where(@Nullable Specification<T> spec) {
		return spec == null ? (root, query, builder) -> null : spec;
	}

	@Nullable
	default Specification<T> and(@Nullable Specification<T> other) {
		return composed(this, other, (builder, left, rhs) -> builder.and(left, rhs));
	}

	@Nullable
	default Specification<T> or(@Nullable Specification<T> other) {
		return composed(this, other, (builder, left, rhs) -> builder.or(left, rhs));
	}

	@Nullable
	Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
}

Specification接口提供了一个toPredicate方法用来构造查询条件。值得注意的是:如果自己定义的数据访问接口希望使用Specification接口的规范,则必须实现JpaSpecificationExecutor接口,JpaSpecificationExecutor接口不属于Repository体系,它实现了一组JPACriteria查询相关的方法,查询JpsSpecificationExecutor接口的源代码如下。

import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.lang.Nullable;

public interface JpaSpecificationExecutor<T> {

	//根据Specification接口封装的查询条件,查询出单一实例
	Optional<T> findOne(@Nullable Specification<T> spec);

	//根据Specification接口封装的查询条件,查询出满足条件的所有对象数据,返回的是List集合
	List<T> findAll(@Nullable Specification<T> spec);

	/**
	 * 根据Specification接口封装的查询条件以及Pageable封装的分页及排序规则查询出满足条件
     * 的对象数据,返回的是Page对象,Page对象中封装了查询出的数据信息以及分页信息。
	 */
	Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);

	//根据Specification接口封装的查询条件以及Sort封装的排序规则查询出满足条件的所有对象数据,返回的是List集合
	List<T> findAll(@Nullable Specification<T> spec, Sort sort);

	//根据Specification接口封装的查询条件,查询出满足此条件的数据总数
	long count(@Nullable Specification<T> spec);
}

从JpaSpecificationExecutor的方法中可以看出,一般通过创建Specification的匿名内部类对象来封装Criteria条件查询信息然后交由JpaSpecification提供的方法进行相关数据的查询操作。

  1. Optional<T> findOne(@Nullable Specification<T> spec);根据Specification接口封装的查询条件,查询出单一实例。
  2. List<T> findAll(@Nullable Specification<T> spec);根据Specification接口封装的查询条件,查询出满足条件的所有对象数据,返回的是List集合。
  3. Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable);根据Specification接口封装的查询条件以及Pageable封装的分页及排序规则查询出满足条件的对象数据,返回的是Page对象,Page对象中封装了查询出的数据信息以及分页信息。
  4. List<T> findAll(@Nullable Specification<T> spec, Sort sort);根据Specification接口封装的查询条件以及Sort封装的排序规则查询出满足条件的所有对象数据,返回的是List集合。
  5. long count(@Nullable Specification<T> spec);根据Specification接口封装的查询条件,查询出满足此条件的数据总数。

所以在实际项目开发中,如果希望使用Specification查询,数据访问层的定义通常如下代码所示。

public interface StuRepository1 extends JpaRepository<Stu, Integer>, JpaSpecificationExecutor<Stu> {
    
}

接下来,以实力的方式来详细讲解使用Specification的查询,分页,动态查询等操作。

1、创建一个springboot项目,在pom.xml中加入对应的依赖,在application.properties文件中配置数据源和JPA相关的属性。

2、创建持久化类。

在项目中新建4个包,分别为entity(放置持久化类)、controller(控制器)、repository(定义数据访问接口的包)、service(业务逻辑处理类)。在entity包中创建两个持久化类:Stu.java和Clazz.java,代码与关联查询实例代码一致,可以参考关联查询中的代码。

3、定义数据访问层接口。

之后在repository包下新建一个接口,命名为StuRepository1,该接口继承JpaRepository接口,以持久化对象Stu作为JpaRepository的第一个类型参数,表示当前所操作的持久化对象类型,Integer作为JpaRepository的第二个类型参数,用于指定ID类型,同时创建一个接口名称为ClazzRepository1,继承JpaRepository的接口,用于访问班级信息的数据。完整代码如下。

ClazzRepository1.java接口

import com.mcy.springdatajpa.entity.Clazz;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface ClazzRepository1 extends JpaRepository<Clazz, Integer>, JpaSpecificationExecutor<Clazz> {
    
}

以上数据访问层接口用于对班级表进行相关的CRUD操作,同时由于实现了JpaSpecificationExecutor接口,ClazzRepository接口也将拥有JpaSpecificationExecutor接口提供的功能。

StuRepository1.java接口

import com.mcy.springdatajpa.entity.Stu;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface StuRepository1 extends JpaRepository<Stu, Integer>, JpaSpecificationExecutor<Stu> {

}

4、定义业务层类

在service包下新建一个SchoolService1.java类,其中主要的几个查询方法有。

根据性别查询学生信息方法。

@SuppressWarnings("serial")
public List<Map<String, Object>> getStusBySex(char sex){
    List<Stu> stus = stuRepository1.findAll(new Specification<Stu>(){
        @Override
        public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            //root.get("sex")表示获取sex这个字段名称,equal表示执行equal查询
            //相当于select s from Stu s where s.sex = ?1
            Predicate p1 = cb.equal(root.get("sex"), sex);
            return p1;
        }
    });
    List<Map<String, Object>> results = new ArrayList<>();
    //遍历查询出的学生对象,提取姓名,年龄,性别信息
    for(Stu s: stus){
        Map<String, Object> stu = new HashMap<>();
        stu.put("name", s.getName());
        stu.put("age", s.getAge());
        stu.put("sex", s.getSex());
        results.add(stu);
    }
    return results;
}

动态查询学生信息:可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息,如果没有传入参数,默认查询所有的学生信息。

@SuppressWarnings("serial")
public List<Map<String, Object>> getStusByDynamic(Stu stu){
    List<Stu> stus = stuRepository1.findAll(new Specification<Stu>() {
        @Override
        public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            //本集合用于封装查询条件
            List<Predicate> predicates = new ArrayList<>();
            if(stu != null){
                //是否传入用于查询的姓名
                if(!StringUtils.isEmpty(stu.getName())){
                    predicates.add(cb.like(root.get("name"), "%"+stu.getName()+"%"));
                }
                //判断是否传入查询的地址
                if(!StringUtils.isEmpty(stu.getAddress())){
                    predicates.add(cb.like(root.get("address"), "%"+stu.getAddress()+"%"));
                }
                //判断是否传入查询的性别
                if(stu.getSex() != '\0'){
                    predicates.add(cb.equal(root.get("sex"), stu.getSex()));
                }
                //判断是否传入用于查询的班级信息
                if(stu.getClazz() != null && !StringUtils.isEmpty(stu.getClazz().getName())){
                    root.join("clazz", JoinType.INNER);
                    Path<String> clazzName = root.get("clazz").get("name");
                    predicates.add(cb.equal(clazzName, stu.getClazz().getName()));
                }
            }
            return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
        }
    });
    List<Map<String, Object>> results = new ArrayList<>();
    //遍历查询出的学生对象,提取姓名,年龄,性别信息
    for(Stu s : stus){
        Map<String, Object> stuMap = new HashMap<>();
        stuMap.put("name", stu.getName());
        stuMap.put("age", stu.getAge());
        stuMap.put("sex", stu.getSex());
        stuMap.put("address", stu.getAddress());
        stuMap.put("clazzName", stu.getClazz().getName());
        results.add(stuMap);
    }
    return results;
}

分页查询某个班级的学生信息 @param clazzName 代表班级名称,@param pageIndex 代表当前查询第几页 ,@param pageSize 代表每页查询的最大数据量。

@SuppressWarnings("serial")
public Page<Stu> getStusByPage(String clazzName, int pageIndex, int pageSize){
    //指定排序参数对象:根据id,进行降序查询
    Sort sort = Sort.by(Sort.Direction.DESC, "id");
    //Specification动态查询
    Specification<Stu> spec = buildSpec(clazzName, pageIndex, pageSize);
    //分页查询学生信息,返回分页实体对象数据
    //pages对象中包含了查询出来的数据信息以及与分页相关的信息
    Page<Stu> pages = stuRepository1.findAll(spec, PageRequest.of(pageIndex-1, pageSize, sort));
    return pages;
}

private Specification<Stu> buildSpec(String clazzName, int pageIndex, int pageSize) {
    Specification<Stu> spec = new Specification<Stu>() {
        @Override
        public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            root.join("clazz", JoinType.INNER);
            Path<String> cn = root.get("clazz").get("name");
            Predicate p1 = cb.equal(cn, clazzName);
            return p1;
        }
    };
    return spec;
}

SchoolService1.java中全部代码。

import com.mcy.springdatajpa.entity.Clazz;
import com.mcy.springdatajpa.entity.Stu;
import com.mcy.springdatajpa.repository.ClazzRepository1;
import com.mcy.springdatajpa.repository.StuRepository1;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class SchoolService1 {
    //注入数据访问层接口对象
    @Resource
    private StuRepository1 stuRepository1;
    @Resource
    private ClazzRepository1 clazzRepository1;

    @Transactional
    public void saveClazzAll(List<Clazz> clazzes){
        clazzRepository1.saveAll(clazzes);
    }

    @Transactional
    public void saveStuAll(List<Stu> stu){
        stuRepository1.saveAll(stu);
    }

    /**
     * 根据性别查询学生信息
     * @param sex
     * @return
     */
    @SuppressWarnings("serial")
    public List<Map<String, Object>> getStusBySex(char sex){
        List<Stu> stus = stuRepository1.findAll(new Specification<Stu>(){
            @Override
            public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                //root.get("sex")表示获取sex这个字段名称,equal表示执行equal查询
                //相当于select s from Stu s where s.sex = ?1
                Predicate p1 = cb.equal(root.get("sex"), sex);
                return p1;
            }
        });
        List<Map<String, Object>> results = new ArrayList<>();
        //遍历查询出的学生对象,提取姓名,年龄,性别信息
        for(Stu s: stus){
            Map<String, Object> stu = new HashMap<>();
            stu.put("name", s.getName());
            stu.put("age", s.getAge());
            stu.put("sex", s.getSex());
            results.add(stu);
        }
        return results;
    }

    /**
     * 动态查询学生信息:可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息
     * 如果没有传入参数,默认查询所有的学生信息
     * @param stu
     * @return
     */
    @SuppressWarnings("serial")
    public List<Map<String, Object>> getStusByDynamic(Stu stu){
        List<Stu> stus = stuRepository1.findAll(new Specification<Stu>() {
            @Override
            public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                //本集合用于封装查询条件
                List<Predicate> predicates = new ArrayList<>();
                if(stu != null){
                    //是否传入用于查询的姓名
                    if(!StringUtils.isEmpty(stu.getName())){
                        predicates.add(cb.like(root.get("name"), "%"+stu.getName()+"%"));
                    }
                    //判断是否传入查询的地址
                    if(!StringUtils.isEmpty(stu.getAddress())){
                        predicates.add(cb.like(root.get("address"), "%"+stu.getAddress()+"%"));
                    }
                    //判断是否传入查询的性别
                    if(stu.getSex() != '\0'){
                        predicates.add(cb.equal(root.get("sex"), stu.getSex()));
                    }
                    //判断是否传入用于查询的班级信息
                    if(stu.getClazz() != null && !StringUtils.isEmpty(stu.getClazz().getName())){
                        root.join("clazz", JoinType.INNER);
                        Path<String> clazzName = root.get("clazz").get("name");
                        predicates.add(cb.equal(clazzName, stu.getClazz().getName()));
                    }
                }
                return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
            }
        });
        List<Map<String, Object>> results = new ArrayList<>();
        //遍历查询出的学生对象,提取姓名,年龄,性别信息
        for(Stu s : stus){
            Map<String, Object> stuMap = new HashMap<>();
            stuMap.put("name", s.getName());
            stuMap.put("age", s.getAge());
            stuMap.put("sex", s.getSex());
            stuMap.put("address", s.getAddress());
            stuMap.put("clazzName", s.getClazz().getName());
            results.add(stuMap);
        }
        return results;
    }

    /***
     * 分页查询某个班级的学生信息
     * @param clazzName 代表班级名称
     * @param pageIndex 代表当前查询第几页
     * @param pageSize  代表每页查询的最大数据量
     * @return
     */
    @SuppressWarnings("serial")
    public Page<Stu> getStusByPage(String clazzName, int pageIndex, int pageSize){
        //指定排序参数对象:根据id,进行降序查询
        Sort sort = Sort.by(Sort.Direction.DESC, "id");
        //Specification动态查询
        Specification<Stu> spec = buildSpec(clazzName, pageIndex, pageSize);
        //分页查询学生信息,返回分页实体对象数据
        //pages对象中包含了查询出来的数据信息以及与分页相关的信息
        Page<Stu> pages = stuRepository1.findAll(spec, PageRequest.of(pageIndex-1, pageSize, sort));
        return pages;
    }

    private Specification<Stu> buildSpec(String clazzName, int pageIndex, int pageSize) {
        Specification<Stu> spec = new Specification<Stu>() {
            @Override
            public Predicate toPredicate(Root<Stu> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                root.join("clazz", JoinType.INNER);
                Path<String> cn = root.get("clazz").get("name");
                Predicate p1 = cb.equal(cn, clazzName);
                return p1;
            }
        };
        return spec;
    }
}

在业务层中需要注入数据访问层对象,在上述代码中我们是通过@Resource注解将StuRepository接口以及ClazzRepository接口对应的实现类对象注入的,同时在业务层方法中定义三个方法,分别实现了对班级信息的条件查询,动态SQL语句以及分页查询。

5、定义分页的页面数据对象

在项目下新建一个包,命名为custom,在custom下新建一个java类,命名为PageData.java,此类用于封装分页查询出的数据信息,主要包含了当前页码(pageIndex)、满足查询条件下用于分页的数据总量(totalCount)、当前条件下总共可以分的总页数(pageSize)、当前页码展示的数据量(pageNum)以及查询出的数据信息(stuDatas)。详细代码如下。

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 定义一个对象用于封装一页的数据
 */
public class PageData {
    //定义一个变量用于存放当前页码
    private int pageIndex;
    //定义一个变量用于保存满足查询条件下用于分页的数据总量
    private long totalCount;
    //定义一个变量用于保存当前条件下可以分页的总页数
    private int pageSize;
    //定义一个变量用于保存当前页码查询出的数据总量
    private int pageNum;
    //定义一个变量用于保存当前查询出来的学生信息
    private List<Map<String, Object>> stuDates = new ArrayList<>();

    public int getPageIndex() {
        return pageIndex;
    }

    public void setPageIndex(int pageIndex) {
        this.pageIndex = pageIndex;
    }

    public long getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(long totalCount) {
        this.totalCount = totalCount;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public List<Map<String, Object>> getStuDates() {
        return stuDates;
    }

    public void setStuDates(List<Map<String, Object>> stuDates) {
        this.stuDates = stuDates;
    }
}

6、定义控制器类。

在controller包下新建一个StuController1类,代码如下(访问方法)。

import com.mcy.springdatajpa.custom.PageData;
import com.mcy.springdatajpa.entity.Clazz;
import com.mcy.springdatajpa.entity.Stu;
import com.mcy.springdatajpa.service.SchoolService1;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/stu1")
public class StuController1 {
    //注入SchoolService1
    @Resource
    private SchoolService1 schoolService1;

    //保存,初始化数据
    @RequestMapping("/save")
    public String save(){
        Clazz clazz1 = new Clazz("软件工程1班");
        Clazz clazz2 = new Clazz("软件工程2班");
        //保存班级对象数据
        List<Clazz> clazzs = new ArrayList<>();
        clazzs.add(clazz1);
        clazzs.add(clazz2);
        schoolService1.saveClazzAll(clazzs);

        Stu stu1 = new Stu("张三", "湖北", 20, '男', clazz1 );
        Stu stu2 = new Stu("李四", "湖北", 18, '女', clazz1 );
        Stu stu3 = new Stu("诸葛亮", "湖北", 19, '女', clazz1 );
        Stu stu4 = new Stu("刘备", "湖北", 21, '男', clazz2 );
        Stu stu5 = new Stu("张飞", "湖北", 32, '女', clazz2 );
        Stu stu6 = new Stu("关于", "湖北", 17, '男', clazz2 );

        List<Stu> stus = new ArrayList<>();
        stus.add(stu1);
        stus.add(stu2);
        stus.add(stu3);
        stus.add(stu4);
        stus.add(stu5);
        stus.add(stu6);
        schoolService1.saveStuAll(stus);
        return "保存学生对象成功";
    }

    @RequestMapping("/getStusBySex")
    public List<Map<String, Object>> getStusBySex(char sex){
        return schoolService1.getStusBySex(sex);
    }
    
    //动态查询学生信息
    //可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息
    @RequestMapping("/getStusByDynamic")
    List<Map<String, Object>> getStusByDynamic(Stu stu){
        return schoolService1.getStusByDynamic(stu);
    }
    
    //分页查询摸个班级的学生信息
    @RequestMapping("/getStusBypage")
    PageData getStusByPage(String clazzName, int pageIndex, int pageSize){
        //分页查询某个班级的学生信息
        Page<Stu> page = schoolService1.getStusByPage(clazzName, pageIndex, pageSize);
        //对查询出来的结果数据进行分析
        List<Stu> stus = page.getContent();
        List<Map<String, Object>> stuDatas = new ArrayList<>();
        for(Stu s: stus){
            Map<String, Object> stuMap = new HashMap<>();
            stuMap.put("name", s.getName());
            stuMap.put("id", s.getId());
            stuMap.put("age", s.getAge());
            stuMap.put("sex", s.getSex());
            stuMap.put("address", s.getAddress());
            stuMap.put("clazzName", clazzName);
            stuDatas.add(stuMap);
        }
        //将分页查询出的结果数据进行分析
        //然后把数据存入PageData对象中响应给浏览器展示
        PageData data = new PageData();
        data.setStuDates(stuDatas);
        data.setPageIndex(page.getNumber()+1);
        data.setPageSize(page.getTotalPages());
        data.setTotalCount(page.getTotalElements());
        data.setPageNum(page.getSize());
        return data;
    }
}

在控制器类中定义了四个方法,其中save方法用于保存数据,也作为初始化的测试数据使用。getStusBySex(char sex)方法用于根据性别查询学生信息。getStusByDynamic(Stu stu)方法用于根据Stu对象中的姓名(模糊匹配),地址查询(模糊匹配),性别,班级信息动态查询满足条件的学生信息,如果没有任何查询条件则查询出系统所有的学生信息。getStusByPage(String clazzName, int pageIndex, int pageSize)方法用于分页查询某个班级的学生信息,返回的是一个分页信息Page<Stu>,其中ClazzName是指班级参数,pageIndex是指查询的当前页码,pageSize是指每页最多显示多少条数据。

7、测试应用

启动MySQL数据库,在数据库中创建名为jpa的数据库。springboot项目启动后,JPA会在数据库中自动创建持久化类对应的tb_stu和tb_clazz表。

测试添加学生和班级信息,在浏览器中输入http://localhost:8080/stu1/save请求会提交到StuController1类的save方法进行处理,执行完成返回“保存学生对象成功”,查询数据库中的数据,如图:

tb_stu表。

tb_clazz表。

测试根据性别查询学生的信息,在浏览器中输入http://localhost:8080/stu1/getStusBySex?sex=女,请求会提交到StuController1类的getStusBySex方法进行处理,执行完成以后,查询出的学生信息如图。

测试动态查询学生信息,可以根据学生对象的姓名(模糊匹配),地址查询(模糊匹配),性别,班级查询学生信息,在浏览器中输入http://localhost:8080/stu1/getStusByDynamic?clazz.name=软件工程1班&sex=女,请求会提交到StuController1类的getStusByDynamic方法进行处理,此地址是通过学生的班级和性别来查询对应的学生信息的,执行完成以后,查询出的学生信息如图。

测试分页查询某个班级下的学生信息,在浏览器中输入http://localhost:8080/stu1/getStusBypage?clazzName=软件工程1班&pageIndex=1&pageSize=2,请求会提交给StuController1类的getStusBypage方法进行处理,地址是降序查询“软件工程1班”的第1页数据,每页最多展示2条数据,执行完成后,查询出的学生信息如图。

可以看出pageIndex表示这是第1页数据,totalCount表示“软件工程班当前总共有3条学生记录”,pageSize表示一页最多展示2条数据,pageNum表示当前总共有2页数据,stuDates即查询出的当前页数据。

在浏览器中输入http://localhost:8080/stu1/getStusBypage?clazzName=软件工程1班&pageIndex=2&pageSize=2,来查询第2页数据,执行完成后结果如图。

从图中可以看到,pageIndex表示这是第2页数据,totalCount表示“软件工程班当前总共有3条学生记录”,pageSize表示一页最多展示2条数据,pageNum表示当前总共有2页数据,stuDates即查询出的当前页数据。大家可以自行更换条件进行测试。

案例代码下载链接:https://github.com/machaoyin/spring-data-jpa

上一篇: Spring-Data-Jpa关联查询

下一篇:Spring-Data-Jpa实现继承实体类详解

发布了112 篇原创文章 · 获赞 223 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/qq_40205116/article/details/103329592