iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 21
0
自我挑戰組

WebAssembly + Rust 的前端應用系列 第 21

[Day 21] Rust Actix PART2

大家好,今天要來優化我們之前做的專案,並且替他加上和 DB 連線的功能,那麼首先我們先來調整一下原本的 main 這支程式還有我們的專案架構,

添加 Log

對於開發者而言 Log 是很重要的工具因此我們來添加一個 library 來幫助我們開發,首先添加依賴

[dependencies]

log = "0.4.8"
env_logger = "0.7.0"

巨集 的方式引入,這樣我們就可以直接調用裡面的程式,不過我其實還不是很懂,未來應該會再寫一篇文章來介紹。

檔案名稱: main.rs

#[macro_use]
extern crate log;
extern crate env_logger;

use actix_web::{web, App, HttpServer, Result, HttpResponse, get};

fn main() {
    // init log
    std::env::set_var("RUST_LOG", "actix_web=info,info");
    env_logger::init();

    // settings
    let url = "127.0.0.1:3000";

    info!("Running server on {}", url);
    
    ...

在 main 裡面初始化 log 之後就會看到 log 的資訊囉,

log 結果圖

https://ithelp.ithome.com.tw/upload/images/20191007/20119807ORMoq0pyes.png

添加 middlewares

接著我們來調整一下啟動 server 的程式並且添加 middlewares,

use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::middleware::Logger;
use actix_web::{dev, http, web, App, HttpServer, Result, HttpResponse, get, middleware};

fn render_500<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    res.response_mut().headers_mut().insert(
        http::header::CONTENT_TYPE,
        http::HeaderValue::from_static("Error"),
    );
    Ok(ErrorHandlerResponse::Response(res))
}

let mut server = HttpServer::new(
        || App::new()
            .wrap(Logger::default())
            .wrap(Logger::new("%a %{User-Agent}i"))
            .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2"))
            .wrap(
                ErrorHandlers::new()
                    .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500),
            )
            .service(
                web::scope("/api/v1").service(translate)
            )
        );

這樣只要有 request 進來時就會顯示使用者的資訊結果如下,

https://ithelp.ithome.com.tw/upload/images/20191007/20119807JMVyUsAWXw.png

DefaultHeaders 會給每個 response 給預設的 header,但如果 api 有另外設定的會預設值就會無效。
ErrorHandlers 則是在錯誤發生時處理 status code 的 middleware。

調整 api 路徑跟參數獲取方式

最後我們還要把 hello 這支 api 重新命名成 translate 讓他語意更清楚,

...

#[derive(Serialize, Deserialize)]
struct Chinese {
    text: String,
}

/**
 * Translate chinese to English with deep learning.
 */
fn exec_translate_from_python(value:&str) -> String {
    let mut _translate = Command::new("python3");
    _translate.arg("run_nn.py").arg("translate").arg(value);
    _translate.current_dir("/Users/liyanxin/Life/myprojects/Chinese2English_Seq2Seq_Attention");
    let output = _translate.output().expect("failed to execute process");
    String::from_utf8_lossy(&output.stdout).to_string()
}

#[get("/translate")]
fn translate(info: web::Query<Chinese>) -> Result<HttpResponse> {
    let output = exec_translate_from_python(&info.text);
    Ok(HttpResponse::Ok().json(Chinese {
        text: output,
    }))
}

...

這樣呼叫時的 api 路徑就是 /api/v1/translate?text=我好餓 ,帶參數就變成是用 query 的方式。

修改後的 api 結果圖

https://ithelp.ithome.com.tw/upload/images/20191007/20119807ozjs3QqoMr.png

最後先附上目前的完整程式碼,

[package]
name = "actixweb"
version = "0.1.0"
authors = ["SP_Penguin <abrcdf1023@gmail.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
actix-web = "1.0.8"
listenfd = "0.3"
serde = { version = "1.0", features = ["derive"] }

log = "0.4.8"
env_logger = "0.7.0"
#[macro_use]
extern crate log;
extern crate env_logger;

use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::middleware::Logger;
use actix_web::{dev, http, web, App, HttpServer, Result, HttpResponse, get, middleware};
use listenfd::ListenFd;
use serde::{Deserialize, Serialize};
use std::process::Command;

#[derive(Serialize, Deserialize)]
struct Chinese {
    text: String,
}

fn render_500<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    res.response_mut().headers_mut().insert(
        http::header::CONTENT_TYPE,
        http::HeaderValue::from_static("Error"),
    );
    Ok(ErrorHandlerResponse::Response(res))
}

/**
 * Translate chinese to English with deep learning.
 */
fn exec_translate_from_python(value:&str) -> String {
    let mut _translate = Command::new("python3");
    _translate.arg("run_nn.py").arg("translate").arg(value);
    _translate.current_dir("/Users/liyanxin/Life/myprojects/Chinese2English_Seq2Seq_Attention");
    let output = _translate.output().expect("failed to execute process");
    String::from_utf8_lossy(&output.stdout).to_string()
}

#[get("/translate")]
fn translate(info: web::Query<Chinese>) -> Result<HttpResponse> {
    let output = exec_translate_from_python(&info.text);
    Ok(HttpResponse::Ok().json(Chinese {
        text: output,
    }))
}

fn main() {
    // init log
    std::env::set_var("RUST_LOG", "actix_web=info,info");
    env_logger::init();

    // settings
    let url = "127.0.0.1:3000";

    info!("Running server on {}", url);
    
    // auto reload
    let mut listenfd = ListenFd::from_env();

    // start server
    let mut server = HttpServer::new(
        || App::new()
            .wrap(Logger::default())
            .wrap(Logger::new("%a %{User-Agent}i"))
            .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2"))
            .wrap(
                ErrorHandlers::new()
                    .handler(http::StatusCode::INTERNAL_SERVER_ERROR, render_500),
            )
            .service(
                web::scope("/api/v1").service(translate)
            )
        );

    // auto reload listener
    server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
        server.listen(l).unwrap()
    } else {
        server.bind(url).unwrap()
    };

    server.run().unwrap();
}

安裝 postgresql

接著我們將用 diesel 這個 ORM library 來處理我們的 CRUD,而官方教學要我們用 postgresql 所以必須先安裝他,用 homebrew 進行安裝

修改最高權限使用者密碼

安裝完接著我們要修改密碼,先找到自己的使用者名稱(筆者安裝完就是電腦的名稱)。

$ sudo -u your_user_name psql postgres

接著下 \password

postgres=# \password

兩次確認密碼之後完成。

用 pgadmin4 建立 Database

先安裝 pgadmin4,

$ brew cask install pgadmin4

安裝完開啟 pgadmin4 設定 DB 連線,

https://ithelp.ithome.com.tw/upload/images/20191008/20119807iRkFoAn8gr.png

https://ithelp.ithome.com.tw/upload/images/20191008/20119807SIDQW5kIni.png

並且新增 Database 命名為 diesel_demo,

https://ithelp.ithome.com.tw/upload/images/20191008/20119807Wvxsnboyin.png

完成!

安裝 diesel 環境

首先新增依賴(除了新增專案外最熟的部分XD

[dependencies]
diesel = { version = "1.0.0", features = ["postgres"] }
dotenv = "0.9.0"

接著安裝 diesel cli 工具,

$ cargo install diesel_cli

然後回到我們的專案根目錄新增一個 .env 的檔案,裡面用來設定 DB 連線位址

$ echo DATABASE_URL=postgres://username:password@localhost/diesel_demo > .env

Note: 這邊記得要改成自己的 username 和 password

接著同樣在根目錄執行 setup 指令讓剛剛安裝的 cli 工具發威,

diesel setup

接著我們要來做一個簡單的 blog 先讓我們新增一個 migration,

$ diesel migration generate create_posts

migration 的作用似乎是讓你可以復原對 DB 所做的操作,這邊我是很不確定所以就把官方文件貼上來了,

Migrations allow us to evolve the database schema over time. Each migration can be applied (up.sql) or reverted (down.sql). Applying and immediately reverting a migration should leave your database schema unchanged.

然後來編輯一下剛剛產生的 sql 檔案

migrations/2019-10-07-143035_create_posts/up.sql

CREATE TABLE posts (
  id SERIAL PRIMARY KEY,
  title VARCHAR NOT NULL,
  body TEXT NOT NULL,
  published BOOLEAN NOT NULL DEFAULT 'f'
)

migrations/2019-10-07-143035_create_posts/down.sql

DROP TABLE posts

完成之後執行 migration run

diesel migration run

若是要復原的話就用 migration redo 指令

diesel migration redo

redo 就會執行 down.sql 來復原 DB,所以 down.sql 要確保是正確的。

如果一切順利的話應該會長這樣,

https://ithelp.ithome.com.tw/upload/images/20191007/20119807QcWZO1armw.png

建立 DB 連線

接著我們就可以開始來寫一點程式了,首先要先建立 DB 的連線,

src/lib.rs

pub mod schema;
pub mod models;

#[macro_use]
extern crate diesel;
extern crate dotenv;

use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok();

    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}

接著新增 Struct 定義 Post 格式,

src/models.rs

#[derive(Queryable)]
pub struct Post {
    pub id: i32,
    pub title: String,
    pub body: String,
    pub published: bool,
}

另外確認一下有沒有 schema.rs,這個檔案在 setup 的時候應該就會自動產生。

src/schema.rs

table! {
    posts (id) {
        id -> Int4,
        title -> Varchar,
        body -> Text,
        published -> Bool,
    }
}

最後就是新增一個呼叫的程式拉,

src/bin/show_posts.rs

extern crate diesel;
extern crate actixweb;

use actixweb::*;
use self::models::*;
use diesel::prelude::*;

fn main() {
    // establish db connection
    use self::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

完成!到目前為止的結果,

https://ithelp.ithome.com.tw/upload/images/20191007/20119807EO36NUaImv.png

由於我們還沒有資料所以這樣是正常的。

總結

今天的內容還蠻多的,應該還要再一天把它完成,那今天就先告一個段落我們明天繼續!

最後一樣有問題歡迎發問

/images/emoticon/emoticon07.gif

參考連結

actix-todo
benchmarks
actix docs


上一篇
[Day 20] Rust FFI with Python
下一篇
[Day 22] Rust Actix PART3
系列文
WebAssembly + Rust 的前端應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言