在移动端开发中,许多应用都需要存储本地数据。SQLite 作为一个轻量级的嵌入式关系型数据库,因其高效、强大、稳定而被广泛应用。对于 Rust 开发者来说,rusqlite 提供了与 SQLite 进行高效交互的能力,这使得它成为在移动端应用中进行数据存储的理想选择。
选用rusqlite最大理由是 Rusqlite is an ergonomic wrapper for using SQLite from Rust
SQLite的高效、强大、稳定已毋庸多言。本文将详细介绍如何在移动端项目中使用 rusqlite 来操作 SQLite 数据库,以及可能遇到的问题。
1. 使用详解
1.1 Bundle other libraries
rusqlite依赖libsqlite3-sys,libsqlite3-sys 提供了 SQLite 的 C API 的 Rust 声明,它负责将 C 语言实现的 SQLite API 封装到 Rust 中,是一个底层绑定库
rusqlite 提供了SQLite 数据库功能的上层封装,使得在 Rust 中与 SQLite 交互变得更加容易和安全,底层依赖 libsqlite3-sys。
rusqlite 提供了一些 bundled feature,当开启这些feature时,SQLite 或 SQLCipher的C代码会参与编译
如果您使用了
bundled、bundled-sqlcipher或bundled-sqlcipher-vendored-openssl特性,libsqlite3-sys将使用cccrate 从源代码编译 SQLite 或 SQLCipher,并将其链接到您的项目中。这些源代码被嵌入在libsqlite3-syscrate 中,目前使用的 SQLite 版本是 3.46.0(截至rusqlite 0.32.0 / libsqlite3-sys 0.30.0)。这可能是解决任何构建问题的最简单方案。
所以如何使用bundled,建议如下
Android、鸿蒙
- 建议开启
bundled- 优点:不依赖系统的sqlite、使用新版sqlite的特性、简化跨平台编译
- 缺点:生成的so变大(不超过2M)
features = ["bundled"]
iOS
- 不建议开启
bundled: 一般来说项目内已经依赖了sqlite, 如果此时开启bundled且sqlite版本不一致,可能会存在运行问题。建议关闭该feature,让rusqlite链接项目内的sqlite。另外就算项目内没有sqlite也可新引入sqlite作为公共库。
1.2 增删改查
#[derive(Debug)]
struct Person {
id: i32,
name: String,
age: i32,
address: Option<String>,
}
impl Person {
// 构造 Person 实例
fn new(id: i32, name: String, age: i32, address: Option<String>) -> Self {
Person {
id,
name,
age,
address,
}
}
}
fn main() -> Result<()> {
let conn = Connection::open("./test/my_database.db")?;
// 创建 person 表
conn.execute(
"CREATE TABLE IF NOT EXISTS person (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INTEGER NOT NULL,
address TEXT
)",
[], // 不传入参数
)?;
// 可选:插入一些数据
conn.execute(
"INSERT INTO person (name, age, address) VALUES (?1, ?2, ?3)",
params!["Alice", 30, "123 Wonderland St"],
)?;
conn.execute(
"INSERT INTO person (name, age, address) VALUES (?1, ?2, ?3)",
params!["Bob", 25, "456 Dream Rd"],
)?;
// 查询并打印所有人
let mut stmt = conn.prepare("SELECT id, name, age, address FROM person")?;
// 查询并构造 Person 实例
let mut stmt = conn.prepare("SELECT id, name, age, address FROM person")?;
let person_iter = stmt.query_map([], |row| {
Ok(Person::new(
row.get(0)?, // id
row.get(1)?, // name
row.get(2)?, // age
row.get(3).ok(), // address,可能为空
))
})?;
// 输出每个 Person 实例
for person in person_iter {
match person {
Ok(p) => println!("{:?}", p), // 打印 Person 实例
Err(e) => println!("Error: {:?}", e),
}
}
// 删除 ID 为 1 的记录
println!("删除 ID 为 1 的记录...");
let rows_deleted = conn.execute("DELETE FROM person WHERE id = ?1", params![1])?;
println!("删除了 {} 行", rows_deleted);
// 更新 ID 为 2 的记录
println!("更新 ID 为 2 的记录...");
let rows_updated = conn.execute(
"UPDATE person SET name = ?1, age = ?2, address = ?3 WHERE id = ?4",
params!["Bob Updated", 26, "789 New Address", 2],
)?;
println!("更新了 {} 行", rows_updated);
// 查询并打印所有 Person 实例(删除和更新后)
println!("更新后的数据:");
let person_iter = stmt.query_map([], |row| {
Ok(Person::new(
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3).ok(),
))
})?;
for person in person_iter {
match person {
Ok(p) => println!("{:?}", p),
Err(e) => println!("Error: {:?}", e),
}
}
Ok(())
}
其他功能参考官网API
2. 问题和解决办法
2.1 版本不一致
上述提到,iOS不建议开启bundled,而且依赖的SQLite版本需要和已有的保持一致,那极有可能Android、iOS用的rusqlite版本不一致,一些api的调用方式不一致,此时可以使用条件编译控制
Android,鸿蒙(复杂些)编译条件
#[cfg(any(
all(
target_os = "linux",
any(
target_env = "musl",
target_env = "ohos", //add ohos here!
all(target_env = "uclibc", target_pointer_width = "32")
)
),
target_os = "android"
))]
iOS编译条件
#[cfg(target_os = "ios")]
2.2 多线程
想在多线程中使用rusqlite时,首先保证sqlite是支持多线程的。关于sqlite本身的多线程支持详见:www.sqlite.org/threadsafe.…
简单总结即:SQLite 提供了三种线程安全模式:
- 单线程模式 (Single-thread)
- 多线程模式 (Multi-thread)
- 序列化模式 (Serialized)
(1) 单线程模式 (Single-thread)
- 说明: 不提供任何线程同步机制,SQLite 假定数据库仅在单个线程中使用。
- 使用场景: 适用于完全由单线程控制的环境,或无需多线程支持的应用。
(2) 多线程模式 (Multi-thread)
- 说明: 允许多个线程同时使用 SQLite,但每个线程只能访问独立的数据库连接。SQLite 会确保多个线程在访问数据库时不会发生冲突。
- 使用场景: 当多个线程并发访问数据库,并且每个线程使用独立的数据库连接时非常有效。
(3) 序列化模式 (Serialized)
- 说明: SQLite 在此模式下提供最高级别的线程安全。所有线程对数据库的访问是串行化的,SQLite 保证每次只有一个线程能够进行数据库操作。
- 使用场景: 适用于多个线程共享同一数据库连接,并且需要完全线程安全的情况。
只要SQLite不是单线程模式,rusqlite即可使用多线程。
Connection是不支持Clone的,无法clone后在多个线程中使用。因此在多线程中每个线程打开一个Connection。
另外如果开启bundled feature,编译SQLite是支持多线程的
flag("-DSQLITE_THREADSAFE=1")
issue讨论见
impl Clone for rusqlite::Connection?
2.3 异步
暂时不支持异步,如果确实需要异步数据库,可参考
Async rusqlite built on top of rusqlite.
github.com/programatik…
或者使用其他数据库。
issue讨论见 How to use it with tokio::spawn?