GraphQL in production

2df5079e89d2e51aeb9203b3410ea61b?s=47 Ryo.Nitami
March 03, 2018

GraphQL in production

2018/03/03 YAPC::Okinawa 2018 ONNASON

2df5079e89d2e51aeb9203b3410ea61b?s=128

Ryo.Nitami

March 03, 2018
Tweet

Transcript

  1. 2.

    ࣗݾ঺հ — ਔଟݟ ྒྷʢʹͨΈ Γΐ͏ʣ @bird_tummy — Android, iOS application

    developer @ CyberAgent, Inc. — ❤ SHISHAMO ! ! ! YAPC::Okinawa 2018 ONNASON 2
  2. 4.

    ࠓ೔࿩͢͜ͱ — ࡢ೥͔ΒΑ͘ݟ͔͚ΔΑ͏ʹͳͬͨ GraphQL — GitHub API v4 Ͱ࠾༻͞Εͨͷ͕͖͔͚ͬͷ 1

    ͭ — ͳ͔ͳ͔࣮૷पΓͷ஌ݟ͕޿·͍ͬͯͳ͍Α͏ʹࢥ͏ ʢࢼͨ͠ܥهࣄ͸͋Δ͚Ͳɻɻʣ YAPC::Okinawa 2018 ONNASON 4
  3. 7.

    ΞδΣϯμ — ͦ΋ͦ΋ GraphQL ͱ͸ʁ — ಋೖฤ — GraphQL ͷ։ൃ؀ڥ

    — ࣮૷ฤ — GraphQL ͷαʔό / ΫϥΠΞϯτͦΕͧΕͷ࣮૷ํ๏ — ӡ༻ฤ — Ωϟογϡ౳ͷύϑΥʔϚϯε໘ʹ͍ͭͯ — ςετͷॻ͖ํ — ·ͱΊ YAPC::Okinawa 2018 ONNASON 7
  4. 9.

    GraphQL ͱ͸ʁ — http://graphql.org — Facebook ੡ — API ༻ͷΫΤϦݴޠͰ͋Γɺطଘ

    ͷσʔλͰΫΤϦΛ࣮ߦ͢ΔͨΊ ͷϥϯλΠϜ YAPC::Okinawa 2018 ONNASON 9
  5. 11.

    GraphQL ͱ͸ʁ — Mutation ͱ Query ͕͋Δ — Mutation: REST

    API Ͱ͍͏ POST, PUT, DELETE — Query: REST API Ͱ͍͏ GET — Fragment Ͱಉ͡ఆٛΛ࢖͍·ΘͤΔ YAPC::Okinawa 2018 ONNASON 11
  6. 17.

    GraphiQL (GraphiQL Feen!) — https://github.com/graphql/graphiql — ϒϥ΢βͰಈ͘ — https://github.com/skevy/graphiql-app —

    Electron ੡ — https://github.com/bpatters/graphiql-feen — Chrome Extension YAPC::Okinawa 2018 ONNASON 17
  7. 20.

    ಋೖฤ — υΩϡϝϯτͱ࢓༷Λݟͳ͕Βɺ֤ Mutation ΍ Query Ͱ ΄͍͠΋ͷ͚ͩఆٛ͢Δ — .graphql,

    .gql — ఆٛ͢Δͱ͖ʹิ׬͕͖͍ͨঢ়ଶͰ΍Γ͍ͨ — ֤ΤσΟλͷϓϥάΠϯ͕࡞ΒΕ͍ͯΔ YAPC::Okinawa 2018 ONNASON 20
  8. 24.

    { "schema": { "file": "graphql.schema.json", "request": { "url" : "http://--graphql-server--/path-to-schema-json-or-introspection-endpoint",

    "method" : "POST", "postIntrospectionQuery" : true, "README_options" : "See the 'Options' section at https://github.com/then/then-request", "options" : { "headers": { "user-agent" : "JS GraphQL" } } } }, "README_endpoints": "A list of GraphQL endpoints that can be queried from '.graphql' files in the IDE", "endpoints" : [ { "name": "Default (http://localhost:8080/graphql)", "url": "http://localhost:8080/graphql", "options" : { "headers": { "user-agent" : "JS GraphQL" } } } ] } YAPC::Okinawa 2018 ONNASON 24
  9. 26.

    ಋೖฤ·ͱΊ — GraphiQL Λ࢖͑͹खݩͰࢼͤΔ — graphql ϑΝΠϧʹฦΓ஋ͷఆٛΛॻ͕͘ɺॻ͘ࡍͷαϙʔτΛͯ͘͠ΕΔΤσΟλ ͷϓϥάΠϯ͕ఏڙ͞Ε͍ͯΔ — vscode

    — Intellij — etc. — ࠓͷͱ͜Ζ࢖͍ͬͯΔϓϥάΠϯͰࠔ͍ͬͯΔ͜ͱ͸ͳ͍͕ɺ͜Ε͕͋ͬͨΒͳʔͱ ͍͏ͱ͜Ζ͕গ͋͠Δ — graphql.config.json ͷ؅ཧ͕೉͍͠ — ߋ৽͕ࢭ·͍͍ͬͯͯͭ࢖͑ͳ͘ͳΔ͔Θ͔Βͳ͍ YAPC::Okinawa 2018 ONNASON 26
  10. 28.

    αʔόαΠυ — apollo-server — https://github.com/apollographql/apollo-server — Լهͷத͔Β޷͖ͳ΋ͷΛબΜͰ࢖͏͜ͱ͕Ͱ͖Δ — Express —

    koa — hapi — restify — lambda — micro — azure-functions — adonis YAPC::Okinawa 2018 ONNASON 28
  11. 29.

    apollo-server-koa import koa from 'koa'; // koa@2 import koaRouter from

    'koa-router'; // koa-router@next import koaBody from 'koa-bodyparser'; // koa-bodyparser@next import { graphqlKoa, graphiqlKoa } from 'apollo-server-koa'; const app = new koa(); const router = new koaRouter(); const PORT = 3000; // koaBody is needed just for POST. router.post('/graphql', koaBody(), graphqlKoa({ schema: myGraphQLSchema })); router.get('/graphql', graphqlKoa({ schema: myGraphQLSchema })); router.get('/graphiql', graphiqlKoa({ endpointURL: '/graphql' })); app.use(router.routes()); app.use(router.allowedMethods()); app.listen(PORT); YAPC::Okinawa 2018 ONNASON 29
  12. 31.

    @nuxtjs/apollo import { ApolloLink } from 'apollo-link' import { HttpLink

    } from 'apollo-link-http' import { InMemoryCache } from 'apollo-cache-inmemory' export default (ctx) => { const httpLink = new HttpLink({ uri: 'http://localhost:8000/graphql' }) // middleware const middlewareLink = new ApolloLink((operation, forward) => { const token = process.server ? ctx.req.session : window.__NUXT__.state.session operation.setContext({ headers: { authorization: `Bearer ${token}` } }) return forward(operation) }) const link = middlewareLink.concat(httpLink) return { link, cache: new InMemoryCache() } } YAPC::Okinawa 2018 ONNASON 31
  13. 33.

    apollo-ios let queryString = "GraphQL" let configuration: URLSessionConfiguration = .default

    configuration.httpAdditionalHeaders = ["Authorization": "Bearer \(token)"] configuration.requestCachePolicy = .reloadIgnoringLocalCacheData // To avoid 412 let url = URL(string: "https://api.github.com/graphql")! let apollo = ApolloClient(networkTransport: HTTPNetworkTransport(url: url, configuration: configuration)) apollo.fetch(query: SearchRepositoriesQuery(query: queryString, count: 2), completionHandler: { (result, error) in if let error = error { print("Error: \(error)"); return } result?.data?.search.edges?.forEach { edge in guard let repository = edge?.node?.asRepository else { return } print("Name: \(repository.name)") print("Path: \(repository.url)") print("Owner: \(repository.owner.path)") print("Stars: \(repository.stargazers.totalCount)") } }) YAPC::Okinawa 2018 ONNASON 33
  14. 35.

    apollo-android private void fetchRepositoryDetails() { ApolloCall<EntryDetailQuery.Data> entryDetailQuery = application.apolloClient() .query(new

    EntryDetailQuery(repoFullName)) .responseFetcher(ApolloResponseFetchers.CACHE_FIRST); //Example call using Rx2Support disposables.add(Rx2Apollo.from(entryDetailQuery) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribeWith(new DisposableObserver<Response<EntryDetailQuery.Data>>() { @Override public void onNext(Response<EntryDetailQuery.Data> dataResponse) { setEntryData(dataResponse.data()); } @Override public void onError(Throwable e) { Log.e(TAG, e.getMessage(), e); } @Override public void onComplete() { } })); } YAPC::Okinawa 2018 ONNASON 35
  15. 38.

    apollo-codegen public struct AsRepository: GraphQLConditionalFragment { public static let possibleTypes

    = ["Repository"] public let __typename = "Repository" public let name: String public let owner: Owner public let stargazers: Stargazer public let url: String public init(reader: GraphQLResultReader) throws { name = try reader.value(for: Field(responseName: "name")) owner = try reader.value(for: Field(responseName: "owner")) stargazers = try reader.value(for: Field(responseName: "stargazers")) url = try reader.value(for: Field(responseName: "url")) } public struct Owner: GraphQLMappable { public let __typename: String public let path: String public init(reader: GraphQLResultReader) throws { __typename = try reader.value(for: Field(responseName: "__typename")) path = try reader.value(for: Field(responseName: "path")) } } public struct Stargazer: GraphQLMappable { public let __typename = "StargazerConnection" public let totalCount: Int public init(reader: GraphQLResultReader) throws { totalCount = try reader.value(for: Field(responseName: "totalCount")) } } } YAPC::Okinawa 2018 ONNASON 38
  16. 39.

    ࣮૷ฤ·ͱΊ — apollo ʹײँ ! — ͍͍ͩͨͷݴޠ͕αϙʔτ͞Ε͍ͯΔ — Perl ͸ͳ͔ͬͨ

    (´ɾТɾʆ) — Shopify Ͱ࢖ΘΕ͍ͯͯɺͦͷ։ൃऀ͕ϝϯςφϯε͍ͯ͠ ΔͷͰ҆৺ — ࡶͳ issue ΋ड͚෇͚ͯ͘ΕΔͷͰػೳͷཁ๬ͱ͔౤͛ͯ ΈΔͱ͍͍͔΋ YAPC::Okinawa 2018 ONNASON 39
  17. 42.

    ӡ༻ฤ — ΩϟογϡͬͯͲ͏ͳΔͷʁ — apollo-android Ͱ͸ 3 ͭͷํ๏Λαϙʔτ — HTTP

    Response Cache: ੜͷϨεϙϯεΛϑΝΠϧʹ͠ ͯΩϟογϡ — Normalized Disk Cache: SQL ʹಥͬࠐΜͰΩϟογϡ — Normalized InMemory Cache: ϝϞϦʹͭͬ͜ΜͰΩ ϟογϡ YAPC::Okinawa 2018 ONNASON 42
  18. 43.

    // HTTP Response Cache //Directory where cached responses will be

    stored File file = new File("/cache/"); //Size in bytes of the cache int size = 1024*1024; //Create the http response cache store DiskLruHttpCacheStore cacheStore = new DiskLruCacheStore(file, size); //Build the Apollo Client ApolloClient apolloClient = ApolloClient.builder() .serverUrl("/") .httpCache(new ApolloHttpCache(cacheStore)) .okHttpClient(okHttpClient) .build(); apolloClient .query( FeedQuery.builder() .limit(10) .type(FeedType.HOT) .build() ) .httpCachePolicy(HttpCachePolicy.CACHE_FIRST) .enqueue(new ApolloCall.Callback<FeedQuery.Data>() { @Override public void onResponse(@Nonnull Response<FeedQuery.Data> dataResponse) { ... } @Override public void onFailure(@Nonnull Throwable t) { ... } }); YAPC::Okinawa 2018 ONNASON 43
  19. 44.

    ӡ༻ฤ — CACHE_ONLY — Ωϟογϡ͚ͩݟʹߦ͘ɻAPI ୟ͍ͯऔಘ͠ʹ͍͔ͳ͍ɻΩϟογϡ͕ଘࡏ͠ͳ͔ͬͨ ͱ͖͸ΤϥʔʹͳΔ — NETWORK_ONLY —

    ΩϟογϡΛݟʹߦ͔ͣʹຖճ API Λୟ͍ͯऔಘ — CACHE_FIRST — ΩϟογϡΛ·ͣୈҰʹݟʹߦ͘ɻΩϟογϡ͕ͳ͔ͬͨΓͨ͠ͱ͖͸ API Λୟ͖ʹߦ ͘ — NETWORK_FIRST — API Λ·ͣୈҰʹୟ͖ʹߦ͘ɻAPI ϨεϙϯεͰΤϥʔ͕ฦ͖ͬͯͯɺΩϟογϡ͕·ͩ ࢒͍ͬͯΕ͹ͦΕΛ࢖͏ YAPC::Okinawa 2018 ONNASON 44
  20. 46.

    ӡ༻ฤ — ςετͷॻ͖ํ — UI ςετ͸جຊ΍Βͳ͍ — ωοτϫʔΫΤϥʔͷςετ — ಛఆͷ

    Type ʹͪΌΜͱύʔεͰ͖Δ͔Ͳ͏͔ YAPC::Okinawa 2018 ONNASON 46
  21. 47.

    @Test public void onNetworkError() throws Exception { final CountDownLatch countDownLatch

    = new CountDownLatch(1); final AtomicBoolean invoked = new AtomicBoolean(); final Handler callbackHandler = mockCallbackHandler(invoked); final AtomicReference<ApolloException> exceptionRef = new AtomicReference<>(); apolloClient.query(EMPTY_QUERY).enqueue(ApolloCallback.wrap(new ApolloCall.Callback() { @Override public void onResponse(@Nonnull Response response) { countDownLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { countDownLatch.countDown(); } @Override public void onNetworkError(@Nonnull ApolloNetworkException e) { exceptionRef.set(e); countDownLatch.countDown(); } }, callbackHandler)); countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(invoked.get()).isTrue(); assertThat(exceptionRef.get()).isInstanceOf(ApolloNetworkException.class); } YAPC::Okinawa 2018 ONNASON 47
  22. 48.

    @Test public void onResponse() throws Exception { final CountDownLatch countDownLatch

    = new CountDownLatch(1); final AtomicBoolean invoked = new AtomicBoolean(); final Handler callbackHandler = mockCallbackHandler(invoked); final AtomicReference<Response> responseRef = new AtomicReference<>(); server.enqueue(new MockResponse().setResponseCode(200).setBody("{" + " \"errors\": [" + " {" + " \"message\": \"Cannot query field \\\"names\\\" on type \\\"Species\\\".\"," + " \"locations\": [" + " {" + " \"line\": 3," + " \"column\": 5" + " }" + " ]" + " }" + " ]" + "}")); apolloClient.query(EMPTY_QUERY).enqueue(ApolloCallback.wrap(new ApolloCall.Callback() { @Override public void onResponse(@Nonnull Response response) { responseRef.set(response); countDownLatch.countDown(); } @Override public void onFailure(@Nonnull ApolloException e) { countDownLatch.countDown(); } }, callbackHandler)); countDownLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertThat(invoked.get()).isTrue(); assertThat(responseRef.get()).isNotNull(); } YAPC::Okinawa 2018 ONNASON 48
  23. 49.

    ·ͱΊ — GraphQL ͷ࣮૷ʹ͍ͭͯ͝঺հ͠·ͨ͠ — ಋೖ͸ϥΠϒϥϦͷ README ͕੔͍ͬͯΔ͓͔͛ͰָʹՄೳ — ࣮૷ɾӡ༻͸ϥΠϒϥϦʹࠨӈ͞ΕΔ

    — ֤ϓϥοτϑΥʔϜͰศརͳϥΠϒϥϦ͕ἧ͍ͬͯΔͨΊɺී௨ʹ࣮ ૷͸Ͱ͖ͦ͏ — ·ͩ·ͩࢀߟهࣄ͕গͳ͘ɺͪΐͬͱ͸·Δͱݫ͍͠ͱ͖͕͋Δ — ͔͜͜Β੝Γ্͕͖ͬͯͯɺ஌ݟ͕ͨ͘͞ΜΞ΢τϓοτ͞ΕΔͷ Λظ଴ʂ YAPC::Okinawa 2018 ONNASON 49