掌握Rust SQLite:基础、进阶与示例 – wiki词典


掌握 Rust SQLite:基础、进阶与示例

引言

在现代软件开发中,数据存储是不可或缺的一环。对于需要轻量级、零配置、嵌入式数据库的应用程序,SQLite 无疑是最佳选择之一。而当这种优势与 Rust 语言的性能、内存安全和并发性结合时,我们便获得了一个强大且可靠的解决方案。

本文将详细探讨如何在 Rust 项目中利用 rusqlite 库与 SQLite 数据库进行交互。我们将从基础知识入手,逐步深入到事务、预处理语句、自定义函数和 BLOB 数据处理等高级特性,并通过丰富的代码示例帮助您全面掌握 Rust SQLite 的使用。

第一部分:基础篇

rusqlite 是 Rust 社区中最常用且功能强大的 SQLite 驱动。它提供了一个安全、符合人体工程学的 API,让您能够轻松地在 Rust 应用程序中操作 SQLite 数据库。

1. 环境搭建

要在您的 Rust 项目中使用 rusqlite,首先需要将其添加到 Cargo.toml 文件中:

“`toml
[dependencies]
rusqlite = { version = “0.30.0”, features = [“bundled”] }

进阶篇中自定义函数可能需要的正则表达式库

regex = “1.10.3”
“`

  • version = "0.30.0":指定 rusqlite 库的版本。请根据最新版本进行调整。
  • features = ["bundled"]:这是一个非常重要的特性。它告诉 rusqlite 将 SQLite C 库静态编译到您的应用程序中。这意味着您的应用程序将不需要在目标系统上预安装 SQLite,极大地简化了部署。如果您的环境已经有 SQLite C 库,也可以不使用此特性。

2. 连接数据库

与 SQLite 数据库建立连接是所有操作的第一步。rusqlite 提供了两种主要的连接方式:

  • 内存数据库Connection::open_in_memory()。这种方式会在内存中创建一个临时的、非持久化的数据库。它非常适合测试、临时数据处理或不需要数据持久化的场景。当程序关闭时,所有数据都会丢失。
  • 文件数据库Connection::open("path/to/your.db")。这会打开或创建一个位于文件系统中的数据库文件。如果文件不存在,它会自动创建。这是生产环境中常用的方式。

示例:

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

fn main() -> Result<()> {
// 连接到内存数据库
let conn_in_memory = Connection::open_in_memory()?;
println!(“成功连接到内存数据库。”);

// 连接到文件数据库 (如果文件不存在则创建)
let conn_file = Connection::open("my_database.db")?;
println!("成功连接到文件数据库 'my_database.db'。");

Ok(())

}
“`

注意:Connection::open()Connection::open_in_memory() 都返回 rusqlite::Result<Connection>,这使得我们可以使用 ? 运算符进行简洁的错误处理。

3. 创建表

连接到数据库后,通常需要创建表来存储数据。这可以通过 conn.execute() 方法完成,它用于执行不返回结果的 SQL 语句(如 CREATE TABLE, INSERT, UPDATE, DELETE)。

rust
// 假设我们已经有了 `conn_file` 连接
conn_file.execute(
"CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER
)",
[], // 空参数列表,因为没有需要绑定的参数
)?;
println!("'users' 表已创建或已存在。");

  • CREATE TABLE IF NOT EXISTS:这是一个好习惯,它确保如果表已经存在,则不会引发错误。
  • PRIMARY KEY AUTOINCREMENT:定义主键并自动递增。
  • NOT NULL:指定该列不允许为空。
  • UNIQUE:指定该列的值必须是唯一的。

4. 插入数据

使用 conn.execute() 方法也可以插入数据,但为了安全和效率,我们强烈推荐使用参数绑定来插入数据。rusqlite 提供了 params! 宏来方便地绑定参数。

“`rust
conn_file.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“Alice”, “[email protected]”, 30],
)?;
println!(“用户 Alice 已插入。”);

conn_file.execute(
“INSERT INTO users (name, email, age) VALUES (:name, :email, :age)”,
params![“:name”: “Bob”, “:email”: “[email protected]”, “:age”: 25],
)?;
println!(“用户 Bob 已插入。”);
“`

  • 位置参数 (?1, ?2, ?3):按照参数在 params! 宏中的顺序进行绑定。
  • 命名参数 (:name, :email, :age):通过名称进行绑定,可读性更好,尤其是在参数较多时。

5. 查询数据

查询数据是数据库操作的核心。rusqlite 提供了 query_row()prepare().query_map() 两种主要方法。

为了更好地组织查询结果,我们通常会定义一个结构体来表示数据库中的一行数据。

“`rust

[derive(Debug, PartialEq)]

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

// … 假设 conn_file 已经连接并插入了数据

// 查询单行数据
let user_id_to_find = 1;
let mut stmt_query_single = conn_file.prepare(“SELECT id, name, email, age FROM users WHERE id = ?1”)?;
let alice: User = stmt_query_single.query_row(params![user_id_to_find], |row| {
Ok(User {
id: row.get(0)?, // 通过索引获取列值
name: row.get(1)?,
email: row.get(2)?,
age: row.get(3)?,
})
})?;
println!(“查询到用户: {:?}”, alice);
assert_eq!(alice.name, “Alice”);

// 查询多行数据
let mut stmt_query_multiple = conn_file.prepare(“SELECT id, name, email, age FROM users WHERE age > ?1”)?;
let user_iter = stmt_query_multiple.query_map(params![20], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
age: row.get(3)?,
})
})?;

println!(“\n查询年龄大于 20 的用户:”);
for user_result in user_iter {
println!(“{:?}”, user_result?);
}
“`

  • query_row():期望返回单行结果。如果查询返回零行或多行,它将返回错误。
    • 它接受一个闭包 |row| { ... },其中 rowrusqlite::Row 类型。
    • row.get(index):通过列的零基索引获取值。类型需要与 Rust 类型匹配。
  • prepare().query_map():用于查询可能返回多行结果的情况。
    • prepare() 创建一个预处理语句,可以重复使用。
    • query_map() 接受与 query_row() 类似的闭包,但它返回一个迭代器,允许您逐行处理结果。

第二部分:进阶篇

在掌握了 rusqlite 的基础用法后,我们将深入探讨一些更高级的特性,它们对于构建健壮、高效和功能丰富的应用程序至关重要。

1. 事务处理 (Transactions)

事务是一组 SQL 操作,它们被视为单个逻辑工作单元。事务要么全部成功提交,要么全部失败回滚。这确保了数据的原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability) (ACID 特性)。

rusqlite 中,事务管理非常直观:

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

// … 假设 conn_file 已经连接

println!(“\n— 演示事务 —“);

// 开始一个事务
// TransactionBehavior::Immediate 表示会立即锁定数据库
let tx = conn_file.transaction_with_behavior(TransactionBehavior::Immediate)?;

// 在事务中插入数据
tx.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“Charlie”, “[email protected]”, 28],
)?;
tx.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“David”, “[email protected]”, 35],
)?;
println!(“在事务中插入了 Charlie 和 David。”);

// 尝试插入一个重复的 email,这将违反 UNIQUE 约束
let insert_result = tx.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“Eve”, “[email protected]”, 22], // email “[email protected]” 已存在
);

if let Err(e) = insert_result {
println!(“尝试插入重复 email 失败,错误: {}”, e);
// 遇到错误时回滚事务
tx.rollback()?;
println!(“事务已回滚。”);
} else {
// 如果没有错误,提交事务
tx.commit()?;
println!(“事务已提交。”);
}

// 验证事务是否回滚成功 (Charlie 和 David 不应该被插入)
let count: i32 = conn_file.query_row(“SELECT COUNT(*) FROM users”, [], |row| row.get(0))?;
println!(“回滚后用户数量: {}”, count);
// 假设初始只有 Alice 和 Bob,回滚后仍是 2
assert_eq!(count, 2);

// 演示一个成功的事务
let tx_success = conn_file.transaction()?; // 默认行为是 Deferred
tx_success.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“Frank”, “[email protected]”, 40],
)?;
tx_success.commit()?;
println!(“成功提交 Frank。”);
let count_after_success: i32 = conn_file.query_row(“SELECT COUNT(*) FROM users”, [], |row| row.get(0))?;
assert_eq!(count_after_success, 3); // Alice, Bob, Frank
“`

  • conn.transaction_with_behavior(TransactionBehavior::Immediate):开始一个事务并指定其行为。Immediate 意味着在事务开始时就锁定数据库。
  • conn.transaction():开始一个事务,默认行为是 Deferred,它在第一个写入操作发生时才锁定数据库。
  • tx.commit():提交事务,使所有更改永久生效。
  • tx.rollback():回滚事务,撤销所有更改。

2. 预处理语句与参数化查询 (Prepared Statements and Parameterized Queries)

预处理语句是数据库性能和安全的关键。当您多次执行相似的 SQL 语句时,预处理语句可以避免重复解析 SQL,从而提高效率。更重要的是,参数化查询是防止 SQL 注入攻击的黄金法则。

“`rust
// … 假设 conn_file 已经连接

println!(“\n— 演示预处理语句 —“);

// 创建一个用于插入的预处理语句
let mut stmt_insert = conn_file.prepare(“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”)?;

// 重复使用预处理语句插入数据
stmt_insert.execute(params![“Grace”, “[email protected]”, 32])?;
stmt_insert.execute(params![“Heidi”, “[email protected]”, 29])?;
println!(“使用预处理语句插入了 Grace 和 Heidi。”);

// 创建一个用于查询的预处理语句
let mut stmt_query = conn_file.prepare(“SELECT id, name, email, age FROM users WHERE name LIKE ?1”)?;
let search_pattern = “%e%”; // 查找名字中包含 ‘e’ 的用户

let users_with_e_iter = stmt_query.query_map(params![search_pattern], |row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
age: row.get(3)?,
})
})?;

println!(“\n名字中包含 ‘e’ 的用户:”);
for user_result in users_with_e_iter {
println!(“{:?}”, user_result?);
}
“`

  • conn_file.prepare(SQL_STRING):编译 SQL 语句并返回 Statement 对象。
  • stmt.execute(params!):执行非查询语句。
  • stmt.query_map(params!, ...)stmt.query_row(params!, ...):执行查询语句。

预处理语句在每次执行时,只需提供参数,数据库会重用已编译的查询计划,从而提升性能。

3. 自定义函数 (Custom Functions)

SQLite 允许用户定义自己的 SQL 函数,这极大地扩展了数据库的功能。rusqlite 提供了 create_scalar_function() 方法来实现这一点。

以下是一个创建 SQL REGEXP 函数的示例,它使用 Rust 的 regex crate 实现正则表达式匹配:

“`rust
use rusqlite::{Connection, Result, functions::FunctionFlags};
use regex::Regex;
use std::sync::Arc; // 用于在多线程环境中共享 Regex 对象

// … 假设 conn_file 已经连接

println!(“\n— 演示自定义标量函数 (REGEXP) —“);

// 注册一个自定义的 REGEXP 函数
// 这个函数接收两个参数:正则表达式模式和待匹配的文本
conn_file.create_scalar_function(
“REGEXP”,
2, // 函数接收 2 个参数
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
move |ctx| {
// 确保参数数量正确
assert_eq!(ctx.len(), 2, “called with unexpected number of arguments”);

    // 获取或创建正则表达式对象。Regex 对象在第一次使用时编译,之后缓存
    let pattern: Arc<Regex> = ctx.get_or_create_aux(0, |vr| -> Result<_, Box<dyn std::error::Error + Send + Sync + 'static>> {
        Ok(Regex::new(vr.as_str()?)?)
    })?;
    let text = ctx.get_raw(1).as_str().map_err(|e| rusqlite::Error::UserFunctionError(e.into()))?;

    // 执行正则表达式匹配并返回结果
    Ok(pattern.is_match(text))
},

)?;
println!(“自定义 ‘REGEXP’ 函数已注册。”);

// 使用自定义 REGEXP 函数查询 email
let matching_users_count: i32 = conn_file.query_row(
“SELECT COUNT() FROM users WHERE email REGEXP ?1″,
params![“^.
@example\.com$”], // 匹配所有以 @example.com 结尾的邮箱
|row| row.get(0),
)?;
println!(“邮箱以 ‘@example.com’ 结尾的用户数量: {}”, matching_users_count);
“`

  • create_scalar_function(name, num_args, flags, closure)
    • name:SQL 函数的名称。
    • num_args:函数期望的参数数量。
    • flags:定义函数的行为,如 SQLITE_UTF8 (处理 UTF-8 字符串) 和 SQLITE_DETERMINISTIC (表示对于相同的输入总是返回相同的输出,这有助于 SQLite 的优化器)。
    • closure:实现函数逻辑的 Rust 闭包。ctx (Context) 对象提供了访问参数和创建辅助数据 (get_or_create_aux) 的能力。
  • ctx.get_or_create_aux():这是一个非常有用的方法,用于在函数调用之间缓存昂贵的资源(如编译后的正则表达式)。它接收一个索引和一个闭包,如果辅助数据不存在,则闭包会被调用来创建它。

4. BLOB 数据处理

BLOB (Binary Large Object) 用于在数据库中存储任意二进制数据,例如图片、文档或加密数据。rusqlite 可以轻松地处理 BLOB 数据。

首先,在您的结构体中为 BLOB 数据定义 Option<Vec<u8>> 类型:

“`rust

[derive(Debug, PartialEq)]

struct User {
id: i32,
name: String,
email: String,
age: i32,
profile_picture: Option>, // BLOB 数据
}

// … 重新创建 users 表以包含 profile_picture 列
conn_file.execute(
“CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INTEGER,
profile_picture BLOB
)”,
[],
)?;
println!(“\n’users’ 表已创建或已存在 (包含 profile_picture)。”);
“`

插入和查询 BLOB 数据:

“`rust
// … 假设 conn_file 已经连接

println!(“\n— 演示 BLOB 数据处理 —“);

// 插入一个带有 BLOB 数据的用户
let profile_pic_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04]; // 模拟一些二进制数据

conn_file.execute(
“INSERT INTO users (name, email, age, profile_picture) VALUES (?1, ?2, ?3, ?4)”,
params![“Iris”, “[email protected]”, 27, &profile_pic_data],
)?;
println!(“用户 Iris (带头像) 已插入。”);

// 查询并检索 BLOB 数据
let user_with_blob_id = 1; // 假设 Iris 是 id 为 1 的用户
let iris_with_pic: User = conn_file.query_row(
“SELECT id, name, email, age, profile_picture FROM users WHERE id = ?1”,
params![user_with_blob_id],
|row| {
Ok(User {
id: row.get(0)?,
name: row.get(1)?,
email: row.get(2)?,
age: row.get(3)?,
profile_picture: row.get(4)?, // 直接获取 Option>
})
},
)?;
println!(“查询到带有 BLOB 的用户: {:?}”, iris_with_pic);
assert_eq!(iris_with_pic.profile_picture, Some(profile_pic_data));
“`

  • profile_picture: Option<Vec<u8>>rusqlite 会自动将 SQLite 的 BLOB 类型映射到 Rust 的 Vec<u8>。使用 Option 是因为 BLOB 列可能是空的 (NULL)。
  • params! 宏中,可以直接传递 &Vec<u8>Option<&Vec<u8>>

5. 错误处理

rusqlite 的所有操作都返回 rusqlite::Result 类型,它是一个 std::result::Result 的特化版本,其错误类型为 rusqlite::Error。这使得 Rust 的错误处理机制(如 ? 运算符、match 表达式)能够很好地集成。

“`rust
// … 假设 conn_file 已经连接

println!(“\n— 演示错误处理 —“);

// 尝试查询一个不存在的列
let query_non_existent_column: Result = conn_file.query_row(
“SELECT non_existent_column FROM users LIMIT 1”,
[],
|row| row.get(0),
);

match query_non_existent_column {
Ok(val) => println!(“意外获取到值: {}”, val),
Err(rusqlite::Error::QueryReturnedNoColumns) => {
println!(“捕获到预期错误: QueryReturnedNoColumns (或类似错误,因为实际是 no such column)”);
},
Err(e) => {
println!(“捕获到错误: {}”, e);
// 如果是 SqliteFailure,可以进一步检查 SQLite 错误码
if let rusqlite::Error::SqliteFailure(error, Some(msg)) = e {
println!(“SQLite 错误码: {:?}, 消息: {}”, error.code, msg);
}
}
}

// 尝试插入违反唯一约束的数据
let result_duplicate_email = conn_file.execute(
“INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)”,
params![“Alice”, “[email protected]”, 30], // email “[email protected]” 已存在
);

if let Err(e) = result_duplicate_email {
println!(“捕获到插入重复 email 的错误: {}”, e);
if let rusqlite::Error::SqliteFailure(error, Some(msg)) = e {
println!(“SQLite 错误码: {:?}, 消息: {}”, error.code, msg);
}
}
“`

通过模式匹配 rusqlite::Error,您可以根据不同的错误类型采取不同的恢复策略。rusqlite::Error::SqliteFailure 包含了底层的 SQLite 错误信息,包括错误码和描述。

第三部分:实战示例

现在,我们将把所有学到的知识整合到一个完整的 CRUD (Create, Read, Update, Delete) 示例中。

“`rust
use rusqlite::{params, Connection, Result, TransactionBehavior, functions::FunctionFlags};
use regex::Regex;
use std::sync::Arc;

// 用户结构体,包含 BLOB

[derive(Debug, PartialEq)]

struct User {
id: i32,
name: String,
email: String,
age: i32,
profile_picture: Option>,
}

fn main() -> Result<()> {
let mut conn = Connection::open_in_memory()?; // 使用内存数据库进行演示

// 启用外键约束 (良好的实践)
conn.execute("PRAGMA foreign_keys = ON", [])?;

// 创建 users 表
conn.execute(
    "CREATE TABLE users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT NOT NULL UNIQUE,
        age INTEGER,
        profile_picture BLOB
    )",
    [],
)?;
println!("数据库和 'users' 表已准备就绪。");

// 注册自定义 REGEXP 函数
conn.create_scalar_function(
    "REGEXP",
    2,
    FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
    move |ctx| {
        assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
        let pattern: Arc<Regex> = ctx.get_or_create_aux(0, |vr| -> Result<_, Box<dyn std::error::Error + Send + Sync + 'static>> {
            Ok(Regex::new(vr.as_str()?)?)
        })?;
        let text = ctx.get_raw(1).as_str().map_err(|e| rusqlite::Error::UserFunctionError(e.into()))?;
        Ok(pattern.is_match(text))
    },
)?;
println!("自定义 'REGEXP' 函数已注册。");


// --- C: Create (创建) ---
println!("\n--- 创建用户 ---");
let initial_profile_pic = Some(vec![0xAA, 0xBB, 0xCC]);
conn.execute(
    "INSERT INTO users (name, email, age, profile_picture) VALUES (?1, ?2, ?3, ?4)",
    params!["Alice", "[email protected]", 30, &initial_profile_pic],
)?;
conn.execute(
    "INSERT INTO users (name, email, age, profile_picture) VALUES (?1, ?2, ?3, ?4)",
    params!["Bob", "[email protected]", 25, None::<&[u8]>], // 没有头像
)?;
println!("插入了 Alice 和 Bob。");

// --- R: Read (读取) ---
println!("\n--- 读取用户 ---");
let mut stmt_select_all = conn.prepare("SELECT id, name, email, age, profile_picture FROM users")?;
let user_iter = stmt_select_all.query_map(params![], |row| {
    Ok(User {
        id: row.get(0)?,
        name: row.get(1)?,
        email: row.get(2)?,
        age: row.get(3)?,
        profile_picture: row.get(4)?,
    })
})?;

for user_result in user_iter {
    println!("{:?}", user_result?);
}

let alice_id = conn.last_insert_rowid(); // 获取 Alice 的 ID (假设她是最后一个插入的)
println!("Alice 的 ID: {}", alice_id);

// --- U: Update (更新) ---
println!("\n--- 更新用户 ---");
let updated_profile_pic = Some(vec![0x11, 0x22, 0x33, 0x44, 0x55]);
conn.execute(
    "UPDATE users SET age = ?1, profile_picture = ?2 WHERE name = ?3",
    params![31, &updated_profile_pic, "Alice"],
)?;
println!("更新了 Alice 的年龄和头像。");

// 再次查询 Alice,验证更新
let updated_alice: User = conn.query_row(
    "SELECT id, name, email, age, profile_picture FROM users WHERE name = ?1",
    params!["Alice"],
    |row| {
        Ok(User {
            id: row.get(0)?,
            name: row.get(1)?,
            email: row.get(2)?,
            age: row.get(3)?,
            profile_picture: row.get(4)?,
        })
    },
)?;
println!("更新后的 Alice: {:?}", updated_alice);
assert_eq!(updated_alice.age, 31);
assert_eq!(updated_alice.profile_picture, updated_profile_pic);

// --- D: Delete (删除) ---
println!("\n--- 删除用户 ---");
conn.execute("DELETE FROM users WHERE name = ?1", params!["Bob"])?;
println!("删除了 Bob。");

// 验证 Bob 是否已被删除
let remaining_users_count: i32 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
println!("剩余用户数量: {}", remaining_users_count);
assert_eq!(remaining_users_count, 1); // 只有 Alice 还在

// --- 事务示例 (回滚) ---
println!("\n--- 事务回滚示例 ---");
let tx_fail = conn.transaction()?;
tx_fail.execute(
    "INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)",
    params!["Charlie", "[email protected]", 28],
)?;
// 尝试插入一个重复的 email,会失败
let error_insert = tx_fail.execute(
    "INSERT INTO users (name, email, age) VALUES (?1, ?2, ?3)",
    params!["David", "[email protected]", 35],
);
if let Err(e) = error_insert {
    println!("事务中插入失败,错误: {}", e);
    tx_fail.rollback()?;
    println!("事务已回滚。");
}

let count_after_rollback: i32 = conn.query_row("SELECT COUNT(*) FROM users", [], |row| row.get(0))?;
println!("回滚后用户数量: {}", count_after_rollback);
assert_eq!(count_after_rollback, 1); // Charlie 和 David 都不应被插入

Ok(())

}
“`

总结

通过本文的详细介绍和示例,您应该已经全面掌握了在 Rust 中使用 rusqlite 库与 SQLite 数据库交互的方法。从基本的连接、创建表、CRUD 操作,到进阶的事务管理、预处理语句、自定义函数以及 BLOB 数据处理,rusqlite 都提供了强大而安全的 API。

Rust 结合 SQLite 的组合,为需要高性能、内存安全和轻量级数据存储的应用程序提供了绝佳的解决方案。无论是桌面应用、嵌入式系统还是高性能后端服务,rusqlite 都能助您一臂之力。现在,您可以自信地在您的 Rust 项目中运用 SQLite 数据库了!

滚动至顶部