Rustを使用してGraphQLサーバを作ってみた
これは、OPENLOGI Advent Calendar 2022 18日目 の投稿記事です。
Rustを触ってみたかったんですが、それに合わせてまだ触ったことがなかったGraphQLにもチャレンジしてみました!
最初に構成などを記載し、Rust初心者として「やったこと」や「つまづいたこと」を後半に書いていきたいと思います。
できあがったもの
実行方法などは、README.mdを参照してください。
構成
時間をかけて調査したわけではないため、間違った情報もあるかもしれません。ご注意ください。
-
axum
- Webフレームワーク
- actix-webは、非同期ランタイム周りで辛みがありそうな話を目にしたので、axumを採用
- tokioという非同期ランタイムのプロジェクトがあり、そこが開発したWebフレームワークなので、非同期ランタイムとの親和性が高いらしい
-
Async GraphQL
- GraphQL Serverライブラリ。
- Juniperは、Apollo Federationに対応していなかったりするらしいので、標準に近そうなAsync Graphqlを採用
- ただ、Apollo Federationに対応できると何が嬉しいかまでは未調査
-
Prisma Client Rust
- ORM
-
Diesel、SeaORMも見たが、気軽に扱えそうなRust製のPrismaクライアントを採用
- バージョンが0.6.3なので、安定版ではないが、触ってみたくなるくらい気軽に使えそうだった
- Prismaとしては非公式クライアントだけど、応援はされているみたい
- https://github.com/Brendonovich/prisma-client-rust#affiliation`
- Go製のPrismaクライアントはメンテナンスがストップしていた
概要
GraphQLで、Post、User、Commentの登録ができ、それぞれ一覧で参照できる。
データの操作はGraphiQL上で行う。
GraphQL Query
GraphiQLでの操作例
Post一覧
{
posts {
id
title
user {
id
name
}
comment {
id
message
}
}
}
User一覧
{
users {
id,
name
}
}
Comment一覧
{
comments {
id
postId
user {
id
name
}
}
}
GraphQL Mutation
GraphiQLでの操作例
User登録
mutation {
createUser(createUserInput: {
name: "Luke"
}) {
id
name
}
}
Post登録
mutation {
createPost(createPostInput: {
title: "Star Wars: Episode IV A New Hope"
userId: 1
}) {
id
title
user {
id
name
}
}
}
Comment登録
mutation {
createComment(createCommentInput: {
message: "May the Force be with you."
postId: 1
userId: 1
}) {
id
message
}
}
スキーマ定義
やらなかったこと
ユニットテスト
Rustに触るのが目的だったので、今回は対応しませんでした。
エラーハンドリング
落ちないシステムなので、対応しませんで・・・、時間がなかったので断念しました。
unwrapを多用しています。
やったこと
Rust
Rustの開発環境をDockerで構築
Dockerを使用して開発環境を構築しました。
Rustの開発用コンテナでは、コマンドを指定せずに、tty:true
でコンテナを常駐できるようにしています。
複数のcargoコマンドが使える環境にしたかったことと、不必要にビルドやコンパイルが起きることを避けたかったため、手動でcargo watch -x run
コマンドを実行する形にしました。
tty:true
めっちゃ便利でした。
moldのインストール
ビルドやコンパイルを高速にしたかったので、リンカをmoldに変更しています。
変更方法は、moldのREDMEに記載されています。
実際に変更してみて、ビルドやコンパイルの速度が劇的に向上したので、設定しておくと幸せになれそうです。
CargoのWorkspaceとconfigのaliasでコマンド作成
Prismaコマンド (cargo prisma)、 GprahQLのスキーマファイル作成コマンド(cargo graphql-sdl)、Seederコマンド(cargo seeder)を用意しました。
最初は、src/bin
配下に定義しようと思いましたが、src配下にあるとごちゃごちゃしてしまう気がしたため、Workspaceを使用して、コマンドを作成しています。
config.tomlをCargo.tomlと見間違えて、エイリアスの設定に苦戦したので、気をつけましょう。
モジュール(ファイル)単位で、パブリックの範囲を設定した
モジュールの可視性が制限できるのはとても便利でした。素晴らしい機能だと思います。
下記の場合だと、親モジュールからのみアクセスが可能になります。
Async GraphQL
Dataloaderを試した
GraphQLに初めて触りましたが、Dataloaderが素晴らしかったです。
N+1を解決する仕組みが、レスポンスの近いところでできるのは本当にいいですね。
ユーザー情報をマイクロサービスとして切り出すような仕組みの場合には、GraphQLを選択していると幸せになれそうです。
今回のリポジトリでは、ユーザー情報は、Prisma Client Rust
のEager Loading
の仕組みを使用せずに、Dataloader
を使用しています。
注意点としては、Async GraphQLの場合、dataloader は、最初から有効ではないので、設定が必要です。
Note: Minimum supported Rust version: 1.60.0 or later
Cargo.tomlのfeaturesで指定することにより、機能が使えるようになります。
タプル構造体でのスキーマ定義
Async GraphQLの場合、フィールド型の構造体を使用すると、GraphQLのフィールドで指定が可能になってしまうため、Dataloaderを使用する場合、タプル構造体を使用した方が良さそうに感じました。
例えば、user_id はレスポンスに含めたくないが、Dataloaderを使用して、userの情報を返したい場合は、フィールド型の構造体だと実現ができないように思えます。
あとから、フィールド型の構造体からタプル構造体に変更すると大変なので、個人的にはタプル構造体で最初から定義した方が良さそうな気がしています。
GraphiQL
ブラウザ上で動作するGraphQL IDEが、Async GraphQLに含まれているので、簡単に表示できるのですが、これがとても便利でした。
当初フロントエンド側も実装しようと思いましたが、簡単に検証できる環境が作れたのでよかったです。
Prisma Client Rust
Prismaのスキーマ定義からコードを自動生成
スキーマさえ定義すれば、構造体やCRUDを行うメソッドを自動的に用意してくれるのが、とても便利でした。
今回実装したリポジトリの場合、下記のスキーマを定義して、
用意したコマンドのcargo prisma generate
を実行すると、下記のコードが自動で生成されます。
※cargo prisma generate
は、今回のリポジトリで定義したエイリアスに依存していますので、注意してください
自動でコードが生成された後は、そのコードを利用するだけでよいため、実装をほぼせずに処理がかけてしまうのは、素晴らしい体験でした。
今回は、Prismaの構造体から作成したインスタンスを直接使用しましたが、直接使うのではなく、EntityやModelのような構造体を用意して、依存をなくすのが良いと思います。
つまづいたところ
Rust
lib.rsを使うかどうかで、main.rsのuseの方法が変わる
main.rsで、testモジュールを使用したい場合、下記のように定義すれば使えるようになりますが、
mod test;
lib.rsで、pub mod test;
を定義した場合、パッケージ名からの参照になります。
use package_name::test;
最初このあたりが理解できず、苦戦しました。
実行ファイルを管理するtargetディレクトリ配下の容量が大きい
そんなに大きなシステムではありませんが、targetディレクトリが数GBもストレージを使用していることに気づきました。
この構成だと容量が大きくなるのかもしれません😅
into_iterの挙動が理解できていない
下記の処理で、Rust Prisma Clientが生成したcomments()
が &vec
を返すため、そこにつまづきました。
上記の場合、cloneしてから、into_iterを使用することにより、mapで要素を参照なしで扱うことができるようになりましたが、ここの理解が不足しています。
わかりやすい例を用意してみましたが、下記のパターン2の「&vec + into_iter」の場合に、anyの処理でxに参照がついている点が理解できていません。
// パターン1: vec + into_iter
let vec1 = vec![1];
vec1.into_iter().any(|x| x == 1);
// パターン2: &vec + into_iter
let vec2 = &vec![2];
vec2.into_iter().any(|&x| x == 2);
// パターン3: vec + iter
let vec3 = vec![3];
vec3.iter().any(|&x| x == 3);
// パターン4: &vec + iter
let vec4 = &vec![4];
vec4.iter().any(|&x| x == 4);
into_iterは、値として処理するものと認識していましたが、そうではないようでした。
後日調査をしてみます。
Prisma Client Rust
Shadow databases
Prismaのマイグレーションは、Shadow databasesという仕組みで動いているので、実行しているMySQLのユーザー権限の調整が必要でした。
今回は、SQLを用意して、コンテナ構成を作成する際に適用するようにしています。
上記のディレクトリをdocker-entrypoint-initdb.d
にマウントしておくと、SQLが自動的に実行されるので、この仕組みを知れてよかったです。
まとめ
少し触った程度ですが、RustでGraphQLサーバが作れてよかったです。
Rustを触るには、参照周りの理解がかなり重要だということが理解できました。
これから、リファレンスを読み、Rustの理解度を高めて行ければと思います。
また、GraphQLやPrismaというエコシステムにも触れる機会もでき、とても良い経験ができました。
RustでGraphQLサーバを構築する方の助けになれば幸いです!
Discussion