$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

GraphQLのクエリについて学んだ

Apollo Clientについて学ぼうと思い、0. Introduction - Apollo Basics - Apollo GraphQL Docsをやっていた。しかし、これをやる中でGraphQLのクエリ言語についてあまり分かってないことに気づいたので、Queries and Mutations | GraphQLを見て、クエリについて学習した。

Fragment、queryの命名、変数を使ってRDBプレースホルダー的なことをする方法、ifの利用などが分かってよかった。以下メモ書き。

  • Fragmentを用いてクエリの再利用ができる
    • 引数名を用いてFragmentに変数を引き継ぐこともできる
query HeroComparison($first: Int = 3) {
  leftComparison: hero(episode: EMPIRE) {
    ...comparisonFields
  }
  rightComparison: hero(episode: JEDI) {
    ...comparisonFields
  }
}

fragment comparisonFields on Character {
  name
  friendsConnection(first: $first) {
    totalCount
    edges {
      node {
        name
      }
    }
  }
}
  • queryなどには命名ができ、そうすることでサーバ上でログなどが取りやすくなる
query HeroNameAndFriends {
  hero {
    name
    friends {
      name
    }
  }
}
  • 動的に値を変えるために一部分を変数にしておいて、分割して変数だけ渡すこともできる
    • default valueもつけられる
query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }

以下のようにvariablesを指定する

{
  "episode": "JEDI"
}
  • @include(if: Boolean)や@skip(if: Boolean)で、条件付きで取得するクエリも作れる
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
  • 複数クエリを発行したとき、queryは並列実行されるが、mutationは直列実行される

Hatena-Textbook 2018学習日記(5) - GraphQL編

Hatena-Textbook 2018 学習日記(4) - $shibayu36->blog;の続き。

https://github.com/shibayu36/go-Intern-Diary/pull/3 で、GraphQLを使って以下の操作ができるように実装した。

  • 現在のログインユーザーの取得
  • user_id指定でユーザーを取得
  • diary_id指定でダイアリーを取得
  • nameを指定して現在のログインユーザーのダイアリーを作成

こんなクエリが書けるようになった

query {
  visitor {
    id
    name
    diaries {
      id
    }
  }
}
query {
  user(userId: "26351446802825216") {
    id
    name
  }
}
query {
  diary(diaryId: "26352158811095040") {
    id
    name
  }
}
mutation {
  createDiary(name: "shibayu36 graphql") {
    id
    name
  }
}

DataLoaderという仕組みを用いたDBクエリ削減

DataLoaderという仕組みがあまり理解できてなかったので、やってみた。

まとめて取得する部分の実装がやや複雑だが、それを実装してしまえば

data, err := ldr.Load(ctx, userIDKey{id: id})()

のように、Key-Value Storeのようなシンプルな取得の仕方をしてもいい感じにまとめてとってきてくれるようになる。github.com/graph-gophers/dataloader がどういう実装になっているか気になるが、今回はそこまで調べなかった。

DBクエリの削減効果

実装前と実装後のクエリの変化を見たかったので、github.com/walf443/go-sql-tracerを利用して簡単にチェックしてみた。

以下のようなひどいクエリを投げたときに

query {
  visitor {
    id
    name
    diaries {
      id
      name
      user {
        id
        name
        diaries {
          id
          name
          user {
            diaries {
              name
            }
          }
        }
      }
    }
  }
}

DataLoaderの仕組みを使う前は次のログが流れ、大量のSQLが発行され、かつ同じデータなのに何度も取得してしまっている。

app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id,name FROM user JOIN user_session
app_1   |                       ON user.id = user_session.user_id
app_1   |                               WHERE user_session.token = ? && user_session.expires_at > ?
app_1   |                               LIMIT 1; args = ["abTQRlcvAk8LIYZDk0YX9Kd5bZUGZHEXsAjvy4d84405nNLJsWKt5IoaZt2PpJev5oEwOjWZLda2bFPyc76VmJXXZZ47S1Tid8yXwy3OVpOmNptauv0sikNY@lRZcf8M", time.Time{wall:0xbf60130bdb2204dc, ext:220321424901, loc:(*time.Location)(0xae5a00)}]; err = "driver: skip fast-path; continue as if unimplemented" (22.3µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id,name FROM user JOIN user_session
app_1   |                       ON user.id = user_session.user_id
app_1   |                               WHERE user_session.token = ? && user_session.expires_at > ?
app_1   |                               LIMIT 1; args = ["abTQRlcvAk8LIYZDk0YX9Kd5bZUGZHEXsAjvy4d84405nNLJsWKt5IoaZt2PpJev5oEwOjWZLda2bFPyc76VmJXXZZ47S1Tid8yXwy3OVpOmNptauv0sikNY@lRZcf8M", time.Time{wall:0xbf60130bdb2204dc, ext:220321424901, loc:(*time.Location)(0xae5a00)}] (788.5µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (20.5µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (16.5µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (601.1µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (17.6µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (17µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (2.3747ms)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (33.2µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (19.5µs)
app_1   | 2019/10/11 01:02:39 Open 0xc4202a20c0 (4.9426ms)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (16.6µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (567.7µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a20c0 (16.3µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (22µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (2.3667ms)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (88.9µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (17.4µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (450.6µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (1.1051ms)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (18µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a20c0 (15.9µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (17.9µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (18.2µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (342.9µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (652.4µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a20c0 (15µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (16.2µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (21.2µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (392.2µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a20c0 (16.1µs)
app_1   | 2019/10/11 01:02:39 Open 0xc4201563c0 (5.1434ms)
app_1   | 2019/10/11 01:02:39 Query 0xc4201563c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (19.1µs)
app_1   | 2019/10/11 01:02:39 Open 0xc420156540 (5.6406ms)
app_1   | 2019/10/11 01:02:39 Query 0xc420156540: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (19.8µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4201563c0: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (608.5µs)
app_1   | 2019/10/11 01:02:39 Close 0xc4201563c0 (124µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (21.8µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a2300: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (536.7µs)
app_1   | 2019/10/11 01:02:39 Query 0xc420156540: SELECT id, name FROM user
app_1   |                       WHERE id = ? LIMIT 1; args = [26351446802825216] (1.5906ms)
app_1   | 2019/10/11 01:02:39 Close 0xc420156540 (87.8µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a2300 (15.2µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (17.8µs)
app_1   | 2019/10/11 01:02:39 Query 0xc4202a20c0: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id = ?
app_1   |                       ORDER BY created_at DESC; args = [26351446802825216] (545.2µs)
app_1   | 2019/10/11 01:02:39 ResetSession 0xc4202a20c0 (17.1µs)

一方同一のGraphQLクエリでも、DataLoaderの仕組みを利用すると、WHERE INでまとめて取得し、かつメモリにキャッシュしてくれているので何度も同じDBクエリを発行しなくなった。まあ今回は引数が一つしかないのでWHERE INの効果は少ないが、キャッシュによって同一のデータを取得しなくても済むのは大きい。

app_1   | 2019/10/11 14:05:44 Query 0xc4202c4000: SELECT id,name FROM user JOIN user_session
app_1   |                       ON user.id = user_session.user_id
app_1   |                               WHERE user_session.token = ? && user_session.expires_at > ?
app_1   |                               LIMIT 1; args = ["rOCbpziPUOEoZznWVe2C64J5Jz@TlC_iNG0KkrnHyfSarXz12y0_aPNpHOuBf1UMOhp5N4J1eBAi0lDfC14xJ@YAwaiTVZa@m_doAKium9ZlpL3aLhzRcYQOy8YexKUk", time.Time{wall:0xbf6040ee23e6db60, ext:31234792601, loc:(*time.Location)(0xaf4a00)}] (1.2528ms)
app_1   | 2019/10/11 14:05:44 ResetSession 0xc4202c4000 (45.5µs)
app_1   | 2019/10/11 14:05:44 Query 0xc4202c4000: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id IN (?); args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (17µs)
app_1   | 2019/10/11 14:05:44 Query 0xc4202c4000: SELECT id, user_id, name FROM diary
app_1   |                       WHERE user_id IN (?); args = [26351446802825216] (584.4µs)
app_1   | 2019/10/11 14:05:44 ResetSession 0xc4202c4000 (21µs)
app_1   | 2019/10/11 14:05:44 Query 0xc4202c4000: SELECT id,name FROM user
app_1   |                       WHERE id IN (?); args = [26351446802825216]; err = "driver: skip fast-path; continue as if unimplemented" (20µs)
app_1   | 2019/10/11 14:05:44 Query 0xc4202c4000: SELECT id,name FROM user
app_1   |                       WHERE id IN (?); args = [26351446802825216] (517.9µs)
app_1   | 2019/10/11 14:05:44 ResetSession 0xc4202c4000 (600.2µs)

React学習メモ

最近のWeb開発わからん...って思って勉強してる。Reactは公式のチュートリアルやドキュメントがわかりやすく、そちらを進めると入門しやすかった。

チュートリアルやったレポジトリ

https://github.com/shibayu36/react-tutorial/tree/84f44577d4bc29efe41ca0ef147092ca6d04233b

今のところ、写経した + eslintの設定してみたくらい。

メモ

  • Getting Started – React
    • ひとまずチュートリアルを終えた。1ページのアプリケーションを作る概念は理解したつもり。eslintを使ってみるとかした。一方、どのように綺麗にコンポーネント設計するか、テストをどうするか、複数ページのspaをどう作るかなど、まだ腹落ちしていないところは多い。次はmain conceptあたりを流すとよいか。
  • React の流儀 – React
    • Reactを使った設計においては以下のような手順で作っていくと良い?
      • モックを作り
      • コンポーネントの階層構造を考え、一度静的なバージョンを作成する
      • どのような状態があるか決定し、どのようなアクションで状態が変化するか決定する
      • 状態を持たせるコンポーネントにstateを作成
      • アクションに対応する関数を用意し、その中で状態を変更する。アクションが実際に行われるコンポーネントまで引き渡す

今後

もうちょっと手を動かしてみるつもり。やりたいこととしては