💡

Rustを使用してGraphQLサーバを作ってみた

2022/12/18に公開

これは、OPENLOGI Advent Calendar 2022 18日目 の投稿記事です。

https://qiita.com/advent-calendar/2022/openlogi

Rustを触ってみたかったんですが、それに合わせてまだ触ったことがなかったGraphQLにもチャレンジしてみました!
最初に構成などを記載し、Rust初心者として「やったこと」や「つまづいたこと」を後半に書いていきたいと思います。

できあがったもの

https://github.com/oneut/rust-web-example

実行方法などは、README.mdを参照してください。

構成

時間をかけて調査したわけではないため、間違った情報もあるかもしれません。ご注意ください。

  • axum
    • Webフレームワーク
    • actix-webは、非同期ランタイム周りで辛みがありそうな話を目にしたので、axumを採用
    • tokioという非同期ランタイムのプロジェクトがあり、そこが開発したWebフレームワークなので、非同期ランタイムとの親和性が高いらしい
  • Async GraphQL
    • GraphQL Serverライブラリ。
    • Juniperは、Apollo Federationに対応していなかったりするらしいので、標準に近そうなAsync Graphqlを採用
    • ただ、Apollo Federationに対応できると何が嬉しいかまでは未調査
  • Prisma Client Rust

概要

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
  }
}

スキーマ定義

https://github.com/oneut/rust-web-example/blob/main/resource/schema.graphql

やらなかったこと

ユニットテスト

Rustに触るのが目的だったので、今回は対応しませんでした。

エラーハンドリング

落ちないシステムなので、対応しませんで・・・、時間がなかったので断念しました。
unwrapを多用しています。

やったこと

Rust

Rustの開発環境をDockerで構築

Dockerを使用して開発環境を構築しました。
Rustの開発用コンテナでは、コマンドを指定せずに、tty:trueでコンテナを常駐できるようにしています。
複数のcargoコマンドが使える環境にしたかったことと、不必要にビルドやコンパイルが起きることを避けたかったため、手動でcargo watch -x runコマンドを実行する形にしました。
tty:trueめっちゃ便利でした。

https://github.com/oneut/rust-web-example/blob/main/docker-compose.yml#L8

moldのインストール

ビルドやコンパイルを高速にしたかったので、リンカをmoldに変更しています。
変更方法は、moldのREDMEに記載されています。

https://github.com/rui314/mold#how-to-use

実際に変更してみて、ビルドやコンパイルの速度が劇的に向上したので、設定しておくと幸せになれそうです。

https://github.com/oneut/rust-web-example/blob/main/.cargo/config.toml#L6-L8

CargoのWorkspaceとconfigのaliasでコマンド作成

Prismaコマンド (cargo prisma)、 GprahQLのスキーマファイル作成コマンド(cargo graphql-sdl)、Seederコマンド(cargo seeder)を用意しました。
最初は、src/bin配下に定義しようと思いましたが、src配下にあるとごちゃごちゃしてしまう気がしたため、Workspaceを使用して、コマンドを作成しています。

https://github.com/oneut/rust-web-example/blob/main/Cargo.toml#L8-L14

https://github.com/oneut/rust-web-example/blob/main/.cargo/config.toml#L1-L4

config.tomlをCargo.tomlと見間違えて、エイリアスの設定に苦戦したので、気をつけましょう。

モジュール(ファイル)単位で、パブリックの範囲を設定した

モジュールの可視性が制限できるのはとても便利でした。素晴らしい機能だと思います。

https://doc.rust-lang.org/reference/visibility-and-privacy.html

下記の場合だと、親モジュールからのみアクセスが可能になります。

https://github.com/oneut/rust-web-example/blob/main/src/graphql/resolver.rs

Async GraphQL

Dataloaderを試した

GraphQLに初めて触りましたが、Dataloaderが素晴らしかったです。
N+1を解決する仕組みが、レスポンスの近いところでできるのは本当にいいですね。
ユーザー情報をマイクロサービスとして切り出すような仕組みの場合には、GraphQLを選択していると幸せになれそうです。
今回のリポジトリでは、ユーザー情報は、Prisma Client RustEager Loadingの仕組みを使用せずに、Dataloaderを使用しています。

https://github.com/oneut/rust-web-example/blob/main/src/graphql/objects.rs#L19-L26

注意点としては、Async GraphQLの場合、dataloader は、最初から有効ではないので、設定が必要です。

Note: Minimum supported Rust version: 1.60.0 or later

https://github.com/async-graphql/async-graphql#features

Cargo.tomlのfeaturesで指定することにより、機能が使えるようになります。

https://github.com/oneut/rust-web-example/blob/main/Cargo.toml#L18

タプル構造体でのスキーマ定義

Async GraphQLの場合、フィールド型の構造体を使用すると、GraphQLのフィールドで指定が可能になってしまうため、Dataloaderを使用する場合、タプル構造体を使用した方が良さそうに感じました。
例えば、user_id はレスポンスに含めたくないが、Dataloaderを使用して、userの情報を返したい場合は、フィールド型の構造体だと実現ができないように思えます。

https://github.com/oneut/rust-web-example/blob/main/src/graphql/objects.rs#L31-L54

あとから、フィールド型の構造体からタプル構造体に変更すると大変なので、個人的にはタプル構造体で最初から定義した方が良さそうな気がしています。

GraphiQL

ブラウザ上で動作するGraphQL IDEが、Async GraphQLに含まれているので、簡単に表示できるのですが、これがとても便利でした。
当初フロントエンド側も実装しようと思いましたが、簡単に検証できる環境が作れたのでよかったです。

Prisma Client Rust

Prismaのスキーマ定義からコードを自動生成

スキーマさえ定義すれば、構造体やCRUDを行うメソッドを自動的に用意してくれるのが、とても便利でした。
今回実装したリポジトリの場合、下記のスキーマを定義して、

https://github.com/oneut/rust-web-example/blob/main/prisma/schema.prisma

用意したコマンドのcargo prisma generate を実行すると、下記のコードが自動で生成されます。

https://github.com/oneut/rust-web-example/blob/main/src/prisma.rs

cargo prisma generateは、今回のリポジトリで定義したエイリアスに依存していますので、注意してください

自動でコードが生成された後は、そのコードを利用するだけでよいため、実装をほぼせずに処理がかけてしまうのは、素晴らしい体験でした。

https://github.com/oneut/rust-web-example/blob/817a31681057a0e68d61547d2f33ee1b8f3695e4/src/graphql/resolver/comment.rs#L27-L37

今回は、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 を返すため、そこにつまづきました。

https://github.com/oneut/rust-web-example/blob/main/src/graphql/objects.rs#L40-L45

上記の場合、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のユーザー権限の調整が必要でした。

https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database

今回は、SQLを用意して、コンテナ構成を作成する際に適用するようにしています。

https://github.com/oneut/rust-web-example/blob/main/docker/db/initdb/01.grant.sql

上記のディレクトリをdocker-entrypoint-initdb.dにマウントしておくと、SQLが自動的に実行されるので、この仕組みを知れてよかったです。

https://github.com/oneut/rust-web-example/blob/main/docker-compose.yml#L25

まとめ

少し触った程度ですが、RustでGraphQLサーバが作れてよかったです。
Rustを触るには、参照周りの理解がかなり重要だということが理解できました。
これから、リファレンスを読み、Rustの理解度を高めて行ければと思います。

また、GraphQLやPrismaというエコシステムにも触れる機会もでき、とても良い経験ができました。

RustでGraphQLサーバを構築する方の助けになれば幸いです!

Discussion