在使用 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.sql
。migrations
中文件可以帮我们在项目开发过程中,高效演进和完善数据库模型。当你在对数据库进行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.sql
和down.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
。如果编译成功的话,你可以在控制台进行用户交互了。下图是一个演示效果的截图。
修改记录的数据
现在我们已经成功写入了一条数据,但是执行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
数据修改成功!再次执行 show_post 命令查看返回的结果。
删除 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库,社区活跃,文档详细。针对不同类型数据的接入都有详细的代码案例,极大降低了学习和使用的成本。未来将结合具体的案例展示更加详细和深入的使用教程。