2022/09/21
[Rust]TiDBを使ったサンプルアプリケーションの実装
概要
TiDBはMySQL互換のNewSQLです。
個人的に以下の点で気に入って導入を決めました。
- 大量のDBへのWriteが発生する大規模アプリにて、DBも水平方向でスケールできる。
- (Readももちろんスケールできますが、すごいパフォーマンスが良いわけではないようなので、超高速なReadを行う場合は、別のDBサーバーをたてたりする方が良さそうでした。)
- MySQLなどの(標準的な)SQLをサポートできる。
- MySQLコネクターがあれば、どんな言語からも接続できる。(弊社は主にRustかPython)
- k8sへのデプロイが(公式サイトで)サポートされている。
導入を決めるにあたって、実際のアプリケーションで想定された使い方をイメージしながらRustでサンプリアプリケーションを実装してみました。
公式のGo言語で実装されたサンプリアプリをRustで書き直しました。
Rustでは主に以下を実装しました。
- コマンドラインアプリ 👈
clap = "3.2"
を利用しています。 - Http Restful APIサーバー 👈
actix-web = "4.2.1"
を利用しています。
- どちらもSQL driver部分は、dieselというライブラリを利用しています。
- 比較的clean architectureになるように気をつけています。
事前準備
ローカルでTiDBをdocker-compose
を使って立ち上げておきます。
% git clone [email protected]:pingcap/tidb-docker-compose.git
% cd tidb-docker-compose
% docker-compose up
これだけでOKでした。
接続するには、以下で接続できました。
% mysql -h 127.0.0.1 -P 4000 -u root
Disesel Setup
まず以下のコマンドを実行します。(事前にdiesel_cliのインストールが必要です。)
# 環境変数を設定しておきます。(direnvなどを使うと便利です。)
% DATABASE_URL="mysql://root:@127.0.0.1:4000/test?charset=utf8mb4"
% export DATABASE_URL
# 初期設定
% diesel setup
# migrationファイルを作成します。
% diesel migration generate init
- up.sqlに記載します。
CREATE TABLE IF NOT EXISTS player
(
id VARCHAR(36),
coins INTEGER,
goods INTEGER,
PRIMARY KEY (id)
);
- down.sqlに記載します。
DROP TABLE IF EXISTS player;
# migrationを実行します。
% diesel migration run
TiDB接続部分
基本的にはMySQLと同じなので、diesel
& mysql
みたいな感じで検索して実装しました。
最初にDTOを準備します。
use crate::schema::player;
#[derive(Queryable, QueryableByName, Debug)]
#[diesel(table_name = player)]
pub struct Player {
pub id: String,
pub coins: Option<i32>,
pub goods: Option<i32>,
}
#[derive(Insertable, Debug)]
#[diesel(table_name = player)]
pub struct NewPlayer<'a> {
pub id: &'a str,
pub coins: Option<i32>,
pub goods: Option<i32>,
}
次に、Data adapter部分の実装です。
use crate::entities::{NewPlayer, Player};
use crate::schema::player;
use crate::Result;
use diesel::prelude::*;
use diesel::result::Error;
use diesel::{QueryDsl, RunQueryDsl};
pub type StoreConnection = diesel::mysql::MysqlConnection;
pub fn create(conn: &mut StoreConnection, entity: NewPlayer) -> Result<usize> {
diesel::insert_into(player::table)
.values(&entity)
.execute(conn)
.map_err(Into::into)
}
pub fn bulk_insert(conn: &mut StoreConnection, entities: &Vec<NewPlayer>) -> Result<usize> {
diesel::insert_into(player::table)
.values(entities)
.execute(conn)
.map_err(Into::into)
}
pub fn update(conn: &mut StoreConnection, coins: i32, goods: i32, id: &str) -> Result<usize> {
diesel::update(player::dsl::player.find(id))
.set((player::coins.eq(coins), player::goods.eq(goods)))
.execute(conn)
.map_err(Into::into)
}
pub fn get(conn: &mut StoreConnection, id: &str) -> Result<Option<Player>> {
let result = player::table.find(id).first::<Player>(conn);
match result {
Ok(entity) => Ok(Some(entity)),
Err(err) => match err {
Error::NotFound => Ok(None),
_ => Err(err.into()),
},
}
}
pub fn count(conn: &mut StoreConnection) -> Result<i64> {
player::table.count().get_result(conn).map_err(Into::into)
}
pub fn select_for_update(conn: &mut StoreConnection, id: &str) -> Result<Player> {
player::table
.find(id)
.for_update()
.first::<Player>(conn)
.map_err(Into::into)
}
pub fn gets_by_limit(conn: &mut StoreConnection, limit: i64) -> Result<Vec<Player>> {
player::table
.limit(limit)
.load::<Player>(conn)
.map_err(Into::into)
}
実際のアプリ
実際のアプリはgithub.com/kumanote/tidb-example-rsに公開しています。
% git clone [email protected]:kumanote/tidb-example-rs.git
% cd tidb-example-rs
# リリースビルドします。
% cargo build --release
# コマンドラインアプリケーションを実行する場合
% ./target/release/tidb-example-cmd
getPlayer: Some(Player { id: "test", coins: Some(1), goods: Some(1) })
countPlayers: 1920
print 1 player: Player { id: "test", coins: Some(1), goods: Some(1) }
print 2 player: Player { id: "16e8539a-58bf-4259-9197-57b2967ade75", coins: Some(10000), goods: Some(10000) }
print 3 player: Player { id: "ecb60313-e851-4a5d-ad39-bbf899c5f082", coins: Some(10000), goods: Some(10000) }
buyGoods:
=> this trade will fail
buyGoods:
=> this trade will success
[buyGoods]:
'trade success'
# Restful APIサーバーを起動する場合は以下で実行できます。
# 8080がtidbのdocker-composeと競合するので、8000番を指定して起動しました。
% ./target/release/tidb-example-server -a 0.0.0.0:8000
# 起動後以下を実行すると、レスポンスが返ってきます。
% curl --location --request GET "http://localhost:8000/player/limit/3"
[{"coins":100,"goods":20,"id":"2e86517c-5cc2-4148-ac61-45bac68ad558"},{"coins":100,"goods":20,"id":"8509a96c-fa93-43aa-a0cd-1648bb24fe80"},{"coins":100,"goods":20,"id":"a48dccad-12b8-49c3-b040-18ec3ba237cb"}]
以上です。
関連する記事
Concordiumノードをローカルで動かしてみた
Concordiumの調査のために、ローカルでソースコードをビルドしてノードを動かしてみました
[小〜中規模向け]GKEにTiDBをデプロイする
MySQL互換のNewSQLであるTiDBをGKEにデプロイしてみました。
[Rust]axumとdragonflyを使ったWebsocket Chatのサンプル実装
redis互換のdragonflyをPUBSUBとして利用して、Websocket Chatアプリのサンプル実装を行いました。
[Rust]TiDBを使ったサンプルアプリケーションの実装
RustからTiDBを使ったアプリケーションの実装を行いました。