乐趣区

Spring-JPA整合QueryDSL

前言

Spring JPA是目前比较常用的 ORM 解决方案,但是其对于某些场景并不是特别的方便,例如查询部分字段,联表查询,子查询等。

而接下来我会介绍与 JPA 形成互补,同时也是与 JPA 兼容得很好的框架QueryDSL

同时由于目前主流使用 Spring Boot,所以本文也会基于Spring Boot 来进行演示

如果对于长文无感,但是又希望了解 QueryDSL 可以直接查看文章最后的总结

环境信息

以下为示例的关键环境信息

  1. JDK 1.8
  2. maven 3.6.1
  3. SpringBoot 2.2.0.RELEASE
  4. IntelliJ IDEA 2019.2.3
  5. lombok
  6. mysql-5.7

源码地址

https://github.com/spring-bas…

项目整合

pom 文件配置

QueryDSL本身定位就是对某些技术的补充或者说是完善,其提供了对 JPAJDBCJDO 等技术的支持。这里引入的是QueryDSL-JPA,需要注意一定要引入 querydsl 代码生成器插件。

    <properties>
        <java.version>1.8</java.version>
        <querydsl.version>4.2.1</querydsl.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- 使用版本较老的 mysql 驱动包,用于连接 mysql-5.7-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- 引入 querydsl-jpa 依赖 -->
        <dependency>
            <groupId>com.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
            <version>${querydsl.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!-- 引入 querydsl 代码生成器插件 -->
            <plugin>
                <groupId>com.mysema.maven</groupId>
                <artifactId>apt-maven-plugin</artifactId>
                <version>1.1.3</version>
                <dependencies>
                    <dependency>
                        <groupId>com.querydsl</groupId>
                        <artifactId>querydsl-apt</artifactId>
                        <version>${querydsl.version}</version>
                    </dependency>
                </dependencies>
                <executions>
                    <!-- 设置插件生效的 maven 生命周期 -->
                    <execution>
                        <goals>
                            <goal>process</goal>
                        </goals>
                        <configuration>
                           <!-- 配置生成文件的目录 --> 
                           <outputDirectory>src/generated-sources/java/</outputDirectory>
                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

application 配置文件

spring:
  datasource:
    ## 数据库相关配置
    url: jdbc:mysql://127.0.0.1:3306/example?useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver # 指定驱动类
  jpa:
    hibernate:
      ddl-auto: update # 自动创建表以及更新表结构,生产环境慎用
    show-sql: true # 打印执行的 SQL

配置类

由于 QueryDSL 不提供starter,所以需要自行准备一个配置类,代码如下所示

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

/**
 * QueryDSL 配置类
 * @author Null
 * @date 2019-10-24
 */
@Configuration
public class QuerydslConfig {

    @Autowired
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory queryFactory(){return new JPAQueryFactory(entityManager);
    }

}

启动类

启动类很简单,只需要使用 @SpringBootApplication 即可

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class QuerydslJpaDemoApplication {public static void main(String[] args) {SpringApplication.run(QuerydslJpaDemoApplication.class, args);
    }
}

实体类

主要有讲师和课程,每个课程都有一个讲师,每个讲师有多个课程,即讲师与课程的关系为一对多

课程

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * 课程,一个课程对应一个讲师
 * @author Null
 * @date 2019-10-24
 */
@Data
@Entity
public class Course {
    /**
     * 课程 ID
     */
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;
    /**
     * 课程名称
     */
    private String name;
    /**
     * 对应讲师的 ID
     */
    private Long lecturerId;
}

讲师

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * 讲师,一个讲师有多个课程
 * @author Null
 * @date 2019-10-24
 */
@Data
@Entity
public class Lecturer {
    /**
     * 讲师 ID
     */
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;
    /**
     * 讲师名字
     */
    private String name;
    /**
     * 性别,true(1)为男性,false(0)为女性
     */
    private Boolean sex;
}

Repository 接口

如果要使用 QuerDSL 需要 Repository 接口除了继承 JpaRepository 接口(此接口为 Spring-JPA 提供的接口)外,还需要继承 QuerydslPredicateExecutor 接口。关键示例如下:

课程 Repository

import com.example.querydsl.jpa.entity.Course;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

/**
 * 课程 Repository
 *
 * @author Null
 * @date 2019-10-24
 */
public interface CourseRepository extends
        JpaRepository<Course, Integer>,
        QuerydslPredicateExecutor<Course> {}

讲师 Repository

import com.example.querydsl.jpa.entity.Lecturer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

/**
 * 讲师 Repository
 * @author Null
 * @date 2019-10-24
 */
public interface LecturerRepository extends
        JpaRepository<Lecturer,Integer>,
        QuerydslPredicateExecutor<Lecturer> {}

代码生成

前面配置 QueryDSL 代码生成器就是用于这一步,== 每次实体类有变更最好重复执行本步骤重新生成新的代码 ==。由于个人习惯使用 IDEA,所以以IDEA 作为演示。

  1. 双击下图内容即可生成代码了,

  2. 然后就会在 src/generated-sources 目录可以看到生成的代码,包名与实体包名一致,但是类名为 Q 开头的文件

  3. 上一步的截图我们可以看到其实生成的代码被 IDEA 识别为普通文件了,所以我们需要标记 src/generated-sources/java 目录的用途,如下图所示

    标记后,效果如下,可以看到代码被正确识别了

    到了这一步其实已经完成整合了,下面就开始验证是否正确整合以及展示 QueryDSL 的优势了

验证整合与演示

下面我会使用单元测试来验证 QueryDSL 是否正确整合以及演示一下 QueryDSL 的优势

单元测试类

这里主要是单元测试类的关键内容,需要注意 @BeforeEachJunit5的注解,表示每个单元测试用例执行前会执行的方法其实对应 Junit4@Before

/**
 * @SpringBootTest 默认不支持事务且自动回滚
 * 使用 @Transactional 开启事务,* 使用 @Rollback(false) 关闭自动回滚
 * @author Null
 * @date 2019-10-24
 */
@SpringBootTest
class QuerydslJpaDemoApplicationTests {

    @Autowired
    private CourseRepository courseRepository;

    @Autowired
    private LecturerRepository lecturerRepository;
    
    @Autowired
    private JPAQueryFactory queryFactory;

    /**
     * 初始化数据
     */
    @BeforeEach
    public void initData(){
        // 清空数据表
        courseRepository.deleteAll();
        lecturerRepository.deleteAll();

        // 初始化讲师
        Lecturer tom=new Lecturer();
        tom.setName("Tom");
        tom.setSex(true);
        lecturerRepository.save(tom);

        Lecturer marry=new Lecturer();
        marry.setName("Marry");
        marry.setSex(false);
        lecturerRepository.save(marry);

        // 初始化课程
        Course chinese=new Course();
        chinese.setName("Chinese");
        chinese.setLecturerId(tom.getId());
        courseRepository.save(chinese);

        Course physics=new Course();
        physics.setName("Physics");
        physics.setLecturerId(tom.getId());
        courseRepository.save(physics);

        Course english=new Course();
        english.setName("English");
        english.setLecturerId(marry.getId());
        courseRepository.save(english);
    }
    
    ... 省略各个用例
    
}

单表模糊查询

    /**
     * 根据课程名称模糊查询课程
     */
    @Test
    public void testSelectCourseByNameLike() {
        // 组装查询条件
        QCourse qCourse = QCourse.course;
        // % 要自行组装
        BooleanExpression expression = qCourse.name.like("P%");
        System.out.println(courseRepository.findAll(expression));
    }

联表查询

    /**
     * 根据讲师姓名查课程
     */
    @Test
    public void testSelectCourseByLecturerName(){
        QCourse qCourse = QCourse.course;
        QLecturer qLecturer = QLecturer.lecturer;
        // 这里包含了组装查询条件和执行查询的逻辑,组装好条件后记得执行 fetch()
        List<Course> courses=queryFactory.select(qCourse)
                .from(qCourse)
                .leftJoin(qLecturer)
                .on(qCourse.lecturerId.eq(qLecturer.id))
                .where(qLecturer.name.eq("Tom"))
                .fetch();
        System.out.println(courses);
    }

更新

    /**
     * 根据姓名更新讲师性别 <br/>
     * 使用 @Transactional 开启事务 <br/>
     * 使用 @Rollback(false)关闭自动回滚 <br/>
     */
    @Test
    @Transactional
    @Rollback(false)
    public void testUpdateLecturerSexByName(){
        QLecturer qLecturer = QLecturer.lecturer;
        // 更新 Tom 的性别为女性,返回的是影响记录条数
        long num=queryFactory.update(qLecturer)
                .set(qLecturer.sex,false)
                .where(qLecturer.name.eq("Tom"))
                .execute();
        // 这里输出被更新的记录数
        System.out.println(num);
    }

删除

    /**
     * 根据根据性别删除讲师
     */
    @Test
    @Transactional
    @Rollback(false)
    public void testDeleteLecturerBySex(){
        QLecturer qLecturer = QLecturer.lecturer;
        // 删除性别为男性的讲师
        long num=queryFactory.delete(qLecturer)
                .where(qLecturer.sex.eq(true))
                .execute();
        // 输出被删除的记录数
        System.out.println(num);
    }

用例分析

从用例中可以看出其实 QueryDSLAPI更加切合原生的 SQL,基本上从代码上就可以看出你希望执行的SQL 了。

细心的朋友会发现 QueryDSL 是没有 insert 方法,因为 JPA 提供的 save() 方法已经足够处理了。

同时要记得要组装好你的 SQL 后别忘记调用 fetch() 或者 execute() 方法。

总结

  • Spring Boot JPA整合 QueryDSL 的关键步骤

    1. 引入依赖和插件
    2. 编写配置类
    3. 使用插件生成代码
    4. 标记生成文件为代码
    5. Repository继承QuerydslPredicateExecutor
  • QueryDSLAPI 类似原生 SQLAPI 风格类似 StringBuilderAPIFluent API风格)。但是不提供 insert 对应的操作。
  • QueryDSL对于复杂的 SQL 的支持十分友好,算是对于 JPA 对这块需求的补充和完善。
退出移动版