cn.cloudself:query-pro

A db tool


License
Apache-2.0

Documentation

目录

例子

// SELECT * FROM `user` WHERE `id` = ? LIMIT 1
User user = UserQueryPro.selectByPrimaryKey(1);

// SELECT * FROM `user` WHERE `user`.`id` = ?
List<User> users = UserQueryPro.selectBy().username().is().equalsTo("hello").run();

// SELECT * FROM `user` WHERE `user`.`id` = ? OR `user`.`age` = ?
List<User> users2 = UserQueryPro
        .selectBy().id().eq(1) // is 是可选的
        .or().age().not().in(10, 11)
        .run();

功能

  • 条件判断
  • 切换数据源
  • 自动切换数据源
  • 逻辑删除

简述

总的来说,QueryPro的运作流程分为两部分。

  1. 在调用run之前,QueryPro的所有代码都是为了合理的生成并修改QueryStructure这个结构,

  2. 调用run之后, 通过ToSqlByQueryStructureQueryStructure转成目标sql, 然后通过JdbcQSR执行sql并生成目标对象返回。 整个resolver包就是处理调用run之后的事情。

QueryStructure的设计哲学: 易于序列化, 以便多端生成,并传输

快速入门 & 文档

1. 生成代码
2. 查询操作
// 使用主键查询
// SELECT * FROM `user` WHERE `id` = ? LIMIT 1
User user0 = UserQueryPro.selectByPrimaryKey(1);

// 使用某个字段查询
// !!! 所有的 is 是可以省略的,但有时候加着更好看
// SELECT * FROM `user` WHERE `user`.`id` = ?
List<User> users1_1 = UserQueryPro.selectBy().id().is().equalsTo(1).run();
List<User> users1_2 = UserQueryPro.selectBy().id().eq(1).run();
List<User> users1_3 = UserQueryPro.selectBy().id(1).run();

// 使用in, between, lessThan, lessThanOrEqual, graterThan, graterThanOrEqual等查询
// SELECT * FROM `user` WHERE `user`.`name` in (?, ?)
List<User> users2_1 = UserQueryPro.selectBy().name().in("hb", "herb").run();
List<User> users2_2 = UserQueryPro.selectBy().name("hb", "herb").run();
// SELECT * FROM `user` WHERE `user`.`age` between (?, ?)
List<User> users2_3 = UserQueryPro.selectBy().age().between(18, 20).run();
// SELECT * FROM `user` WHERE `user`.`age` < ?
List<User> users2_5 = UserQueryPro.selectBy().age().lessThan(18).run();

// 使用and查询
// !!! and是可以省略的,但有时候加着更好看
// SELECT * FROM `user` WHERE `user`.`name` = ? AND `user`.`age` = ?
List<User> usersRun3 = UserQueryPro
        .selectBy().name().is().equalsTo("hb")
        .and().age().is().equalsTo(18)
        .run();

// 使用or查询
// SELECT * FROM `user` WHERE `user`.`id` = ? OR `user`.`age` = ?
List<User> usersRun5_1 = UserQueryPro.selectBy().id().is().equalsTo(1).or().age().equalsTo(10).run();
// SELECT * FROM `user` WHERE `user`.`id` = ? OR (`user`.`age` = ? AND `user`.`name` like ?)
List<User> usersRun5_2 = UserQueryPro
        .selectBy().id().is().equalsTo(1)
        .or((it) -> it.age().equalsTo(18).and().name().like("%rb%"))
        .run();
// SELECT * FROM `user` WHERE `user`.`id` = ? OR (`user`.`age` = ? AND `user`.`name` like ?)
List<User> usersRun5_3 = UserQueryPro
        .selectBy().id().is().equalsTo(1)
        .or().parLeft().age().equalsTo(18).and().name().like("%rb%").parRight()
        .run();

// 使用not查询
// SELECT * FROM `user` WHERE `user`.`id` <> ?
List<User> usersRun6_1 = UserQueryPro.selectBy().id().is().not().equalsTo(2).run();
// SELECT * FROM `user` WHERE `user`.`id` not in (?, ?)
List<User> usersRun6_2 = UserQueryPro.selectBy().id().is().not().in(1, 2).run();
// SELECT * FROM `user` WHERE `user`.`id` not between (?, ?)
List<User> usersRun6_2 = UserQueryPro.selectBy().id().not().between(1, 2).run();
// SELECT * FROM `user` WHERE `user`.`name` not like ?
List<User> usersRun6_2 = UserQueryPro.selectBy().name().not().like("%H%").run();

// 忽略大小写
// SELECT * FROM `user` WHERE UPPER(`user`.`name`) like UPPER(?)
List<User> users7 = UserQueryPro.selectBy().name().ignoreCase().like("%H%").run();

// is null 查询
// SELECT * FROM `user` WHERE `user`.`age` is null
List<User> users8_1 = UserQueryPro.selectBy().age().is().nul().run();
// SELECT * FROM `user` WHERE `user`.`age` is not null
List<User> users8_2 = UserQueryPro.selectBy().age().is().not().nul().run();

// like查询
// SELECT * FROM `user` WHERE `user`.`name` like ? ORDER BY `user`.`id` DESC
List<User> users9 = UserQueryPro.selectBy().name().like("%h%").orderBy().id().desc().run();

// 排序
// SELECT * FROM `user` ORDER BY `user`.`id` DESC
List<User> users10_1 = UserQueryPro.orderBy().id().desc().run();
// SELECT * FROM `user` ORDER BY `user`.`age` ASC, `user`.`id` DESC
List<User> users10_2 = UserQueryPro.orderBy().age().asc().id().desc().run();

// 限制返回结果数量
// SELECT * FROM `user` ORDER BY `user`.`age` DESC, `user`.`id` ASC LIMIT 1
List<User> users11_1 = UserQueryPro.orderBy().age().desc().id().asc().limit(1).run();
// SELECT * FROM `user` LIMIT 1
User user11_2 = UserQueryPro.selectBy().runLimit1();

// 只需要返回部分字段
// SELECT `setting`.`id` FROM `setting` WHERE `setting`.`id` = ?
List<Long> ids12_1 = SettingQueryPro.selectBy().id().equalsTo(1).columnLimiter().id();
// SELECT `user`.`id`, `user`.`age` FROM `user` WHERE `user`.`id` = ?
List<User> users12_2 = UserQueryPro.selectBy().id().equalsTo(1).columnsLimiter().id().age().run();
// SELECT `user`.`id`, `user`.`name` FROM `user` ORDER BY `user`.`age` DESC, `user`.`id` DESC LIMIT 1
List<User> usersRun14 = UserQueryPro
        .orderBy().age().desc().id().desc().limit(1)
        .columnsLimiter().id().name()
        .run();

// take方法(方便写if等条件)
// SELECT * FROM `user` WHERE `user`.`id` = ? AND `user`.`name` = ?
List<User> usersRun15 = UserQueryPro
        .selectBy().id().equalsTo(1)
        .take(it -> True ? it.name().equalsTo("hb") : it.name().equalsTo("hb2"))
        .run();

// 自定义sql查询
3. 插入操作

支持批量插入,当数据量大于20条时,会自动启用BigMode
BigMode下,当预估sql大于0.5M(实际一般在2M以内, 取决于非ascii字符的数量)或插入的行数大于1000,会自动分多次插入

UserQueryPro.insert(...); // 参数支持 User, Map, Collection<User>, vararg User
4. 更新操作
UserQueryPro.updateSet(new User(19)).where.id.equalsTo(1).run()
UserQueryPro.updateSet().id(5).age(NULL).run()
5. 删除操作
// 逻辑删除默认启用,当存在deleted字段时,自动使用deleted字段进行逻辑删除
// UPDATE `setting` SET `deleted` = ? WHERE `setting`.`id` = ?
SettingQueryPro.deleteBy().id().is().equalsTo(1).run()

// SELECT * FROM `setting` WHERE  ( `setting`.`id` = ? OR `setting`.`kee` = ? )  AND `setting`.`deleted` = ? LIMIT 1
SettingQueryPro.selectBy().id().equalsTo(1).or().kee().equalsTo("lang").runLimit1()
6. 直接执行sql
QueryProSql.create().query()
QueryProSql.create().update()
QueryProSql.create().exec()
7. 事务

spring环境下, 直接使用@Transactional即可。 非spring环境下,使用

QueryProTransaction.use(() -> {
    // 业务代码
    return null;
})
8. 多表关联支持

目前多表关联查询仍不够优雅,但能用

配置

QueryPro的配置通过QueryProConfig进行。
QueryProConfig的配置有如下五个作用域:
global(全局), request(请求), thread(不推荐), context(代码块), code(针对某次查询)

数据源

如果你使用了Spring, QueryPro会自动装载由Spring管理的DataSource。 当然你也可以手动指定它。

DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(drive);

// 针对本次请求,临时切换数据源
QueryPro.request.dataSource(dataSource)
        
// 针对某段查询,临时切换数据源
QueryProConfig.context.use((config) -> {
    config.dataSource(dataSource);

    User user = UserQueryPro.selectBy().name().equalsTo("username").runLimit1();
    List<Setting> themes = SettingQueryPro.selectBy().kee().equalsTo("theme").run();
});

// 针对某次查询,临时切换数据源
UserQueryPro.dataSource(dataSource).selectBy().name().equalsTo("username").run();
逻辑删除

逻辑删除默认就是启用行为。如果表字段存在deleted字段会使用逻辑删除。

// 关闭逻辑删除
QueryProConfig.global.logicDelete(false); 
// 更改逻辑删除的字段,逻辑删除默认使用deleted字段,默认使用true代表逻辑已删除,false代表逻辑未删除
QueryProConfig.global.logicDelete(true, "removed", "Y", "N");
日志信息
QueryProConfig.global
    .printLog(true) // 打印sql日志,默认开启
    .beautifySql(true) // 美化sql(加入空格,换行等),默认开启
    .printCallByInfo(true) // 打印调用QueryPro所在的代码行,默认开启
    .printResult(true); // 打印返回结果,默认开启


// 会在INFO级别关闭日志的打印,但是DEBUG级别还是会打印
QueryProConfig.global.printLog(false)
// 关闭所有级别的日志打印
QueryProConfig.global.printLog(false, LogLevel.DEBUG)
设置需要忽略字段

例如,忽略serialVersionUID这个字段 (这是默认行为)

QueryProConfig.global.shouldIgnoreFields().add("serialVersionUID");
自定义QueryStructure解析器

QueryPro在处理QueryStructure转返回结果时,默认实现并使用了JdbcQSR
它使用Jdbc进行查询,不依赖Mybatis, Spring Data等框架。
出于某些目的,你也可以替换它。

QueryProConfig.global.setQueryStructureResolver(new JdbcQSR());
处理返回结果
1. 指定返回结果的类型

JdbcQSR会解析需返回结果的类型, 并尝试将JDBC的返回结果ResultSet转换成目标类型。

但是当无法解析出具体的字段类型时,例如:runAsMap方法,返回一个Map,此时便无法知晓需转换的具体类型是什么,
这时JdbcQSR会使用JDBC自带的getObject将返回结果转换成java类型,其行为参考下表,具体可至 com.mysql.cj.jdbc.result.ResultSetImpl.getObject查看

SQL Type Java Type
BIT byte[], deserialized
BOOLEAN Boolean
TINYINT Integer
TINYINT_UNSIGNED Integer
SMALLINT Integer
SMALLINT_UNSIGNED Integer
MEDIUMINT Integer
MEDIUMINT_UNSIGNED Integer
INT Integer
INT_UNSIGNED Long
BIGINT Long
BIGINT_UNSIGNED BigInteger
DECIMAL BigDecimal
DECIMAL_UNSIGNED BigDecimal
FLOAT Float
FLOAT_UNSIGNED Float
DOUBLE Double
DOUBLE_UNSIGNED Double
CHAR String
ENUM String
SET String
VARCHAR String
TINYTEXT String
TEXT String
MEDIUMTEXT String
LONGTEXT String
JSON String
GEOMETRY byte[]
BINARY byte[], deserialized
VARBINARY byte[], deserialized
TINYBLOB byte[], deserialized
MEDIUMBLOB byte[], deserialized
LONGBLOB byte[], deserialized
BLOB byte[], deserialized
YEAR Date, Short
DATE Date
TIME Time
TIMESTAMP Timestamp
DATETIME LocalDateTime
- String

可以看到它会将无符号的长整型BIGINT_UNSIGNED类型转为BigInteger
但是针对id字段,通常我们希望转为Long类型, 就可以使用如下代码配置一个dbColumn解析器。

// 如果没有指定返回结果的类型,将id或_id结尾的无符号BIGINT类型转为Long。(这是默认行为,无需额外配置)
QueryProConfig.global.dbColumnInfoToJavaType().put(
        (columnInfo) -> {
            if (columnInfo.getType().startsWith("BIGINT")) { // 以BIGINT开头,包括了BIGINT_UNSIGNED
                String label = columnInfo.getLabel();
                if (label.equals("id") || label.endsWith("_id")) { // 字段名为id或者以_id结尾的
                    return true;
                }
            }
            return false;
        },
        Long.class // 转为Long类型
);
2.增加某种返回类型的支持

上面说到:JdbcQSR会解析需返回结果的类型,
并尝试将JDBC的返回结果ResultSet转换成目标类型。

这是通过配置中的ResultSetParsers实现的,我们可以通过QueryProConfig.global.addResultSetParser()添加它。 默认支持的类型有BigDecimal, Byte, ByteArray, Date, LocalDate, LocalTime, LocalDateTime, java.sql.Date, Double, Float, Int, Long, Time, Timestamp, Short, String 以及 枚举类型

// 例如,这两个ResultSetParser一个简单一个略复杂,都已经内置在了`QueryPro`中
QueryProConfig.global
        .addResultSetParser(
            LocalDateTime.class,
            (resultSet, columIndex) -> resultSet.getTimestamp(columIndex).toLocalDateTime()
        )
        .addResultSetParserEx((resultSet, targetClass, columnIndex) -> { // 当需要同时支持多种返回类型时,可以使用addResultSetParserEx
            if (!targetClass.isEnum()) {
                return Optional.empty();
            }
            return Optional.of(Enum.valueOf((Class) targetClass, resultSet.getString(columnIndex)));
        });
setParam
lifecycle
QueryProConfig.global
        .lifecycle()
        .beforeInsert(
            builder -> builder
                .addField("deleted", Boolean.class, () -> false)
                .addField("create_time", Date.class, Date::new)
                .addField("create_by", String.class, XX::getCurrentUser)
        )
        .beforeUpdate(
            builder -> builder
                .overrideField("update_time", Date.class, Date::new)
                .overrideField("update_by", String.class, XX::getCurrentUser)
        );

其它

访问ResultSet

public class ResultSetWalkerTest {

    private static class ResultSetWalker implements JdbcQSR.IResultSetWalker {
        final private List<String> columnNames = new ArrayList<>();

        @Override
        public void walk(@NotNull ResultSet rs) throws Exception {
            final ResultSetMetaData metaData = rs.getMetaData();
            final int columnCount = metaData.getColumnCount();
            for (int i = 1; i <= columnCount; i++) {
                final String columnLabel = metaData.getColumnLabel(i);
                columnNames.add(columnLabel);
            }
        }
    }

    @Test
    public void test() { QueryProConfig.context.use(conf -> {
        // 测试用例中,需要初始化QueryPro
        initLogger();
        conf.dataSource(getDataSource());

        // 提供一个实现了JdbcQSR.IResultSetWalker的类
        final ResultSetWalker walker = QueryProSql.create("select * from user left join setting on user.id = setting.user_id").queryOne(ResultSetWalker.class);
        assert walker != null;
        System.out.println(walker.columnNames);
    });}

}

项目结构

代码结构

  • config包 QueryPro支持很多不同级别的配置(全局请求线程上下文以及单次查询)该包主要用于处理这些配置信息。
  • resolver包QueryStructure等结构转换为SQL并执行或直接执行SQL
  • QueryStructure.kt 描述查询,更新,插入,删除的一个可序列化的结构
  • QueryPayload.kt QueryStructure是为了生成SQL, QueryPayload是为了记录一些查询时产生的非SQL组成部分的配置,有了这两个,就可以进行resolve了,QueryPayload包含单次查询中编写的配置信息(如有),查询的元信息等
  • QueryPro.kt 查询(也包含增删改)入口,所有生成的QueryPro文件均'继承'(实际上是委托(delegate))该类
  • QueryProSql.kt 直接执行SQL语句或者动态生成插入语句并执行
  • plus包 额外添加多表联合查询的支持
  • util包 一些工具类
  • Pageable.kt 为查询添加分页的支持(含两种模式:总条数模式和是否存在下一页模式)

后续规划

类似这样的语句的处理 UPDATE word SET score = score + 1 WHERE id = 1 对sum, concat, group_concat, discount等 的支持 添加带下划线驼峰式的列(更新操作) 配置模块测试用例 大数据量log优化(默认显示前几条数据,另外可配置显示所有数据) 添加ENUM的支持 常规批量插入中添加DEFAULT的支持