Db 工具类
基础使用
1. 常见用法
Db
类及其配套的 Row
类,提供了在 Model
类之外更为丰富的数据库操作功能。使用 Db
与 Row
类时,无需对数据库表进行映射,Row
相当于一个通用的 Model
。以下是 Db + Row
模式的一些常见用法:
导入配置
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.model.db.IAtom;
// 创建 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. 事务处理示例
以下示例展示了如何在事务中执行多个数据库操作:
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
后的字段只有一个时,可以使用合适的泛型接收数据:
List<String> titleList = Db.query("select title from blog");
以上 SQL 中 SELECT
后只有一个 title
字段,因此使用 List<String>
来接收数据。接收数据的泛型变量可根据返回值类型变化,例如:
List<Integer> idList = Db.query("select id from blog");
在此例中,id
字段的返回值为 Integer
,因此接收变量为 List<Integer>
。
第二种用法
当 SELECT
后的字段有多个时,必须使用 List<Object[]>
接收数据,例如:
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 十分方便,例如:
int total = Db.queryInt("select count(*) from account");
以上 SQL 使用了 count(*)
聚合函数,使用 Db.queryInt
不仅方便,而且性能最佳。
此外,还可以用于查询某条记录的某个字段值,例如:
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 使用示例
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
来定制所需的功能。例如:
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
的功能:
ActiveRecordPlugin arp = new ActiveRecordPlugin(...);
arp.setDbProFactory(configName -> new MyDbPro(configName));
通过上述配置,在使用 Db.find(String, Object...)
方法时,将使用自定义的 MyDbPro
中实现的 find
方法。这种方法可以替换、增强或改变所有 DbPro
中 public
和 protected
方法的行为,极为灵活方便。
分页
1. 常用 paginate 方法
Model
与 Db
中提供了最常用的分页 API:
paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras)
参数含义:
pageNumber
:当前页的页号pageSize
:每页数据条数select
:SQL 语句的SELECT
部分sqlExceptSelect
:SQL 语句除了SELECT
以外的部分paras
:查询参数
绝大多数情况下,使用此 API 即可。以下是使用示例:
dao.paginate(1, 10, "select *", "from girl where age > ? and weight < ?", 18, 50);
2. 带有 Group By 的 paginate
API 原型:
paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras)
相对于第一种,仅多了一个 boolean isGroupBySql
参数。以下是代码示例:
dao.paginate(1, 10, true, "select *", "from girl where age > ? group by age", 18);
在以上代码中,SQL 的最外层包含 GROUP BY age
,因此第三个参数 isGroupBySql
需传入 true
。如果是嵌套 SQL,但 GROUP BY
不在最外层,则第三个参数必须为 false
,例如:
select * from (select x from t group by y) as temp
重点提示:
isGroupBySql
参数仅在最外层 SQL 包含GROUP BY
子句时设为true
。- 如果
GROUP BY
仅存在于嵌套 SQL 的内层,则应设为false
。
3. 使用 paginateByFullSql 方法
API 原型:
paginateByFullSql(int pageNumber, int pageSize, String totalRowSql, String findSql, Object... paras)
相对于其他 paginate
API,将查询总行数与查询数据的两条 SQL 独立出来。这主要用于应对复杂的 ORDER BY
语句或 SELECT
中带有 DISTINCT
的情况。当使用第一种 paginate
方法出现异常时,可考虑使用此 API。以下是代码示例:
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 原型:
paginate(int pageNumber, int pageSize, SqlPara sqlPara)
此方法用于配合 SQL 管理功能使用,具体将在 SQL 管理功能章节中详细介绍。
5. 常见问题及解决方案
为了有效解决分页过程中可能遇到的问题,需了解 paginate
方法的底层实现原理。以下假设有如下分页代码:
paginate(1, 5, "select *", "from article where id > ? order by id", 10);
底层实现:
生成用于获取满足查询条件的所有记录数(
totalRow
)的 SQL:select count(*) from article where id > 10
"select count(*)"
是固定写死的。"from article where id > 10"
是根据用户提供的第四个参数,去除ORDER BY id
后得到的。
去除
ORDER BY
子句的原因:- 许多数据库不支持带有
ORDER BY
子句的SELECT count(*)
查询,必须去除以避免错误。 SELECT count(*)
查询在有无ORDER BY
子句时结果相同,去除后有助于提升性能。
- 许多数据库不支持带有
常见错误及解决方案:
第一类错误:如果用户分页的第四个参数中含有多个问号占位符,可能导致参数错位。
示例:
paginate(1, 5, "select (... where x = ?)", "from article where id = ?", 8, 10);
此时生成的用于计算 totalRow
的 SQL 为:
select count(*) from article where id = ?, 8, 10
由于多了一个参数 8
,会导致 SQL 执行异常。解决方法是将原 SQL 外层套一个 SQL,使第三个参数中不含问号占位符:
paginate(1, 5, "select *", "from (" + 原sql + ") as t", 8, 10);
即将原 SQL 外层再套一个 SELECT _ FROM (...) as t
,避免 count(*)
查询时参数错位。
第二类错误:如果 ORDER BY
子句使用了子查询或函数调用,paginate
方法可能无法正确移除 ORDER BY
子句,导致 SQL 错误。
示例:
paginate(1, 5, "select *", "from ... order by concat(...)", ...);
解决方法:使用 paginateByFullSql
方法,让用户手动编写用于计算 totalRow
的 SQL 与查询数据的 SQL,避免 paginate
方法自动处理 ORDER BY
子句带来的问题。
总结:
- 了解
paginate
的底层工作机制有助于有效解决分页过程中遇到的问题。 - 在复杂 SQL 情况下,优先考虑使用
paginateByFullSql
方法以获得更大的灵活性。
通过以上内容,您可以全面掌握 Db
工具类的使用方法及其分页功能,并有效应对常见问题,提升数据库操作的效率与稳定性。
批量操作
批量操作方法
在实际开发中,批量操作能够显著提高数据库的处理效率,减少单条操作带来的性能开销。本文将以 Java-db 框架中的Db
类为例,详细介绍批量操作的使用方法,包括批量保存和批量更新。
类名:Db.java
方法签名
public static int[] batch(String sql, String columns, List modelOrRowList, int batchSize)
参数说明
sql
:指的是INSERT INTO
或UPDATE
语句。动态参数使用问号(?
)作为占位符,例如:String sql = "insert into user(name, cash) values(?, ?)";
上述 SQL 语句中需要插入
name
和cash
两个字段,values
中的两个问号将从后续的modelOrRowList
中获取对应的值。columns
:指的是前面 SQL 语句中问号占位符对应的参数名称。Java-db 在填充字段值时,会根据这个名称从modelOrRowList
中提取数据。modelOrRowList
:指的是List<User>
或List<Row>
等类型的列表,包含需要批量操作的数据。batchSize
:指的是每批次写入数据库的数据条数。
批量保存
int[] batchSave(String tableName, List<? extends Row> RowList, int batchSize)
批量更新
int[] batchUpdate(String tableName, List<? extends Row> RowList, int batchSize)
批量保存执行案例
以下示例展示了如何批量保存数据到数据库:
List<Row> insertList = new ArrayList<Row>();
for (Dependency dependency : listDependencies) {
Row e = new Row();
e.set("group_id", dependency.getGroupId());
e.set("artifact_id", dependency.getArtifactId());
e.set("version", dependency.getVersion());
insertList.add(e);
}
// 插入数据库
Db.batchSave("t_maven_dependency", insertList, 2000);
执行的 SQL 语句
INSERT INTO `t_maven_dependency`(`group_id`, `artifact_id`, `version`) VALUES(?, ?, ?)
结论
即使"t_maven_dependency"
表中包含多个字段,batchSave
方法只会插入Row
中存在的字段,从而提高了操作的灵活性和效率。
Batch Update 使用示例
以下代码展示了如何使用 Java-db 的Db
模块进行批量更新操作。
import com.Java-db.kit.Kv;
import com.litongjava.data.model.DbJsonBean;
import com.litongjava.data.services.DbJsonService;
import com.litongjava.Java-db.aop.Aop;
import com.litongjava.Java-db.plugin.activeRow.Db;
import com.litongjava.Java-db.plugin.activeRow.Row;
import com.litongjava.tio.boot.admin.config.TableToJsonConfig;
import com.litongjava.tio.boot.tesing.TioBootTest;
import org.junit.Test;
public class BatchUpdateExample {
@Test
public void batchUpdateByIds() {
String tableName = "tio_boot_admin_system_constants_config";
DbJsonService dbJsonService = Aop.get(DbJsonService.class);
Long[] ids = new Long[]{1L, 100L, 369029537511587840L};
Kv kv = Kv.create();
kv.set("ids", ids);
kv.set("deleted", 1);
DbJsonBean<Kv> kvDbJsonBean = dbJsonService.batchUpdateByIds(tableName, kv);
System.out.println(kvDbJsonBean);
}
public DbJsonBean<Kv> batchUpdateByIds(String f, Kv kv) {
DbPro dbPro = Db.use();
return batchUpdateByIds(dbPro, f, kv);
}
public DbJsonBean<Kv> batchUpdateByIds(DbPro dbPro, String tableName, Kv kv) {
Object[] ids = kv.getAs("ids", new Object[0]);
kv.remove("ids");
String primaryKeyName = primaryKeyService.getPrimaryKeyName(tableName);
List<Row> lists = new ArrayList<>();
for (Object id : ids) {
Row Row = new Row();
Row.setColumns(kv.toMap());
Row.set(primaryKeyName, id);
lists.add(Row);
}
int[] results = dbPro.batchUpdate(tableName, lists, lists.size());
return new DbJsonBean<>(Kv.by("data", results));
}
}
代码分析
测试方法
batchUpdateByIds
- 方法定义:使用
@Test
注解标记,表示这是一个 JUnit 的测试方法。 - 初始化数据:
tableName
:指定要操作的数据库表名为"tio_boot_admin_system_constants_config"
。dbJsonService
:通过Aop.get(DbJsonService.class)
获取数据库操作的服务对象。ids
:包含三个 ID,用于标识将要更新的数据库记录。kv
:创建一个Kv
对象,设置ids
和deleted
。deleted
设置为 1,可能表示将这些记录标记为删除。
- 执行更新:
- 调用
dbJsonService.batchUpdateByIds(tableName, kv)
方法执行批量更新。 - 输出更新结果
kvDbJsonBean
,这是一个包装了更新结果的对象。
- 调用
- 方法定义:使用
方法
batchUpdateByIds(String f, Kv kv)
- 该方法使用默认的数据库配置创建一个
DbPro
对象(Db.use()
),然后调用重载的batchUpdateByIds
方法进行具体操作。
- 该方法使用默认的数据库配置创建一个
方法
batchUpdateByIds(DbPro dbPro, String tableName, Kv kv)
- 提取 IDs:从
kv
对象中提取ids
数组,并移除ids
键。 - 获取主键名:使用
primaryKeyService.getPrimaryKeyName(tableName)
获取指定表的主键字段名。 - 构造更新记录:
- 创建一个空的
Row
列表。 - 遍历
ids
,对每一个 ID,创建一个Row
,将除ids
外的kv
键值对设置为Row
的列,并设置主键字段为对应的 ID。
- 创建一个空的
- 执行批量更新:
- 使用
dbPro.batchUpdate(tableName, lists, lists.size())
执行批量更新,其中lists
是待更新的记录列表,lists.size()
指定批次大小。
- 使用
- 返回结果:将更新操作的结果(更新了多少行)封装在
DbJsonBean<Kv>
对象中并返回。
- 提取 IDs:从
执行的 SQL 语句
UPDATE "tio_boot_admin_system_constants_config" SET "deleted" = ? WHERE "id" = ?
结论
这段代码展示了如何使用 Java-db 的Db
模块进行批量更新操作,特别适用于需要批量更新多个数据库记录且每条记录的更新内容相同的场景。
独立使用 ActiveRow 进行数据批量保存
以下示例展示了如何独立使用 Java-db 的 ActiveRow Java-db 进行批量保存操作。
代码实现
package com.litongjava.ws.schild.demo;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONObject;
import com.Java-db.plugin.activeRow.ActiveRowPlugin;
import com.Java-db.plugin.activeRow.Db;
import com.Java-db.plugin.activeRow.Row;
import com.Java-db.plugin.druid.DruidPlugin;
import com.litongjava.Java-db.models.voidetotext.model.Recognized;
import com.litongjava.utils.json.fastjson.FastJsonUtils;
import com.litongjava.ws.schild.utils.BaiduAsrUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 将视频转为文本并保存到数据库
*
* @author
*
*/
@Slf4j
public class AudioToTextAndSaveDb {
public static void main(String[] args) throws IOException {
// 1.读取json文件,获取文件路径
List<String> pcmFileList = FastJsonUtils.readFileToList("pcm-file-list.json", String.class);
// 识别两个文件测试
// List<String> pcmFileList = new ArrayList<>();
// pcmFileList.add("H:\\video\\软件使用学习视频\\MAYA建模\\P01.MAYA:界面讲解_chunks\\0000.pcm");
// pcmFileList.add("H:\\video\\软件使用学习视频\\MAYA建模\\P01.MAYA:界面讲解_chunks\\0001.pcm");
int size = pcmFileList.size();
log.info("pcmFileList size:{}", size);
// 2.将pcm文件转为文本
List<Recognized> recognizedList = new ArrayList<>();
for (int i = 0; i < size; i++) {
log.info("开始处理:{}", i);
String filePath = pcmFileList.get(i);
JSONObject jsonObject = BaiduAsrUtils.asr(filePath, "PCM", 16000, null);
Recognized recognized = new Recognized();
recognized.setPcmFile(filePath);
recognized.setText(jsonObject.toString());
recognizedList.add(recognized);
}
// 3.保存到数据库
List<Row> insertList = new ArrayList<Row>();
for (Recognized recognized : recognizedList) {
Row Row = new Row();
Row.set("pcm_file", recognized.getPcmFile());
Row.set("text", recognized.getText());
insertList.add(Row);
}
String[] datasource1 = {
"jdbc:sqlite:D:/sqllite/java-se-ws-schild-voide-to-audio.db", // url
"", "" // user and password
};
DruidPlugin plugin1 = new DruidPlugin(datasource1[0], datasource1[1], datasource1[2]);
ActiveRowPlugin arp1 = new ActiveRowPlugin(plugin1);
plugin1.start();
arp1.start();
// 插入数据库
log.info("开始批量保存数据");
Db.batchSave("recognized", insertList, 2000);
log.info("批量保存数据完成");
}
}
代码说明
- 读取 PCM 文件列表:从
pcm-file-list.json
文件中读取 PCM 文件路径列表。 - 将 PCM 文件转为文本:使用
BaiduAsrUtils.asr
方法将 PCM 文件转为文本,并存储在Recognized
对象中。 - 准备批量插入数据:将
Recognized
对象转换为Row
对象,添加到insertList
中。 - 配置数据库连接:使用 Druid Java-db 和 ActiveRow Java-db 配置数据库连接。
- 批量保存数据:调用
Db.batchSave
方法将数据批量保存到recognized
表中。
注意:代码中还有许多优化空间,例如将数据进行拆分、分线程处理等。
独立使用 ActiveRow 进行数据的批量插入
以下示例展示了如何使用 Java-db 的 ActiveRow Java-db 从源数据库查询水位数据,处理后插入到目标数据库。
背景
将水位数据从源数据库中查询出来,处理后插入到目标数据库。源数据库的数据格式如下:
- 源数据库表名:
water_level
目标数据库的格式:
- 目标数据库表名:
river_level
SELECT * FROM river_level LIMIT 10;
两个数据库的数据表的time
字段格式不同
第一步:在本地创建目标数据库和表
CREATE DATABASE cjwb DEFAULT CHARACTER SET utf8;
USE cjwb;
GRANT ALL PRIVILEGES ON cjwb.* TO cjwb@'127.0.0.1' IDENTIFIED BY 'xxx';
GRANT ALL PRIVILEGES ON cjwb.* TO cjwb@'localhost' IDENTIFIED BY 'xxx';
FLUSH PRIVILEGES;
CREATE TABLE `river_level` (
`id` VARCHAR(255) NOT NULL,
`level` DECIMAL(19,2) DEFAULT NULL,
`site_name` VARCHAR(255) DEFAULT NULL,
`time` DATETIME DEFAULT NULL,
`name` VARCHAR(255) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
第二步:编写代码
package com.esunward.spider.service;
import java.util.ArrayList;
import java.util.List;
import com.Java-db.plugin.activeRow.ActiveRowPlugin;
import com.Java-db.plugin.activeRow.Db;
import com.Java-db.plugin.activeRow.Row;
import com.Java-db.plugin.druid.DruidPlugin;
import com.litong.utils.string.UUIDUtils;
import lombok.extern.slf4j.Slf4j;
/**
* 从水位APP上查询并导入数据到目标数据库
*/
@Slf4j
public class WaterLevelImportService {
private String ds1 = "datasource1";
private String ds2 = "datasource2";
private String[] datasource1 = {
"jdbc:mysql://xxx/yangtze_river_app?useUnicode=true&characterEncoding=utf8&useSSL=false",
"yangtze_river_ap", ""
};
private String[] datasource2 = {
"jdbc:mysql://127.0.0.1:3306/cjwb?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC",
"cjwb", "xxx"
};
/**
* 启动数据源
*/
private void start() {
DruidPlugin plugin1 = new DruidPlugin(datasource1[0], datasource1[1], datasource1[2]);
ActiveRowPlugin arp1 = new ActiveRowPlugin(ds1, plugin1);
plugin1.start();
arp1.start();
DruidPlugin plugin2 = new DruidPlugin(datasource2[0], datasource2[1], datasource2[2]);
ActiveRowPlugin arp2 = new ActiveRowPlugin(ds2, plugin2);
plugin2.start();
arp2.start();
}
/**
* 判断数据源是否连接成功
*/
public void selectFromAllDatasource() {
List<Row> find = Db.use(ds1).find("SELECT 1");
if (find != null) {
log.info(ds1 + "连接成功");
}
find = Db.use(ds2).find("SELECT 1");
if (find != null) {
log.info(ds2 + "连接成功");
}
}
/**
* 从datasource1查询water_level,处理后插入到datasource2的river_level,名称设置为spider
*/
public void fromDatasource1ToDatasource2() {
String sqlSource = "SELECT * FROM water_level WHERE time > '2020-08-15'";
List<Row> sourceRows = Db.use(ds1).find(sqlSource);
log.info("水位总条数:{}", sourceRows.size());
String sqlCheck = "SELECT COUNT(*) FROM river_level WHERE site_name = ? AND time = ?";
List<Row> insertList = new ArrayList<Row>();
for (Row r : sourceRows) {
String timeString = r.getStr("time");
String timeAMString = timeString + " 08:00:00";
Row countRow = Db.use(ds2).findFirst(sqlCheck, r.getStr("site_name"), timeAMString);
if (countRow.getInt("COUNT(*)") == 0) {
String randomId = UUIDUtils.random();
Row insertRow = new Row();
insertRow.set("id", randomId);
insertRow.set("site_name", r.getStr("site_name"));
insertRow.set("level", r.getStr("level"));
insertRow.set("time", timeAMString);
insertRow.set("name", "spider");
insertList.add(insertRow);
}
String timePMString = timeString + " 17:00:00";
countRow = Db.use(ds2).findFirst(sqlCheck, r.getStr("site_name"), timePMString);
if (countRow.getInt("COUNT(*)") == 0) {
String randomId = UUIDUtils.random();
Row insertRow = new Row();
insertRow.set("id", randomId);
insertRow.set("site_name", r.getStr("site_name"));
insertRow.set("level", r.getStr("level"));
insertRow.set("time", timePMString);
insertRow.set("name", "spider");
insertList.add(insertRow);
}
}
log.info("插入的水位总条数:{}", insertList.size());
// 批量保存
Db.use(ds2).batchSave("river_level", insertList, insertList.size());
}
public static void main(String[] args) {
WaterLevelImportService service = new WaterLevelImportService();
service.start();
service.fromDatasource1ToDatasource2();
// service.selectFromAllDatasource();
// service.getDataSourceCountResult();
}
}
代码说明
- 启动数据源:通过 Druid 和 ActiveRow Java-db 分别配置源数据库(
datasource1
)和目标数据库(datasource2
),并启动连接。 - 连接测试:通过
selectFromAllDatasource
方法检查两个数据源是否连接成功。 - 数据迁移:
- 从源数据库的
water_level
表中查询出符合条件的数据。 - 检查目标数据库中是否已存在相同的记录(根据
site_name
和time
字段)。 - 如果不存在,则生成一个随机的
id
,设置其他字段值,并添加到插入列表中。
- 从源数据库的
- 批量插入:将处理后的记录批量插入到目标数据库的
river_level
表中。
注意:代码中还有许多优化空间,例如将数据进行拆分、分线程处理等。
从网络获取数据进行批量保存示例
以下示例展示了如何从网络获取数据,转换为JSONObject
,再转换为 Java 对象,最终转为Row
对象并批量保存到数据库中。
代码实现步骤
- 从网络中获取数据。
- 转换为
JSONObject
。 - 转换为 Java 对象。
- 转换为
Row
对象。 - 批量保存到数据库。
具体代码
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.Java-db.plugin.activeRow.Db;
import com.Java-db.plugin.activeRow.Row;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import top.ppnt.chaofu.job.common.model.CfCameraEventWarm;
import top.ppnt.chaofu.job.db.test.ActiveRowInit;
@Slf4j
public class GetHttpData {
@Before
public void before() {
ActiveRowInit.init();
}
@Test
public void getHttpData() throws IOException {
OkHttpClient client = new OkHttpClient().newBuilder().build();
MediaType mediaType = MediaType.parse("text/plain");
RequestBody body = RequestBody.create(mediaType, "");
String cmd = "smart_jobs/get";
String url = "http://221.7.96.175:8182/api?cmd=" + cmd;
Request request = new Request.Builder()
.url(url)
.method("POST", body)
.addHeader("User-Agent", "apifox/1.0.0 (https://www.apifox.cn)")
.build();
Response response = client.newCall(request).execute();
String responseString = response.body().string();
System.out.println(responseString);
JSONObject jsonObject = JSON.parseObject(responseString);
JSONArray jsonArray = jsonObject.getJSONArray("rows");
int size = jsonArray.size();
System.out.println(size);
// 3.保存到数据库
List<Row> insertList = new ArrayList<Row>(size);
for (int i = 0; i < size; i++) {
JSONObject item = jsonArray.getJSONObject(i);
CfCameraEventWarm javaObject = item.toJavaObject(CfCameraEventWarm.class);
javaObject.setId(BigInteger.valueOf(IdUtil.getSnowflakeNextId()));
Row Row = new Row();
Row.setColumns(javaObject);
insertList.add(Row);
}
// 插入数据库
log.info("开始批量保存数据" + insertList.size());
Db.batchSave("cf_camera_event_warm", insertList, 2000);
log.info("批量保存数据完成");
}
}
代码说明
- 初始化 ActiveRow:在测试开始前,通过
ActiveRowInit.init()
方法初始化 ActiveRow Java-db。 - 获取 HTTP 数据:
- 使用 OkHttp 客户端发送 POST 请求到指定的 URL,获取响应数据。
- 将响应数据转换为字符串并打印输出。
- 解析 JSON 数据:
- 使用 FastJSON 库将响应字符串解析为
JSONObject
。 - 提取
rows
数组,并获取其大小。
- 使用 FastJSON 库将响应字符串解析为
- 转换为 Java 对象:
- 遍历
JSONArray
,将每个JSONObject
转换为CfCameraEventWarm
类的 Java 对象。 - 生成唯一的
id
并设置到 Java 对象中。
- 遍历
- 转换为
Row
对象:- 将 Java 对象的属性设置到
Row
对象中,并添加到插入列表insertList
中。
- 将 Java 对象的属性设置到
- 批量保存数据:
- 使用
Db.batchSave
方法将insertList
中的数据批量保存到cf_camera_event_warm
表中。
- 使用
总结
本文详细介绍了如何在 Java-db 框架中使用Db
类进行批量操作,包括批量保存和批量更新。通过具体的代码示例,展示了从数据准备、转换到批量保存的完整流程。这些操作能够显著提高数据库操作的效率,适用于大规模数据处理场景。
SQL 模板 (SqlTemplates
)
1. 简介
SqlTemplates
是一个强大且灵活的SQL管理工具,旨在将SQL语句从Java代码中分离出来,实现更清晰的代码结构和更便捷的SQL维护。它通过解析特定格式的 .sql
文件,将SQL语句加载到内存中,并允许通过唯一的ID进行调用。
该工具支持两种灵活的加载模式:
- 主文件模式:通过一个入口文件(如
main.sql
)统一管理所有SQL文件的导入。 - 自动扫描模式:自动扫描指定目录下的所有
.sql
文件,实现零配置加载。
SqlTemplates
能够无缝地从文件系统或JAR包中加载资源,使其在开发和生产环境中都能稳定工作。
2. 核心特性
- SQL与代码分离:将SQL集中存放在
.sql
文件中,保持Java代码的整洁。 - 模块化管理:支持通过
--@
指令将大型SQL按模块拆分到不同文件中。 - SQL片段复用:新增支持
--#include
指令,允许在一个SQL块中嵌入另一个SQL块,实现SQL逻辑的高度复用(如公共的查询字段、JOIN子句或WHERE条件)。 - 唯一ID调用:通过
--#
为每个SQL片段定义一个唯一的ID,方便在代码中引用。 - 智能加载机制:
- 优先加载指定的入口文件(默认为
sql-templates/main.sql
)。 - 若入口文件不存在,则自动扫描并加载
sql-templates/
目录下的所有.sql
文件。
- 优先加载指定的入口文件(默认为
- 环境兼容性:无论是在IDE中开发,还是将应用打包成JAR文件,都能正确加载SQL资源。
3. 使用方法
3.1. 组织 SQL 文件
推荐将所有SQL文件存放在 src/main/resources/sql-templates/
目录下。
方式一:使用主文件 (main.sql
) 进行管理(推荐用于大型项目)
这种方式结构清晰,便于追踪SQL文件的依赖关系。
创建主 SQL 模板文件 路径:
src/main/resources/sql-templates/main.sql
-- 导入用户相关的SQL --@ user.sql -- 导入订单相关的SQL --@ order.sql
创建模块化的 SQL 文件 路径:
src/main/resources/sql-templates/user.sql
--# user.baseColumns -- 定义用户表的基础查询字段,用于复用 id, username, email, status, create_time --# user.findById -- 根据ID查询用户,复用了基础字段 SELECT --#include(user.baseColumns) FROM mw_user WHERE id = ? AND deleted = 0; --# user.findByUsername -- 根据用户名查询用户,同样复用了基础字段 SELECT --#include(user.baseColumns) FROM mw_user WHERE username = ? AND deleted = 0;
路径:
src/main/resources/sql-templates/order.sql
--# order.findActiveOrdersByUser -- 查询用户的有效订单 SELECT * FROM mw_order WHERE user_id = ? AND status = 'active';
方式二:自动扫描模式(推荐用于中小型项目或快速开发)
如果您不想维护一个 main.sql
文件,只需将所有 .sql
文件直接放入 sql-templates
目录即可。
- 直接创建 SQL 文件 路径:
src/main/resources/sql-templates/user.sql
路径:--# user.findById SELECT id, username, email FROM mw_user WHERE id = ? AND deleted = 0;
src/main/resources/sql-templates/product.sql
--# product.findAll SELECT * FROM mw_product WHERE is_available = true;
SqlTemplates
在启动时若未找到 main.sql
,会自动加载这两个文件。
3.2. SQL 语法说明
--# <namespace>.<sqlId>
- 定义:用于定义一个SQL查询块的开始,并为其分配一个全局唯一的ID。
- 格式:推荐使用
命名空间.具体操作
的方式,如user.findById
,以避免ID冲突。 - 注意:
--#
与ID之间必须有至少一个空格。
--@ <filePath>
- 定义:用于导入另一个SQL文件。此指令仅在主文件模式下生效。
- 路径:
filePath
是相对于当前文件的相对路径。 - 注意:
--@
与文件名之间必须有至少一个空格。
--#include(<sqlId>)
- 定义:用于在一个SQL块的内部,嵌入另一个已定义的SQL块。这对于复用公共的列名、JOIN子句、WHERE条件或完整的子查询非常有用。
- 用法:在需要插入代码的地方,直接写入此指令。
SqlTemplates
在get()
时会自动将其替换为对应ID的SQL内容。 - 注意:工具会自动检测并防止循环引用(例如,A include B,B include A)。
3.3. 在 Java 代码中使用
步骤 1: 加载 SQL 模板
通常,您可以在应用启动时加载所有SQL模板。SqlTemplates
会自动处理加载逻辑。
import com.litongjava.template.SqlTemplates;
// 在应用启动类或静态初始化块中
public class Application {
public static void main(String[] args) {
// 无需任何参数,SqlTemplates 会自动执行默认加载策略
// 1. 查找并加载 sql-templates/main.sql
// 2. 如果找不到,则扫描并加载 sql-templates/ 目录下的所有 .sql 文件
SqlTemplates.load();
// ... 启动您的应用
}
}
注意:SqlTemplates
内部有防重复加载机制,多次调用 load()
是安全的。
步骤 2: 获取并执行 SQL
在您的Service或DAO层,通过ID获取SQL语句,并配合数据库工具类(如 litongjava-db
的 Db
类)执行。
import com.litongjava.db.activerecord.Db;
import com.litongjava.db.activerecord.Row;
import com.litongjava.template.SqlTemplates;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserService {
public Row findUserById(long userId) {
// 1. 通过ID获取SQL语句。
// SqlTemplates.get() 会自动解析内部的 --#include 指令。
String sql = SqlTemplates.get("user.findById");
log.info("Executing SQL for user.findById with userId: {}", userId);
// 2. 配合数据库工具类执行查询
Row user = Db.findFirst(sql, userId);
if (user != null) {
log.info("User found: {}", user.getStr("username"));
} else {
log.warn("User with ID {} not found.", userId);
}
return user;
}
}
当调用 SqlTemplates.get("user.findById")
时,返回的 sql
字符串将是:
SELECT
id, username, email, status, create_time
FROM mw_user WHERE id = ? AND deleted = 0;
--#include(user.baseColumns)
已经被智能地替换掉了。
3.4. 显式加载
如果您需要加载非默认路径下的SQL文件,可以使用带参数的 load()
方法。
// 加载位于 "config/custom-sql/main.sql" 的主文件
SqlTemplates.load("config/custom-sql/main.sql");
4. 最佳实践
- 统一命名规范:为SQL ID建立清晰的命名规范(如
模块名.实体名.操作名
),便于团队协作和维护。 - 利用模块化:对于复杂的业务,积极使用
--@
指令将SQL拆分到不同的文件中,保持每个文件的专注和简洁。 - 高度复用SQL片段:善用
--#include
来定义和复用公共的SQL逻辑,如列清单、复杂的JOIN或WHERE子句,减少重复代码,提高可维护性。 - 应用启动时加载:在应用启动时执行一次
SqlTemplates.load()
,将所有SQL预加载到内存中,以获得最佳性能。 - 避免在循环中
get()
:如果在一个循环中需要多次使用同一个SQL,请在循环外调用一次SqlTemplates.get()
,将SQL字符串存储在一个局部变量中。
通过遵循这些指南,SqlTemplates
将成为您项目中一个强大而可靠的SQL管理助手。