Querydsl-JPA 框架(推荐)

官网:传送门

参考:

  • JPA整合Querydsl入门篇
  • SpringBoot环境下QueryDSL-JPA的入门及进阶

概述及依赖、插件、生成查询实体

1.Querydsl支持代码自动完成,因为是纯Java API编写查询,因此主流Java IDE对起的代码自动完成功能支持几乎可以发挥到极致(因为是纯Java代码,所以支持很好)

2.Querydsl几乎可以避免所有的SQL语法错误(当然用错了Querydsl API除外,因为不写SQL了,因此想用错也难)

3.Querydsl采用Domain类型的对象和属性来构建查询,因此查询绝对是类型安全的,不会因为条件类型而出现问题

4.Querydsl采用纯Java API的作为SQL构建的实现可以让代码重构发挥到另一个高度

5.Querydsl的领一个优势就是可以更轻松的进行增量查询的定义

使用

在Spring环境下,可以通过两种风格来使用QueryDSL。

一种是使用JPAQueryFactory的原生QueryDSL风格, 另一种是基于Spring Data提供的QueryDslPredicateExecutor的Spring-data风格。

使用QueryDslPredicateExecutor可以简化一些代码,使得查询更加优雅。 而JPAQueryFactory的优势则体现在其功能的强大,支持更复杂的查询业务。甚至可以用来进行更新和删除操作。

依赖

 <dependencies>        <dependency>            <groupId>com.querydsl</groupId>            <artifactId>querydsl-apt</artifactId>        </dependency>        <dependency>            <groupId>com.querydsl</groupId>            <artifactId>querydsl-jpa</artifactId>        </dependency></dependencies>

添加maven插件(pom.xml)

添加这个插件是为了让程序自动生成query type(查询实体,命名方式为:“Q”+对应实体名)。

上文引入的依赖中querydsl-apt即是为此插件服务的。

注:在使用过程中,如果遇到query type无法自动生成的情况,用maven更新一下项目即可解决(右键项目->Maven->Update Project)。

<project><build>        <plugins>            <plugin>                             <groupId>com.mysema.maven</groupId>                <artifactId>apt-maven-plugin</artifactId>                <version>1.1.3</version>                <executions>                    <execution>                        <phase>generate-sources</phase>                        <goals>                            <goal>process</goal>                        </goals>                        <configuration>                            <outputDirectory>target/generated-sources/java</outputDirectory>                            <processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>                        </configuration>                    </execution>                </executions>        </plugin>        </plugins></build></project>

补充:

QueryDSL默认使用HQL发出查询语句。但也支持原生SQL查询。

若要使用原生SQL查询,你需要使用下面这个maven插件生成相应的query type。

<project><build><plugins><plugin>                <groupId>com.querydsl</groupId>                <artifactId>querydsl-maven-plugin</artifactId>                <version>${querydsl.version}</version>                <executions>                    <execution>                        <goals>                            <goal>export</goal>                        </goals>                    </execution>                </executions>                <configuration>                    <jdbcDriver>org.apache.derby.jdbc.EmbeddedDriver</jdbcDriver>                    <jdbcUrl>jdbc:derby:target/demoDB;create=true</jdbcUrl>                    <packageName>com.mycompany.mydomain</packageName>                    <targetFolder>${project.basedir}/target/generated-sources/java</targetFolder>                </configuration>                <dependencies>                    <dependency>                        <groupId>org.apache.derby</groupId>                        <artifactId>derby</artifactId>                        <version>${derby.version}</version>                    </dependency>                </dependencies></plugin></plugins></build></project>

生成查询实体

idea 工具 为maven project 自动添加了对应的功能。添加好依赖和 plugin 插件后,就可以生成查询实体了。

打开右侧的 Maven Projects,如下图所示:

双击 clean 清除已编译的 target

双击 compile 命令执行,执行完成后会在 pom.xml 配置文件内配置生成目录内生成对应实体的 QueryDSL 查询实体。

生成的查询实体如下图所示:

JPAQueryFactory 风格

QueryDSL 在支持JPA的同时,也提供了对 Hibernate 的支持。可以通过 HibernateQueryFactory 来使用。

装配 与 注入

SpringBoot注解方式装配

/** * 方式一。使用Spring的@Configuration注解注册实例进行容器托管 */@Configurationpublic class QueryDslConfig {    @Bean    public JPAQueryFactory jpaQueryFactory(EntityManager em){        return new JPAQueryFactory(em);    }}/** * 方式二。在Dao类中初始化 */// 实体管理@Autowiredprivate EntityManager entityManager;// 查询工厂private JPAQueryFactory queryFactory;// 初始化JPA查询工厂@PostConstruct// Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)public void init(){        queryFactory = new JPAQueryFactory(entityManager);    }

注入

    @Autowired    private JPAQueryFactory queryFactory;

更新、删除

JPAQueryFactory 更新

在Querydsl JPA中,更新语句是简单的 update-set/where-execute 形式。

execute()执行后返回的是被更新的实体的数量。

注意:使用QueryDsl更新实体时需要添加事务

@Test@Transactionalpublic void testUpdate() {    QStudent qStudent = QStudent.student;    Long result = queryFactory.update(qStudent)            .set(qStudent.name, "haha")// 可以用if条件判断更新值来确定字段是否.set()      .setnull(qStudent.age)// 设置null值            .where(qStudent.id.eq(111L)).execute();    assertThat(result, equalTo(1L));}

JPAQueryFactory 删除

删除语句是简单的 delete-where-execute 形式。

注意:使用QueryDsl删除实体时需要添加事务

@Test@Transactionalpublic void testDelete() {    QStudent qStudent = QStudent.student;    //删除指定条件的记录    Long result = queryFactory.delete(qStudent)            .where(qStudent.id.eq(111L))            .execute();    assertThat(result, equalTo(1L));        //删除所有记录。即不加where条件    Long totalResult = queryFactory.delete(qStudent).execute();    System.out.println(totalResult);}

查询

表达式工具类

Expressions 表达式工具类

// when-then 条件表达式函数。when传参必须为名为eqTrue或eqFalse的PredicateT cases().when(Predicate).then(T a).otherwise(T b)    DateExpression<Date> currentDate()// 返回当前日历(年-月-日)的 DateExpressionTimeExpression<Time> currentTime()// 返回当前时刻(时:分:秒)的 TimeExpressionDateTimeExpression<Date> currentTimestamp()// 返回当前时间(年-月-日 时:分:秒)的 DateTimeExpression    // exprs 均为名为eqTrue的Predicate ,则返回名为eqTrue的Predicate,否则返回eqFalse的PredicateBooleanExpression allOf(BooleanExpression... exprs)// exprs 至少存在一个名为eqTrue的Predicate,则返回名为eqTrue的Predicate,否则返回eqFalse的PredicateBooleanExpression anyOf(BooleanExpression... exprs)// 转类型为 BooleanExpression。特别注意:asBoolean(Boolean).isTrue() 才是可用PredicateBooleanExpression asBoolean(Boolean)// asBoolean(true)    booleanPath("true")NumberExpression asNumber(T)StringrExpression asString(T)DateExpression asDate(T)TimeExpression asTime(T)DateTimeExpression asDateTime(T)// 自定义语法StringTemplate stringTemplate(String template, Object... args)NumberTemplate<T> numberTemplate(Class<" />,                                     qm.name,                                     qm.address))                                .fetch();//使用stringTemplate充当查询语句的某一部分String date = queryFactory                    .select(                        Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')",                         qm.registerDate))                    .from(qm)                    .fetchFirst();//在where子句中使用stringTemplateString id = queryFactory                .select(qm.id)                .from(qm)                .where(                    Expressions.stringTemplate("DATE_FORMAT({0},'%Y-%m-%d')",                     qm.registerDate).eq("2018-03-19"))                .fetchFirst();

QueryDslPredicateExecutor 风格

通常使用Repository来继承QueryDslPredicateExecutor接口。通过注入Repository来使用。

Repository 接口

Spring Data JPA中提供了QueryDslPredicateExecutor接口,用于支持QueryDSL的查询操作。

public interface tityRepository extends JpaRepository<City, Integer>, QuerydslPredicateExecutor<city> {}

QueryDslPredicateExecutor接口提供了findOne()findAll()count()exists()四个方法来支持查询。并可以使用更优雅的BooleanBuilder 来进行条件分支管理。

  • count()会返回满足查询条件的数据行的数量
  • exists()会根据所要查询的数据是否存在返回一个boolean值

findOne()、findAll()

findOne

从数据库中查出一条数据。没有重载方法。

Optional<T> findOne(Predicate var1);

JPAQueryfetchOne()一样,当根据查询条件从数据库中查询到多条匹配数据时,会抛NonUniqueResultException。使用的时候需要慎重。

findAll()

findAll是从数据库中查出匹配的所有数据。提供了以下几个重载方法。

Iterable<T> findAll(Predicate var1);Iterable<T> findAll(Predicate var1, Sort var2);Iterable<T> findAll(Predicate var1, OrderSpecifier<?>... var2);Iterable<T> findAll(OrderSpecifier<?>... var1);Page<T> findAll(Predicate var1, Pageable var2);

使用示例:

QMemberDomain qm = QMemberDomain.memberDomain;// QueryDSL 提供的排序实现OrderSpecifier<Integer> order = new OrderSpecifier<>(Order.DESC, qm.age);Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"),order);QMemberDomain qm = QMemberDomain.memberDomain;// Spring Data 提供的排序实现Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC, "age"));Iterable<MemberDomain> iterable = memberRepo.findAll(qm.status.eq("0013"), sort);

单表动态分页查询

单表动态查询示例:

//动态条件QTCity qtCity = QTCity.tCity; //SDL实体类//该Predicate为querydsl下的类,支持嵌套组装复杂查询条件Predicate predicate = qtCity.id.longValue().lt(3).and(qtCity.name.like("shanghai"));//分页排序Sort sort = new Sort(new Sort.Order(Sort.Direction.ASC,"id"));PageRequest pageRequest = new PageRequest(0,10,sort);//查找结果Page<TCity> tCityPage = tCityRepository.findAll(predicate, pageRequest);

Querydsl SQL 查询

Querydsl SQL 模块提供与 JDBC API 的集成。可以使用更多的 JDBC SQL方法。比如可以实现 from 的查询主体为子查询出来的临时表unionunion All 等Querydsl-JPA限制的相关操作。还可以根据 JDBC API 获取数据库的类型使用不同的数据库语法模板。

依赖及配置

依赖:

        <dependency>            <groupId>com.querydsl</groupId>            <artifactId>querydsl-sql</artifactId>            <version>${querydsl.version}</version>        </dependency>        <dependency>            <groupId>joda-time</groupId>            <artifactId>joda-time</artifactId>            <version>2.10.5</version>        </dependency>

yaml配置:

logging:  level:    com.querydsl.sql: debug# 打印日志

SQLQuery 的 Q 类

需要自己创建编写(可以基于apt 插件生成的 JPA 的Q类改造),并放到主目录(src)启动类下的包里。

  • 使用 extends RelationalPathBase 的Q类。推荐

    需要将数据库表名传入构造方法的table参数里,path 可以传别名,所有的property参数为实体类的属性名(驼峰命名),addMetadata()ColumnMetadata.named("FeildNmae")FeildNmae 为数据库字段名。

    使用该Q类查询所有字段数据时(即select(Q类))可以自动映射封装结果集。

  • 使用extends EntityPathBase的Q类。

    需要将传入构造方法的variable参数改成数据库表名,并且将所有的property参数改成相对应的数据库字段名

    **注意:**使用 extends EntityPathBase 的实体Q类,直接 select(Q类) 会报错,无法自动映射封装结果集,需要使用Projections.bean(Entity.class,Expression... exprs) 手动封装结果集。

/** * extends RelationalPathBase 的Q类示例 */public class QEmployee extends RelationalPathBase<Employee> {    private static final long serialVersionUID = 1394463749655231079L;    public static final QEmployee employee = new QEmployee("EMPLOYEE");    public final NumberPath<Integer> id = createNumber("id", Integer.class);    public final StringPath firstname = createString("firstname");    public final DatePath<java.util.Date> datefield = createDate("datefield", java.util.Date.class);    public final PrimaryKey<Employee> idKey = createPrimaryKey(id);    public QEmployee(String path) {        super(Employee.class, PathMetadataFactory.forVariable(path), "PUBLIC", "EMPLOYEE");        addMetadata();    }    public QEmployee(PathMetadata metadata) {        super(Employee.class, metadata, "PUBLIC", "EMPLOYEE");        addMetadata();    }    protected void addMetadata() {        addMetadata(id, ColumnMetadata.named("ID").ofType(Types.INTEGER));        addMetadata(firstname, ColumnMetadata.named("FIRSTNAME").ofType(Types.VARCHAR));        addMetadata(datefield, ColumnMetadata.named("DATEFIELD").ofType(Types.DATE));    }}
/** * extends EntityPathBase 的Q类示例 */public class QUserAddressS extends EntityPathBase<UserAddress> {    private static final long serialVersionUID = -1295712525L;    public static final QUserAddressS userAddress = new QUserAddressS("tb_user_address");        public final NumberPath<Integer> id = createNumber("id", Integer.class);    public final StringPath address = createString("address");    public final DateTimePath<java.util.Date> createTime = createDateTime("create_time", java.util.Date.class);    public QUserAddressS(String variable) {        super(UserAddress.class, forVariable(variable));    }    public QUserAddressS(Path<? extends UserAddress> path) {        super(path.getType(), path.getMetadata());    }    public QUserAddressS(PathMetadata metadata) {        super(UserAddress.class, metadata);    }}

SQLQueryFactory 方式

装配及基本使用

装配

@Configuration@Slf4jpublic class QueryDslConfig {    @Bean    public SQLQueryFactory sqlQueryFactory(DataSource druidDataSource){        SQLTemplates t;        try(Connection connection = druidDataSource.getConnection()){            t = new SQLTemplatesRegistry().getTemplates(connection.getMetaData());        }catch (Exception e){            log.error("", e);            t = SQLTemplates.DEFAULT;        }        com.querydsl.sql.Configuration configuration = new com.querydsl.sql.Configuration(t);        configuration.addListener(new SQLBaseListener(){            @Override            public void end(SQLListenerContext context) {                if (context != null && !DataSourceUtils.isConnectionTransactional(context.getConnection(), druidDataSource)){                    // 若非事务连接                    SQLCloseListener.DEFAULT.end(context);                }            }        });        configuration.setExceptionTranslator(new SpringExceptionTranslator());        // 创建SQLQueryFactory,且数据库连接由spring管理        return new SQLQueryFactory(configuration, () -> DataSourceUtils.getConnection(druidDataSource));    }}

注入

    @Autowired    private SQLQueryFactory sqlQueryFactory;

SQLQueryFactory 基本使用

    /**     * 子查询作为临时表传入from()中     */@Test    public void selectBySqlQueryFactory(){        // 使用 extends RelationalPathBase 的QEntity,自动映射封装        QUserAddressSql uaSql = QUserAddressSql.userAddress;        // 子查询        SQLQuery<Tuple> q = SQLExpressions            .select(                // 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致                uaSql.addressee                , uaSql.userId            )            .from(uaSql);        List<Tuple> fetch = sqlQueryFactory            .select(                // 查询字段须是临时表中的字段别名,且类型一致                Expressions.template(String.class, "q.addressee").as("addressee")                , Expressions.numberTemplate(Integer.class, "q.user_id").as("userId")            )            .from(q, Expressions.stringPath("q"))   // 子查询作为临时表            .fetch();        System.out.println(fetch);    }    /**     * 子查询结果集 union     */@Test    public void selectBySqlQueryFactory(){        // 使用 extends EntityPathBase 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收        QUserAddressSql uaSql = QUserAddressSql.userAddress;        QUserSql uSql = QUserSql.user;                SQLQuery<Tuple> a = SQLExpressions            .select(uaSql.userId.as("user_id") , uaSql.phone)            .from(uaSql)            .where(uaSql.userId.eq(30));        SQLQuery<Tuple> b = SQLExpressions            .select(uSql.id.as("user_id") , uSql.phone)            .from(uSql)            .where(uSql.id.eq(29).or(uSql.id.eq(30)));                Union<Tuple> union = sqlQueryFactory.query().union(a, b);                long count = sqlQueryFactory            .from(union, Expressions.stringPath("q")).fetchCount();        List<UserAddressDTO> list = sqlQueryFactory            .from(union, Expressions.stringPath("q"))            .orderBy(Expressions.numberPath(Integer.class, "user_id").desc()                , Expressions.stringTemplate("phone").desc())            .offset(0)            .limit(5)            .transform(                GroupBy.groupBy(Expressions.template(String.class, "q.user_id")).list(                    Projections.bean(UserAddressDTO.class                        , Expressions.template(Integer.class, "q.user_id").as("userId")                        , GroupBy.list(Projections.bean(UserAddress.class                            , Expressions.stringTemplate("q.phone").as("phone")                        )).as("userAddresses")                    )));        System.out.println(count);        list.forEach(s -> System.out.println(JSON.toJSONString(s)));    }

SQLExpression 表达式工具类

// 合并多张表记录。union为去重合并,unionAll为不去重合并static <T> Union<T> union(SubQueryExpression<T>... sq)static <T> Union<T> union(List<SubQueryExpression<T>> sq)static <T> Union<T> unionAll(SubQueryExpression<T>... sq)static <T> Union<T> unionAll(List<SubQueryExpression<T>> sq)// 调用函数查询序列static SimpleExpression<Long> nextval(String sequence)static <T extends Number> SimpleExpression<T> nextval(Class<T> type, String sequence)// 使用示例:SQL写法:select seq_process_no.nextval from dual;Long nextvalReturn = sqlQueryFactory.select(SQLExpressions.nextval("序列名")).fetchOne;// 将多列记录聚合为一列记录。delimiter为分隔符。Oracle数据库专属,其他数据库报错static WithinGroup<Object> listagg(Expression<?> expr, String delimiter)// 使用示例:SQLExpression.listagg(qEntity.name, ",").withinGroup.OrderBy(qEntity.name.asc()).getValue.as("Name")// 将多列记录聚合为一列记录。separator为分隔符。MySQL、PostgreSQL都可用,PostgreSQL会根据模板翻译成String_agg函数static StringExpression groupConcat(Expression<String> expr, String separator)static StringExpression groupConcat(Expression<String> expr)    static <T> RelationalFunctionCall<T> relationalFunctionCall(Class<? extends T> type, String function, Object... args)static <D extends Comparable> DateExpression<D> date(DateTimeExpression<D> dateTime)static <D extends Comparable> DateExpression<D> date(Class<D> type, DateTimeExpression<?> dateTime)    static <D extends Comparable> DateTimeExpression<D> dateadd(DatePart unit, DateTimeExpression<D> date, int amount)static <D extends Comparable> DateExpression<D> dateadd(DatePart unit, DateExpression<D> date, int amount)// 获取两个日期的时间间隔(end-start)static <D extends Comparable> NumberExpression<Integer> datediff(DatePart unit, DateTimeExpression<D> start, DateTimeExpression<D> end)

JPASQLQuery 方式

使用 JPASQLQuery 作为查询引擎时,使用的QEntity(extends EntityPathBase),传入构造方法的 variable 参数可以不为数据库表名(因为 JPASQLQuery 可以找到映射的真实表名,仅把此参数作为表别名),但所有的 property 参数仍必需为相对应的数据库字段名

故并不能直接使用 apt 插件生成 的 jpa 使用的 Q类,仍需要使用改造版的 Q类(extends EntityPathBase)

@Test    public void selectBySqlQueryFactory(){        // 使用 extends EntityPathBase 的改造版QEntity,结果集如需封装到实体类,必须手动指定实体类来接收        QUserAddress ua = QUserAddress.userAddress;        // jpa+sql的查询工具,本例使用的oracle的sql模板        JPASQLQuery<?> jpasqlQuery = new JPASQLQuery<Void>(em, new OracleTemplates());        // 子查询        SQLQuery<Tuple> q = SQLExpressions            .select(                // 查询字段须是数据库表中的字段名(不是实体属性名),且类型一致。如直接不使用QEntity的属性,则需手动指定                Expressions.stringPath("addressee").as("addressee")                , Expressions.numberPath(Integer.class, "user_id").as("user_id")        )            .from(ua);        List<Tuple> fetch = jpasqlQuery            .select(                // 查询字段须是临时表中的字段名或别名,且类型一致。结果集字段需添加别名手动映射封装                Expressions.template(String.class, "q.addressee").as("addressee")                , Expressions.numberTemplate(Integer.class, "q.user_id").as("userId")        )            .from(q, Expressions.stringPath("q"))   // 子查询作为临时表            .fetch();        System.out.println(fetch);    }