本文需要用到以下工具/库:
- Rust and Cargo toolchain
actix-web
web 框架sea-orm
ORM 框架serde
序列化/反序列化dotenvy
环境变量配置
项目初始化
首先 cargo new workspace
创建一个新项目,但该项目只作为workspace
工作空间,因此可以将src
目录直接删掉,然后编辑Cargo.toml
:
[workspace]members = ["app"]default-members=["app"]
设置default-members
,这样cargo run
的默认项目就是app
。
然后,cargo new app
,新建app
crate。接着给app
的Cargo.toml
添加如下依赖:
[dependencies]actix-web="4"sea-orm = { version = "^0", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ] }serde = { version = "1.0", features = ["derive"] }dotenvy = "0.15.6"
actix 准备
首先我们到app
的 main.rs
,把 actix
先跑起来看看。
use actix_web::{web, App, HttpServer};
#[actix_web::main]async fn main() -> Result<(), std::io::Error> { HttpServer::new(|| App::new().route("/ping", web::get().to(|| async { "pong!" }))) .bind(("127.0.0.1", 5000))? .run() .await}
cargo run
然后 curl http://localhost:5000
,你会看到一个”pong!”.
用 dotenvy 控制环境变量
监听选项写死是不太好的,这时可以使用dotenvy
包。在workspace目录下创建一个.env
文件,内容为:
LISTEN=0.0.0.0
然后,我们在main中通过读取环境变量来实现动态配置。
use actix_web::{web, App, HttpServer};use dotenvy::dotenv;use std::env;
#[actix_web::main]async fn main() -> Result<(), std::io::Error> { dotenv().ok();
let listen = env::var("LISTEN").unwrap_or("127.0.0.1".into()); let port = env::var("PORT").unwrap_or("5000".into()); let port = port .parse() .expect(format!("Error parsing port number PORT={}", port).as_str());
println!("listening on http://{}:{}", listen, port);
HttpServer::new(|| App::new().route("/ping", web::get().to(|| async { "pong!" }))) .bind((listen, port))? .run() .await}
数据库准备
本例使用 Sea-Orm 作为 ORM 框架。 首先安装 sea-orm-cli 准备数据库迁移。
cargo install sea-orm-cli
Schema first 还是 Entity first?
SeaORM文档也提到了这个问题。
对于涉及数据库的业务,有一个重要的开发流程问题,那就是先有数据库还是先有代码?.NET EntityFrame Core 的文档给了很好的说明。大多数业务框架推荐 Entity First(Code First)。即先有业务模型,然后去生成数据库。这是因为数据库通常是通过 SQL 管理的,而在有ORM框架的情况下,应当尽可能减少原生SQL操作。
但是SeaORM采用了一个折中的工作流程,Schema First
。首先,应当写一个迁移逻辑(用Rust
而非SQL
),然后根据这个迁移逻辑去调整数据库,最后从迁移过的数据库生成Entity
,也就代码中的Model
。这一套流程和EF Core
的Code First
模式是很像的,只不过EF Core
会先从Model
的变化生成迁移文件(这些推断并不总是准确的,而且可能造成毁灭性的后果)。另一些ORM框架,比如gorm
,它们的自动迁移只会新增而不会修改或删除,为了避免不可挽回的数据丢失,但是这种情况下,修改一个现有字段的约束就会变得困难。事实上,SeaORM的工作模式虽然相比之下更麻烦了一点,但是开发者获得了更多的控制权,而且由于迁移文件也是用Rust
代码写的,效率并不会很低。
执行sea-orm-cli migrate init
,我们会得到一个新的Crate,名叫migration
。然后删掉默认的一个示例迁移文件。
添加 migration Crate 到 WorkSpace
接下来,修改workspace
即根目录的Cargo.toml
以包含这个 migration
Crate.
[workspace]members=["app","migration"] #新增一个migrationdefault-members=["app"]
Migration File
通过sea-orm-cli migrate generate create_todo_table
,可以得到一个以当前时间为前缀,名为create_todo_table
的迁移文件。修改其内容如下:
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]pub struct Migration;
#[async_trait::async_trait]impl MigrationTrait for Migration { async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .create_table( Table::create() .table(Todo::Table) .if_not_exists() .col( ColumnDef::new(Todo::Id) .integer() .not_null() .auto_increment() .primary_key(), ) .col(ColumnDef::new(Todo::ExpireAt).date_time().not_null()) .col(ColumnDef::new(Todo::Content).string()) .col(ColumnDef::new(Todo::IsFinished).boolean().not_null()) .to_owned(), ) .await }
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Todo::Table).to_owned()) .await }}
/// Learn more at https://docs.rs/sea-query#iden#[derive(Iden)]enum Todo { Table, Id, ExpireAt, Content, IsFinished,}
这样,就定义好了一次迁移的逻辑。up
方法新建了一张表,并设置了字段的类型和约束。down
方法应当是up
的逆过程,由于up
是新建表,因此down
直接删除表就可以了。
执行迁移
接下来,我们要将写好的迁移文件应用到数据库。可以在应用程序中完成这一步,也可以使用sea-orm-cli
完成。migration
Crate是一个 bin crate,它编译的结果就是一个负责执行迁移的程序。sea-orm-cli
提供了一个捷径来编译并执行迁移。
如果采用sea-orm-cli
手动迁移,要注意为migration
crate配置一个数据库驱动,就像app
crate一样,本文以sqlite为例。
需要用到环境变量DATABASE_URL
,由于我们已经使用了dotenvy
来管理环境变量,而sea-orm-cli
也是支持这个的,因此直接在.env
文件添加一行:
DATABASE_URL="sqlite:./sqlite.db?mode=rwc"
[package]name = "migration"# ......[dependencies.sea-orm-migration]version = "^0.10.0"features = [ # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI. # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime. # e.g. # "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature "runtime-tokio-rustls", # de-comment this line so that we have an async runtime # "sqlx-postgres", # `DATABASE_DRIVER` feature "sqlx-sqlite" # Add this line so that we can connect to a sqlite db]
然后,执行sea-orm-cli migrate
,等待编译并运行成功后,数据库就迁移完成了!
生成 Entity
要执行 CRUD,当然还需要 Entity 定义,可以用sea-orm-cli
从线上数据库生成。注意是从数据库生成,而不是从前面编写的 migration 文件。因此务必确保所连接到的数据库Schema是最新的!
执行sea-orm-cli generate entity
,会发现生成了三个文件,默认是一个mod
的构造,即mod.rs
,prelude.rs
以及若干个 Entity
定义,由于我们只有一个todo
,因此也只有一个todo.rs
。
不过默认生成在了当前目录下,显然不太合理。你可以根据自己的需要调整目录结果,通过-o
参数来指定生成位置。
CRUD API
然后,我们就可以开始进入无聊的CRUD环节了。无聊的代码就不贴了,详见github.com/artiga033/rust_rest_api_demo