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只支持一个写入者。如果你有多线程并发写入,会报
database is locked错误。解决方案是用连接池,或者把写操作串行化。 - 事务别忘了提交:
conn.execute默认是自动提交的。但如果你用conn.execute_batch执行多条语句,记得显式调用conn.commit()。 - 路径问题:
Connection::open的路径是相对于当前工作目录的。我曾经在测试时没注意路径,结果数据库文件跑到了项目根目录外面,找了好久才发现。 - 类型映射:Rust的类型和SQLite的类型要对应好。比如
INTEGER对应i64,REAL对应f64,TEXT对应String。类型不匹配会在运行时panic。
我的建议:刚开始学的时候,先用内存数据库做测试。等逻辑跑通了,再切换到文件数据库。这样调试起来快很多。
好了,SQLite的基础就讲到这里。记住一句话:SQLite是嵌入式的首选,但不是万能的。选对工具,事半功倍。