- 作ったもの
- Tips
- 気になっていること
RustでGraphQLアプリを構築して、気づいたことや感想まとめ
作ったもの
https://github.com/vraisamis/graphql-rust-sample
- Kanban風のデータを取得できるアプリ
- GraphQLアプリ
- Queryのみ実装済みで、Mutation, Subscriptionは未実装
- QueryではDataLoaderを実装
- 言語: Rust 1.71.0
- ライブラリ(主要)
- サーバー: axum 0.6.19
- GraphQL: async-graphql 6.0.0
- DI: shaku 0.6.1
- SQL: sqlx 0.7.2
- データストア
- Postgres 16.0
- dynamodb on localstack 2.3 (未使用)
- オニオンアーキテクチャ
- DDD(ドメイン駆動設計)
- とはいえ、クエリしか作っていないのでIDくらいしか活用していない
ドメイン
だいたいこんな。未実装箇所もある https://github.com/vraisamis/graphql-rust-sample#domain
レイヤー
だいたいこんな。 https://github.com/vraisamis/graphql-rust-sample#layer
やっていないこと(完了していないこと)
- テストの実装
- Mutation, Subscription
- ドメインの実装
- dynamodb
- CI構築
Tips
Rust: 言語機能
Rustの言語機能関連
Trait alias
何がしたかったか
shaku
のモジュール M
が Box<dyn UsersQuery>
をprovideできるとき、 M: HasProvider<dyn UsersQuery>
を満たしている。
M
の具体型は実装レイヤで決まるので、実装に関与しないレイヤでは抽象的に扱いたい。
しかしこのとき、 M
の指定が大変なことになりやすい。
fn foo<M>(m: &M) where M: HasProvider<dyn UsersQuery>, M: HasProvider<dyn BoardQuery>, M: HasProvider<dyn ColumnsQuery>, M: HasProvider<dyn CardsQuery>, // ... { todo!() }
これを解消するために直感的には type
でエイリアスを定義したい。
// これはできない type MyFuncTrait = HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>;
これはできない。
対策
SEE: https://zenn.dev/ttttkkkkk/articles/e213d91ef7b47a
上の記事に全部書いてあるが……以下をやる。
- すべてを前提とするtrait定義
- すべての前提traitを実装している型に対して全称実装
// trait定義 pub trait QueryProvider where Self: HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>, { } // すべての前提traitを実装している型に対して全称実装 impl<T> QueryProvider for T where Self: HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery>, { }
ちなみに、この用途だと FooQuery
が増えるたびに2箇所に追記していくことになる。マクロで解決できないかと思ったが、構文エラーになって駄目だった。
// これはできない macro_rules! queries { () => { HasProvider<dyn UsersQuery> + HasProvider<dyn BoardQuery> + HasProvider<dyn ColumnsQuery> + HasProvider<dyn CardsQuery> }; } pub trait QueryProvider where Self: queries!(), { } impl<T> QueryProvider for T where Self: queries!(), { }
pub(crate)
とか re-exportとか
Rustのstruct, enum定義は縦に長くなりがちである。これは主に以下のような理由による。
- メンバの定義とメソッド定義を別に書く必要があるため、構文要素が多くなる
- ジェネリクスを持つ(
Foo<T>
)場合、T
が実装しているtraitによって実装するメソッドを増やしたりするともっと多くなる
- ジェネリクスを持つ(
- traitを実装する記述を書くほど長くなる
- メソッド本体よりも周りの
impl FooTrait for Foo where ...
がいっぱいになる
- メソッド本体よりも周りの
- テストを同じファイルに書く習慣がある
また、DDDでは以下のようにしたくなる。
- ユーザー情報に関連するモデルを複数作りたい
User
UserName
Email
- 同じサブドメインに関連するモデルがわかりやすい構造で外部(上位レイヤ)から参照したい
use domain_kanban::user::{User, UserName, Email}
これをそのまま実現しようとすると、1ファイルに大量の定義や実装を書くことになる。対策として、複数ファイルに分ける方法を利用できる。
具体的には、以下のようにuser/email.rs, user/user_name.rs
を作成する。
src/ |-- lib.rs |-- user/ | |-- email.rs | `-- user_name.rs `-- user.rs
そして、以下のようにする。
// -------- user/email.rs -------- // `pub(crate)` で(直接)参照可能範囲を制限する pub(crate) struct Email { // ... } impl Email { // ... } // -------- user/user_name.rs -------- pub(crate) struct UserName { // ... } impl Email { // ... } // -------- user.rs -------- // mod にはpubをつけない( `domain_kanban::user::email::Email` というふうには参照させない) mod email; mod user_name; // pub use (=re-export) で `domain_kanban::{Email, UserName}` で参照できるようにする pub use email::Email; pub use user_name::UserName; pub struct User { // ... } // ...
こうすることで、ファイルが長くなるのを抑えつつ、外部への見せ方を変える事ができる。 ただし、こうするとファイル数が多くなるので、やり過ぎは禁物。
Vec<Result<T, E>> to Result<Vec, E>
SEE: https://qnighy.hatenablog.com/entry/2017/06/14/220000
Vec<T>
を map
していると、軽率に Vec<Result<T, E>>
になる。
これだと扱いにくいのだが、 collect
でどうにかできる。
let vec_of_results = vec![Some(1), Some(2)]; // collectは複数の型に変換できるので、要求する型を束縛する変数側に指定する // `collect::<Result<Vec<_>, _>()` でもいいが、個人的にはこちらのほうが書きやすい let result_of_vec: Result<Vec<_>, _> = vec_of_results.into_iter().collect(); // `?` が使える文脈なら、それを使って処理を続ける let v = result_of_vec?; // `?` を使うなら、collect側に書いたほうが1行で完結できる let v = vec_of_results.into_iter().collect::<Result<Vec<_>, _>>()?;
Vec<Future> to Vec
dataloader実装(後述)をやっていても、複数回クエリを回したいときもある。
let result_futures: Vec<impl Future<Output=Foo>> = keys .into_iter() // some_queryがFutureを返す .map(|k| some_query(/* ... */)) .collect();
このあとどうやって処理するかというと、futures-util join_all
を使う。
let result_vec: Vec<Foo> = join_all(result_futures).await;
Rust: (Virtual) Workspaceの活用
オニオンアーキテクチャにおけるレイヤー分けを見据えて、リポジトリ全体はVirtual Workspaceにした。
ワークスペースのメンバcrateは、 crates/
ディレクトリ配下に置いた。rootディレクトリがゴチャつかないようにするためで、好みの問題かもしれない。
メンバcrateの指定は Cargo.toml
で以下のようにした。
[workspace] members = [ 'crates/*' ]
bin crateが crates/main
だけで他がlib crateであれば、 cargo run
で問題なくmainが起動できた。
Workspace Dependencies
https://doc.rust-lang.org/cargo/reference/workspaces.html#the-dependencies-table
Workspaceでこれが一番便利だったかもしれない。
/Cargo.toml
の workspace.dependencies
配下にいつも通り依存関係を書く。
各 /crates/*/Cargo.toml
の dependencies
で、 package-name.workspace = true
とすると、そのcrateを利用できる。
例:
# -------- on /Cargo.toml -------- [workspace.dependencies] anyhow = "1.0.72" # -------- on /crates/main/Cargo.toml -------- [dependencies] anyhow.workspace = true # -------- on /crates/domain-util/Cargo.toml -------- [dependencies] anyhow.workspace = true
実際に定義したものを列挙してコメントしておく。説明の都合sourceとは掲載順が違うことがある。
# -------- on /Cargo.toml -------- [workspace.dependencies] # anyhow, thiserror: エラー処理はどの層でも行うので共通で入れた # ちゃんとエラー処理するほうに倒すと、anyhowは減っていく可能性もある anyhow = "1.0.72" thiserror = "1.0.47" # tracing, tracing-subscriber: ロギング。 # ログに使っているcrate versionが揃うように。 # tracing-subscriberはもしかしたらmainだけでいいかもしれない。ちゃんとログ実装したら除去できるかも。 # 各層から依存するlog crateでtracingを使っていることを隠蔽しようかとも思ったけど、 # マクロの構文が物によって違いそうなのでやめた。 tracing = "0.1.37" tracing-subscriber = "0.3.17" # async-trait: infrastructure層で外部リソースを使用することが必至なため、だいたいasyncにする。 # I/Fはquery層などに置くことになるので、trait methodとしてasyncにする必要がある。 async-trait = "0.1.72" # futures-util, itertools: 一般的なutility futures-util = "0.3.28" itertools = "0.11.0" # shaku: DI。I/F側と実装側でバージョン統一するため。 shaku = "0.6.1" # sqlx: これはinfrastructure層でしか使わない……と見せかけて、migrateとかscriptとかで使っている(後述) # ここではバージョンのみ定義していて、featuresは使う側のcrateで決めている。 sqlx = "0.7.2" # ulid: これはdomain-utilで吸収しているのでここではいらないかも…… ulid = "1.0.1" # 以下各レイヤの定義。利用側でpathを毎回書かなくてよくなる。 # 依存されないcrateはここには列挙していない presentation-axum = { path = "./crates/presentation-axum" } presentation-graphql = { path = "./crates/presentation-graphql" } infrastructure-rdb = { path = "./crates/infrastructure-rdb" } query-resolver = { path = "./crates/query-resolver" } domain-kanban = { path = "./crates/domain-kanban" } domain-util = { path = "./crates/domain-util" } # tokio: main関数を使う場所で使っている。mainアプリケーションでは下層のspawnerにも使っている。 [workspace.dependencies.tokio] version = "1.29.1" features = ["macros", "rt-multi-thread"]
- 便利ポイント
- シンプルに同じバージョンや同じfeaturesを共有できるので便利
- featuresは利用側で追加できる。共通のものだけ置いておくのが便利
- レイヤー間の依存関係について、個別にpathで書くのがちょっとめんどくさかった。これが解消されるので便利。
- イマイチポイント
cargo-add
とかのツールのサポート(後述)
Workspace: cargo-(add|outdated)
それぞれ依存crate管理に関わるツール。ツール単体に気になるところはなく、普通に便利だった。
workspace dependencies を書くうえではコツがいりそうなので、ポイントに絞って書く。
cargo-add
端的に言うとworkspaceはサポートしていない。
issueはここにある: https://github.com/rust-lang/cargo/issues/10608
workspace dependenciesには自分で書くとして、そのヒントとなる情報は --dry-run
オプションで得るのがちょっと便利だった。
# --package オプションの指定がめんどくさいので、移動しておく。 $ cd crates/main $ cargo add frunk --dry-run Updating crates.io index Adding frunk v0.4.2 to dependencies. Features: + frunk_proc_macros + proc-macros + std + validated - serde warning: aborting add due to dry run
この結果を見ると以下がわかる。Webで調べる場合との比較も書いておく。
- 最新版は0.4.2っぽい
- crates.io をみればわかるが、GitHubを見てもわからないことが多い
- releasesを使ってリリースしているところもあればそうでないところもある。なんなら途中から切り替わったところもある
/Cargo.toml
に書いてないこともある。Virtual Workspaceを使っていてmulti crateそれぞれpublishされている場合とか- デフォルトブランチの内容がリリースの最新とは限らない。大体のリポジトリには、リリースバージョンごとにタグをつけてあるけど、「大体」
- crates.io はREADMEの幅が狭いので、説明文が読みにくいことがある。 feature flagsはREADMEにあることが多いので、読みにくいと困る
- crates.io をみればわかるが、GitHubを見てもわからないことが多い
- default featuresは
frunc_proc_macros, proc-macros, std, validated
で、optional featuresはserde
らしい- これもどの
Cargo.toml
をみたらいいかわかりにくい。みてもわからないことも…… - そもそもRust全体として、外部から指定するfeatureなのか内部のためにあるfeatureなのかがわからないという話がある。ここは変わらない
- たとえばこのfrunkなら、実際は
validated, proc-macros, serde
が指定するもので、残りは依存されるものでおそらく外部から指定するものではない - ……が、実は
serde
はCargo.toml
に載ってない。READMEにだけある
- たとえばこのfrunkなら、実際は
- これもどの
cargo-outdated
workspace supportはあるっぽい。
w, --workspace
- なしだと workspace dependenciesをチェックする。というより、rootの
Cargo.toml
をみるみたい - ありだと、workspace memberのdependenciesをチェックする
- なしだと workspace dependenciesをチェックする。というより、rootの
R, --root-deps-only
- 直接依存しているcrateのみを表示するようになる
- デフォルトだと依存しているものの依存先も再帰的に辿っていくので、更新対象を確認する用途だとほぼ必須っぽい
上記を整理すると、以下のような使い分けになる。
- workspace dependenciesを確認するとき:
cargo outdated -Rw
- 各crateのdependenciesを確認するとき:
cargo outdated -R
Workspace: スクリプト
進むうちに、いくつかスクリプトが欲しくなった。
- Rustコード系
- シェルスクリプト系
- 開発用の
cargo ...
などのコマンド。オプションが多くて思い出すのがめんどくさくなってきた
- 開発用の
それぞれ検討した方法は以下の通り。1番目に記載したものが採用したもの。
- Rustコード系
- シェルスクリプト系
- READMEに書く
- 特に説明することはない……READMEに1行説明とともに列挙するだけ
cargo-make
にまとめるcargo-make
にまとめるとcargo make migrate
みたくすることができる- CIとかを見据えると、1箇所で管理できて名前付きにできるこの方法のほうがリッチだけど、
Makefile.toml
を書くのがやや面倒 - 多機能ゆえに機能を把握していないと触りにくい(性格の問題)
- READMEに書く
Workspace: default members
前述のように、bin crateを追加したことで実行時引数が増えた。
- before:
cargo run
- after:
cargo --project main run
ちょっと増えたぐらいだから別にいいんだけど、 default-members
に指定することで引数不要にできるのでそうした。
[workspace] members = [ 'crates/*' ] default-members = [ 'crates/main' ]
cargo-watch
GraphQLサーバーのコードを更新するたびに再ビルドさせて実行させるのに、 cargo-watch
が便利だった。
cargo watch -x run
で実行できる。
watch
はデフォルトでcheck
をするので、x
で実行内容を指定するdefault-members
でないものは-package
などを指定する必要があるが、x 'check --package scripts'
のようにquoteが必要- GitHub を見たら、
cargo watch -- cargo check --package scripts
のようにもできるらしい
cargo-expand
マクロをそれなりに使うときは、 cargo-expand
を使うとコンパイルエラー調査が便利である。
今回だと、async-trait, tokio, async-graphql, shaku, sqlxあたりがマクロをよく使う。
cargo-expand
はデフォルトだと全部を展開しようとする。itemの指定ができるので、以下のように使うと便利。
# 該当のcrate rootまで移動しておく cd crates/infrastructure-rdb # mod単位でexpand cargo expand query::card
- 本当にstruct単位でexpandしようとすると、implが表示されないなど情報不足なことがある
- かといってmoduleだと多いなーという場合は確認用にsubmoduleに切り出すといいかもしれない
- ただ、期待しすぎない方がいい。普通にコードを読むより多くの知識を要求されることが多いので、ある程度「わからんものはわからん」という気持ちでいないとつらくなる
- 見ても分からなければ強い人に聞いて、また強くなったら帰ってこよう
Rust utilityライブラリ
あまり書くことはないけど使ったもの。
itertools
VecをgroupingしてHashMapにするなどのときに便利だった。 でもびっくりするぐらいメソッドがあるので、iterator関連のほしいメソッドはググるのが賢明。
futures-util
future関連のメソッドが追加されていて便利。
どうしてもiteratorの要素ごとにFutureが生成されてしまったので、 join_all
を使った。
GraphQL
GraphiQL UI
GraphiQLというGraphQLのUIがあり、 async-graphql
でもすぐ利用できるようになっている。
cargo watch -x run
で起動し、8000ポートにマップした。
GraphQLの要素が補完され、ドキュメントも出るのでクエリ組み立てに便利。
watch
でサーバーが再起動しても、UIは残るので使いやすかった。
サーバー再起動時にはschemaを取り直さないので、 Refresh GraphQL schema
ボタンを押すことで補完されるようになる。
dataloaderと相互参照
dataloaderを実装した(というより、schemaのresolverを分けた)ことで、相互参照が実現できた。
{ usersAll { id name # userがオーナーのボードを参照 ownedBoards { id title # ボードのオーナーを参照 owner { id name } } } boardsAll { id title # ボードのオーナーを参照 owner { id name } } }
schemaのresolverを分けたりしなくても相互参照はできる。 しかし、dataloaderの実装に任せることで、実装をシンプルに保つことができた。
async-graphql
GraphQL実装はRust製のasync-graphqlを使った。一通りの機能があり、ドキュメントもあるのであまり困らず使えた。
Schema生成
ここにある通りに書いた。
実装上の考慮事項として、Schema型にはdataやextentionsなどのデータを紐づけているが、これはスキーマ生成だけを考えると不要である。そのため、以下のように考慮しておく(と楽)。
- schema生成時はdataやextentionsを紐付けない
- アプリとschema生成で同じbuildを通るように共通化
共通化にあたって、SchemaやBuilderの型引数にquery, mutation, subscriptionがあって煩雑になったので、 type
キーワードでエイリアスを作って対策した。
// 定義 type SchemaType = Schema<Query, EmptyMutation, EmptySubscription>; type SchemaBuilderType = SchemaBuilder<Query, EmptyMutation, EmptySubscription>; // 利用 fn schema_builder() -> SchemaBuilderType { Schema::build(Query, EmptyMutation, EmptySubscription) } fn schema() -> SchemaType { schema_builder().finish() } fn schema_with<F>(f: F) -> SchemaType where F: FnOnce(SchemaBuilderType) -> SchemaBuilderType, { f(schema_builder()).finish() }
また、rust-scriptだと補完が効かなくて作りにくかったのでscripts crateにまとめた。
dataloader実装
dataloaderの実装方法自体はドキュメントにコードで記載があり、それをなぞった。以下が私にとってのポイントだった。
- Trait実装として提供するので、同じキーのdataloaderを複数個同じ型に実装できない
- たとえば
Id<User>
をキーにユーザー一覧をとりたいときとボード一覧をとりたいときとか - →未確認だが、
UserIdForBoard
のようなstructにラップして利用すれば良さそう?もしくはdataloaderを分ける?- そもそもdataloaderを複数にするべきかどうかがわかっていない。一つでいいような気がする。
- たとえば
- dataloaderはadapter層で、実行するクエリはquery層で定義している。そのため、IDの型があわない。せっかく
keys: &[K]
でもらっても変換するのでVecにしている。- →仕方ないと思って諦めた
- ちなみに、返却時も再変換が必要
- 戻り値が
HashMap<K, V>
であるので、この形に変換が必要- →あきらめてquery層をこちらに寄せてHashMapを返すようにした
Data
取得
async-graphqlは context
から参照できるデータを持つ。このデータは格納時に Any
にして、取り出すときにダウンキャストしている。
let modules: &Modules = context.data()?; // or let modules = context.data::<&Modules>()?;
そのままでも使えるが、型注釈が必須な点がやや面倒である。
個人的には前者のほうが読みやすいが、続けてメソッドを呼び出したい場合などでは後者の書き方になり、このとき型注釈を忘れるとエラーになってしまったり、 ::<>
のように装飾が多かったりとあまり好きではない。
以下のようにcontextを拡張するメソッドをつくって、型注釈を不要にした。メソッド名から何が返るのかもわかりやすく、実装コストに対して効果的な印象。
// ContextにDataLoader, Modulesを取得するメソッドを作成する // dataはAnyで型消去しているけど、このプロジェクト内ではSchemaを作っている箇所で必ず設定しているはずなので気にしないことにする pub trait ContextExt { fn data_loader(&self) -> Result<&DataLoader<Modules>, GqlError>; fn modules(&self) -> Result<&Modules, GqlError> { Ok(self.data_loader()?.loader()) } } impl<'ctx> ContextExt for Context<'ctx> { fn data_loader(&self) -> Result<&DataLoader<Modules>, GqlError> { self.data() } }
sqlx
マクロ
sqlxはマクロで「コンパイル時にSQL結果の型をチェック」できる。
これの利用に向けてはちょっとコツがいる。
- コンパイル時に参照できる場所にテーブルが存在するDBを立てる必要がある
- dockerを使って立てるのが楽
- docker内にテーブルを用意するのは、sqlxのmigrate機能等を使うといい
- offlineモードというのもあるが、試していない
- dockerを使って立てるのが楽
- 環境変数で参照するDBを指定する必要がある
- コマンドが長くなるので、スクリプトにまとめた方がいい
- (
query!()
マクロの場合)コンパイル時に型チェック=コンパイルしないと型が決まらない- rust-analyzerが補完してくれない
- クエリを書いたあと、後述処理を書くまえにコードを保存(ないしは再コンパイル)するといい
- コンパイル時にSQLでエラーになったとき、エラー箇所がわからない
感想としては、「すごいが、コンパイル時チェックなしの関数でもいいかな……」という感じだった。理由は以下。
- クエリがstatic文字列でなくてはいけない
- 動的に
where
句のパターンを書き換えたいときにつらい - 似たような or 同じクエリを使い回せない
query_file!()
マクロがあれば完全に同じクエリならいける
- 動的に
- (前述)クエリ結果を束縛した状態でsaveしないとrust-analyzerが補完してくれない
- 安全性は(テストをちゃんと書くなら)関数で書いてもあまり変わらない
poolとtransaction
sqlxの(postgres)クエリを実行するとき、 PgPool
か Transaction<'a, Postgres>
が必要になる。それぞれの使い方は以下の通り。
pool: &PgPool
の場合.execute(&pool)
とする
mut transaction: Transaction<'a, Postgres>
の場合.execute(&mut transaction)
とする
transaction: &mut Transaction<'a, Postgres>
の場合.execute(&mut **transaction)
とする- 特にこれを忘れやすい
- 関数にクエリをまとめていると引数に登場しがち
特にC言語の経験からすると3番目は謎が深いが、とりあえず覚えることにする。
derefして &Connection
に、更にderefして '&mut
を取っている感じで認識している。
migrate
sqlxがCLIを提供していて、それを通してmigrateした。
migrateファイルの作成も適用もCLIで問題なく行えた。
shaku: Providerのエラー型をasync-graphqlに合わせる
RustのDIライブラリ。async-graphqlのcontextと組み合わせて、以下のようにしてクエリを取得したい。
let user_query: Box<dyn UsersQuery> = context.modules()?.query().provide()?;
これは工夫しなくても実現できる……が、エラー型がちょっと扱いにくい。最終的には async_graphql::Error
型にしたいが、 shaku::HasProvider::provide()
が返すエラーは Box<dyn std::error::Error>
であり async_graphql::Error
型に変換できない。
そこで、この変換だけをラップするためのメソッドをtraitで拡張して作成した。
// shakuのErrorをasync-graphqlにあわせる pub trait HasProviderGql<I: ?Sized>: HasProvider<I> { fn provide_gql_result(&self) -> Result<Box<I>, GqlError>; } impl<T, I: ?Sized> HasProviderGql<I> for T where T: HasProvider<I> + ?Sized, { fn provide_gql_result(&self) -> Result<Box<I>, GqlError> { self.provide().map_err(|e| { GqlError::new(format!( "providing <{}> failed: {}", type_name::<I>(), e )) }) } }
これを使うと、GraphQL クエリメソッド内でも ?
でエラー型を変換できる。
開発体験: ChatGPT
使い込んでいないので3.5での結果。
ChatGPTにやらせたこと(1): ドメインモデル図を描かせる
ありきたりなkanbanアプリのドメインモデル図だったので、ChatGPTに書かせた。
Kanbanアプリケーションをドメイン駆動設計で作成します。ドメインモデル図をmermaid形式で示してください。
指示としては以上で、それ以降は「集約を小さくしてください」「操作を追加してください」などを追加指示した。
結果として取得できたコードは以下で、いくつか間違っている箇所がある。手動で直せばいい範囲なので、ChatGPTに再修正させず手で直した。
classDiagram class User { <<Entity>> +userId: int +name: string +email: string +boards: Board[] } class Board { <<Aggregate>> +boardId: int +title: string +owner: User +members: User[] +columns: Column[] +addColumn(title: string): Column +removeColumn(columnId: int): void } class Column { <<Aggregate>> +columnId: int +title: string +tasks: Task[] %% --------間違った結果の箇所: addColumnではなくaddTaskっぽい。-------- +addColumn(taskTitle: string, taskDescription: string): Task +removeTask(taskId: int): void } class Task { <<Entity>> +taskId: int +title: string +description: string +status: string } User --> Board : owns User --> Board : member of Board --> Column : has Column --> Task : contains
ChatGPTにやらせたこと(2): レイヤー構造を描かせる
同じ理屈で、オニオンアーキテクチャのレイヤー構造を図にしてもらった。
mermaidの図の書き方を思い出すより早くできた。
ChatGPTにやらせたこと(3): 典型的な docker-compose.yml
を書かせる
RDBを構築したくなったタイミングでChatGPTに docker-compose.yml
も書かせてみた。
今回は典型的な(後で使うつもりのlocalstackと)Postgresを立てるだけだったので、ほとんどそのまま出力できた。
注意点は以下。
- localstackは古いバージョンでのみ利用できる環境変数があったので除去した
- ポート番号はそのままで割り当てられるので、それを
60000
から振るように追加指示した
ChatGPTにやらせたこと(4): RDBテーブル定義を作らせる
RDBのテーブル定義を作るのが面倒だったので、吐き出させた。
何を入力にしたかというと、GraphQL Schemaを入力にした。
以下graphqlのデータをPosgresで構築します。テーブル定義のDDLを示してください。
追加注文したのは以下の通り。
- 各
Id
はVARCHAR
型にしてください - テーブル間の関連を別テーブルに切り分けてください
- これは2回くらい注文した。
- テーブル名をsnake_caseに変更してください
- テーブル名を複数形にしてください
ChatGPTにやらせたこと(5): Rustコードをサンプルデータ投入SQLに変換させる
infrastructure層の実装が終わって、RDBと接続して確認したかったが、データがない。
仮実装の段階でRustコードでデータをメモリ上に作る実装があったので、これを入力にINSERT文を吐かせてみたら、思ったより精度良く吐かせることができた。
以下Rustコードで作成するのと同等のデータをPostgreSQL DBに用意するINSERT文を示してください。 [Rustコード...] なお、テーブル定義は以下のとおりです。 [テーブル定義...]
特に、配列参照を使ってIDがおなじになるようにしていた箇所が全体的に正しく解決されていたので驚いた。
気になっていること
frunkで変換が楽になる?
レイヤーの関係で変換が必要な presentation_graphql::scalar::Id<T>
と domain_util::Identifier<T>
とか、クエリの返却値とgraphql responseとか、細かい変換をいくつも書いた。
frunkという関数型プログラミング系のライブラリがあって、この中のGenericという機能がこの辺りの変換をサポートしてくれていそう。活用したら楽になりそう。
infrastructureのテスト
互いのテストが影響しないようにするために、インメモリDBを使いたいが、sqlxでそのあたりうまくできるのかを調査する必要がある。
認証とか
まだアプリケーション内での認証処理とかについて調べていないので確認する必要がある。