Web框架常會搭配資料庫一起使用,Rocket框架當然也有一定程度的資料庫支援。雖然我們可以直接使用Rocket框架提供的應用程式狀態機制來註冊任意套件提供的資料庫實體,但是在程式撰寫上會比較麻煩一點,像是資料庫的位址、帳密等資訊,我們可能就必須要寫死在程式碼內,或是自己再另外開發出可以讓Rocket框架從外部讀取資料庫設定值的功能。Rocket官方提供的rocket_db_pools套件,能替Rocket框架添加資料庫支援,可以快速地將資料庫實體變成請求守衛來取用,也可以將資料庫的設定融入至Rocket框架的設定中。



rocket_db_pools套件根據不同的資料庫類種和驅動程式,分出不同的特色,並提供不同的型別來支援它們。在應用時,設定檔和程式碼的基本架構如下:

rocket_db_pools = { version = "0.1.0", features = ["資料庫特色"] }
[default.databases.名稱]
url = "資料庫URL"
#[macro_use]
extern crate rocket;

use rocket_db_pools::{Connection, Database};

#[derive(Database)]
#[database("名稱")]
struct 資料庫結構體(資料庫Pool型別);

#[launch]
fn rocket() -> _ {
    rocket::build().attach(資料庫結構體::init())
}

在路由處理程序中可以使用rocket_db_pools::Connection結構體來包裹我們的資料庫結構體作為請求守衛。如下:

#[HTTP請求方法("路徑")]
fn handler(變數名稱: Connection<資料庫結構體>) -> 要回應的資料型別 { 
    處理請求的程式區塊
}

當HTTP請求成功交給上面的處理程序處理時,就會拿到資料庫結構體所包裹的Pool的資料庫連線。至於資料庫Pool型別具體到底會用到哪個型別,請參考下表:

資料庫種類 驅動程式 資料庫特色 資料庫型別
Postgres deadpool deadpool_postgres deadpool_postgres::Pool
Redis deadpool deadpool_redis deadpool_redis::Pool
Postgres sqlx sqlx_postgres sqlx::PgPool
MySQL sqlx sqlx_mysql sqlx::MySqlPool
SQLite sqlx sqlx_sqlite sqlx::SqlitePool
MongoDB mongodb mongodb mongodb::Client
Postgres diesel diesel_postgres diesel::PgPool
MySQL diesel diesel_mysql diesel::MysqlPool

實際演練

MySQL

目標:讓Rocket應用程式連接MySQL資料庫,並完成資料的存取。

首先建立新的MySQL資料庫和使用者。使用者名稱和資料庫名稱均為magic_rocket,使用者密碼為B+Mj~#mHE5W@

在一般的shell下執行:

mysql -u root -p

在MySQL的shell下執行以下SQL,來建立出資料庫和資料表:

CREATE DATABASE `magic_rocket`;

在MySQL的shell下執行以下SQL,來建立出可以存取剛才建立出來的資料庫的使用者:

CREATE USER 'magic_rocket'@'localhost' IDENTIFIED BY '4M{}^;dkn7Eg';
GRANT ALL ON `magic_rocket`.* TO 'magic_rocket'@'localhost';
# GRANT ALL PRIVILEGES ON `magic_rocket`.* TO 'magic_rocket'@'localhost' IDENTIFIED BY '4M{}^;dkn7Eg';

然後開始撰寫Rust程式專案吧!

[dependencies]
rocket = { version = "0.5.0" }
rocket_db_pools = { version = "0.1.0", features = ["sqlx_mysql"] }
[default.databases.example]
url = "mysql://magic_rocket:4M{}^;dkn7Eg@localhost:3306/magic_rocket"
#[macro_use]
extern crate rocket;

use rocket::fairing::AdHoc;
use rocket_db_pools::{
    sqlx::{self, Row},
    Connection, Database,
};

#[derive(Database)]
#[database("example")]
struct MyDatabase(sqlx::MySqlPool);

#[put("/<key>/<value>")]
async fn put(mut db: Connection<MyDatabase>, key: String, value: String) -> &'static str {
    sqlx::query("INSERT INTO `example` (`key`, `value`) VALUES (?, ?)")
        .bind(key)
        .bind(value)
        .execute(&mut **db)
        .await
        .unwrap();

    "ok"
}

#[get("/<key>")]
async fn get(mut db: Connection<MyDatabase>, key: String) -> Option<String> {
    let result = sqlx::query("SELECT `value` FROM `example` WHERE `key` = ?")
        .bind(key)
        .fetch_optional(&mut **db)
        .await
        .unwrap();

    result.map(|row| row.get("value"))
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(MyDatabase::init())
        .attach(AdHoc::try_on_ignite("MySQL Initializer", |rocket| {
            Box::pin(async move {
                let mut connection = match MyDatabase::fetch(&rocket) {
                    Some(pool) => match pool.acquire().await {
                        Ok(connection) => connection,
                        Err(_) => return Err(rocket),
                    },
                    None => return Err(rocket),
                };

                let result = sqlx::query(
                    "CREATE TABLE IF NOT EXISTS `example` ( `id` INT NOT NULL AUTO_INCREMENT, \
                     `key` varchar(100), `value` TEXT , PRIMARY KEY (`id`), UNIQUE KEY (`key`) )",
                )
                .execute(&mut *connection)
                .await;

                match result {
                    Ok(_) => Ok(rocket),
                    Err(_) => Err(rocket),
                }
            })
        }))
        .mount("/", routes![put, get])
}

以上的Rust程式,我們除了將MyDatabase的整流片註冊給Rocket之外,還另外再註冊了一個AdHoc整流片來初始化MySQL資料庫。

MongoDB

目標:讓Rocket應用程式連接MongoDB資料庫,並完成資料的存取。

使用的MongoDB資料庫資料庫名稱為magic_rocket,不必事先建立出來。

直接開始撰寫Rust程式專案吧!

[dependencies]
rocket = { version = "0.5.0" }
rocket_db_pools = { version = "0.1.0", features = ["mongodb"] }
[default.databases.example]
url = "mongodb://localhost:27017/magic_rocket"
#[macro_use]
extern crate rocket;

use rocket::fairing::AdHoc;
use rocket_db_pools::{
    mongodb::{
        self,
        bson::{doc, Bson, Document},
        options::IndexOptions,
        IndexModel,
    },
    Connection, Database,
};

#[derive(Database)]
#[database("example")]
struct MyDatabase(mongodb::Client);

#[put("/<key>/<value>")]
async fn put(db: Connection<MyDatabase>, key: String, value: String) -> &'static str {
    let db = db.default_database().unwrap();

    let collection_example = db.collection("example");

    collection_example.insert_one(doc! {"key": key, "value": value}, None).await.unwrap();

    "ok"
}

#[get("/<key>")]
async fn get(db: Connection<MyDatabase>, key: String) -> Option<String> {
    let db = db.default_database().unwrap();

    let collection_example = db.collection("example");

    let result: Option<Document> =
        collection_example.find_one(Some(doc! {"key": key}), None).await.unwrap();

    match result {
        Some(mut doc) => {
            if let Bson::String(value) = doc.remove("value").unwrap() {
                Some(value)
            } else {
                panic!("Value should be a string!")
            }
        },
        None => None,
    }
}

#[launch]
fn rocket() -> _ {
    rocket::build()
        .attach(MyDatabase::init())
        .attach(AdHoc::try_on_ignite("MongoDB Initializer", |rocket| {
            Box::pin(async move {
                let db = match MyDatabase::fetch(&rocket) {
                    Some(client) => match client.default_database() {
                        Some(database) => database,
                        None => return Err(rocket),
                    },
                    None => return Err(rocket),
                };

                let collection_example = db.collection::<Document>("example");

                let result = collection_example
                    .create_index(
                        {
                            let mut index = IndexModel::default();

                            let mut options = IndexOptions::default();

                            options.unique = Some(true);

                            index.keys = doc! {"key": 1};
                            index.options = Some(options);

                            index
                        },
                        None,
                    )
                    .await;

                match result {
                    Ok(_) => Ok(rocket),
                    Err(_) => Err(rocket),
                }
            })
        }))
        .mount("/", routes![put, get])
}

同樣地,以上的Rust程式,我們除了將MyDatabase的整流片註冊給Rocket之外,還另外再註冊了一個AdHoc整流片來初始化MongoDB資料庫。

總結

這個章節介紹了在Rocket框架存取資料庫的方式,也使我們更能體會Rocket框架的整流片以及請求守衛究竟是有多麼便利。結合了資料庫的Rocket應用程式,就可以拿來實現更為龐大的Web架構,一旦商業邏輯愈來愈複雜,就會使得程式開發人員犯錯的機率增高,所以下一章我們要學習撰寫測試(testing),來自動驗證程式邏輯的正確性。

最後,除了rocket_db_pools套件之外,Rocket官方其實還有提供rocket_sync_db_pools套件,它支援採用阻塞式I/O的資料庫驅動程式,如今不是很建議使用了。但rocket_sync_db_pools套件有支援rocket_db_pools套件所沒支援的資料庫,如果真的有需要的話,還是可以考慮將其加入專案中使用,不過本系列文章就不提這個套件的用法了。

下一章:測試