-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Java|Kotlin 高级接口
本文将介绍 WCDB 的一些高级接口,包括链式调用与和核心层接口。
在增删查改一章中,已经介绍了通过 Database
和Table
操作数据库的方式。它们是经过封装的便捷接口,其实质都是通过调用链式接口完成的。
//Java
Select<Sample> select = database.<Sample>prepareSelect().select(DBSample.allFields()).from("sampleTable");
List<Sample> objects = select.where(DBSample.id.gt(1)).limit(10).allObjects();
Delete delete = database.prepareDelete().fromTable("sampleTable").where(DBSample.id.notEq(0));
delete.execute();
System.out.print(delete.getChanges());// 获取该操作删除的行数
//Kotlin
val select = database.prepareSelect<Sample>().select(*DBSample.allFields()).from("sampleTable")
val objects = select.where(DBSample.id.gt(1)).limit(10).allObjects()
val delete = database.prepareDelete().fromTable("sampleTable").where(DBSample.id.notEq(0))
delete.execute()
print(delete.changes) // 获取该操作删除的行数
链式接口都以 prepareXXX
开始,根据不同的调用返回 Insert
、Update
、Delete
、Select
对象。
这些对象的基本接口都返回其 this
,因此可以链式连续调用。
最后调用对应的函数使其操作生效,如 allObjects
、execute()
等。
通过链式接口,可以更灵活的控制数据库操作。
在之前的所有教程中,WCDB 通过其封装的各种接口,简化了最常用的增删查改操作。但 SQL 的操作千变万化,仍会有部分功能无法通过便捷接口满足。此时则可以直接操作Handle
和 PreparedStatement
,来做更精细的控制。
Handle
是单个数据库连接(具体见:Database Connection Handle)的包装,Database
则是相当于一个数据库连接的池子。Handle
可以通过Database.getHandle()
方法来获取,Handle
具备Database
的全部建表和CRUD接口。
前面介绍的那些CRUD接口都是便捷接口,如果需要精细控制 SQL 的执行过程,可以使用PreparedStatement
。PreparedStatement
是sqlite3_stmt
(具体见Prepared Statement Object)的封装,它保存了 SQL 的语法解析结果,用它来重复执行SQL语句的话,就可以节省SQL语句的解析耗时,提高性能。
PreparedStatement
可以通过Handle
的preparedWithMainStatement(Statement)
和getOrCreatePreparedStatement(Statement)
两个方法来创建。preparedWithMainStatement(Statement)
限制当前Handle
同一时间只能处理一个SQL语句,适合使用Handle
来串行执行不同SQL的场景,下面是一个示例:
// 获取handle和建表
Handle handle = database.getHandle();
handle.createTable("sampleTable", DBSample.INSTANCE);
// 先 prepare statement, 其实是sqlite3_prepare函数的封装。
PreparedStatement insertStatement = handle.preparedWithMainStatement(
new StatementInsert()
.insertInto("sampleTable")
.columns(DBSample.allFields())
.valuesWithBindParameters(DBSample.allFields().length));
Sample object = new Sample();
for(int i = 0; i < 100000; i++) {
// 先 reset,其实是sqlite3_reset函数的封装。
insertStatement.reset();
//1. 可以直接使用对象来bind,会逐个属性调用sqlite3_bind系列接口
object.id = i;
insertStatement.bindObject(object, DBSample.allFields());
//2. 也可以逐个字段bind,更高效一点
insertStatement.bindInteger(i, 1);
insertStatement.bindNull(2);
insertStatement.step();
}
//一个statement用完之后需要调用finalizeStatement,底下会调用sqlite3_finalize函数
insertStatement.finalizeStatement();
ArrayList<Sample> objects = new ArrayList<>();
// prepare 新的 statement, 其实是sqlite3_prepare函数的封装。
PreparedStatement selectStatement = handle.preparedWithMainStatement(
new StatementSelect().select(DBSample.allFields()).from("sampleTable"));
selectStatement.step();
while (!selectStatement.isDone()){
//1. 可以直接读取对象,会逐个属性来调用sqlite3_column系列接口来读取数据,并赋值给对象
Sample object = selectStatement.getOneObject(DBSample.allFields());
//2. 也可以逐个字段读取,更灵活一点
Sample object = new Sample();
object.id = selectStatement.getInt(0);
object.content = selectStatement.getText(1);
objects.add(object);
selectStatement.step();
}
//一个statement用完之后需要调用finalize,底下会调用sqlite3_finalize函数
selectStatement.finalizeStatement();
//handle用完之后也需要调用一下invalidate来回收
handle.invalidate();
一个值得注意的点是,
handle
是用到的时候才获取,不能长时间持有它。
以上内容都可以用Kotlin实现,因为篇幅关系就不演示了,下同。
如果需要同时执行多个SQL的话,就需要用到getOrCreatePreparedStatement(Statement)
方法。它不仅可以同时处理多个SQL,而且在当前Handle
的生命周期内,之前解析过的SQL不会重复解析,同个SQL语句可以重复调用这个方法来获取之前的解析结果,可以更加灵活得应对复杂的数据开发场景,下面是个是应用示例::
// 获取handle和建表
Handle handle = database.getHandle();
handle.createTable("sampleTable", DBSample.INSTANCE);
handle.createTable("newSampleTable", DBSample.INSTANCE);
PreparedStatement selectSTMT = handle.getOrCreatePreparedStatement(
new StatementSelect().select(DBSample.allFields()).from("sampleTable"));
PreparedStatement insertSTMT = handle.getOrCreatePreparedStatement(
new StatementInsert().insertInto("newSampleTable")
.columns(DBSample.allFields()).valuesWithBindParameters(DBSample.allFields().length));
selectSTMT.step();
while(!selectSTMT.isDone()){
Sample obj = selectSTMT.getOneObject(DBSample.allFields());
obj.id += 100000;
// 先 reset,其实是sqlite3_reset函数的封装。
insertSTMT.reset();
insertSTMT.bindObject(obj, DBSample.allFields());
insertSTMT.step();
selectSTMT.step();
}
// statement用完之后可以一次性finalize,底下会调用sqlite3_finalize函数逐个释放preparedStatement的资源
// 不调用的话,handle 回收之后也会自动finalize它所创建的所有preparedStatement
handle.finalizeAllStatements();
// 回收handle
handle.invalidate();
在需要对数据库进行大量数据更新的场景,我们的开发习惯一般是将这些更新操作统一到子线程处理,这样可以避免阻塞主线程,影响用户体验。
对于这类场景,如果只是将数据更新操作放到子线程执行,是不能完整解决问题的。因为 SQLite 的同个DB不支持并行写入,如果子线程的数据更新操作耗时太久,而主线程又有数据写入操作,比如用户在收消息的同时还会发消息,这样也会造成主线程阻塞。一种可行的做法是,将子线程的数据更新操作拆成一个个耗时很小的独立操作分别执行。但这样又会导致磁盘 IO 量大和增加子线程耗时的问题。
因为SQLite读写数据库时以一个数据页为单位的,一个数据页的大小在 WCDB 中是4kb,单个数据页一般可以存多条数据,逐条数据更新容易导致同一个数据页被读写多次。为了减少磁盘写入量,只能将所有的数据更新操作放到一个事务中执行,这样又会造成主线程阻塞的问题。
为了解决大事务会阻塞主线程的问题,我们在 WCDB 中开发了一种可中断事务。可中断事务把一个流程很长的事务过程看成一个循环逻辑,每次循环执行一次短时间的DB操作。操作之后根据外部传入的参数判断当前事务是否可以结束,如果可以结束的话,就直接Commit Transaction
,将事务修改内容写入磁盘。如果事务还不可以结束,再判断主线程是否因为当前事务阻塞,没有的话就回调外部逻辑,继续执行后面的循环,直到外部逻辑处理完毕。如果检测到主线程因为当前事务阻塞,则会立即 Commit Transaction
,先将部分修改内容写入磁盘,并唤醒主线程执行DB操作。等到主线程的DB操作执行完成之后,在重新开一个新事务,让外部可以继续执行之前中断的逻辑。可中断事务的整体逻辑如下图所示:
下面是可中断事务的使用示例:
ArrayList<Sample> objects = new ArrayList<>();
for(int i = 0; i < 100000; i++) {
Sample object = new Sample();
object.id = i;
objects.add(object);
}
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
final int[] index = {0};
database.runPausableTransaction(new PausableTransaction() {
@Override
public boolean insideTransaction(Handle handle, boolean isNewTransaction) throws WCDBException {
// isNewTransaction表示第一次执行,或者事务在上次循环结束之后被中断提交了
if(isNewTransaction){
//新事务先建一下表,避免事务被中断之后,表已经被其他逻辑删除
handle.createTable("sampleTable", DBSample.INSTANCE);
}
//写入一个对象,这里还可以用PreparedStatement来减少SQL解析的耗时
handle.insertObject(objects.get(index[0]), DBSample.allFields(), "sampleTable");
index[0]++;
//返回true表示事务结束,false则是继续处理
return index[0] >= objects.size();
}
});
}
});
thread.start();
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程