我是Asel,今天我將展示如何用Rust搭建一個簡單的REST API。
教程中使用的是Rocket框架編寫API,藉助Diesel ORM框架處理持久特徵。這個框架覆蓋了以下所有的點,讓我們可以更容易地從最基礎開始搭建:
- 啓動網頁服務器並打開一個端口。
- 監聽端口上的請求。
- 如果有請求接入,查看HTTP header中的路徑。
- 根據路徑將請求路由到處理器(
handler
) - 提取請求中的信息
- 打包由用戶生成的數據(
data
),並生成響應(response
) - 將響應(
response
)發回給發送者
安裝Nightly Rust
因爲Rocket大量使用了Rust語法擴展及其他高級、不穩定的特性,所以我們必須要安裝nightly
版。
rustup default nightly
如果只想將nightly
安裝到項目文件夾,那可以使用以下命令:
rustup override set nightly
依賴
[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"]
在後面的應用部分,我會解釋具體該怎麼寫。
安裝Diesel
下一步要做的就是安裝Diesel。Diesel有自己的CLI(命令行界面),這是我們第一步要做的(假設您使用的是PostgreSQL)。
cargo install diesel_cli — no-default-features — features postgre
然後我們需要告訴Diesel該在哪裏找到我們的數據庫,以下命令將生成一個.env
文件。
echo DATABASE_URL=postgres://username:password@localhost:port/diesel_demo > .env
然後執行以下命令:
diesel setup
這樣可以搭建一個數據庫(如果沒有的話),並創建一個空的遷移目錄,我們可以用該目錄來管理我們的構架(更詳細的會在後面講到)。
運行代碼的時候可能會出現以下錯誤信息:
= note: LINK : fatal error LNK1181: cannot open input file ‘libpq.lib’
把 PG lib folder
路徑添加到環境變量中就可以輕易解決。
setx PQ_LIB_DIR “[path to pg lib folder]”
神奇的是Diesel文檔中竟然沒有提及這種錯誤信息。
強烈建議在CMD或者Powershell中執行這些命令。如果你用的是IDE終端,那麼你會看不到這個錯誤信息,最終把時間浪費在找錯誤上。
若要解決這個問題,可以把PG的bin文件路徑添加到Path變量。
下面我們創建一個用戶表併爲此創建一個遷移:
diesel migration generate users
執行完這個命令後,你會看到遷移文件夾中出現兩個文件。
下一步是爲遷移編寫SQL命令:
up.sql
CREATE TABLE users
(
id SERIAL PRIMARY KEY,
username VARCHAR NOT NULL,
password VARCHAR NOT NULL,
first_name VARCHAR NOT NULL
)
down.sql
DROP TABLE users
應用遷移的話可以用這個命令:
diesel migration run
最好先回滾之後再重新遷移,以確保down.sql
準確無誤。
diesel migration redo
你可以看到DB.right出現了用戶表。
差點忘了提,在運行Diesel安裝命令的時候會生成一個文件schema.rs
。應該是這樣的:
table! {
users (id) {
id -> Int4,
username -> Varchar,
password -> Varchar,
first_name -> Varchar,
}
}
下面是Rust部分
因爲要使用ORM,所以需要先將用戶表映射到Rust中。Java中用的是Class來映射表格,這種方式被稱作Beans。Rust中我們要用的是結構(struct
)。首先先創建一個結構。
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。#[derive(Serialize)]
以及 #[derive(Deserialize)]
可以用來映射數據到響應和請求上。
下面再創建兩個struct,後面都會用到。
// 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,
}
下面要做的是應用User
。這樣就可以對數據庫進行操作了。
這裏可以看到,我們將連接傳遞到方法,返回用戶向量(Vector of User)。我們獲取了用戶表中的所有行,然後將其映射到用戶結構上。
出錯可能在所難免,如果擔心的話可以把錯誤信息打印出來。
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!")
}
}
現在有了表和映射到表的結構,接下來就需要創建使用它的方法。首先,我們要建一個route
文件,通常稱之爲handler。
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文檔中摘抄的關於連接池的簡介。
“Rocket內建了對ORM無關數據庫的支持,Rocket提供了一個過程宏,使您可以通過連接池輕鬆連接Rocket應用程序到數據庫。
“數據庫連接池是一種數據結構,用於維護活動的數據庫連接以便後續在應用程序中使用。”
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文件中啓動服務器。
#![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後端來支持。
運行程序使用:cargo run
。
啓動服務器
下面用Insomnia測試一下我們的服務器。
希望本文能對你有所幫助。祝好!
英文原文: