Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9042

TypeGraphQLでN+1問題を解決した話

$
0
0

はじめに

GraphQLをサービスで使い始めて、N+1問題にぶち当たったのでその解決策を紹介する。

プロジェクト構成

  • Node.js
  • TypeScript
  • Express
  • GraphQL(Apollo, TypeGraphQL)

実際何が起こったか

DBにはとあるレコードが入っており、それぞれにuseridを保持している。
useridからユーザー名やメアドなどのユーザー情報を取り出すには、別の内部APIに問い合わせる必要がある。

GraphQLのスキーマはこのように定義している。

schema.gql
typeQuery{record(id:Int!):Recordrecords(name:String):[Record!]!}typeRecord{id:Int!name:String!user:Useruserid:Int!}typeUser{userid:Int!username:String!}

レコードはこのように取得しているとする。

RootResolver.ts
@Resolver()exportclassRootResolver{@Query(returns=>[Record])asyncrecords():Promise<Record[]>{constrecords=awaitconn.query(`
            SELECT
                id,
                name,
                userid
            FROM
                xxx
        `);returnrecords;}@Query(returns=>Record,{nullable:true})asyncrecord(@Arg('id',type=>Int)id:number):Promise<Record|undefined>{constrecords=awaitconn.query(`
            SELECT
                id,
                name,
                userid
            FROM
                xxx
            WHERE
                id = ?
        `,[id]);returnrecords[0];}}

このとき、N+1問題を気にせずにuserリゾルバを書くことこのようになる。
fetchUsersは内部APIにリクエストを送ってユーザー情報を返す関数とする。

@Resolver(of=>Record)classRecordResolver{@FieldResolver()user(@Root()record:Record){returnfetchUsers([record.userid])[0];}}

例えばRecordを1件だけ取得する場合は、内部APIへのリクエストは1回で済むが、
一覧画面などで100件取得する場合はfetchUsersがほぼ同時に100回呼ばれることとなり、内部APIサーバーやDBの負荷が上がってしまう。

10件取得した場合のログ

fetchUsers(0)
fetchUsers(1)
fetchUsers(2)
fetchUsers(3)
fetchUsers(4)
fetchUsers(5)
fetchUsers(6)
fetchUsers(7)
fetchUsers(8)
fetchUsers(9)

改善方法

リゾルバで即座にリクエストを送るのではなく、問い合わせたいIDを溜めて、バッチ処理で一つのリクエストに複数IDを載せて送ることでリクエストの量を削減させる。
※この場合、内部APIの方を複数IDに対応させる必要がある。

DataLoaderTypeGraphQL-DataLoaderを使うことでこれを簡単に実現できる。
DataLoaderは遅延読み込みをするためのFacebook製のライブラリで、TypeGraphQL-DataLoaderはDataLoaderをTypeGraphQLに適用させたライブラリである。

組み込み方法

ライブラリをインストールする。

npm i -S dataloader type-graphql-dataloader

プラグインを読み込む。

constserver=newApolloServer({schema:awaitmakeSchema(),validationRules:[depthLimit(7)],plugins:[// これを追加ApolloServerLoaderPlugin({}),]});

userリゾルバをこのように修正する。

@Resolver(of=>Record)classRecordResolver{@FieldResolver()@Loader<number,User|undefined>(async(ids)=>{constusers=awaitfetchUsers([...ids]);returnids.map((id)=>users.find((user)=>user.userid===id));})user(@Root()record:Record){returnasync(dataloader:DataLoader<number,User|undefined>)=>{constuser=awaitdataloader.load(record.userid);returnuser;};}}

10件取得するとこのようなログになる。

fetchUsers(0,1,2,3,4,5,6,7,8,9)

おわりに

N+1問題は気づかずにDBや他のサーバーに負荷をかけてしまう可能性があるので注意して設計してほしい。
DataLoaderを使えば、意外と簡単に改善できるのでこれからも活用していきたい。


Viewing all articles
Browse latest Browse all 9042

Trending Articles