私が歌川です

@utgwkk が書いている

趣味でGraphQL APIを使ったwebアプリケーションを作ってみた感想

GraphQL APIを備えたwebアプリケーションを趣味で作っていて、完成したので感想をまとめる。

目次

作ったアプリケーション

サークル内で使うイラストアップローダーを作った。機能としてはこういう感じ。有り体に言えばclosedなpixivである。

  • 作品をアップロードできる
  • 作品には1個以上のイラストがある
  • 作品に「いいね」ができる
  • 作品にコメントできる
  • 作品にタグを付けられる
  • タグで作品を検索できる

もともと同様のアプリケーションが動いていたのだけれど、今回それをリプレイスする形で実装した。

使ったライブラリ

Flask

Pythonの薄いwebアプリケーションフレームワークである。RubyでいうところのSinatraポジション。使い慣れているので採用した。

SQLAlchemy

これまではO/Rマッパーは使わず生SQLをゴリゴリ書いていく主義だったのだけれど、SQLを気にして書く段階から脱却したいなと思ったので採用した。

結論としては、Graphene-Pythonと連携してかなり楽に実装できたと思う。declarative_base() を使ってモデル定義をPythonのクラスとして書き下す感じで、最初はぎこちなく書いていたけどだんだん世界観が分かってきた。

知見をいくつかScrapboxにまとめている。典型的な多対多関係とか、一対多関係とは別に関係を保持しておきたいとか、こういうユースケースはあると思う。SQLAlchemyのドキュメントがかなり詳しいので、ユースケースに合った書き方が分からないときは関連していそうな箇所をじっくり読むのがよさそう。

scrapbox.io

alembic

SQLAlchemyのマイグレーションツール。

ActiveRecordだとDBマイグレーション機能があるけどSQLAlchemy本体にはなくて、頑張るか何らかのライブラリを併用することになると思う。alembicはSQLAlchemyの開発チームが開発していて、信頼感がある。

SQLAlchemyのモデル定義を書き換えたあと、alembicでマイグレーションスクリプトを生成する、という方法が典型だと思う。スキーマファーストで差分を生成していくみたいなイメージ?

Graphene-Python

PythonのGraphQLフレームワーク。FlaskやSQLAlchemyを含めた各種フレームワークと連携できる機能がある。RelayのGraphQL Server Specificationに準拠するのも難なくできた。

graphene-file-uploadを使うとMultipart Request Specificationに沿ったファイルアップロードも実装できる。

作ってみてどうだったか

思ったよりも楽に作れたと思う。素朴に実装するとN+1クエリの温床になるけど、dataloaderを適切に差し込むとうまくクエリをまとめてくれる。Multipart Request Specificationに準拠するのは最初けっこうハマって、うまくファイルアップロードできなくて結局Flaskのrequestオブジェクトを直接見ていたけど、やり方が間違っていたことに気づいて修正できた。ここに関しては別途記事にしたい。

2021/7/18 追記: ファイルアップロードについての記事を書いた。

blog.utgw.net

作品詳細ページにはいろいろな情報を出したい、というときに、従来なら必要な情報を頑張ってまとめて集めてくる (N+1クエリに気をつけつつ) ことになると思うけど、GraphQL APIだとクエリを1つ書いて適切なフィールドを要求したらできた。↓みたいなクエリと同じことをバックエンドで頑張っていたのか、と思うと感慨深い。

query ArtworkDetailQuery($id: ID!) {
  node(id: $id) {
    ... on Artwork {
      id
      title
      caption
      createdAt
      account {
        id
        name
      }
      illusts {
        edges {
          node {
            id
            imageUrl
          }
        }
      }
      likes {
        edges {
          node {
            account {
              id
              name
            }
          }
        }
      }
      comments {
        edges {
          node {
            text
            createdAt
            account {
              name
            }
          }
        }
      }
      tags {
        edges {
          node {
            id
            name
          }
        }
      }
    }
  }
}

fragmentで共通部分をくくり出せるのもよくて、作品一覧ページの個別作品コンポーネントをrenderするのに必要な情報をfragmentにしておくと再利用できて便利。どこまで分けるかはけっこう思想があるかもしれない。fragmentにしておくと親コンポーネントがフィールドの詳細を知らなくてもよい、と思って作ってみているけどこれでいいのかは分からない。

POSTリクエストがバンバン飛んでくるので、趣味程度ならいいけど、パフォーマンスなど本番導入する際には気をつけることになりそう。とはいえ、重たいクエリを発行できないようにしたり、dataloaderを実装したりすれば案外なんとかなるのだろうか。あるいはGETリクエストで特定のクエリだけキャッシュできるようにするとか?

おわりに

いくつか (6個ぐらい) のモデルがあって互いに関連し合っている、というwebアプリケーションをGraphQLで実装してみた。従来の開発体験とはまた違った感じで新鮮なので、機会があればGraphQL APIを作ってみるとよさそう。