チュートリアル
インストール
# cargo install senax
初期ファイル生成
$ senax init example
$ cd example
Actix サーバ生成
$ senax new-actix server
serverの部分は任意のパッケージ名で、actix-web を使用したWebサーバを生成します。
生成されるコードはSIGUSR2シグナルによるホットデプロイに対応しています。
DBテンプレート生成
$ senax use-db server data
スキーマに data.yml を生成し、先程の server パッケージに data というDBを使用することを設定します。
envファイル
$ cp .env.sample .env
.env の SESSION_DB_URL, DATA_DB_URL などのDB設定を必要に応じて変更してください。
スキーマファイル
スキーマファイルは基本的には設定ファイルとグループごとのモデル記述ファイルに分けて記述します。 シンプルなケースでは設定ファイル内にまとめて記述することもできます。
下記にチュートリアル用の内容を記述していますので、該当するファイルを修正してください。
schema/data.yml
# yaml-language-server: $schema=https://github.com/yossyX/senax/releases/download/0.2.0/schema.json#definitions/ConfigDef
title: Example DB
author: author name
db: mysql
ignore_foreign_key: true
timestampable: real_time
time_zone: local
timestamp_time_zone: utc
tx_isolation: read_committed
read_tx_isolation: repeatable_read
use_cache: true
use_cache_all: true
preserve_column_order: false
groups:
note:
type: model
複数のデータベース設定のための設定ファイルです。
yaml-language-serverの設定はVSCodeであればYAMLのプラグインを使用することにより、入力時にアシスタントが効きます。
db は現在のところ MySQL しか対応していません。
schema/data/note.yml に下記のファイルを作成してください。
# yaml-language-server: $schema=https://github.com/yossyX/senax/releases/download/0.2.0/schema.json#properties/model
note:
timestampable: fixed_time
soft_delete: time
versioned: true
on_delete_fn: true
use_fast_cache: true
columns:
id:
type: int
primary: true
auto_increment: auto
key: varchar
category_id: int
content:
type: text
length: 2000
not_null: true
relations:
category:
use_cache: true
tags:
type: many
in_cache: true
indexes:
key:
fields:
key:
length: 20
type: unique
content:
type: fulltext
parser: ngram
tag:
columns:
id:
type: int
primary: true
auto_increment: auto
note_id: int_not_null
name: varchar_not_null
relations:
note:
category:
columns:
id:
type: int
primary: true
auto_increment: auto
name: varchar_not_null
counter:
counting: counter
timestampable: none
use_save_delayed: true
columns:
note_id:
type: int
primary: true
date:
type: date
primary: true
counter: bigint_not_null
relations:
note:
グループ内のテーブルの設定ファイルです。
note モデルは tag と紐づいていますが、 relations の type が many では複数形の tags で自動的に紐づきます。
モデル生成
senaxのコマンドで db/data, db/session 下にクレートを生成します。
$ senax model data
$ senax model session
生成されたファイルの note.rs 等はカスタマイズ用で、 _note.rs が本体です。再度モデル生成を実行すると note.rs は上書きされず、 _note.rs は常に上書きされます。
モデル名が _Tags のようにアンダースコアが付くのは他のライブラリと被らないようにそのような命名規則になっています。
Docker での開発環境で VS Code 等を使用していてモデルの再生成後に変更が反映されていない場合は、ウィンドウの再読み込みや生成されたモデルのファイルを VS Code 上で再度保存するなどしてモデルの更新を VS Code に認識させる必要があります。
マイグレーション生成
db/data/migrations下にDDLファイルを生成します。
$ senax gen-migrate data init
$ senax gen-migrate session init
スキーマファイルを修正して再実行すると現状のDBを確認して差分のDDLを出力します。
DB仕様書の更新履歴出力のためにコメント部分に更新内容が出力されています。コメントの追加や不要な更新内容を削除して仕様書に出力される更新内容を変更することができます。
※ テーブル名はデフォルトでグループ名とモデル名を結合した名前になります。変更する場合はスキーマで table_name を指定してください。 また、 plural_table_name の設定でテーブル名を複数形で生成することが出来ます。
マイグレーション実行
$ cargo run -p db_data -- migrate -c
$ cargo run -p db_session -- migrate -c
sqlxのマイグレーションを実行します。
シャーディング設定がある場合すべてのシャードにクエリーを発行するようになっています。
-c はクリーンマイグレーションで、DBを作成してからマイグレーションを実行します。
シードスキーマ生成
$ cargo run -p db_data -- gen-seed-schema > db/data/seed-schema.json
もしくは、
$ target/debug/db_data gen-seed-schema > db/data/seed-schema.json
シードファイルの入力アシスタントのためのスキーマファイルを生成します。
シードファイル生成
$ senax gen-seed data init
シードファイルもマイグレーションと同様にDBに登録済みかのバージョン管理を行いますので、コマンドでシードファイルを生成します。
シードファイル記述
db/data/seeds/20221120120831_init.yml (数値部分は生成日時によって変化します。)
# yaml-language-server: $schema=../seed-schema.json
note:
category:
diary:
name: diary
note:
note_1:
category_id: diary
key: diary
content: content
tag:
tag_1:
note_id: note_1
name: tag 1
category_id の「diary」と note_id の「note_1」はそれぞれ category と note が生成されたときのオートインクリメントで登録された ID が渡されるようになっています。
シードデータ投入
$ cargo run -p db_data -- seed
DBテーブル定義書生成
DB仕様書のER図出力のために graphviz が必要となります。
# apt-get install graphviz
$ senax gen-db-doc data -e -H 10 > db-document.html
環境変数のLC_ALL, LC_TIME, LANGの設定により日本語の定義書を生成します。 "-e"はER図出力、"-H 10"は仕様書更新履歴を10件分出力します。
コード記述
_Noteを取得して日毎のカウンターを加算しています。 save_delayed ではこの処理が終わった後で同一の更新対象をまとめてaddの内容を加算して更新します。その更新内容をキャッシュに反映して他のサーバにも伝達します。
server/src/routes/api/cache.rs
#![allow(unused)] fn main() { #[get("/cache/{key}")] async fn handler( key: web::Path<String>, http_req: HttpRequest, session: Session<_SessionStore>, ) -> impl Responder { let ctx = get_ctx_and_log(&http_req); let result = async move { // セッションの更新例 let session_count = session .update(|s| { let v: Option<u64> = s.get_from_base(SESSION_KEY)?; match v { Some(mut v) => { v += 1; s.insert_to_base(SESSION_KEY, v)?; Ok(v) } None => { s.insert_to_base(SESSION_KEY, 1)?; Ok(1) } } }) .await?; let mut conn = DataConn::new(); let mut note = _Note::find_by_key_from_cache(&conn, &*key) // ユニークキーからのキャシュ取得 .await .with_context(|| NotFound::new(&http_req))?; note.fetch_category(&mut conn).await?; // これもキャシュから取得 let category = match note.category() { None => None, Some(v) => Some(v.name().to_owned()), }; let date = Local::now().date_naive(); let counter = _Counter::find_optional_from_cache(&conn, (note.id(), date)).await?; let count = counter.map(|v| v.counter()).unwrap_or_default() + 1; // ここまではキャッシュから当日のカウント取得 let mut counter_for_update = _CounterFactory { // 当日のカウントが未登録の場合 INSERT note_id: note.id(), date, counter: 0, } .create(&conn); let _ = counter_for_update.counter().add(1); // UPDATE加算 counter_for_update._upsert(); // INSERT ... ON DUPLICATE KEY UPDATE の指示 _Counter::update_delayed(&mut conn, counter_for_update).await?; Ok(Response { id: note.id(), category, article: note.content().to_string(), tags: note.tags().iter().map(|v| v.name().to_string()).collect(), count, }) } .await; json_response(result, &ctx) } }
キャッシュを使用しないバージョンです。
server/src/routes/api/no_cache.rs
#![allow(unused)] fn main() { #[get("/no_cache/{key}")] async fn handler( key: web::Path<String>, http_req: HttpRequest, session: Session<_SessionStore>, ) -> impl Responder { let ctx = get_ctx_and_log(&http_req); let result = async move { let mut conn = DataConn::new(); let mut note = _Note::find_by_key(&mut conn, &*key) .await .with_context(|| NotFound::new(&http_req))?; note.fetch_category(&mut conn).await?; note.fetch_tags(&mut conn).await?; let category = match note.category() { None => None, Some(v) => Some(v.name().to_owned()), }; let date = Local::now().date_naive(); let counter = _Counter::find_optional(&mut conn, (note.id(), date)).await?; let count = counter.map(|v| v.counter()).unwrap_or_default() + 1; let note_id = note.id(); let cond = db_data::cond_note_counter!((note_id=note_id) AND (date=date)); // WHERE句を生成するマクロ conn.begin().await?; let mut update = _Counter::for_update(&mut conn); // 更新内容を指定するための空のUpdate用オブジェクト生成 let _ = update.counter().add(1); _Counter::query() .cond(cond) .update(&mut conn, update) .await?; conn.commit().await?; Ok(Response { id: note.id(), category, article: note.content().to_string(), tags: note.tags().iter().map(|v| v.name().to_string()).collect(), count, }) } .await; json_response(result, &ctx) } }
この例ではWHERE句の使用例のため、UPDATEクエリーで記述しています。
また、簡略化のため当日のカウンターが未登録の場合を考慮していません。
upsert を使用すれば未登録の場合は登録、すでに登録されている場合は更新を実行できます。
サーバ起動
$ cargo run -p server
http://localhost:8080/api/cache/diary にアクセスして結果を確認できます。
本番環境ではリリースモードでビルドして起動します。
$ cargo build -p server -r
$ target/release/server
s