深入理解 Rust SQLite:从入门到实践 – wiki基地

“`markdown

深入理解 Rust SQLite:从入门到实践

1. 引言

在现代软件开发中,数据存储是不可或缺的一环。无论是桌面应用、移动应用、嵌入式系统,还是小型服务端应用,我们都需要高效、可靠地管理数据。在众多数据库解决方案中,SQLite以其轻量、无服务器、零配置的特性脱颖而出,成为许多场景下的理想选择。

与此同时,Rust语言凭借其卓越的性能、内存安全和并发控制能力,正迅速成为系统编程和高性能应用开发的新宠。当这两者结合时,我们便能够构建出既高效又安全的数据驱动型应用。

Rust与数据库:为什么选择SQLite?

  • 轻量与嵌入式: SQLite是一个嵌入式数据库引擎,不需要独立的服务器进程。数据库就是一个文件,这极大地简化了部署和管理。对于需要本地数据存储的桌面应用、命令行工具或移动应用来说,SQLite是完美的选择。
  • 零配置: 无需复杂的安装或配置步骤,开箱即用。
  • ACID兼容: 尽管轻量,SQLite仍然完全支持事务(ACID属性:原子性、一致性、隔离性、持久性),确保数据操作的可靠性。
  • 高性能: 对于单用户或并发量不高的场景,SQLite的读写性能非常出色。
  • 内存安全与并发: Rust的类型系统和所有权模型在编译时保证内存安全,配合SQLite的单文件特性,可以有效避免传统数据库操作中常见的内存泄漏或数据竞争问题(尤其是在处理数据时)。

本文目标:从零开始,掌握Rust与SQLite的结合使用

本文旨在为Rust开发者提供一份全面、深入的SQLite使用指南。我们将从环境搭建开始,逐步讲解如何使用Rust的rusqlite库进行基础的CRUD(创建、读取、更新、删除)操作,进而探讨错误处理、事务管理等进阶主题,并分享一些性能优化和最佳实践。无论您是Rust新手还是经验丰富的开发者,都将从本文中获得宝贵的知识和实践经验,从而能够自信地在您的Rust项目中集成和利用SQLite数据库。

2. 环境搭建

在开始使用Rust与SQLite进行开发之前,我们需要确保开发环境已正确配置,并引入必要的Rust库。

2.1 安装Rust开发环境

如果您尚未安装Rust,可以访问Rust官方网站 (https://www.rust-lang.org/) 按照指引安装 rustuprustup 是Rust的官方安装器和版本管理工具,它将帮助您安装Rust编译器 rustc、包管理器 cargo 以及其他必要的工具。

打开您的终端或命令提示符,运行以下命令:

bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

安装完成后,您可能需要重启终端或运行 source $HOME/.cargo/env 来更新环境变量。验证安装是否成功,可以运行:

bash
rustc --version
cargo --version

2.2 选择SQLite Rust库:rusqlite 简介

在Rust生态系统中,与SQLite数据库交互最常用且功能强大的库是 rusqlite。它提供了对SQLite C API的Ffi(Foreign Function Interface)绑定,允许Rust程序高效、安全地与SQLite数据库进行通信。rusqlite 关注于提供低级的、直接的SQLite操作,同时提供了Rust友好的API设计。

2.3 项目初始化与Cargo.toml配置

首先,创建一个新的Rust项目:

bash
cargo new rust_sqlite_app
cd rust_sqlite_app

接下来,我们需要在项目的 Cargo.toml 文件中添加 rusqlite 作为依赖。打开 Cargo.toml 并添加以下行:

toml
[dependencies]
rusqlite = "0.29" # 请根据crates.io上的最新版本进行调整

说明:
* rusqlite = "0.29" 指定了 rusqlite 库的版本。建议查看 crates.io 以获取最新稳定版本。
* rusqlite 默认会编译SQLite的捆绑版本(vendored SQLite),这意味着您不需要单独安装SQLite库。如果您需要使用系统预装的SQLite库,可以配置 rusqlite 的特性(features),但这超出了入门的范畴。

保存 Cargo.toml 文件后,cargo build 命令将自动下载并编译 rusqlite 及其所有依赖项。至此,您的Rust项目已准备好与SQLite数据库进行交互。

3. 基础操作:CRUD

CRUD(Create, Read, Update, Delete)是数据库操作的核心。本节将详细介绍如何使用rusqlite库在Rust中实现这些基本操作。

我们将围绕一个简单的“用户”表来演示这些操作。

3.1 连接数据库

在使用SQLite之前,首先需要建立一个数据库连接。rusqlite::Connection::open 方法用于打开(如果文件不存在则创建)一个SQLite数据库文件。

“`rust
use rusqlite::{Connection, Result};

fn main() -> Result<()> {
// 连接到数据库文件。如果文件不存在,它会被创建。
// :memory: 是一个特殊名称,用于创建一个内存数据库,程序结束时数据丢失。
let conn = Connection::open(“my_database.db”)?;

println!("成功连接到SQLite数据库!");

// 后续的CRUD操作将在这里进行

Ok(())

}
“`

错误处理
Connection::open 返回一个 Result<Connection, Error> 类型,我们使用 ? 操作符来处理可能的错误。这是Rust中常见的错误处理模式。

3.2 创建表

连接成功后,下一步是定义并创建数据表。这通常通过执行SQL的DDL(Data Definition Language)语句来完成。Connection 结构体提供了 execute 方法来执行不返回结果集的SQL语句(如 CREATE TABLE, INSERT, UPDATE, DELETE)。

“`rust
use rusqlite::{Connection, Result};

fn main() -> Result<()> {
let conn = Connection::open(“my_database.db”)?;

conn.execute(
    "CREATE TABLE IF NOT EXISTS users (
        id    INTEGER PRIMARY KEY,
        name  TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE
    )",
    [], // 参数列表,这里没有参数
)?;

println!("用户表创建或已存在。");

Ok(())

}
“`

说明:
* CREATE TABLE IF NOT EXISTS:如果表不存在则创建,避免重复创建报错。
* INTEGER PRIMARY KEY:SQLite中的主键通常是自动递增的。
* TEXT NOT NULL:字段非空。
* UNIQUE:确保email字段的值是唯一的。

3.3 插入数据 (Create)

使用 conn.execute 方法插入数据。为了防止SQL注入攻击并提高代码可读性,强烈建议使用参数绑定。

“`rust
use rusqlite::{Connection, Result};

[derive(Debug)]

struct User {
id: i32,
name: String,
email: String,
}

fn main() -> Result<()> {
let mut conn = Connection::open(“my_database.db”)?;

conn.execute(
    "CREATE TABLE IF NOT EXISTS users (
        id    INTEGER PRIMARY KEY,
        name  TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE
    )",
    [],
)?;

// 单条数据插入
let user_name = "Alice".to_string();
let user_email = "[email protected]".to_string();
conn.execute(
    "INSERT INTO users (name, email) VALUES (?1, ?2)",
    [&user_name, &user_email],
)?;
println!("插入用户:{} ({})", user_name, user_email);

// 再次尝试插入相同的email,会因UNIQUE约束报错
let user_name_bob = "Bob".to_string();
let user_email_bob = "[email protected]".to_string(); // 注意:故意重复email
let insert_result = conn.execute(
    "INSERT INTO users (name, email) VALUES (?1, ?2)",
    [&user_name_bob, &user_email_bob],
);

match insert_result {
    Ok(rows_affected) => println!("插入用户成功,影响行数: {}", rows_affected),
    Err(e) => println!("插入用户失败:{}", e), // 这里会因为UNIQUE约束报错
}

// 批量数据插入与事务
// 在事务中执行多个插入操作,可以保证原子性,如果其中一个失败,所有操作都会回滚。
let tx = conn.transaction()?; // 开启事务

tx.execute("INSERT INTO users (name, email) VALUES ('Charlie', '[email protected]')", [])?;
tx.execute("INSERT INTO users (name, email) VALUES ('David', '[email protected]')", [])?;

tx.commit()?; // 提交事务
println!("批量插入用户 Charlie 和 David");

Ok(())

}
“`

参数绑定:
* ?1, ?2 是占位符,分别对应参数列表中的第一个和第二个元素。
* [&user_name, &user_email] 是参数列表,它需要是一个实现了 ToSql trait 的类型数组(或元组),&strString 都实现了。

事务:
* 使用 conn.transaction()? 开启一个事务,它返回一个 Transaction 对象。
* 在 Transaction 对象上执行 execute 操作。
* tx.commit()? 提交事务,tx.rollback()? 回滚事务(如果发生错误,通常会自动回滚)。

3.4 查询数据 (Read)

查询操作使用 conn.prepare 准备一个 Statement,然后使用 queryquery_row 执行。

“`rust
use rusqlite::{Connection, Result, params};

[derive(Debug)]

struct User {
id: i32,
name: String,
email: String,
}

fn main() -> Result<()> {
let conn = Connection::open(“my_database.db”)?;

// ... (假设表和一些数据已经存在)

// 简单查询所有用户
let mut stmt = conn.prepare("SELECT id, name, email FROM users")?;
let user_iter = stmt.query_map([], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
    })
})?;

println!("\n所有用户:");
for user_result in user_iter {
    println!("{:?}", user_result?);
}

// 带条件的查询
let target_email = "[email protected]";
let mut stmt_filtered = conn.prepare("SELECT id, name, email FROM users WHERE email = ?1")?;
let user_alice: User = stmt_filtered.query_row(params![target_email], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
    })
})?;
println!("\n通过邮箱查询用户 Alice:{:?}", user_alice);

// 查询不存在的用户
let nonexistent_email = "[email protected]";
let mut stmt_nonexistent = conn.prepare("SELECT id, name, email FROM users WHERE email = ?1")?;
let result_nonexistent = stmt_nonexistent.query_row(params![nonexistent_email], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
    })
});

match result_nonexistent {
    Ok(user) => println!("错误:查询到不存在的用户:{:?}", user),
    Err(rusqlite::Error::QueryReturnedNoRows) => println!("未找到邮箱为 {} 的用户。", nonexistent_email),
    Err(e) => println!("查询错误:{}", e),
}

Ok(())

}
“`

说明:
* conn.prepare(SQL_STRING):预编译SQL语句,返回一个 Statement 对象。这对于多次执行相同查询但参数不同的场景非常高效。
* stmt.query_map(params, |row| { ... }):用于查询多行数据。它返回一个迭代器,每个元素是一个 Result<T, Error>。闭包 |row| { ... } 将每一行数据映射到自定义的 User 结构体。row.get(index)?row.get("column_name")? 用于按索引或列名获取值。
* stmt.query_row(params, |row| { ... }):用于查询单行数据。如果查询返回多行或零行,将会报错。
* params![...]rusqlite 提供的宏,用于方便地创建参数列表。

3.5 更新数据 (Update)

更新数据与插入数据类似,使用 conn.execute 方法,配合参数绑定。

“`rust
use rusqlite::{Connection, Result, params};

// … (假设User结构体已定义)

fn main() -> Result<()> {
let conn = Connection::open(“my_database.db”)?;

// ... (假设表和一些数据已经存在)

// 更新用户 Alice 的姓名
let new_name = "Alicia".to_string();
let target_id = 1; // 假设 Alice 的 id 是 1
let rows_affected = conn.execute(
    "UPDATE users SET name = ?1 WHERE id = ?2",
    params![new_name, target_id],
)?;
println!("更新用户 ID {} 的姓名,影响行数:{}", target_id, rows_affected);

// 再次查询 Alice 验证更新
let mut stmt_updated = conn.prepare("SELECT id, name, email FROM users WHERE id = ?1")?;
let updated_user: User = stmt_updated.query_row(params![target_id], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
    })
})?;
println!("更新后的用户:{:?}", updated_user);

Ok(())

}
“`

3.6 删除数据 (Delete)

删除数据同样使用 conn.execute 方法。

“`rust
use rusqlite::{Connection, Result, params};

// … (假设User结构体已定义)

fn main() -> Result<()> {
let conn = Connection::open(“my_database.db”)?;

// ... (假设表和一些数据已经存在)

// 删除用户 David (假设其 ID 为 3)
let target_id_to_delete = 3;
let rows_affected = conn.execute(
    "DELETE FROM users WHERE id = ?1",
    params![target_id_to_delete],
)?;
println!("删除用户 ID {},影响行数:{}", target_id_to_delete, rows_affected);

// 尝试查询被删除的用户
let mut stmt_deleted = conn.prepare("SELECT id, name, email FROM users WHERE id = ?1")?;
let result_deleted = stmt_deleted.query_row(params![target_id_to_delete], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
    })
});

match result_deleted {
    Ok(user) => println!("错误:查询到已删除的用户:{:?}", user),
    Err(rusqlite::Error::QueryReturnedNoRows) => println!("用户 ID {} 已成功删除。", target_id_to_delete),
    Err(e) => println!("查询错误:{}", e),
}

// 清空表 (谨慎操作)
// conn.execute("DELETE FROM users", [])?;
// println!("已清空用户表。");

Ok(())

}
“`

4. 进阶实践

掌握了基础的CRUD操作后,我们可以进一步探索rusqlite的进阶功能,以及在Rust中与SQLite交互时的最佳实践。

4.1 错误处理

Rust以其强大的错误处理机制而闻名。rusqlite库遵循Rust的这一哲学,其所有可能失败的操作都返回 Result<T, rusqlite::Error> 类型。

rusqlite::Error 是一个枚举类型,涵盖了多种可能的错误情况,例如:
* rusqlite::Error::SqliteFailure(ErrorCode, Option<String>):底层SQLite C API返回的错误。
* rusqlite::Error::QueryReturnedNoRowsquery_row 方法未找到匹配行。
* rusqlite::Error::InvalidPath(PathBuf):数据库路径无效。
* rusqlite::Error::IntegralValueOutOfRange(i, i):数值溢出。
* rusqlite::Error::InvalidColumnType(i, Type):列类型不匹配。

通过模式匹配(match)可以精确地处理不同类型的错误,或者使用 ? 运算符将错误向上层传递。

“`rust
use rusqlite::{Connection, Result, Error, params};

fn get_user_by_id(conn: &Connection, user_id: i32) -> Result> {
let mut stmt = conn.prepare(“SELECT id, name, email FROM users WHERE id = ?1”)?;
let result = stmt.query_row(params![user_id], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
})
});

match result {
    Ok(user) => Ok(Some(user)),
    Err(Error::QueryReturnedNoRows) => Ok(None), // 未找到用户
    Err(e) => Err(e), // 其他错误
}

}

// 在main函数或其他地方调用
// if let Some(user) = get_user_by_id(&conn, 1)?. {
// println!(“找到用户: {:?}”, user);
// } else {
// println!(“未找到用户。”);
// }
“`

4.2 事务管理

事务是确保数据库操作原子性、一致性、隔离性和持久性(ACID)的关键。rusqlite 提供了两种主要方式来管理事务:

  1. 手动事务: 使用 BEGIN, COMMIT, ROLLBACK SQL命令。
  2. rusqlite 的事务API: Connection::transaction() 方法提供了一个更具Rust风格的事务管理方式。

“`rust
use rusqlite::{Connection, Result};

fn transfer_funds(conn: &mut Connection, from_account: i32, to_account: i32, amount: f64) -> Result<()> {
let tx = conn.transaction()?; // 开启事务

// 从from_account中扣除
tx.execute(
    "UPDATE accounts SET balance = balance - ?1 WHERE id = ?2",
    rusqlite::params![amount, from_account],
)?;

// 向to_account中增加
tx.execute(
    "UPDATE accounts SET balance = balance + ?1 WHERE id = ?2",
    rusqlite::params![amount, to_account],
)?;

tx.commit()?; // 提交事务
Ok(())

}
// 事务的生命周期与Transaction对象绑定,当Transaction对象Drop时,如果未显式commit或rollback,
// 会自动回滚。这是一个非常安全的特性。
“`

4.3 数据模型与ORM选择 (可选)

rusqlite 本身不提供ORM(Object-Relational Mapping)功能,它更专注于提供低级、灵活的数据库访问。这意味着您需要手动将数据库行映射到Rust结构体。

“`rust

[derive(Debug)]

struct User {
id: i32,
name: String,
email: String,
}

// 映射函数示例 (如前所述)
// let user_iter = stmt.query_map([], |row| {
// Ok(User {
// id: row.get(0)?,
// name: row.get(1)?,
// email: row.get(2)?,
// })
// })?;
“`

对于更复杂的项目,如果需要更高级的ORM功能,可以考虑Rust生态系统中的其他ORM框架,例如 dieseldiesel 是一个功能强大且类型安全的ORM,但它的学习曲线相对陡峭,并且通常用于关系型数据库,对SQLite的支持需要额外的配置和理解。对于仅使用SQLite的轻量级应用,手动映射通常是足够且更直接的选择。

4.4 性能优化与最佳实践

  • 索引的创建与使用:
    为经常用于 WHERE 子句、JOIN 条件或 ORDER BY 子句的列创建索引,可以显著提高查询速度。

    sql
    CREATE INDEX idx_users_email ON users (email);

  • 预编译语句(Prepared Statements):
    rusqliteconn.prepare() 方法就是创建预编译语句。对于会多次执行的SQL语句(尤其是带有参数的),预编译可以避免每次执行都解析SQL语句的开销,从而提高效率。

    rust
    let mut stmt = conn.prepare("INSERT INTO users (name, email) VALUES (?1, ?2)")?;
    stmt.execute(params!["Frank", "[email protected]"])?;
    stmt.execute(params!["Grace", "[email protected]"])?;
    // ...多次执行

  • 避免N+1查询问题:
    当您从一个表中查询数据,然后对每一行结果又执行一个单独的查询来获取相关数据时,就会出现N+1查询问题。这会导致大量的数据库往返,严重影响性能。应尽量使用 JOIN 操作在单个查询中获取所有需要的数据。

  • 连接池:
    对于单线程或低并发的SQLite应用,连接池通常不是必需的,因为SQLite是文件级的锁,且连接的创建成本很低。但在某些特定的高并发场景(如异步Web服务),如果需要管理多个独立的数据库连接(例如每个请求一个连接),可以使用像 r2d2 这样的通用连接池库,并为其实现 rusqlite 的连接管理。但这会增加复杂性,对于大多数SQLite用例来说,可能过度设计。

  • WAL模式:
    SQLite默认使用回滚日志(rollback journal)模式。切换到预写日志(Write-Ahead Logging, WAL)模式可以提高并发性,允许多个读取器在写入器活动时继续操作,并通常能提高写入性能。

    rust
    conn.execute_batch("PRAGMA journal_mode = WAL;")?;

    请注意,WAL模式会创建额外的 .wal.shm 文件。

5. 常见问题与解决方案

在Rust中使用SQLite时,可能会遇到一些特定问题。了解这些问题及其解决方案可以帮助您更顺畅地开发。

5.1 数据库锁定问题

SQLite是一个文件级数据库,这意味着在任何给定时间,只有一个写入操作可以进行。当一个写入事务正在进行时,其他写入尝试会被阻塞。如果并发写入请求很高,或者一个写入事务耗时过长,可能会导致数据库锁定,表现为 SQLITE_BUSY 错误。

解决方案:

  • 启用WAL模式 (Write-Ahead Logging): 如前所述,WAL模式可以显著改善并发性。它允许多个读取器在写入器活动时并发访问数据库,并且通常能提高写入性能。
    rust
    conn.execute_batch("PRAGMA journal_mode = WAL;")?;
  • 缩短事务: 保持写入事务尽可能短小,减少数据库被锁定的时间。
  • 重试逻辑: 在遇到 SQLITE_BUSY 错误时,可以实现一个简单的重试机制,等待一小段时间后再次尝试。rusqliteError 类型可以帮助您识别这类错误。
  • 连接池 (针对多线程场景): 虽然SQLite本身不直接支持高并发写入,但在多线程应用中,如果每个线程都尝试打开自己的连接,可能会加剧锁定问题。使用连接池可以集中管理连接,并确保在并发访问时,连接请求得到有序处理,减少直接的文件锁定冲突。

5.2 并发写入处理

SQLite的写入是串行化的,这意味着同一时间只能有一个连接执行写入操作。即使启用了WAL模式,并发写入仍然会被序列化。

解决方案:

  • 设计模式: 如果您的应用需要高并发写入,SQLite可能不是最佳选择。可以考虑使用专门为高并发设计的数据库,如PostgreSQL、MySQL等。
  • 异步处理: 对于Web服务等场景,可以将写入操作放入后台任务队列中异步处理,而不是直接在请求处理线程中同步执行。
  • 优化SQL: 确保写入操作的SQL语句尽可能高效,例如使用事务进行批量插入。

5.3 数据类型映射

SQLite是弱类型的,它允许您在任何列中存储任何类型的数据(尽管您可以在 CREATE TABLE 语句中指定类型)。然而,rusqlite 是强类型的,它需要您明确地将SQLite数据类型映射到Rust数据类型。

常见问题:

  • 类型不匹配: 尝试将 TEXT 列的值读取为 INTEGER 类型可能会导致运行时错误。
  • 日期/时间: SQLite没有内置的日期时间类型。通常将其存储为 TEXT (ISO8601字符串)、INTEGER (Unix时间戳) 或 REAL (Julian天数)。在Rust中,您需要手动解析这些值到 chrono::DateTimestd::time::SystemTime

解决方案:

  • 准确的类型转换:query_mapquery_row 的闭包中,使用 row.get::<usize, T>(index)?row.get::<&str, T>(column_name)? 时,确保 T 是与SQLite列数据兼容的Rust类型。
  • 自定义序列化/反序列化: 对于复杂的类型(如自定义结构体或枚举),可以实现 FromSqlToSql trait 来定义它们与SQLite数据之间的转换逻辑。
  • 使用 Option<T> 处理NULL: 如果数据库列允许NULL值,在Rust结构体中应使用 Option<T> 来表示该字段可能为空。例如,email: Option<String>

“`rust
// 示例:处理可能为NULL的列

[derive(Debug)]

struct UserWithOptionalEmail {
id: i32,
name: String,
email: Option, // email 字段可能为 NULL
}

// 查询时
// let user_with_optional: UserWithOptionalEmail = stmt.query_row(params![1], |row| {
// Ok(UserWithOptionalEmail {
// id: row.get(0)?,
// name: row.get(1)?,
// email: row.get(2)?, // rusqlite 能够自动将 NULL 映射到 Option::None
// })
// })?;
“`

6. 总结

本文带领读者深入探索了Rust与SQLite的结合使用,从环境搭建到基础CRUD操作,再到进阶实践和常见问题解决方案。我们看到了rusqlite库如何为Rust开发者提供了一个强大、安全且高效的途径来管理应用程序数据。

回顾Rust与SQLite的优势:

  • 性能与安全: Rust的内存安全和零成本抽象与SQLite的轻量和高性能相结合,使得构建稳定、高效的数据驱动应用成为可能。
  • 简洁易用: rusqlite 提供了清晰的API,让开发者能够以Rust惯用的方式与SQLite数据库进行交互。参数绑定和Result类型的使用,有效避免了SQL注入和运行时错误。
  • 灵活性: 作为嵌入式数据库,SQLite无需复杂的配置和部署,非常适合桌面应用、命令行工具、嵌入式系统以及作为小型服务的本地存储解决方案。

展望未来与更多学习资源:

Rust与SQLite的组合在许多场景下都展现出巨大的潜力。随着您的项目变得更加复杂,您可能会考虑:

  • 异步操作: 对于需要非阻塞数据库操作的应用,可以探索 tokio-rusqlite 或其他异步SQLite驱动。
  • ORM框架: 对于大型项目或需要更高级抽象的场景,可以研究像 diesel 这样的ORM框架,尽管它们引入了额外的复杂性。
  • WebAssembly (Wasm) 集成: Rust编译到Wasm的能力,结合SQLite,有望在浏览器端或边缘计算环境中实现更强大的数据存储能力。

希望本文能为您在Rust项目中使用SQLite提供坚实的基础。通过不断实践和探索,您将能够充分发挥Rust和SQLite的优势,构建出卓越的应用程序。

进一步学习资源:
* rusqlite 官方文档: https://docs.rs/rusqlite/
* SQLite 官方网站: https://www.sqlite.org/
* Rust 官方文档: https://doc.rust-lang.org/
“`

滚动至顶部