點燈坊

失くすものさえない今が強くなるチャンスよ

使用 Apollo GraphQL 連接 MySQL

Sam Xiao's Avatar 2019-11-19

GraphQL 定位為 API Query Language,本質只是 Middleware 負責 Parse GraphQL,最終還是得搭配資料庫,本文以 Apollo GraphQL + Knex + MySQL 為例。

Version

macOS Catalina 10.15.1
WebStorm 2019.2.4
MySQL 8.0.18
Node 13.1.0
Knex 0.20.2
Apollo GraphQL 2.9.6

MySQL

mysql/docker-compose.yml

version: "3"

services:
  mysql:
    image: mysql:latest
    container_name: MySQL
    volumes:
      - ${MYSQL_HOST_DIR}:/var/lib/mysql
    ports:
      - ${MYSQL_PORT}:3306
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
    command: --default-authentication-plugin=mysql_native_password
    restart: always

使用 Docker 執行 MySQL,優點是每個 project 都可有自己的 MySQL 與 database,而不必如傳統所有 project 都在同一個 MySQL。

11 行

environment:
  - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}

MySQL 8 的 authentication 從原本 mysql_native_password 改成 caching_sha2_password,這導致 Knex 無法連上 MySQL 8 以上,特別在 command 加上 --default-authentication-plugin=mysql_native_password,命令 MySQL 8 以 mysql_native_password 方式啟動。

mysql/.env

MYSQL_HOST_DIR=.mysql
MYSQL_PORT=3306
MYSQL_ROOT_PASSWORD=demo

設定 MySQL 環境變數。

Install Package

$ yarn add apollo-server graphql
$ yarn add knex mysql
  • Apollo Server 安裝 apollo-servergraphql
  • Knex 安裝 knexmysql

src/index.js

import { ApolloServer, gql } from 'apollo-server'
import knex from 'knex'

let mySQL = knex({
  client: 'mysql',
  connection: {
    host: process.env.MYSQL_HOST || 'localhost',
    port: process.env.MYSQL_PORT || 3306,
    user: 'root',
    password: process.env.MYSQL_ROOT_PASSWORD || 'demo',
    database: 'Bookshelf'
  }
})

let typeDefs = gql`
  type Query {
    books(category: BookCategory!): [Book]
  }

  type Book {
    title: String
    category: BookCategory
  }

  enum BookCategory {
    FP
    FRP
    JS
  }
`

let books = (_, { category }) => mySQL('books')
  .select()
  .where({
    'category': category
  })

let resolvers = {
  Query: {
    books
  }
}

let apolloServer = new ApolloServer({ typeDefs, resolvers })

apolloServer.listen()
  .then(({ url }) => `GraphQL Server ready at ${url}`)
  .then(console.log)

第 1 行

import { ApolloServer, gql } from 'apollo-server'

apollo-server 引入 AplloServergql

第 2 行

import knex from 'knex'

knex 引入 knex

第 4 行

let mySQL = knex({
  client: 'mysql',
  connection: {
    host: process.env.MYSQL_HOST || 'localhost',
    port: process.env.MYSQL_PORT || 3306,
    user: 'root',
    password: process.env.MYSQL_ROOT_PASSWORD || 'demo',
    database: 'Bookshelf'
  }
})

knex() 為 higher order function,透過傳入 configuration object,建立 mySQL()

Knex 支援 Postgres、MSSQL、MySQL、MariaDB、SQLite3 與 Oracle,可在 client 指定。

connection 則指定連線資訊。

15 行

let typeDefs = gql`
  type Query {
    books(category: BookCategory!): [Book]
  }

  type Book {
    title: String
    category: BookCategory
  }

  enum BookCategory {
    FP
    FRP
    JS
  }
`

使用 typeDefs 定義 GraphQL schema。

38 行

let resolvers = {
  Query: {
    books
  }
}

resolvers 內宣告 books query。

32 行

let books = (_, { category }) => mySQL('books')
  .select()
  .where({
    'category': category
  })

實作 books query,在此調用 Knex 存取 MySQL,其中 Knex 回傳為 promise。

由於 GraphQL 是 API query language,因此在其 resolver 可使用任意 ORM 或 query builder,只是本文使用了 Knex

44 行

let apolloServer = new ApolloServer({ typeDefs, resolvers })

apolloServer.listen()
  .then(({ url }) => `GraphQL Server ready at ${url}`)
  .then(console.log)

啟動 Apollo GraphQL。

GraphQL Playground

mysql000

Conclusion

  • 與原本 Express 的 REST API 比較,GraphQL API 嚴格來說只多了 schema,resolver 使用 ORM 是本來就要寫的,因此後端要多的功並沒有想像中多
  • 至於 ORM 或 query builder 可依個人喜好選擇,Knex 的寫法很精簡,可讀性亦高,設定好 connection 就可使用,不需再定義額外 model

Sample Code

完整範例可在我的 GitHub 上找到

Reference

Apollo Docs, Connet a Database
Knex, Installation