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



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

[dependencies.rocket_contrib]
version = "0.4"
default-features = false
features = ["資料庫特色"]
[global.databases]
名稱 = { url = "資料庫URL", pool_size = 資料庫連接數 }

以上的pool_size設定項目是可以省略的,預設值為Rocket框架的workers設定項目的值。

extern crate rocket;

#[macro_use]
extern crate rocket_contrib;

use 資料庫型別;

#[database("名稱")]
struct 資料庫請求守衛名稱(資料庫型別);

fn main() {
    rocket::ignite()
        .attach(資料庫請求守衛名稱::fairing())
        .launch();
}

在路由處理程序中使用資料庫請求守衛的用法就如同一般的請求守衛。

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

資料庫請求守衛要怎麼用?它其實就是資料庫型別實體的智慧型指標,所以就跟操作資料庫型別的實體是一樣的用法。資料庫型別為rocket_contrib::databases模組下的某個型別,至於具體到底會用到哪個型別,請參考下表:

資料庫種類 驅動程式 資料庫特色 資料庫型別
MySQL Diesel diesel_mysql_pool diesel::MysqlConnection
MySQL mysql mysql_pool mysql::Conn
Postgres Diesel diesel_postgres_pool diesel::PgConnection
Postgres postgres postgres_pool postgres::Connection
Sqlite Diesel diesel_sqlite_pool diesel::SqliteConnection
Sqlite rusqlite sqlite_pool rusqlite::Connection
Neo4j rusted_cypher cypher_pool rusted_cypher::GraphClient
Redis redis redis_pool redis::Connection
MongoDB mongodb mongodb_pool mongodb::db::Database
Memcache memcache memcache_pool memcache::Client

實際演練

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_contrib]
version = "0.4"
default-features = false
features = ["mysql_pool"]
[global.databases]
example = { url = "mysql://magic_rocket:4M{}^;dkn7Eg@localhost:3306/magic_rocket" }
#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;

use rocket::fairing::AdHoc;

use rocket_contrib::databases::mysql::Conn;

#[database("example")]
struct MyDatabase(Conn);

#[put("/<key>/<value>")]
fn put(mut db: MyDatabase, key: String, value: String) -> &'static str {
    db.prep_exec(r"INSERT INTO `example` (`key`, `value`) VALUES (?, ?)", (key, value)).unwrap();

    "ok"
}

#[get("/<key>")]
fn get(mut db: MyDatabase, key: String) -> Option<String> {
    let db: &mut Conn = &mut *db;
    let mut result = db.prep_exec(r"SELECT `value` FROM `example` WHERE `key` = ?", (key, )).unwrap();

    match result.next() {
        Some(row) => {
            Some(row.unwrap().take("value").unwrap())
        }
        None => {
            None
        }
    }
}

fn main() {
    rocket::ignite()
        .attach(MyDatabase::fairing())
        .attach(AdHoc::on_attach("MySQL Initializer", |rocket| {
            let mut db: MyDatabase = MyDatabase::get_one(&rocket).unwrap();

            db.query(r"CREATE TABLE IF NOT EXISTS `example` ( `id` INT NOT NULL AUTO_INCREMENT, `key` varchar(100), `value` TEXT , PRIMARY KEY (`id`), UNIQUE KEY (`key`) )").unwrap();

            Ok(rocket)
        }))
        .mount("/", routes![put, get])
        .launch();
}

以上的Rust程式,我們除了將MyDatabase的整流片註冊給Rocket之外,還另外再註冊了一個AdHoc整流片來初始化MySQL資料庫。另外在程式碼第24行,我們先將MyDatabase實體解參考後再重新取得Conn的參考,這是為了要讓Rust程式專案的IDE知道db到底是指到哪個實體啦!這樣它比較能夠正常地檢查型別,程式碼才會好寫、好閱讀。

MongoDB

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

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

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

[dependencies.rocket_contrib]
version = "0.4"
default-features = false
features = ["mongodb_pool"]
[global.databases]
example = { url = "mongodb://localhost:27017/magic_rocket" }
#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use]
extern crate rocket;
#[macro_use]
extern crate rocket_contrib;

#[macro_use]
extern crate mongodb;

use rocket::fairing::AdHoc;

use rocket_contrib::databases::mongodb::db::{Database, ThreadedDatabase};
use rocket_contrib::databases::mongodb::coll::{Collection, options::IndexOptions};

use mongodb::{Bson, Document};

#[database("example")]
struct MyDatabase(Database);

#[put("/<key>/<value>")]
fn put(db: MyDatabase, key: String, value: String) -> &'static str {
    let collection_example: Collection = db.collection("example");

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

    "ok"
}

#[get("/<key>")]
fn get(db: MyDatabase, key: String) -> Option<String> {
    let collection_example: Collection = db.collection("example");

    let result: Option<Document> = collection_example.find_one(Some(doc! {"key": key}), None).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
        }
    }
}

fn main() {
    rocket::ignite()
        .attach(MyDatabase::fairing())
        .attach(AdHoc::on_attach("MongoDB Initializer", |rocket| {
            let db: MyDatabase = MyDatabase::get_one(&rocket).unwrap();

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

            collection_example.create_index(doc! {"key": 1}, Some({
                let mut options = IndexOptions::new();

                options.unique = Some(true);

                options
            })).unwrap();

            Ok(rocket)
        }))
        .mount("/", routes![put, get])
        .launch();
}

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

總結

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

下一章:測試