作者 | Asel Siriwardena
策划 | 万佳
本文主要介绍了如何用 Rust 搭建一个简单的 REST API。
教程中使用的是 Rocket 框架编写 API,借助 Diesel ORM 框架处理持久特征。这个框架覆盖了以下所有的点,让我们可以更容易地从最基础开始搭建:
-
启动网页服务器并打开一个端口
-
监听端口上的请求
-
如果有请求接入,查看 HTTP header 中的路径
-
根据路径将请求路由到处理器(handler)
-
提取请求中的信息
-
打包由用户生成的数据(data),并生成响应(response)
-
将响应(response)发回给发送者
1 安装 Nightly Rust
因为 Rocket 大量使用了 Rust 语法扩展及其他高级、不稳定的特性,所以我们必须要安装 nightly 版。
1 | rustup default nightly |
如果只想将 nightly 安装到项目文件夹,那可以使用以下命令:
1 | rustup override set nightly |
2 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [dependencies] rocket = "0.4.4" rocket_codegen = "0.4.4" diesel = { version = "1.4.0", features = ["postgres"] } dotenv = "0.9.0" r2d2-diesel = "1.0" r2d2 = "0.8" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" custom_derive ="0.1.7" [dependencies.rocket_contrib] version = "*" default-features = false features = ["json"] |
在后面的应用部分,我会解释具体该怎么写。
3 安装 Diesel
下一步要做的就是安装 Diesel。Diesel 有自己的 CLI(命令行界面),这是我们第一步要做的(假设您使用的是 PostgreSQL)。
http://diesel.rs/guides/getting-started/
1 | cargo install diesel_cli — no-default-features — features postgre |
然后我们需要告诉 Diesel 该在哪里找到我们的数据库,以下命令将生成一个
1 | echo DATABASE_URL=postgres://username:password@localhost:port/diesel_demo > .env |
然后执行以下命令:
1 | diesel setup |
这样可以搭建一个数据库(如果没有的话),并创建一个空的迁移目录,我们可以用该目录来管理我们的构架(更详细的会在后面讲到)。
运行代码的时候可能会出现以下错误信息:
1 | = note: LINK : fatal error LNK1181: cannot open input file ‘libpq.lib’ |
把
1 | setx PQ_LIB_DIR “[path to pg lib folder]” |
神奇的是 Diesel 文档中竟然没有提及这种错误信息。
http://diesel.rs/guides/getting-started/
强烈建议在 CMD 或者 Powershell 中执行这些命令。如果你用的是 IDE 终端,那么你会看不到这个错误信息,最终把时间浪费在找错误上。
若要解决这个问题,可以把 PG 的 bin 文件路径添加到 Path 变量。
下面我们创建一个用户表并为此创建一个迁移:
1 | diesel migration generate users |
执行完这个命令后,你会看到迁移文件夹中出现两个文件。
下一步是为迁移编写 SQL 命令:
1 2 3 4 5 6 7 | CREATE TABLE users ( id SERIAL PRIMARY KEY, username VARCHAR NOT NULL, password VARCHAR NOT NULL, first_name VARCHAR NOT NULL ) |
1 | DROP TABLE users |
应用迁移的话可以用这个命令:
1 | diesel migration run |
最好先回滚之后再重新迁移,以确保
1 | diesel migration redo |
你可以看到 DB.right 出现了用户表。
差点忘了提,在运行 Diesel 安装命令的时候会生成一个文件
1 2 3 4 5 6 7 8 | table! { users (id) { id -> Int4, username -> Varchar, password -> Varchar, first_name -> Varchar, } } |
4Rust 部分
因为要使用 ORM,所以需要先将用户表映射到 Rust 中。Java 中用的是 Class 来映射表格,这种方式被称作 Beans。Rust 中我们要用的是结构(
1 2 3 4 5 6 7 8 9 10 11 12 13 | use diesel; use diesel::pg::PgConnection; use diesel::prelude::*; use super::schema::users; use super::schema::users::dsl::users as all_users; // this is to get users from the database #[derive(Serialize, Queryable)] pub struct User { pub id: i32, pub username: String, pub password: String, pub first_name: String, } |
你大概会好奇结构定义中的这些标注都是什么。他们被称作导出(derives),也就是说,这些代码会导出序列化、可查询的 traits。
下面再创建两个 struct,后面都会用到。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // decode request data #[derive(Deserialize)] pub struct UserData { pub username: String, } // this is to insert users to database #[derive(Serialize, Deserialize, Insertable)] #[table_name = "users"] pub struct NewUser { pub username: String, pub password: String, pub first_name: String, } |
下面要做的是应用
这里可以看到,我们将连接传递到方法,返回用户向量(Vector of User)。我们获取了用户表中的所有行,然后将其映射到用户结构上。
出错可能在所难免,如果担心的话可以把错误信息打印出来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | impl User { pub fn get_all_users(conn: &PgConnection) -> Vec<User> { all_users .order(users::id.desc()) .load::<User>(conn) .expect(error!) } pub fn insert_user(user: NewUser, conn: &PgConnection) -> bool { diesel::insert_into(users::table) .values(&user) .execute(conn) .is_ok() } pub fn get_user_by_username(user: UserData, conn: &PgConnection) -> Vec<User> { all_users .filter(users::username.eq(user.username)) .load::<User>(conn) .expect(error!) } } |
现在有了表和映射到表的结构,接下来就需要创建使用它的方法。首先,我们要建一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | use super::db::Conn as DbConn; use rocket_contrib::json::Json; use super::models::{User, NewUser}; use serde_json::Value; use crate::models::UserData; #[post(/users, format = application/json)] pub fn get_all(conn: DbConn) -> Json<Value> { let users = User::get_all_users(&conn); Json(json!({ status: 200, result: users, })) } #[post(/newUser, format = application/json, data = <new_user>)] pub fn new_user(conn: DbConn, new_user: Json<NewUser>) -> Json<Value> { Json(json!({ status: User::insert_user(new_user.into_inner(), &conn), result: User::get_all_users(&conn).first(), })) } #[post(/getUser, format = application/json, data = <user_data>)] pub fn find_user(conn: DbConn, user_data: Json<UserData>) -> Json<Value> { Json(json!({ status: 200, result: User::get_user_by_username(user_data.into_inner(), &conn), })) } |
现在要做的就只剩下设置连接池了。以下是从 Rocket 文档中摘抄的关于连接池的简介。
https://rocket.rs/v0.4/guide/state/#databases
“Rocket 内建了对 ORM 无关数据库的支持,Rocket 提供了一个过程宏,使您可以通过连接池轻松连接 Rocket 应用程序到数据库。
“数据库连接池是一种数据结构,用于维护活动的数据库连接以便后续在应用程序中使用。”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | use diesel::pg::PgConnection; use r2d2; use r2d2_diesel::ConnectionManager; use rocket::http::Status; use rocket::request::{self, FromRequest}; use rocket::{Outcome, Request, State}; use std::ops::Deref; pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>; pub fn init_pool(db_url: String) -> Pool { let manager = ConnectionManager::<PgConnection>::new(db_url); r2d2::Pool::new(manager).expect(db pool failure) } pub struct Conn(pub r2d2::PooledConnection<ConnectionManager<PgConnection>>); impl<'a, 'r> FromRequest<'a, 'r> for Conn { type Error = (); fn from_request(request: &'a Request<'r>) -> request::Outcome<Conn, ()> { let pool = request.guard::<State<Pool>>()?; match pool.get() { Ok(conn) => Outcome::Success(Conn(conn)), Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), } } } impl Deref for Conn { type Target = PgConnection; #[inline(always)] fn deref(&self) -> &Self::Target { &self.0 } } |
最后,我们需要在 main 文件中启动服务器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | #![feature(plugin, const_fn, decl_macro, proc_macro_hygiene)] #![allow(proc_macro_derive_resolution_fallback, unused_attributes)] #[macro_use] extern crate diesel; extern crate dotenv; extern crate r2d2; extern crate r2d2_diesel; #[macro_use] extern crate rocket; extern crate rocket_contrib; #[macro_use] extern crate serde_derive; #[macro_use] extern crate serde_json; use dotenv::dotenv; use std::env; use routes::*; use std::process::Command; mod db; mod models; mod routes; mod schema; fn rocket() -> rocket::Rocket { dotenv().ok(); let database_url = env::var("DATABASE_URL").expect("set DATABASE_URL"); let pool = db::init_pool(database_url); rocket::ignite() .manage(pool) .mount( "/api/v1/", routes![get_all, new_user, find_user], ) } fn main() { let _output = if cfg!(target_os = "windows") { Command::new("cmd") .args(&["/C", "cd ui && npm start"]) .spawn() .expect("Failed to start UI Application") } else { Command::new("sh") .arg("-c") .arg("cd ui && npm start") .spawn() .expect("Failed to start UI Application") }; rocket().launch(); } |
在我的项目中,我还添加了 Angular 前端,但用的还是我们的 Rust 后端来支持。
运行程序使用:
启动服务器
下面用 Insomnia 测试一下我们的服务器。
希望本文能对你有所帮助。祝好!
延展阅读:
https://medium.com/better-programming/rest-api-in-rust-step-by-step-guide-b8a6c5fcbff0
点个在看少个 bug ????