【Next.js】GCPのデータベースをNext.jsプロジェクトに組み込みたい

こちらではGCPGoogle Cloud Platform)をNext.jsのプロジェクトに組み込む方法を解説していきます。

Next.jsプロジェクトの立ち上げ

まずはNext.jsのプロジェクトを立ち上げましょう。

mkdir myproject
cd myproject
npm init

GCPでデータベースの作成

GCPへアクセスし、[コンソールへ移動]のボタンを押しましょう。
次に

インスタンスを作成]ボタン > MySQL 

と進み、インスタンスの詳細設定を行なっていきます。
f:id:nekorokkekun:20190920081724p:plain

インスタンス名やパスワードは特に決まりがありません。
またリージョンに関しては、東京の場合、「asia-southeast1」となります。
ゾーンは任意で構いません。

GCPシェルからデータベースの作成

次にGCPの画面上部にあるシェルボタンから、シェルプロンプトを起動します。
一番左にあるアイコンですね。
f:id:nekorokkekun:20190920082115p:plain

シェルのインストールが終わると起動できるようになるので、起動しましょう。

シェルに以下のようなメッセージが表示されるので、

Welcome to Cloud Shell! Type "help" to get started.
Your Cloud Platform project in this session is set to {YOUR_GCP_PROJECT}.
Use “gcloud config set project [PROJECT_ID]” to change to a different project.

以下のようにコマンドを打ちます。

gcloud sql connect インスタンス名 --user=root

インスタンス名の部分には、先ほど作成したインスタンスの名前を入れましょう。
次にパスワードが聞かれるので、こちらも先ほど設定したパスワードを入れましょう。

すると以下のようなメッセージが表示され、MySQLの操作が可能となります。

Whitelisting your IP for incoming connection for 5 minutes...done.
Connecting to database with SQL user [root].Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 57
Server version: 5.7.14-google-log (Google)

データベース、テーブルの作成

次にデータベース、テーブルを作成していきます。
これまでMySQLを使用されてきた方は、同様の方法で作成することが可能です。

create database myproject
use myproject
create table posts (id INT NOT NULL AUTO_INCREMENT, title VARCHAR(255), body VARCHAR(255), PRIMARY KEY(id));

これで以下のようなテーブルが作成されました。
f:id:nekorokkekun:20190920083402p:plain

試しにレコードの挿入をしましょう。

insert into posts (title, body) values ("first title", "first body");
insert into posts (title, body) values ("second title", "second body");
select * from posts;

以下のように表示されていればレコードの挿入が成功しています。
f:id:nekorokkekun:20190920083925p:plain

Next.jsプロジェクトにMySQLを接続する準備

まずは必要なパッケージをプロジェクト直下でインストールしていきましょう。

(ここからはGCPのシェルプロンプトではなく、Next.jsのプロジェクトをインストールしたターミナルにコマンドを打ち込んでいきます。)

npm i serverless-mysql sql-template-strings


package.jsonにもbuildコマンドを追記します。(すでにscriptsに書き込まれているコマンドは消さないでください。)

  "scripts": {
   "build": "next build"
  },

Serverless MySQL

GCPのようなサーバーレスMySQLへの接続を管理するためのパッケージ。このパッケージを導入することでデータベースに接続できるようになります。

またスケーリング可能なため、接続数が増えても自動的に拡大してくれる柔軟性があります。
GitHub - jeremydaly/serverless-mysql: A module for managing MySQL connections at SERVERLESS scale

SQL Template Strings

SQLのクエリを簡潔に書くことができるようになるパッケージ。mysql、mysql2、postgres、sequelizeで動作。

以下のような違いが出てくるようです。

const SQL = require('sql-template-strings')

const book = 'harry potter'
const author = 'J. K. Rowling'

// SQL Template Stringsを導入しなかった場合
mysql.query('SELECT author FROM books WHERE name = ? AND author = ?', [book, author])
// SQL Template Stringsを導入した場合
mysql.query(SQL`SELECT author FROM books WHERE name = ${book} AND author = ${author}`)

またZEIT公式にも以下のように書かれている通り、SQLインジェクションの対策にも繋がるようです。

パラメータ化されたクエリを使用してSQLインジェクションによる攻撃を防ぐために、sql-template-stringsを使用することを強くお勧めします。

GitHub - felixfbecker/node-sql-template-strings: ES6 tagged template strings for prepared SQL statements 📋

データベースへの接続

まずはnowコマンドが使えるようにインストールをしましょう。

npm i -g now

次にデータベースの情報をnowコマンドを使って登録していきましょう。

以下のフォーマットを使用して該当箇所にデータベースの情報を記入していきます。

now secrets add MYSQL_HOST $database-hostname && now secrets add MYSQL_USER $database-username && now secrets add MYSQL_DATABASE $database-name && now secrets add MYSQL_PASSWORD $database-password

私の場合であれば、以下のようになります。

now secrets add MYSQL_HOST "%" && now secrets add MYSQL_USER "root" && now secrets add MYSQL_DATABASE "myproject" && now secrets add MYSQL_PASSWORD "xxxxxxxx"

MYSQL_HOSTとMYSQL_USERに関してはGCPで作成したインスタンスの詳細画面からユーザータブにアクセスすると確認可能です。
f:id:nekorokkekun:20190920093019p:plain

データベースの情報に登録が成功すると、ターミナルに以下のようなメッセージが表示されます。

> Success! Secret mysql_host added (hiramori) [569ms]
> Success! Secret mysql_user added (hiramori) [564ms]
> Success! Secret mysql_database added (hiramori) [600ms]
> Success! Secret mysql_password added (hiramori) [695ms]

これでデータベースの情報が登録され、接続ができるようになったはずです。

データベース接続用のファイルを作成する

先ほど登録した情報をもとに、データベース接続用関数を書き込んでおくためのファイルを作成しましょう。

プロジェクト直下で以下のコマンドを入力します。

mkdir lib
cd lib
touch db.js

lib/db.js

const mysql = require('serverless-mysql')

const db = mysql({
  config: {
    host: process.env.MYSQL_HOST,
    database: process.env.MYSQL_DATABASE,
    user: process.env.MYSQL_USER,
    password: process.env.MYSQL_PASSWORD
  }
})

exports.query = async query => {
  try {
    const results = await db.query(query)
    await db.end()
    return results
  } catch (error) {
    return { error }
  }
}

db.jsファイルは次の機能を実行します。
定義されたデータベースの情報を使用して、MySQLデータベースへの接続を作成します。クエリが解決されると接続が閉じられるようにする関数をエクスポートします。

最も重要な行は、await db.end()です。これにより、アプリが利用可能なすべての接続を使い果たすことを防ぎます。

これで、サーバーレス環境に最適な再利用可能なデータベース接続ができました。

APIの作成

次にAPIを作成していきましょう。

まずはpostsテーブルの全レコード取得を行うためのAPI作成です。

mkdir api && mkdir api/posts
cd api/posts
touch index.js

api/posts/index.js

const db = require('../../lib/db')
const escape = require('sql-template-strings')

module.exports = async (req, res) => {
  let page = parseInt(req.query.page) || 1
  const limit = parseInt(req.query.limit) || 9
  if (page < 1) page = 1
  const posts = await db.query(escape`
      SELECT *
      FROM posts
      ORDER BY id
      LIMIT ${(page - 1) * limit}, ${limit}
    `)
  const count = await db.query(escape`
      SELECT COUNT(*)
      AS postsCount
      FROM posts
    `)
  const { postsCount } = count[0]
  const pageCount = Math.ceil(postsCount / limit)
  res.status(200).json({ posts, pageCount, page })
}

index.jsファイルは次の機能を実行します。

  • 要求されたクエリパラメータを解析します
  • クエリパラメータを使用して、必要なpostを決定します
  • データベースから必要なpostのみを要求します
  • データベースを照会して合計レコードを取得します
  • レコード数を使用してページネーションを計算します
  • 取得したpostとページネーションの詳細を応答として送信します

これが、サーバーレス環境でページネーションを正常に使用するために必要なすべてのAPIコードです。


次にpostsテーブルから指定したidのレコードのみを取得するAPIを作成します。

api/postsディレクトリ直下に新たなファイルを作成します。

touch post.js

api/posts/post.js

const db = require('../../lib/db')
const escape = require('sql-template-strings')

module.exports = async (req, res) => {
  const [post] = await db.query(escape`
    SELECT *
    FROM posts
    WHERE id = ${req.query.id}
  `)
  res.status(200).json({ profile })
}

post.jsファイルは次の機能を実行します。

  • リクエストクエリパラメータを解析します
  • クエリパラメータを使用して、データベースから単一のプロファイルを選択します
  • 取得したプロファイルを応答として送信します。

これで、ルートに応じて、すべてのプロファイルまたは単一のプロファイルのいずれかを提供するAPIが作成されました。

取得したデータの表示

次に、それらを表示するアプリインターフェイスを作成する必要があります。
まずは必要なモジュールを取得しましょう。

npm i isomorphic-unfetch next react react-dom
mkdir pages