使用SQLite做为Rust项目的数据库

https://www.notion.so/images/page-cover/met_goya_1789.jpg

在使用 Rust 开发应用时,需要用到 SQLite 做为应用的本地数据库。在目前Rust的生态中,SQLite相关的 Crate 还是挺多的。我选择了 Diesel 这个ORM库。原因无他,文档全,star多,使用相对简单。本文我将通过一个简单的案例,演示如何使用 Diesel,在 SQLite 中实现对数据的 CRUD。

安装依赖

首先将Diesel添加到项目依赖中,Diesel推荐使用 .env 文件来管理项目的环境变量,所以还需要再加上 dotenv这个工具。

[dependencies] diesel = { version = "1.4.4", features = ["postgres"] } dotenv = "0.15.0"

初次之外,Diesel提供一个独立的命令行工具:diesel-cli,用以帮助管理项目。这是一个独立的包,不会影响项目的结构和代码。只需要安装在系统中即可。

cargo install diesel-cli

diesel 是一个支持MySQL,PostgreSQL和SQLite的ORM工具。这三种数据库都有各自的依赖。

  • libpq PostgreSQL 依赖的客户端二进制

  • libmysqlclient  MySQL 依赖的客户端二进制

  • libsqlite3 SQlite 依赖的客户端二进制

默认情况下,diesel-cli 在安装时会检查这些依赖,你可以事先在你的操作系统中安装好对应的依赖,也可以在安装cli时使用-no-default-features 跳过。你也可以在安装时指定对应的数据库。

cargo install diesel_cli --no-default-features --features postgres cargo install diesel_cli --no-default-features --features sqlite

在项目中设置Diesel

Diesel 通过设置的环境变量 DATABASE_URL 找到我们的数据库。在项目中创建 .env 文件做为项目的环境相关的配置文件,通过dotenv来解析。比如,将SQLite的DB文件放在项目根目录中。

echo DATABASE_URL=./my.db > .env

然后为项目添加依赖。

[dependencies] diesel = { version = "^1.1.0", features = ["sqlite"] } dotenv = "0.9.0"

接下来就可以初始化数据库,创建脚本了。

diesel setup diesel migration generate create_posts

如果一切顺利的话,项目中会出现一个 my.db 文件和一个 migrations 目录。在 migrations 目录中有一个up.sql和一个down.sqlmigrations 中文件可以帮我们在项目开发过程中,高效演进和完善数据库模型。当你在对数据库进行migration时,实际上是执行了这个版本中的up.sql。而在回滚时,执行的则是down.sql

另外 setup 时还会创建一个diesel.toml 。这是 diesel-cli 在当前项目中的配置文件,CLI 默认会从Cargo.toml文件所在的目录中查找这个文件。当然你也可以通过环境变 DIESEL_CONFIG_FILE修改。配置文件在创建时会自动填入以下配置,这段配置指定了 diesel 创建的数据库 schema 保存的文件路径。

[print_schema] file = "src/schema.rs"

创建数据库模型

接下来创建第一个版本的数据库模型。分别修改up.sqldown.sql

CREATE TABLE posts ( id INTEGER NOT NULL PRIMARY KEY, title VARCHAR NOT NULL, body TEXT NOT NULL, published BOOLEAN NOT NULL DEFAULT 'f' ) DROP TABLE channels

SQL语句准备就绪,我们可以试着使用这个版本。

diesel migration run

通常来说,如果命令执行成功,会在 src 目录下创建一个 schema.rs

table! { posts (id) { id -> Integer, title -> Text, body -> Text, published -> Bool, } }

前文提到过数据库schema输出的路径可以通过diesel.toml 设置

每次执行 diesel migration run 或者 diesel migration redo 时,新的schema都会创建并输出到目录中。

写点代码试试

现在项目准备就绪了,我们写一点Rust代码,实现post数据的展示。首先先实现数据库的连接。

连接数据库,创建 Post 结构体

src/lib.rs中实现连接数据库的逻辑。

pub mod schema; pub mod models; #[macro_use] extern crate diesel; extern crate dotenv; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; use dotenv::dotenv; use std::env; pub fn establish_connection() -> SqliteConnection { dotenv().ok(); let database_url = env::var("DATABASE_URL") .expect("DATABASE_URL must be set"); SqliteConnection::establish(&database_url) .expect(&format!("Error connecting to {}", database_url)) }

然后在src/models.rs中创建刚才声明的两个 Module。

#[derive(Queryable)] pub struct Post { pub id: i32, pub title: String, pub body: String, pub published: bool, }

#[derive(Queryable)] 将生成从 SQL 查询加载 Post 结构所需的所有代码。数据库的schema由Diesel自动创建,这一点已经在前文中提及,这里就不再赘述。

读取 Post 数据

接下来实现一个简单的数据读取的过程。新建文件src/bin/show_posts.rs ,这个文件做的事情是连接数据库、查询数据和输出数据。

extern crate diesel_demo; extern crate diesel; use self::diesel_demo::*; use self::models::*; use self::diesel::prelude::*; fn main() { use diesel_demo::schema::posts::dsl::*; let connection = establish_connection(); let results = posts.filter(published.eq(true)) .limit(5) .load::<Post>(&connection) .expect("Error loading posts"); println!("Displaying {} posts", results.len()); for post in results { println!("{}", post.title); println!("----------\n"); println!("{}", post.body); } }

试着执行cargo run --bin show_posts看看效果,编译通过之后控制台会输出以下内容:

Displaying 0 posts

写入 Post 数据

因为现在数据库中还没有数据,我们需要创建Post数据。首先在src/models.rs中定义结构体NewPost,然后在src/lib.rs中实现create_post

use super::schema::posts; #[derive(Insertable)] #[table_name="posts"] pub struct NewPost<'a> { pub title: &'a str, pub body: &'a str, } use self::models::{Post, NewPost}; pub fn create_post<'a>(conn: &SqliteConnection, title: &'a str, body: &'a str) -> usize { use schema::posts; let new_post = NewPost { title, body, }; diesel::insert_into(posts::table) .values(&new_post) .execute(conn) .expect("Error saving new post") }

现在已经万事俱备,接下来新建src/bin/write_post.rs,实现写入一条Post的逻辑。

extern crate diesel_demo; extern crate diesel; use self::diesel_demo::*; use std::io::{stdin, Read}; fn main() { let connection = establish_connection(); println!("What would you like your title to be?"); let mut title = String::new(); stdin().read_line(&mut title).unwrap(); let title = &title[..(title.len() - 1)]; // Drop the newline character println!("\nOk! Let's write {} (Press {} when finished)\n", title, EOF); let mut body = String::new(); stdin().read_to_string(&mut body).unwrap(); let post = create_post(&connection, title, &body); println!("\nSaved draft {} with id {}", title, post.id); } #[cfg(not(windows))] const EOF: &'static str = "CTRL+D"; #[cfg(windows)] const EOF: &'static str = "CTRL+Z";

首先创建一个数据库的连接,在打印一句提示语之后,调用Rust内置的stdin,获取IO输入。最后使用两个条件编译,针对不同系统绑定结束的快捷键。

让我们试着执行写入逻辑的脚本cargo run --bin write_post。如果编译成功的话,你可以在控制台进行用户交互了。下图是一个演示效果的截图。

6c7c5873213d7257

修改记录的数据

现在我们已经成功写入了一条数据,但是执行cargo run --bin show_posts 并不会返回我们预期的数据。因为上一步虽然写入了一条Post数据,但是数据的published默认是false。而show_posts中数据查询的逻辑是.filter(published.eq(true)) ,这样无法查找出想要的数据,我们需要提供修改数据的能力。新建src/bin/publish_post.rs,对数据进行修改。

extern crate diesel_demo; extern crate diesel; use self::diesel::prelude::*; use self::diesel_demo::*; use self::models::Post; use std::env::args; fn main() { use diesel_demo::schema::posts::dsl::{posts, published}; let id = args().nth(1).expect("publish_post requires a post id") .parse::<i32>().expect("Invalid ID"); let connection = &mut establish_connection(); let _ = diesel::update(posts.find(id)) .set(published.eq(true)) .execute(connection) .expect(&format!("Unable to find post {}", id)); let post: models::Post = posts .find(id) .first(connection) .unwrap_or_else(|_| panic!("Unable to find post {}", id)); println!("Published post {}", post.title); }

执行 cargo run --bin publish_post 1

ff1d2935afffb0e1

数据修改成功!再次执行 show_post 命令查看返回的结果。

78b985abb91cb1d2

删除 Post 数据

到目前为止,CRUD中的”CRU”都已经实现了,现在就差最后的“D”,也就是删除操作了。按照前面的思路,我们依样画葫芦,创建src/bin/delete_post.rs,在这个文件中实现数据库数据的删除。

use diesel::prelude::*; use diesel_demo_step_3_sqlite::*; use std::env::args; fn main() { use diesel_demo_step_3_sqlite::schema::posts::dsl::*; let target = args().nth(1).expect("Expected a target to match against"); let pattern = format!("%{}%", target); let connection = &mut establish_connection(); let num_deleted = diesel::delete(posts.filter(title.like(pattern))) .execute(connection) .expect("Error deleting posts"); println!("Deleted {} posts", num_deleted); }

执行 cargo run --bin delete_post Good 将会删除数据。

结束语

Diesel 是一个相当不错的ORM库,社区活跃,文档详细。针对不同类型数据的接入都有详细的代码案例,极大降低了学习和使用的成本。未来将结合具体的案例展示更加详细和深入的使用教程。