Rust移动端开发:SQLite数据库集成实战

901 阅读5分钟

在移动端开发中,许多应用都需要存储本地数据。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-syslibsqlite3-sys 提供了 SQLite 的 C API 的 Rust 声明,它负责将 C 语言实现的 SQLite API 封装到 Rust 中,是一个底层绑定库

rusqlite 提供了SQLite 数据库功能的上层封装,使得在 Rust 中与 SQLite 交互变得更加容易和安全,底层依赖 libsqlite3-sys

rusqlite 提供了一些 bundled feature,当开启这些feature时,SQLite 或 SQLCipher的C代码会参与编译

如果您使用了 bundledbundled-sqlcipherbundled-sqlcipher-vendored-openssl 特性,libsqlite3-sys 将使用 cc crate 从源代码编译 SQLite 或 SQLCipher,并将其链接到您的项目中。这些源代码被嵌入在 libsqlite3-sys crate 中,目前使用的 SQLite 版本是 3.46.0(截至 rusqlite 0.32.0 / libsqlite3-sys 0.30.0)。这可能是解决任何构建问题的最简单方案。

所以如何使用bundled,建议如下

Android、鸿蒙

  1. 建议开启bundled
    1. 优点:不依赖系统的sqlite、使用新版sqlite的特性、简化跨平台编译
    2. 缺点:生成的so变大(不超过2M)
features = ["bundled"]

iOS

  1. 不建议开启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讨论见

how to use multi-thread mode?

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?