セットアップ

このページではaxumでデータベースを使うための準備をします。

ライブラリの導入

まず、データベースを取り扱うにあたって、いくつか便利なクレートを追加します。

Cargo.toml
[package]
name = "rustwi"
version = "0.1.0"
edition = "2018"

[dependencies]
dotenv = "0.15"  # 追加
axum = "0.3"
tokio = { version = "1.0", features = ["full"] }
askama = "0.10"
tower-http = { version = "0.1", features = ["fs"] }
serde = { version = "1.0", features = ["derive"] }
chrono = { version = "0.4", features = ["serde"] }  # 追加
bb8 = "0.7.1"  # 追加
bb8-postgres = "0.7.0"  # 追加
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] }  # 追加

各クレートの役割は下記のとおりです。

  • dotenv
    • 環境変数をファイルから読み込むユーティリティ
  • chrono
    • 日付と時間を取り扱うユーティリティ
  • bb8
    • 非同期処理に対応したデータベースユーティリティ
  • bb8-postgres
    • bb8でPostgreSQLを取り扱うためのドライバー
  • tokio-postgres
    • PostgreSQLを非同期で取り扱うためのユーティリティ

データベース接続の実装

続いて、データベース接続部分を実装します。といっても、開発者が行うことはコネクションプール1を作成するだけです。

use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use std::env;
use tokio_postgres::NoTls;

type ConnectionPool = Pool<PostgresConnectionManager<NoTls>>;

async fn establish_connection() -> ConnectionPool {
    dotenv::dotenv().ok();
    let manager =
        PostgresConnectionManager::new_from_stringlike(env::var("DATABASE_URL").unwrap(), NoTls)
            .unwrap();
    Pool::builder().build(manager).await.unwrap()
}

fn main() {
    let pool = establish_connection();
    let conn = pool.get().await.unwrap();
    let rows = conn.query("SELECT 1", &[]).await.unwrap();
}

コネクションプールには、接続情報を管理するトレイト ManageConnection を実装した構造体を渡します。今回はPostgreSQLを使用するので、 PostgresConnectionManager を使います。

PostgresConnectionManager::new_from_stringlike(str: &str) を使って、データベース接続情報からインスタンスを生成します。データベース接続情報は .env ファイルに書いておき、これを読み込むことにします。こうすることで、ローカル環境では環境設定を設定せずに .env を利用し、本番環境では直接設定した環境変数を利用することができます。

.env
DATABASE_URL=postgres://[username]:[password]@[address]:[port]/[database]

下記の部分は実際の内容に書き換えてください。

  • [username]: DBユーザー名
  • [password]: DBユーザーのパスワード
  • [address]: DBの接続先アドレス( localhost など)
  • [port]: DBの接続先ポート(通常は5432)
  • [database]: DBのデータベース名

dotenv::dotenv().env ファイルがある場合にファイルの内容を環境変数として読み込みます。環境変数は std::env::var(key: &str) で読み込むことができます。

axumでのコネクションプールの利用

コネクションプールをaxumで使えるようにします。状態の共有には ExtensionLayer という仕組みを使います。ExtensionLayerを設定すると、各handlerで引数として受け取って利用することができます。

ExtensionLayerを生成する関数を作成してみましょう。

src/database.rs
use bb8::Pool;
use bb8_postgres::PostgresConnectionManager;
use std::env;
use tokio_postgres::NoTls;
use tower_http::add_extension::AddExtensionLayer;

pub type ConnectionPool = Pool<PostgresConnectionManager<NoTls>>;

pub async fn init() -> AddExtensionLayer<ConnectionPool> {
    dotenv::dotenv().ok();
    let manager =
        PostgresConnectionManager::new_from_stringlike(env::var("DATABASE_URL").unwrap(), NoTls)
            .unwrap();
    let pool = Pool::builder().build(manager).await.unwrap();
    AddExtensionLayer::new(pool)
}

ExtensionLayerを公開し、 main.rs でインスタンス化したRouterに登録します。

src/lib.rs
mod controllers;
mod database;
mod entities;
mod forms;
mod views;

pub use controllers::routes;
pub use database::init as init_database;
src/main.rs
use std::net::SocketAddr;

#[tokio::main]
async fn main() {
    let pool = rustwi::init_database().await;
    let routes = rustwi::routes().layer(pool);  // コネクションプールを持つExtensionLayerをlayerメソッドで追加する
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(routes.into_make_service())
        .await
        .unwrap();
}

コネクションプールの利用

これでハンドラーでコネクションプールを利用する準備ができました。 ハンドラーからコネクションプールを使ってデータベースからデータを取り出してみます。

事前に、PostgreSQLに下記のSQLを流してテーブルを作成しておいてください。

CREATE TABLE tweets(
    id        serial      primary key, 
    name      text        not null,
    message   text        not null,
    posted_at timestamptz not null
);
INSERT INTO tweets VALUES (1, '太郎', 'こんにちは', now());

ツイート一覧を表示する部分で、データベースからツイートを取得します。

src/controllers/root.rs
use crate::database::ConnectionPool;
use crate::entities::Tweet;
use crate::views::Index;
use axum::extract::Extension;
use axum::response::IntoResponse;
use axum::routing;
use axum::Router;
use chrono::{DateTime, Utc};

pub fn root() -> Router {
    Router::new().route("/", routing::get(get))
}

async fn get(Extension(pool): Extension<ConnectionPool>) -> impl IntoResponse {  // 引数でExtensionLaterを受け取る
    let conn = pool.get().await.unwrap();  // コネクションプールからコネクションを取得する
    let rows = conn.query("SELECT * FROM tweets", &[]).await.unwrap();  // コネクションを使ってSQLを発行する
    let tweets = rows
        .into_iter()
        .map(|r| {
            Tweet::new(
                r.get("name"),
                r.get("message"),
                r.get::<&str, DateTime<Utc>>("posted_at")
                    .format("%F")
                    .to_string(),
            )
        })
        .collect();
    Index::new(tweets).into_html()
}

ツイート一覧にアクセスすることで、先ほど発行したSQLで挿入されたデータが表示されるようになります。


1

データベースからデータを取得する手順として、データベースに接続し、クエリを実行し、終われば切断します。しかし、データベースにアクセスする度にこの手順を行っていてはデータベースに負荷がかかってしまうので、一般的にはクエリ実行したあとも接続を維持しておき、再度データベースにアクセスするときに、維持しておいた接続を再利用する方法がとられます。これをコネクションプーリングといい、このとき接続を維持しておく領域をコネクションプールといいます。