核心接口 Baki
直接提供数据库访问功能的接口 Baki ,通过实例化此接口的默认实现 BakiDao 来执行各种操作,初始化流程如下:
DataSource datasource = new HikariDataSource();
...
BakiDao baki = new BakiDao(dataSource);
至此,现已可以通过 BakiDao 来传入 SQL 执行数据库访问操作,还可以选择配置 XQL 文件管理器:
XQLFileManager xqlFileManager = new XQLFileManager("xql-file-manager.yml");
...
baki.setXqlFileManager(xqlFileManager);
具体配置项请参考文档 XQL 文件管理器。
Baki 接口提供的所有能传入 SQL 字符串的方法现在都可以传入 “SQL 地址“,通过取地址符 & 来获取 XQL 文件管理器中的 SQL,并进行动态解析,例如:&my.users,格式为 别名.SQL名 :
graph LR
Baki["query(...)"] --> A[#quot;select ...#quot;]
Baki --> B[#quot;&my.users#quot;]
B --> X[XQLFileManager]
X --> A
A --> R[Do execute]
baki.query("select … where id = :id").args("id", "1")
baki.query("&my.users").args("id", "1")
SQL 语法中形如
:id是默认的命名参数占位符语法。
查询
通过 query 方法来获得一个查询执行器 QueryExecutor ,查询执行器可以返回的数据类型如下:
stream():返回一个DataRow类型惰性查询对象;maps():返回一组Map结果;rows():返回一组DataRow包装接口;entities():返回一组实体;pageable(page?, size?):返回一个分页查询构建器,如果不传入page和size,则默认从参数中取对应 key 的值;findFirstRow():查询第一条,返回一个DataRow对象,不会为null;findFirstEntity():查询第一条,返回一个实体对象,可能为null;需要配置属性
BakiDao#entityMetaProviderfindFirst():查询第一条,返回一个Optional可空对象;需要配置属性
BakiDao#entityMetaProvider
惰性查询
惰性查询返回一个 Stream 对象,其内部持有 JDBC Connection。执行终端操作后,必须关闭 Stream,否则连接不会释放,最终可能导致连接池耗尽。
只有执行终端操作时才会真正开始查询,例如 Stream#collect()。请特别注意,推荐使用 try-with-resource 包裹,查询完成后自动释放连接对象:
try (Stream<DataRow> s = baki.query("&my.query").args("id", 5).stream()) {
s.forEach(System.out::println);
}
如果需要对结果集做二次处理,例如调用
.map(...)、.filter(...)等操作,使用惰性查询可以有效提高性能。
分页查询
默认的分页查询将自动根据数据库生成分页查询语句和生成 count 查询语句。
内置支持 oracle,mysql,postgresql,sqlite,mariadb,db2,其他可通过实现接口 com.github.chengyuxing.sql.plugins.PageHelperProvider 并添加到 BakiDao 进行支持。
PagedResource<DataRow> resource = baki.query("select ... where id < :id")
.arg("id", 8)
.pageable(1, 7)
.collect();
内置的条数查询 SQL 只会做简单包裹。如果要获得最佳性能,可以自行编写条数查询 SQL,并通过
.count(sql)指定。
自定义分页查询
在一些特殊情况下,内建的分页查询构建器无法满足特别的需求,如下 SQL 分页在视图或子查询中:
/*[custom_paged]*/
with cte as (
select * from test.region
where id > :id limit :start offset :end
)
select * from t;
;
分页构建器需要增加一些配置:
PagedResource<DataRow> res = baki.query("&data.custom_paged")
.pageable(1, 7)
.disableDefaultPageSql("&data.custom_paged_count", "start", "end")
.collect();
count(sql):必要的条数查询语句;disableDefaultPageSql(sql):禁用默认生成的分页 SQL,并指定条数查询 SQL 语句,重写默认的分页参数,因为您自定义的查询 SQL 分页参数名不被限制,所以很有必要调用此方法进行重写,重写为如上例子start和end;
增删改
execute(sql, Map?):支持 select , ddl , dml 和 plsql 语句;execute(sql, Iterable):使用参数集合执行预编译批处理,各行生成的 SQL 结构必须一致;execute(Iterable<String>):批量执行原始 SQL,可用于 DDL、DML;
单表实体操作
- 实现接口
com.github.chengyuxing.sql.EntityManager.EntityMetaProvider - 配置
BakiDao#entityMetaProvider - 调用方法:
baki#entity来执行简单的单表实体增删改查query()insert()update()delete()
查询
下面对条件构建器 where 做进一步说明。条件构建器支持条件嵌套,默认情况下每个条件都以 and 关键字连接。
框架会根据嵌套条件表达式在多数情况下自动调整逻辑。对于 and() 和 or() 嵌套的特殊情况:
and():内部所有条件判断以关键字or相连or():内部所有条件判断以关键字and相连
条件:
where id > 5 and (id in (17, 18, 19) or id = 10)
条件构建器:
baki.entity(Guest.class)
.query()
.where(w -> w.gt(Guest::getId, 5)
.and(o -> o.in(Guest::getId, List.of(17, 18, 19))
.eq(Guest::getId, 10))
)
// ...
.forEach(System.out::println);
增删改
主要对 insert() 和 update() 进行特别的说明。
在实现接口方法 EntityManager.ColumnMeta columnMeta(Field field) 时,可根据自定义注解或 JPA 注解属性来配置字段约束:
- primaryKey :唯一主键,不允许更新;按主键更新及
NONE策略插入时不允许为null,IDENTITY策略插入时由数据库生成 - insertable :是否可插入
- updatable :是否可更新
- ignore:是否忽略字段(为
true则不参与 SQL 的生成)
集合字段
默认情况下直接调用 save(T entity) 和 save(Iterable entities),通过数据生成的 SQL 会排除值为 null 的列,批量操作通过普通的循环遍历。
通过调用方法 withNullValues() ,无论字段是否为 null 都进行保留,底层调用 JDBC 的批量插入。
baki.entity(Guest.class)
.insert()
.withNullValues()
.save(guests);
部分字段
通过调用方法 set(column, value) 进行部分字段的写入和更新,允许值为 null ,生成 SQL 仅包含指定的字段。
baki.entity(Guest.class)
.insert()
.set(Guest::getXm, "cyx")
.set(Guest::getAddress, "kunming")
.save();
baki.entity(Guest.class)
.update()
.where(w -> w.eq(Guest::getXm, "cyx"))
.set(Guest::getXm, "mike")
.set(Guest::getAddress, "USA")
.save();
单表 DML 操作
对于单表的简单 DML 操作,通过方法 baki#table 传入表名执行相应的操作。
默认情况下 update() 和 insert() 操作集合为循环遍历。
通过调用方法 enableBatch() 执行底层 JDBC 的批量操作。插入、更新支持空输入和只能遍历一次的 Iterable,每行参数映射只调用一次。各行 SQL 结构必须一致,整批回滚需要事务,详见 批量操作。
按条件更新和删除通过方法 by(column, ...) 实现,内部会用 and 把多个字段构建为等式连接:
baki.table("test.guest")
.by("id" , "name")
.update(...)
update test.guest set ... where id = :id and name = :name
执行存储过程/函数
方法 call(params) 返回 DataRow 包装对象结果。通过命名参数名获取结果;如果返回值是游标,结果类型为 List<DataRow>,其他情况下返回数据库字段类型对应的 Java 数据类型。
baki.call("{:res = call test.sum(:a, :b)}",
Args.of("res", Param.OUT(StandardOutParamType.INTEGER))
.add("a", Param.IN(34))
.add("b", Param.IN(56))
).getOptional("res")
通过一个函数过程定义来说明一下几种不同的写法:
单返回值
create function sum(a integer, b integer) returns integer
language plpgsql
...
命名返回值的写法,此时可以通过参数名 res 取到结果:
{:res = call sum(:a, :b)}
匿名返回值写法,此时可按顺序通过索引 0 或默认的 key result 来取到结果:
{call sum(:a, :b)}
多返回值
多返回值函数定义通常在参数中有多个出参,根据出参的个数按顺序通过索引或者 key 名来取到结果,例如:
{call multiple_result(:id, :res1, :res2)}
注意事项
不同数据库和不同的函数定义方式,可能会限定只能用某种写法,具体可以在调试过程中调整合适的语法。
例如,在 PostgreSQL v13+ 中使用 create procedure 创建的过程,调用时不能加 {} 括号:
call procedure()
PostgreSQL 中使用 Python 创建的函数,返回值只能使用匿名返回值写法,否则会抛出异常:
create function mvnd(keyword text)
returns TABLE(group_id text, artifact_id text, latest_version text)
language plpython3u
as
$$
...
for item in arr:
yield ( item['g'],item['a'],item['latestVersion'] )
$$;
调用写法和获取结果:
baki.call("{call mvnd(:keyword)}",
Args.of("keyword", Param.IN("chengyuxing")))
.<List<DataRow>>getAs(0);