14

手把手教你用 Rust 搭建 REST API

 4 years ago
source link: https://www.infoq.cn/article/GITBBOtMoo0o3H2Q6rXT
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

bUjUVzN.jpg!web

我是 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 终端,那么你会看不到这个错误信息,最终把时间浪费在找错误上。

IneUVrr.png!web

若要解决这个问题,可以把 PG 的 bin 文件路径添加到 Path 变量。

下面我们创建一个用户表并为此创建一个迁移:

复制代码

diesel migration generate users

执行完这个命令后,你会看到迁移文件夹中出现两个文件。

下一步是为迁移编写 SQL 命令:

up.sql

复制代码

CREATETABLEusers
(
idSERIALPRIMARYKEY,
usernameVARCHARNOTNULL,
passwordVARCHARNOTNULL,
first_nameVARCHARNOTNULL
)

down.sql

复制代码

DROPTABLEusers

应用迁移的话可以用这个命令:

复制代码

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 )。首先先创建一个结构。

复制代码

usediesel;
usediesel::pg::PgConnection;
usediesel::prelude::*;
usesuper::schema::users;
usesuper::schema::users::dsl::usersasall_users;
// this is to get users from the database
#[derive(Serialize, Queryable)]
pubstructUser{
pubid:i32,
pubusername:String,
pubpassword:String,
pubfirst_name:String,
}

你大概会好奇结构定义中的这些标注都是什么。他们被称作导出(derives),也就是说,这些代码会导出序列化、可查询的 traits。 #[derive(Serialize)] 以及 #[derive(Deserialize)] 可以用来映射数据到响应和请求上。

下面再创建两个 struct,后面都会用到。

复制代码

// decode request data
#[derive(Deserialize)]
pubstructUserData{
pubusername:String,
}
// this is to insert users to database
#[derive(Serialize, Deserialize, Insertable)]
#[table_name ="users"]
pubstructNewUser{
pubusername:String,
pubpassword:String,
pubfirst_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

复制代码

usesuper::db::ConnasDbConn;
userocket_contrib::json::Json;
usesuper::models::{User, NewUser};
useserde_json::Value;
usecrate::models::UserData;

#[post("/users", format ="application/json")]
pubfnget_all(conn: DbConn) -> Json<Value> {
   letusers = User::get_all_users(&conn);
    Json(json!({
       "status":200,
       "result": users,
    }))
}

#[post("/newUser", format ="application/json", data ="<new_user>")]
pubfnnew_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>")]
pubfnfind_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 应用程序到数据库。
“数据库连接池是一种数据结构,用于维护活动的数据库连接以便后续在应用程序中使用。”

复制代码

usediesel::pg::PgConnection;
user2d2;
user2d2_diesel::ConnectionManager;
userocket::http::Status;
userocket::request::{self, FromRequest};
userocket::{Outcome, Request, State};
usestd::ops::Deref;

pubtypePool= r2d2::Pool<ConnectionManager<PgConnection>>;

pubfninit_pool(db_url:String) -> Pool {
letmanager = ConnectionManager::<PgConnection>::new(db_url);
r2d2::Pool::new(manager).expect("db pool failure")
}

pubstructConn(pubr2d2::PooledConnection<ConnectionManager<PgConnection>>);

impl<'a,'r> FromRequest<'a,'r>forConn {
typeError= ();

fnfrom_request(request: &'aRequest<'r>) -> request::Outcome<Conn, ()> {
letpool = request.guard::<State<Pool>>()?;
matchpool.get() {
Ok(conn) => Outcome::Success(Conn(conn)),
Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())),
}
}
}

implDerefforConn {
typeTarget= PgConnection;

#[inline(always)]
fnderef(&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]
externcratediesel;
externcratedotenv;
externcrater2d2;
externcrater2d2_diesel;
#[macro_use]
externcraterocket;
externcraterocket_contrib;
#[macro_use]
externcrateserde_derive;
#[macro_use]
externcrateserde_json;
usedotenv::dotenv;
usestd::env;
useroutes::*;
usestd::process::Command;
moddb;
modmodels;
modroutes;
modschema;
fnrocket() -> rocket::Rocket {
    dotenv().ok();
   letdatabase_url = env::var("DATABASE_URL").expect("set DATABASE_URL");
   letpool = db::init_pool(database_url);
    rocket::ignite()
        .manage(pool)
        .mount(
           "/api/v1/",
            routes![get_all, new_user, find_user],
        )
}
fnmain() {
   let_output =ifcfg!(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

6nQ3iiZ.png!web

启动服务器

下面用 Insomnia 测试一下我们的服务器。

eQJz2qY.png!web

希望本文能对你有所帮助。祝好!

英文原文:

How to Build a REST API in Rust — A Step-by-Step Guide


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK