Spring Data JPA多表查询的几种方法

Spring Data JPA多表查询的几种方法

前言

公司目前在ORM框架上选型只有两个选择

MyBatis-Plus

Spring Data JPA

相信很多人会选择MyBatis-Plus(MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生),主要因为JPA对多表复杂查询很不友好,特别是现在建表很多不用外键,所以很多人使用JPA查询就很不方便,但如果有人说:不行,我一定要用JPA,怎么办呢?所以本文就简单说明一下自己所知道的几种JPA的多表复杂查询方法。

讲解前先创建两张表一张student(学生)表和一张teacher(教师)表,对应两个实体类:

@Entity
@Data
@Accessors(chain = true)
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @ApiModelProperty("学生ID")
    @Column(insertable = false, updatable = false, columnDefinition = "INT UNSIGNED  COMMENT '学生ID'")
    private Integer studentId;

    @ApiModelProperty("学生名")
    @Column(length = 50, columnDefinition = "VARCHAR(255) NOT NULL COMMENT '学生名'")
    private String studentName;

}
@Entity
@Data
@Accessors(chain = true)
public class Teacher {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @ApiModelProperty("教师ID")
    @Column(insertable = false, updatable = false, columnDefinition = "INT UNSIGNED COMMENT '教师ID'")
    private Integer teacherId;

    @ApiModelProperty("教师名")
    @Column(length = 50, columnDefinition = "VARCHAR(255) NOT NULL COMMENT '教师名'")
    private String teacherName;

}

两张表目前是没有什么关联的

一、单纯使用注解

相信大家去百度或者谷歌第一种推荐的就是使用自带的注解:

  • @OneToOne
单向一对一

如果一个学生对应一个老师,那么就需要在教师表或者学生表添加对应关联的ID,而使用注解就只需在学生表添加

@OneToOne
@JoinColumn(name = "teacher_id", referencedColumnName = "teacherId")
//设置生成的外键名称为teacher_id,对应Teacher类中的teacherId
private Teacher teacher;

自动创建表你会发现student表中会增加teacher_id的外键,如果不想自动创建外键可以在@JoinColumn中加入 foreignKey = @ForeignKey(name = “none”, value = ConstraintMode.NO_CONSTRAINT),表示不创建外键。这时查询单独查Student也会查询到Teacher。

双向一对一

有时我想根据Teacher查询到对应的Student,那么需要在Teacher类中添加

@OneToOne(mappedBy = "teacher")//对应Student中的Teacher对象的名字
private Student student;

这样就可以两边都查询到对应的信息。

  • @OneToMany
  • @ManyToOne

一对多和多对一和一对一其实基本一样,只不过把对象换成List,查询时就能获取对应的集合,这里不多描述。

  • @ManyToMany

多对多其实和上面的也基本一样,只不过这是不能使用单个字段,需要创建一张中间表进行映射,需要加入把Student类中的teacher修改:

@ManyToMany
@JoinTable(name = "student_teacher_inner",  //创建中间表student_teacher_inner
joinColumns = {@JoinColumn(name = "student_id", referencedColumnName = "studentId")},
inverseJoinColumns = {@JoinColumn(name =  "teacher_id",referencedColumnName = "teacherId")})
private List<Teacher> teacher;

@JoinTable

joinColumns元素描述关系所有方在中间表的连接列,inverseJoinColumns元素指定了反方在中间表的连接列。

对于这种注解相信大家都很熟悉了,我这边就不仔细讲解了,主要注意的点有:

  • 自动建立外键

如果用这种方法查询就避免不了建外键,而外键主要是保证数据库数据的完整性和一致性,对于使用外键有好处也有坏处,主要坏处是操作数据方面有了很多的限制,增加了维护成本,需要看你怎么取舍,但很多项目都是业务进行控制数据库的完整性和一致性,不需要建立外键。

  • 不能分页查询

如果是一对多,多对多,那么只能一次查询全部对应的集合,不能使用分页。

  • 双向关联会出现死循环

例如你查询Student对象,哪里Student里有Teacher,而Teacher里又有Student,所以会一直循环下去,如果用toString方法或转成JSON那么是必然会造成死循环的,所以利用sping boot等框架将后台数据返回给前台也是同理,需要Student对象的teacher变量上加上注解:

@JsonIgnoreProperties(value = {"student"})

表明忽略字段名称student,同理Teacher对象也需要加上对应的注解

  • 运行时报错

No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer
and no properties discovered to create BeanSerializer

需要在对象上加上

@JsonIgnoreProperties(value = {"handler", "hibernateLazyInitializer", "fieldHandler"})
  • 复杂查询需要额外使用Specification

如果两张表都是双向多对多关系,那么我想根据教师查出学生名字等于“学生3”的Student集合,这时就需要

Specification<Teacher> specification = Specification.where((root, query, cb) -> {
    Join<Teacher, student> join = root.join("student", JoinType.LEFT);
    return cb.equal(join.get("studentName"), "学生3");
});
List<Teacher> all = teacherDao.findAll(specification);

这里只写了实现代码,具体解析就不详细说明

二、使用@Query注解

如果觉得上面的方法很难用,而且没有分页查询,那么可以使用@Query注解查询,需要说明一下@Query默认使用hsql语言进行查询,如果不知道hsql是什么可以百度一下,两者最大的区别就是sql是使用表的字段名,而hsql是使用表的字段名,其他语法略有不同。

  • 现在学生教师一对一,在学生表手动创建一个teacher_id字段用来对应教师表,通过学生获取对应的教师
  • 创建一个StudentVO展示获取到的数据
@Data
@Accessors(chain = true)
public class StudentVO {

    @ApiModelProperty("学生ID")
    private Integer studentId;

    @ApiModelProperty("学生名")
    private String studentName;

    @ApiModelProperty("教师ID")
    private Integer teacherId;

    @ApiModelProperty("教师名称")
    private String teacherName;

    public StudentVO(Integer studentId, String studentName, Integer teacherId, 	   String teacherName) {
        this.studentId = studentId;
        this.studentName = studentName;
        this.teacherId = teacherId;
        this.teacherName = teacherName;
    }
    
    public StudentVO() {
    }

}

  • 通过在Dao编写方法获取到对应的值
//没有构造函数会报Unable to locate appropriate constructor on class
@Query(value = "select new com.luwei.model.student.StudentVO(s.studentId,s.studentName ,t.teacherId ,t.teacherName) " +
"from Student s left join Teacher t  on s.teacherId=t.teacherId")
Page<StudentVO> findCustom(Pageable pageable);

这样很简单就能进行联合查询,如果里想用传统的sql语句可以把nativeQuery 改为true表示使用传统sql语句,需要注意的是分页sql语句需要自己实现

//不用驼峰,不然会报找不到列名
@Query(value = "select s.student_id ,s.student_name,t.teacher_id " +
"from tb_student s left join tb_teacher t  on s.teacher_id = t.teacher_id ",
countQuery = "SELECT COUNT(*) FROM tb_student", nativeQuery = true)
Page<Student> findCustom(Pageable pageable);

运用@Query也很容易根据自己的sql语句查询到对应的字段,但有唯一的缺点,不能动态查询,也是致命的缺点,不能像mybatis哪有可以根据你的参数是否为空来进行动态添加,对于参数不确定时需要编写不同的方法,所以如果需要动态查询的不适用使用该注解。那么有没有一种可以动态查询的方法了?答案是有的。

三、使用EntityManager

EntityManager是JPA中用于增删改查的接口,它的作用相当于一座桥梁,连接内存中的java对象和数据库的数据存储。那么如何获得EntityManager对象呢?这取决于你的EntityManger对象的托管方式,主要有以下两种方式:

  • 容器托管的EntityManager对象

容器托管的EntityManager对象最为简单,编程人员不需要考虑EntityManger的连接,释放以及复杂的事务问题等等,所有这些都交给容器来完成。

  • 应用托管的EntityManager对象

应用托管的EntityManager对象,编程人员需要手动地控制它的释放和连接、手动地控制事务等。

我这边使用容器托管,获取EntityManager对象如下:

@PersistenceContext
private EntityManager entityManager;

现在我获取学生名字带有小明的StudentVO的信息就需要这样编写代码:

//需要无参构造方法,编写sql语句
StringBuilder sql = new StringBuilder("select s.student_id StudentId,s.student_name StudentName,t.teacher_id teacherId,t.teacher_name teacherName " +
"from tb_student s left join tb_teacher t  on s.teacher_id = t.teacher_id where 1=1 ");
//查询参数
String studentName = "小明";
if (!TextUtils.isEmpty(studentName)) {
	sql.append(" and s.student_name = ? ");
}

//添加普通sql查询
NativeQueryImpl sqlQuery = entityManager.createNativeQuery(sql).unwrap(NativeQueryImpl.class);

//映射返回类
Query query = sqlQuery.setResultTransformer(Transformers.aliasToBean(StudentVO.class));
//添加查询参数
query.setParameter(1, studentName);

//执行
List<StudentVO> list = query.getResultList();

这样就可以实现查询,如果有动态条件只需拼接好sql就可以了,但这个方法还有一个缺点,就是不能自动帮我们分页,需要我们自己拼接分页条件。有的人就可能会说了,这样每次都需要拼接是不是很麻烦啊,但如果你把所有逻辑抽成一个方法,就会发现其实感觉还可以。

示例

下面示范下分页查询如果学生名字带有小明的学生和教师的信息,返回依然时StudentVO。准备一下抽出来的方法:

  • 把基本查询sql语句转为查询总数的sql语句的方法

其实我们用page,看日志就会发现,每次分页查询都是执行两条sql语句,一条是查询总数的sql,一条原来的sql,是因为框架帮我们把原来的sql语句转成可以查询总数的sql语句,我们也写一个类似的方法

/**
 * 转为查询总数的sql语句
 *
 * @param sql 原来的sql语句
*/
private static String getCountSql(String sql) {
    String substring = null, replace = null;
    if (sql.toLowerCase().startsWith("select")) {
        int from = sql.lastIndexOf("FROM");
        if (from != -1) {
            substring = sql.substring(6, from);

        } else if ((from = sql.lastIndexOf("from")) != -1) {
            substring = sql.substring(6, from);
        }
    }
    if (substring != null) {
        replace = sql.replace(substring, " count(*) ");
    }
    return replace;
}

这方法只是简单的转换,找到select后把后面的参数都换为count(*),这其实并不严谨,仅供参考

  • 拼接分页条件方法

原来的sql语句也需要拼接limt 关键字,

/**
 * 拼接分页字符串
 *
 * @param stringBuilder
 * @param page 多少页
 * @param size 每页多少个
 */
public static StringBuilder createLimit(StringBuilder stringBuilder, Integer page, Integer size) {
    return stringBuilder.append(" LIMIT ").append(page * size).append(" , ").append(size);
}

  • 获取总页数的方法
/**
 * @param total 总数
 * @param size  每页多少个
 * @return 一共有多少页
 */
public static int getTotalPages(int total, int size) {
    return (total + size) / size;
}

  • 查询方法

最后还需把上面这些方法抽成一个查询方法中

 /***
     *
     * @param sql 查询的原sql语句
     * @param page 分页
     * @param clazz 返回的对象
     * @param conditionList where条件集合
     * @param <T>
     * @return 返回page
     */
    private <T> Page<T> findPage(StringBuilder sql, Pageable page, T clazz, List<String> conditionList) {
        //原sql转换查询总数sql语句
        String countSql = getCountSql(sql.toString());
        Query countQuery = entityManager.createNativeQuery(countSql);
        //原sql语句添加limit
        StringBuilder limit = createLimit(sql, page.getPageNumber(), page.getPageSize());
        NativeQueryImpl sqlQuery = entityManager.createNativeQuery(limit.toString()).unwrap(NativeQueryImpl.class);
        Query nativeQuery = sqlQuery.setResultTransformer(Transformers.aliasToBean(clazz.getClass()));

        //添加where条件
        for (int i = 1; i <= conditionList.size(); i++) {
            countQuery.setParameter(i, conditionList.get(i - 1));
            nativeQuery.setParameter(i, conditionList.get(i - 1));
        }
        //查询总数
        int total = ((BigInteger) countQuery.getResultList().get(0)).intValue();
        //原sql查询到内容
        List<T> list = nativeQuery.getResultList();
        //设置分页信息
        return new PageImpl<>(list, page, getTotalPages(total, page.getPageSize()));
    }

最后调用findPage查询方法,就能获取到对应的参数了

//分页参数
Pageable pageRequest = PageRequest.of(1, 2);
//查询的sql语句
StringBuilder sql = new StringBuilder("select s.student_id StudentId,s.student_name StudentName,t.teacher_id teacherId,t.teacher_name teacherName " +
        "from tb_student s left join tb_teacher t  on s.teacher_id = t.teacher_id where 1=1 ");
String studentName = "小明";
//where条件的list
List<String> conditionList = new ArrayList<>();
//模拟是否需要添加条件
if (!TextUtils.isEmpty(studentName)) {
    sql.append(" and s.student_name = ? ");
    conditionList.add(studentName);
}
Page<StudentVO> page = findPage(sql, pageRequest, new StudentVO(), conditionList);

四、总结

总的来说查询有三种方法:

  • 单纯使用注解

比较简单,一般需要创建外键,如果不是多对多也可以不创,不能分页查询,如果只是简单一对一可以使用,其他情况复杂不推荐。

  • 使用@Query注解

也是比较简单,可以帮你实现分页逻辑,但不能动态条件参数查询,不适合查询条件参数不确定的情况,如果查询条件确定,那么是推荐使用这种。

  • 使用EntityManager

用起来相当复杂,需要自己拼接查询条件和分页条件,但如果抽成方法用起来还是蛮舒服的,所以最终还是推荐使用EntityManager进行复杂查询。

  • 15
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Spring Data JPA 是什么? Spring Data JPASpring 框架中的一个模块,它提供了一种方便的方式来访问和操作数据库,同时也简化了开发人员的工作。它基于 JPA 规范,提供了一些常用的 CRUD 操作方法,同时也支持自定义查询和分页查询等功能。 Spring Data JPA 的优点是什么? Spring Data JPA 的优点包括: 1. 简化了数据访问层的开发,提高了开发效率。 2. 提供了一些常用的 CRUD 操作方法,减少了重复的代码编写。 3. 支持自定义查询和分页查询等功能,提高了查询效率。 4. 可以与 Spring 框架无缝集成,方便使用。 5. 支持多种数据库,包括关系型数据库和 NoSQL 数据库等。 6. 提供了一些高级特性,如二级缓存、延迟加载等。 Spring Data JPA 的缺点是什么? Spring Data JPA 的缺点包括: 1. 学习曲线较陡峭,需要掌握 JPA 规范和 Spring 框架的相关知识。 2. 对于复杂的查询,需要编写自定义查询语句,增加了开发难度。 3. 对于大规模数据的查询和操作,可能会出现性能问题。 4. 对于一些特殊的需求,可能需要使用原生 SQL 或其他 ORM 框架来实现。 Spring Data JPA 和 Hibernate 有什么区别? Spring Data JPA 是基于 JPA 规范的,而 Hibernate 是一个 ORM 框架,它实现了 JPA 规范。因此,Spring Data JPA 和 Hibernate 之间的区别主要在以下几个方面: 1. Spring Data JPA 是一个数据访问层框架,而 Hibernate 是一个 ORM 框架。 2. Spring Data JPA 提供了一些常用的 CRUD 操作方法,而 Hibernate 更加灵活,可以编写任意复杂的查询语句。 3. Spring Data JPA 可以与 Spring 框架无缝集成,而 Hibernate 可以与任何 Java 应用程序集成。 4. Spring Data JPA 支持多种数据库,包括关系型数据库和 NoSQL 数据库等,而 Hibernate 主要支持关系型数据库。 5. Spring Data JPA 提供了一些高级特性,如二级缓存、延迟加载等,而 Hibernate 也提供了类似的特性。 如何使用 Spring Data JPA? 使用 Spring Data JPA 的步骤如下: 1. 添加依赖:在项目的 pom.xml 文件中添加 Spring Data JPA 的依赖。 2. 配置数据源:在 Spring 的配置文件中配置数据源。 3. 定义实体类:定义与数据库表对应的实体类,并使用 JPA 注解进行映射。 4. 定义 DAO 接口:定义一个继承 JpaRepository 接口的 DAO 接口。 5. 编写业务逻辑:在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行数据操作。 6. 运行程序:启动应用程序,测试数据访问和操作是否正常。 如何进行分页查询? 使用 Spring Data JPA 进行分页查询的步骤如下: 1. 在 DAO 接口中定义一个继承 PagingAndSortingRepository 接口的方法。 2. 在 Service 层中调用 DAO 接口中的分页查询方法,并指定分页参数。 3. 在控制器中接收分页参数,并将查询结果传递给前端页面。 4. 在前端页面中显示分页信息和查询结果。 如何进行自定义查询? 使用 Spring Data JPA 进行自定义查询的步骤如下: 1. 在 DAO 接口中定义一个自定义查询方法,并使用 @Query 注解指定查询语句。 2. 在 Service 层中调用 DAO 接口中的自定义查询方法。 3. 在控制器中接收查询结果,并将结果传递给前端页面。 4. 在前端页面中显示查询结果。 如何进行事务管理? 使用 Spring Data JPA 进行事务管理的步骤如下: 1. 在 Spring 的配置文件中配置事务管理器。 2. 在 Service 层中使用 @Transactional 注解标记需要进行事务管理的方法。 3. 在控制器中调用 Service 层中的方法。 4. 如果方法执行成功,则事务会自动提交,否则事务会自动回滚。 如何进行多表查询? 使用 Spring Data JPA 进行多表查询的步骤如下: 1. 在 DAO 接口中定义一个自定义查询方法,并使用 @Query 注解指定查询语句。 2. 在查询语句中使用 JOIN 关键字连接多个表。 3. 在 Service 层中调用 DAO 接口中的自定义查询方法。 4. 在控制器中接收查询结果,并将结果传递给前端页面。 5. 在前端页面中显示查询结果。 如何进行级联操作? 使用 Spring Data JPA 进行级联操作的步骤如下: 1. 在实体类中使用 @OneToMany 或 @ManyToOne 注解定义关联关系。 2. 在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行级联操作。 3. 在控制器中接收操作结果,并将结果传递给前端页面。 4. 在前端页面中显示操作结果。 如何进行缓存管理? 使用 Spring Data JPA 进行缓存管理的步骤如下: 1. 在 Spring 的配置文件中配置缓存管理器。 2. 在实体类中使用 @Cacheable 或 @CacheEvict 注解指定缓存策略。 3. 在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行数据操作。 4. 在控制器中接收操作结果,并将结果传递给前端页面。 5. 在前端页面中显示操作结果。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值