9.8K Views
October 09, 23
スライド概要
DjangoCongress JP 2023 での発表スライドです。
サーバーサイドエンジニアをしています
GraphQLライブラリ Strawberry の Djangoプロジェクトへの適用事例 〜実践から学ぶヒントと戦略〜 DjangoCongress JP 2023 Miyashita Yosuke 1
Strawberryとは ● GraphQLサーバーを構築するPythonライブラリの一つ ○ Star数はGrapheneに次いで2番目 ● dataclassを使ったシンプルなType定義 ● 開発が活発 ○ https://strawberry.rocks/ FastAPIの公式ドキュメントでも推奨 @strawberry.type class UserType: id: str name: str birthday: Optional[datetime.date] team: TeamType type UserType { id: String! name: String! birthday: Date team: TeamType! } 2
お前、誰よ 宮下 陽介 (Miyashita Yosuke) @mysh_iiii @miyashiiii 株式会社ビープラウド(2023/06〜) サーバーサイドエンジニア ● Python歴 6年 ○ webapp2でのサーバーサイド開発 1年 ○ 画像系AI開発 4年半 ○ Djangoでのサーバーサイド開発 半年 3
ビープラウドについて 株式会社ビープラウド - BeProud inc. 自社サービスの運営、受託開発 絶賛採用募集中! https://www.beproud.jp/ 4
発表の動機 ● Django × GraphQL開発を経験したので、知見を広めたい ○ DjangoでのGraphQLサーバー開発の事例紹介が世の中に少ない ■ ● 特にstrawberryは日本語情報がほぼない(2023年10月現在) (社内で「DjangoCongress出てみれば?」と唆された) 5
発表のゴール Django × GraphQL(Strawberry)の開発事例を知ってもらう GraphQL使いたい人 → 導入に当たって参考にしてもらう GraphQL開発している人 → 自分のPJと比較して、自身のPJに対する洞察を深めてもらう ※あくまで一事例の紹介。「ベストプラクティス」の紹介ではないです 6
アジェンダ ● はじめに ○ ● GraphQL, Strawberryの紹介 Django × GraphQL事例紹介 ○ 設計方針、実装方針 ○ テスト、パフォーマンス、セキュリティ…… ○ フロントエンドとのコミュニケーション 7
話さないこと ● GraphQLの基礎知識 ● クライアント側の実装の話 ● Subscriptionの話 8
本編の前に…… ● ● DjangoCongress JP 2023のX(Twitter)公式ハッシュタグがあります ○ #djangocongress ○ ご意見ご感想お待ちしてます! 資料も↑のハッシュタグで公開しています 9
GraphQL・Strawberryとは 10
この発表での用語 ● Type ○ GraphQLの”Type”を指します。 ○ (strとかのデータ型ではない) type UserType { id: ID! name: String! birthday: Date team: TeamType! } 11
GraphQLとは? WebAPIの仕様の1つ ● SDLというスキーマ定義の標準言語がある ● グラフ構造でオブジェクトどうしを関連させる ● 自由度の高いクエリ呼び出し ○ オーバーフェッチ、アンダーフェッチの解消 ● Query, Mutation, Subscriptionの3つの操作 ● 単一のエンドポイント 12
Strawberryの特徴 ● dataclassを使ったシンプルなType定義 ● 型解析との親和性 ○ ● コードファースト ○ ● スキーマの型をPythonの型ヒントで定義 Pythonコードからスキーマを生成 Pydanticとの連携(experimental feature) 13
Djangoとの連携 Strawberry integration with Django ● Django ModelをベースにGraphQLのTypeを定義できる ● GraphQLサーバー用のDjangoのviewを提供 ● Filtering, Ordering, ページネーション、認証機能 ● Relay仕様のサポート など https://strawberry-graphql.github.io/strawberry-graphql-django/ 14
ModelをベースにType定義 このデコレータをつけると ModelをベースにTypeを作れる class User(models.Model): name = models.CharField(max_length=255) birthday = models.DateField(null=True) password = models.CharField(max_length=255) team = models.ForeignKey( Team, related_name="members", on_delete=models.CASCADE ) @strawberry.django.type(User) class UserType: id: strawberry.auto name: strawberry.auto birthday: strawberry.auto team: strawberry.auto 型をstrawberry.autoとすると Modelの型を受け継いでくれる 15
スキーマ定義
@strawberry.type
class Query: # read
@strawberry.field
def users(self, info) -> list[UserType]:
return User.objects.all()
@strawberry.type
class Mutation: # create, update, delete
@strawberry.mutation
def create_user(self, input: CreateUserInput) -> UserType:
return User.objects.create(**input)
schema = strawberry.Schema(query=Query, mutation=Mutation)
type Query {
users: [UserType]!
}
type Mutation {
createUser(input: CreateUserInput): UserType!
}
16
Strawberry Djangoにコントリビュートしてみた ドキュメントのリンク切れを直したら、その日のうちにマージしてくれました 初めてのOSSコントリビュートにもおすすめです 17
Djangoプロジェクトへの Strawberryの適用 18
Djangoプロジェクトへの適用 ● 業務でのDjango × Strawberry開発の事例を紹介 ○ 既存のDjangoプロジェクトに部分的にGraphQL(Strawberry)+Next.jsを導入し ている。 ■ ○ ● 既存部分はDjango Template フロントエンド開発は別チーム まだ十分検討できいないトピックにも触れる。ぜひフィードバックを頂きたいです 19
Strawberry採用理由 20
Pythonの主なGraphQLライブラリ比較 Graphene Stars 初回リリース Django対応 特徴 7.8k 2015.10 ✅ ● ● 歴史が長い DRFからの移行が容易 記法がシンプル 型ヒントとの親和性 開発が活発 スキーマファースト 実装がシンプル Strawberry 3.4k 2019.5 ✅ ● ● ● Ariadne 2.1k 2018.11 ✅ ● ● 21
PJでのStrawberry選定理由 ● 開発が継続している ● コードファーストである ○ 既存の実装を活かしたい ○ ゼロからスキーマを書くより生産性が高そう 他ライブラリの落選理由: ● ● Graphene: 当時graphene-djangoがDead ProjectだというIssueが立っていた ○ https://github.com/graphql-python/graphene-django/issues/1324 ○ 現在は開発継続中 Ariadne: スキーマファーストなので不採用 22
スキーマ設計のポイント 23
RESTとの設計方針の違い ● 大きなAPIにすることを恐れない ● 関連オブジェクトはネストさせる ● 安易にQueryを定義せず、Typeのフィールドにすることを検討する 24
大きなAPIにすることを恐れない ● クライアント側で取得するフィールドは選択できる。 ○ サーバー側では返せるものは返して、クライアント側で選択させる ○ もちろん秘匿情報のフィールドを返さないよう気をつける class User(models.Model): name = models.CharField(max_length=255) birthday = models.DateField(null=True) password = models.CharField(max_length=255) team = models.ForeignKey( Team, related_name="members", on_delete=models.CASCADE ) @strawberry.django.type(User) class UserType: id: strawberry.auto name: strawberry.auto birthday: strawberry.auto team: strawberry.auto 現状ユースケースがなくても元Modelの全フィールド返しておく 25
関連オブジェクトはネストさせる ● 関連オブジェクトはIDだけ返すのではなく、オブジェクトごと返す ○ 関連オブジェクトのIDを返す→IDをキーにオブジェクトを取得……という二度 手間が発生しないのがGraphQLの強み # NG # OK @strawberry.django.type(User) class UserType: id: strawberry.auto name: strawberry.auto birthday: strawberry.auto team_id: str @strawberry.django.type(User) class UserType: id: strawberry.auto name: strawberry.auto birthday: strawberry.auto team: strawberry.auto # TeamType 26
安易にQueryを定義せず、Typeのフィールドにすることを検討する 例: あるチームのメンバー一覧を取得したい # NG # OK type User { # 省略 } type User { # 省略 } type Team { # 省略 } type Team { # 省略 members: [User!]! # add } type Query { user(id: ID!): User team(id: ID!): Team teamUsers(teamId: ID!): [User!]! # add } type Query { user(id: ID!): User team(id: ID!): Team } 27
安易にQueryを定義せず、Typeのフィールドにすることを検討する ● あるTypeに関する情報はそのTypeにまとめる。 ○ 階層が深くなっても問題ない。何を取るかはクライアント側で選べる ■ ● 「大きいAPIになることを恐れない」 たどり方が複雑な場合はQueryを定義することも検討 28
実装方針 29
PJでのディレクトリ構成イメージ PJ特有のモジュール: /my_django_project/ |-- apps/ |-- myapp |-- graphql |-- input_types.py |-- types.py |-- models.py |-- schema.py |-- urls.py |-- usecases.py |-- values.py |-- urls.py ● input_types.py - Input Type定義 ● types.py - Type定義 ● schema.py - Query, Mutation定義 ● usecases.py - ビジネスロジック ● values.py - Pydanticモデルの定義 30
実装方針 ● Typeをstrawberryで直接定義しない ● GraphQL層に直接ロジックを書かない 31
Typeをstrawberryで直接定義しない ● 基本はDjango Modelをベースに定義する ● Django Modelをそのまま返さない場合、Pydantic Modelをベースに定義 ● →strawberry依存度を下げたい ○ 導入当時、GraphQLライブラリ情勢が不安定だったため、乗り換えの可能性 を考慮 32
Django ModelベースでType生成 ● Modelの構造を受け継いでType定義 class User(models.Model): name = models.CharField(max_length=255) birthday = models.DateField(null=True) password = models.CharField(max_length=255) team = models.ForeignKey( Team, related_name="members", on_delete=models.CASCADE ) Modelと衝突しないよう Model名に”Type”をつけて定義している @strawberry.django.type(User) class UserType: id: strawberry.auto name: strawberry.auto birtyday: strawberry.auto team: strawberry.auto 33
PydanticベースでType生成 ● Django Modelをそのまま返さない場合 ○ Modelとは別の構造で返す・複数Modelを1オブジェクトで返す class UserStatisticsModel(pydantic.BaseModel): name: str birthday: Optional[datetime.date] password: str team: Team commit_count: num # 別テーブルから取得 resolved_issue_count: num # 別テーブルから取得 pydanticのクラスは 〜Modelという名前にしている @strawberry.experimental.pydantic.type(model=UserStatisticsModel, all_fields=True) class UserStatisticsType: pass 34
GraphQL層に直接ロジックを書かない ● GraphQL層: Type, Query, Mutation ○ これらに直接ロジックを書かず、usecases.pyやDjango Modelからのロジックを 呼ぶ ○ 責務の分離、テスト対象の集約(後述) 35
GraphQL層に直接ロジックを書かない # models.py class User(models.Model): name = models.StringField() # models.py class User(models.Model): name = models.StringField() @property def is_long_name(self): return len(self.name) > 10 # types.py @strawberry.django.type(User) class UserType: name: strawberry.auto @strawberry.django.field def is_long_name(self) -> bool: return len(self.name) > 10 # types.py @strawberry.django.type(User) class UserType: name: strawberry.auto is_long_name: strawberry.auto 36
テスト 37
テストについて ● テストの種類 ○ Django Modelのテスト - 制約やpropertyのテスト ○ ビジネスロジックのテスト ○ APIのテスト: 疎通テスト、認証のテスト ■ GraphQL層の挙動はここで担保 38
テストしていないもの ● Type, Query, MutationといったGraphQL層 ○ ロジックを基本的に書かないため、単体テストもかかない。 ○ API疎通テストで挙動を担保 39
Modelのテスト・ビジネスロジックのテスト ※特にGraphQL特有のことはしていない Modelのテスト ● 制約のテスト ● propertyのロジックのテスト ● その他各関数のテスト ビジネスロジックのテスト ● ロジックをGraphQL層から切り離しているので、単体でテストできる 40
APIのテスト ● ● 疎通テスト ○ フィールド全取得テスト ○ 必須フィールドだけ取得するテスト 認証テスト ○ 未ログインでアクセスできないこと ※ロジックの細かいテストは他に任せて、最低限のチェックを実施。 41
APIのテスト (Pytest, Factory Boy) @pytest.mark.django_db class TestGraphQLView: def test_users(client, endpoint): “””usersクエリのテスト””” # FactoryBoyでDBにレコードを追加 team = TeamFactory() user = UserFactory(team=team) # クエリを記述 query = "query { users { id name birthday } }" # ログイン処理・API実行 login(client, user) res = client.post( "/graphql", data={"query": query}, content_type="application/json" ) # レスポンスを確認 assert res.status_code == 200 assert res.json()["data"]["users"] == [ {"id": str(user.id), "name": user.name, "birthday": user.birthday} ] 42
パフォーマンス 43
パフォーマンス(N+1問題対応) ● GraphQLとN+1問題 ○ ● クエリの自由度が高いため、必ずしも最適なSQL呼び出しができない PJでの対応 ○ N+1は発生しているが、特に何もしていない ○ 性能要件的に大きく問題になってない 44
Strawberryで提供されているパフォーマンス対策 ● ● strawberry.dataloader ○ Dataloader: バッチ処理・キャッシュを使った効率化の実装パターン ○ asgi対応する必要がある。PJではwsgiで動かしているので対応できず strawberry_django.optimizer.DjangoOptimizerExtension ○ select_related(), prefetch_related(), only()を使ってよしなに最適化してくれるら しい 45
セキュリティ 46
認証・認可 認証 ● セッション認証。認証用のデコレータを各Query・Mutationに付与 ● ログイン処理自体は別途Djangoのform+Templateで行っている 認可 ● 複雑な権限分けがないので、必要に応じてロジック部分でロール判別処理を実装 ○ 権限が複雑になってきたら、認可のデコレータなどの仕組みが必要になりそ う。 47
悪意のあるクエリの対策 ● 特にやってない。クローズドなAPIなので ○ とはいえ、フロントの不具合で大量のデータを取得するクエリが投げられたり、悪意の あるユーザーからDoS攻撃を受けたり、という懸念はある ● セキュリティ周りのStrawberryのExtention ○ 深さ制限: QueryDepthLimiter ○ トークン数制限: MaxTokensLimiter ○ 複雑度計算はなさそう ○ Extention一覧: https://strawberry.rocks/docs/extensions 48
スキーマ漏洩対策 ● 本番環境では以下を無効にする ○ イントロスペクション: スキーマを取得するクエリ ○ GraphiQL: ブラウザでGraphQLを実行するGUIツール urlpatterns = [ path("graphql/", GraphQLView.as_view(schema=schema, graphiql=False)), ] こう指定すると2つとも無効になる ※デフォルトはTrueなので注意 49
フロントエンドチームとの コミュニケーション 50
「API仕様の共有はスキーマを渡せば済むからラク?」 ● スキーマがある利点 ○ Typeやフィールドの名前・型は共有できる ○ 実装から生成されるので、手動メンテ不要 ■ ● PJでは、バックエンド実装をPushするとCIでスキーマ生成される しかし問題も…… ○ 「このフィールド何?」「このデータはどこで取れる?」みたいなコミュニケー ションは結局発生する ■ GraphQL開発だと、チーム間、実装者間でスキーマ設計に関する考え方や理解 度が違う場合も 51
フロントエンドチームとのコミュニケーション ● スキーマ共有時に、わかりにくそうなフィールドは説明を添える ○ 想定しているクエリ例を渡したり ● 必要に応じてスキーマにコメントを ● ユビキタス言語を設定しておく(日本語名・英語名) ○ 随時メンテしていく。新しい用語が増えたら英語名を確認 52
おわりに 53
まとめ ● GraphQL開発にはRESTとは違う悩み事も多い。 ○ ● Strawberry、使ってみてください! ○ ● スキーマ設計・パフォーマンス・セキュリティ https://strawberry.rocks/ Django × GraphQLサーバー開発をしたい/している方の参考になっていれば幸い です。 ○ ぜひご意見ご感想をお願いします。 54
おすすめ資料 ● ● GraphQL 公式ドキュメント ○ https://graphql.org/learn/ ○ 入門資料、各仕様、ベストプラクティス 初めてのGraphQL ○ https://www.oreilly.co.jp/books/9784873118932/ ○ 基礎理論や仕様の紹介、ハンズオン 55
おすすめ資料 ● ● Production Ready GraphQL ○ https://book.productionreadygraphql.com/ ○ GraphQLの設計・運用に関するベストプラクティス ○ 社内で多くの人が読んでいた WEB+DB PRESS Vol.135 - 実務レベルのGraphQL導入ガイド ○ https://gihyo.jp/dp/ebook/2023/978-4-297-13572-0 ○ DjangoでのStrawberry導入手順 ○ 同じPJのdelhi09さんが書いた記事 56
ご清聴ありがとうございました! We Are Hiring ! Pythonエンジニア https://www.beproud.jp/careers/python/ カジュアル面談 https://onl.tw/LPVc2hd 57