SQLite基础:从零开始认识这个"嵌入式数据库之王"

说实话,我第一次接触SQLite是在一个嵌入式项目里。当时要在资源受限的设备上存数据,MySQL太重,文件存储又太乱。后来发现SQLite——嗯,就是它了。一个数据库,就一个文件,不需要安装,不需要配置,直接就能用。

SQLite是什么?说白了,它是一个C语言库,实现了自包含、无服务器、零配置的事务性SQL数据库引擎。你想想看,整个数据库就是一个文件,你的程序直接调用它的API就能读写数据。这在移动端、嵌入式、甚至桌面应用里,简直是神器。

为什么选择SQLite?

我在项目中遇到过好几次这样的场景:客户说"我们要上数据库",结果一调研,他们连DBA都没有。这时候SQLite就是最佳选择。它的核心优势有这些:

  • 零配置:不需要安装,不需要启动服务,直接调用API
  • 单文件存储:整个数据库就是一个.db文件,备份、迁移都方便
  • 轻量级:库文件只有几百KB,内存占用极低
  • 事务支持:支持ACID事务,数据安全有保障
  • 跨平台:Windows、Linux、macOS,甚至嵌入式系统都能跑

重要提示:SQLite不适合高并发写入场景。如果你需要每秒几千次的写入操作,还是考虑PostgreSQL或MySQL吧。但如果是读多写少的场景,SQLite完全够用。

Rust中集成SQLite:rusqlite库

在Rust生态里,操作SQLite最常用的库就是rusqlite。我个人习惯用这个库,因为它API设计得很Rusty——类型安全、错误处理到位、性能也不错。

先加依赖。在Cargo.toml里加上:

[dependencies]
rusqlite = { version = "0.31", features = ["bundled"] }

注意这个bundled特性。它会把SQLite的C源码编译进你的程序里。这样你就不需要系统里预装SQLite了。我建议新手都用这个特性,省得折腾环境问题。

创建数据库与表

来看一个完整的例子。我们创建一个用户表,包含ID、姓名、邮箱和创建时间:

use rusqlite::{Connection, Result};

fn main() -> Result<()> {
    // 打开数据库,如果不存在则创建
    let conn = Connection::open("users.db")?;

    // 创建表
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            name    TEXT NOT NULL,
            email   TEXT NOT NULL UNIQUE,
            created DATETIME DEFAULT CURRENT_TIMESTAMP
        )",
        [],
    )?;

    println!("数据库和表创建成功!");
    Ok(())
}

这里有个细节:Connection::open如果文件不存在,会自动创建。但如果你想要一个纯内存数据库,可以用Connection::open_in_memory()。我在做单元测试时经常用内存数据库,跑完就销毁,干净利落。

小技巧:生产环境建议用Connection::open指定文件路径。内存数据库虽然快,但程序退出数据就没了,只适合测试场景。

CRUD操作:增删改查

好,表建好了,接下来就是最核心的CRUD操作。我按顺序来,先插入数据。

插入数据(Create)

fn insert_user(conn: &Connection, name: &str, email: &str) -> Result<i64> {
    conn.execute(
        "INSERT INTO users (name, email) VALUES (?1, ?2)",
        rusqlite::params![name, email],
    )?;

    // 返回最后插入的ID
    Ok(conn.last_insert_rowid())
}

注意这里的?1?2是参数占位符。我刚开始用Rust写SQLite时,总想着用字符串拼接,后来发现这样容易出SQL注入。用参数化查询才是正道。

查询数据(Read)

fn query_users(conn: &Connection) -> Result<Vec<(i64, String, String, String)>> {
    let mut stmt = conn.prepare("SELECT id, name, email, created FROM users")?;
    let users = stmt.query_map([], |row| {
        Ok((
            row.get(0)?,
            row.get(1)?,
            row.get(2)?,
            row.get(3)?,
        ))
    })?;

    let mut result = Vec::new();
    for user in users {
        result.push(user?);
    }
    Ok(result)
}

这里用了query_map,它会把每一行映射成一个Rust类型。我个人觉得这种方式比直接拿Row对象更安全,因为类型检查在编译期就完成了。

更新数据(Update)

fn update_email(conn: &Connection, id: i64, new_email: &str) -> Result<usize> {
    let affected = conn.execute(
        "UPDATE users SET email = ?1 WHERE id = ?2",
        rusqlite::params![new_email, id],
    )?;
    Ok(affected)
}

返回的usize表示受影响的行数。如果返回0,说明没找到这个ID。我曾经踩过一个坑:忘记检查返回值,以为更新成功了,结果数据根本没变。所以,一定要检查返回值

删除数据(Delete)

fn delete_user(conn: &Connection, id: i64) -> Result<usize> {
    let affected = conn.execute(
        "DELETE FROM users WHERE id = ?1",
        rusqlite::params![id],
    )?;
    Ok(affected)
}

注意:删除操作不可逆。我曾经在生产环境上误删过数据,还好有备份。建议在删除前先做一次查询确认,或者用软删除(加一个deleted字段)代替物理删除。

完整示例:整合CRUD

把上面的函数串起来,就是一个完整的CRUD示例:

fn main() -> Result<()> {
    let conn = Connection::open("users.db")?;

    // 创建表
    conn.execute(
        "CREATE TABLE IF NOT EXISTS users (
            id      INTEGER PRIMARY KEY AUTOINCREMENT,
            name    TEXT NOT NULL,
            email   TEXT NOT NULL UNIQUE,
            created DATETIME DEFAULT CURRENT_TIMESTAMP
        )",
        [],
    )?;

    // 插入数据
    let id = insert_user(&conn, "张三", "zhangsan@example.com")?;
    println!("插入用户,ID: {}", id);

    // 查询数据
    let users = query_users(&conn)?;
    for (id, name, email, created) in &users {
        println!("{}: {} ({}) - {}", id, name, email, created);
    }

    // 更新数据
    update_email(&conn, id, "newemail@example.com")?;
    println!("邮箱已更新");

    // 删除数据
    delete_user(&conn, id)?;
    println!("用户已删除");

    Ok(())
}

SQLite在Rust中的知识体系

为了让你更直观地理解本章内容,我画了一张图:

SQLite + Rust 知识体系 rusqlite 库 连接管理 创建表 CRUD操作 事务处理 Create Read Update Delete 数据类型 约束条件 核心:Connection → Statement → Row 的数据流 参数化查询避免SQL注入 | 事务保证数据一致性

常见问题与避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 并发写入问题:SQLite只支持一个写入者。如果你有多线程并发写入,会报database is locked错误。解决方案是用连接池,或者把写操作串行化。
  • 事务别忘了提交conn.execute默认是自动提交的。但如果你用conn.execute_batch执行多条语句,记得显式调用conn.commit()
  • 路径问题Connection::open的路径是相对于当前工作目录的。我曾经在测试时没注意路径,结果数据库文件跑到了项目根目录外面,找了好久才发现。
  • 类型映射:Rust的类型和SQLite的类型要对应好。比如INTEGER对应i64REAL对应f64TEXT对应String。类型不匹配会在运行时panic。

我的建议:刚开始学的时候,先用内存数据库做测试。等逻辑跑通了,再切换到文件数据库。这样调试起来快很多。

好了,SQLite的基础就讲到这里。记住一句话:SQLite是嵌入式的首选,但不是万能的。选对工具,事半功倍。

专注资料整理