Skip to content

Db 工具类 ​

使用 java-db 库时,单数据源业务直接调用静态 Db,复用 ActiveRecord 已初始化的默认配置,无需持有 DbPro 实例;JSONB 显式解析、命名数据源、事务边界和测试隔离见 PostgreSQL 业务实践。


基础使用 ​

1. 常见用法 ​

Db 类及其配套的 Row 类,提供了在 Model 类之外更为丰富的数据库操作功能。使用 Db 与 Row 类时,无需对数据库表进行映射,Row 相当于一个通用的 Model。以下是 Db + Row 模式的一些常见用法:

导入配置

java
import nexus.io.db.activerecord.Db;
import nexus.io.db.activerecord.Row;
import nexus.io.model.db.IAtom;
java
// 创建 name 属性为 James, age 属性为 25 的 record 对象并添加到数据库
Row user = new Row().set("name", "James").set("age", 25);
Db.save("user", user);

// 删除 id 值为 25 的 user 表中的记录
Db.deleteById("user", 25);

// 查询 id 值为 25 的 Record,将其 name 属性改为 James 并更新到数据库
user = Db.findById("user", 25).set("name", "James");
Db.update("user", user);

// 获取 user 的 name 属性
String userName = user.getStr("name");
// 获取 user 的 age 属性
Integer userAge = user.getInt("age");

// 查询所有年龄大于 18 岁的用户
List<Row> users = Db.find("select * from user where age > 18");

// 分页查询年龄大于 18 岁的用户,当前页号为 1,每页 10 个用户
Page<Row> userPage = Db.paginate(1, 10, "select *", "from user where age > ?", 18);

2. 事务处理示例 ​

以下示例展示了如何在事务中执行多个数据库操作:

java
boolean succeed = Db.tx(new IAtom(){
    public boolean run() throws SQLException {
        int count = Db.updateBySql("update account set cash = cash - ? where id = ?", 100, 123);
        int count2 = Db.updateBySql("update account set cash = cash + ? where id = ?", 100, 456);
        return count == 1 && count2 == 1;
    }
});

以上两次数据库更新操作在一个事务中执行。如果执行过程中发生异常或 run() 方法返回 false,则事务会自动回滚。

3. Db.query 方法 ​

第一种用法 ​

当 SELECT 后的字段只有一个时,可以使用合适的泛型接收数据:

java
List<String> titleList = Db.query("select title from blog");

以上 SQL 中 SELECT 后只有一个 title 字段,因此使用 List<String> 来接收数据。接收数据的泛型变量可根据返回值类型变化,例如:

java
List<Integer> idList = Db.query("select id from blog");

在此例中,id 字段的返回值为 Integer,因此接收变量为 List<Integer>。

第二种用法 ​

当 SELECT 后的字段有多个时,必须使用 List<Object[]> 接收数据,例如:

java
List<Object[]> list = Db.query("select id, title, content from blog");
List<Object[]> list = Db.query("select * from blog");

4. Db.queryXxx 方法 ​

Db.queryXxx 系列方法包括 queryInt、queryLong、queryStr 等,对于使用聚合函数的 SQL 十分方便,例如:

java
int total = Db.queryInt("select count(*) from account");

以上 SQL 使用了 count(*) 聚合函数,使用 Db.queryInt 不仅方便,而且性能最佳。

此外,还可以用于查询某条记录的某个字段值,例如:

java
String nickName = Db.queryStr("select nickName from account where id = ? limit 1", 123);

上述代码通过 queryStr 方便地查询了 id 值为 123 的 account 的 nickName。

需要注意,Db.queryXxx 系列方法要求 SELECT 后只能有一个字段名,或者说只能返回一个列值(例如 count(*))。

5. Db.find 与 Db.query 系列方法的区别 ​

  • Db.find 系列方法:将返回值一律封装到一个 Row 对象中。
  • Db.query 系列方法:不进行封装,直接将数据原样返回。

两者在查询所使用的 SQL 及参数用法上完全相同。

6. updateBySql ​

如果编写了 sql 语句,需要进行插入,更新获取删除操作,请使用 updateBySql 方法,默认的 update 方法不支持传入 sql 使用示例

java
String updateBalanceBind2 = "UPDATE users SET balance = balance + 0.025, bind2_count = bind2_count + 1 WHERE tg_id = ?";
Db.updateBySql(updateBalanceBind2, bind2Id);

7. 扩展 Db 功能 ​

Db 工具类的所有功能依赖于底层的 DbPro。可以通过继承 DbPro 来定制所需的功能。例如:

java
public class MyDbPro extends DbPro {
    public MyDbPro(String configName) {
        super(configName);
    }

    @Override
    public List<Row> find(String sql, Object... paras) {
        System.out.println("Sql: " + sql);
        System.out.println("Paras: " + Arrays.toString(paras));
        return super.find(sql, paras);
    }
}

以上代码扩展了 DbPro 并覆盖了父类的 find(String, Object...) 方法,该方法在调用 super.find(...) 之前输出了 SQL 及其参数值。

然后,通过配置让 MyDbPro 取代 DbPro 的功能:

java
ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
arp.setDbProFactory(configName -> new MyDbPro(configName));

通过上述配置,在使用 Db.find(String, Object...) 方法时,将使用自定义的 MyDbPro 中实现的 find 方法。这种方法可以替换、增强或改变所有 DbPro 中 public 和 protected 方法的行为,极为灵活方便。

分页 ​

1. 常用 paginate 方法 ​

Model 与 Db 中提供了最常用的分页 API:

java
paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras)

参数含义:

  • pageNumber:当前页的页号
  • pageSize:每页数据条数
  • select:SQL 语句的 SELECT 部分
  • sqlExceptSelect:SQL 语句除了 SELECT 以外的部分
  • paras:查询参数

绝大多数情况下,使用此 API 即可。以下是使用示例:

java
dao.paginate(1, 10, "select *", "from girl where age > ? and weight < ?", 18, 50);

2. 带有 Group By 的 paginate ​

API 原型:

java
paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras)

相对于第一种,仅多了一个 boolean isGroupBySql 参数。以下是代码示例:

java
dao.paginate(1, 10, true, "select *", "from girl where age > ? group by age", 18);

在以上代码中,SQL 的最外层包含 GROUP BY age,因此第三个参数 isGroupBySql 需传入 true。如果是嵌套 SQL,但 GROUP BY 不在最外层,则第三个参数必须为 false,例如:

sql
select * from (select x from t group by y) as temp

重点提示:

  • isGroupBySql 参数仅在最外层 SQL 包含 GROUP BY 子句时设为 true。
  • 如果 GROUP BY 仅存在于嵌套 SQL 的内层,则应设为 false。

3. 使用 paginateByFullSql 方法 ​

API 原型:

java
paginateByFullSql(int pageNumber, int pageSize, String totalRowSql, String findSql, Object... paras)

相对于其他 paginate API,将查询总行数与查询数据的两条 SQL 独立出来。这主要用于应对复杂的 ORDER BY 语句或 SELECT 中带有 DISTINCT 的情况。当使用第一种 paginate 方法出现异常时,可考虑使用此 API。以下是代码示例:

java
String from = "from girl where age > ?";
String totalRowSql = "select count(*) " + from;
String findSql = "select * " + from + " order by age";
dao.paginateByFullSql(1, 10, totalRowSql, findSql, 18);

在上述代码中,ORDER BY 子句并不复杂,因此使用第一种 paginate 方法即可。

重点:

  • totalRowSql 与 findSql 必须共用最后一个参数 paras,即:

    • dao.find(totalRowSql, paras)
    • dao.find(findSql, paras)

    这两条 SQL 都必须能够正确执行,否则无法使用 paginateByFullSql。

4. 使用 SqlPara 参数的 paginate ​

API 原型:

java
paginate(int pageNumber, int pageSize, SqlPara sqlPara)

此方法用于配合 SQL 管理功能使用,具体将在 SQL 管理功能章节中详细介绍。

5. 常见问题及解决方案 ​

为了有效解决分页过程中可能遇到的问题,需了解 paginate 方法的底层实现原理。以下假设有如下分页代码:

java
paginate(1, 5, "select *", "from article where id > ? order by id", 10);

底层实现:

  1. 生成用于获取满足查询条件的所有记录数(totalRow)的 SQL:

    sql
    select count(*) from article where id > 10
    • "select count(*)" 是固定写死的。
    • "from article where id > 10" 是根据用户提供的第四个参数,去除 ORDER BY id 后得到的。
  2. 去除 ORDER BY 子句的原因:

    • 许多数据库不支持带有 ORDER BY 子句的 SELECT count(*) 查询,必须去除以避免错误。
    • SELECT count(*) 查询在有无 ORDER BY 子句时结果相同,去除后有助于提升性能。

常见错误及解决方案:

第一类错误:如果用户分页的第四个参数中含有多个问号占位符,可能导致参数错位。

示例:

java
paginate(1, 5, "select (... where x = ?)", "from article where id = ?", 8, 10);

此时生成的用于计算 totalRow 的 SQL 为:

sql
select count(*) from article where id = ?, 8, 10

由于多了一个参数 8,会导致 SQL 执行异常。解决方法是将原 SQL 外层套一个 SQL,使第三个参数中不含问号占位符:

java
paginate(1, 5, "select *", "from (" + 原sql + ") as t", 8, 10);

即将原 SQL 外层再套一个 SELECT _ FROM (...) as t,避免 count(*) 查询时参数错位。

第二类错误:如果 ORDER BY 子句使用了子查询或函数调用,paginate 方法可能无法正确移除 ORDER BY 子句,导致 SQL 错误。

示例:

java
paginate(1, 5, "select *", "from ... order by concat(...)", ...);

解决方法:使用 paginateByFullSql 方法,让用户手动编写用于计算 totalRow 的 SQL 与查询数据的 SQL,避免 paginate 方法自动处理 ORDER BY 子句带来的问题。

总结:

  • 了解 paginate 的底层工作机制有助于有效解决分页过程中遇到的问题。
  • 在复杂 SQL 情况下,优先考虑使用 paginateByFullSql 方法以获得更大的灵活性。

通过以上内容,您可以全面掌握 Db 工具类的使用方法及其分页功能,并有效应对常见问题,提升数据库操作的效率与稳定性。

直接使用 Db 的补充入口 ​

  • Db.txResult(...):事务提交后返回业务数据,无需手写结果容器。
  • Db.findFirstWithJsonField(...):读取第一条记录并解析明确指定的 JSON 列;无记录返回 null。

两者都提供对应的 DbPro 入口。参数绑定、回滚和业务层职责见 PostgreSQL 业务实践。