1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Node.js(Express)+MySQLを使ったHeroku上で動くプログラムを作ってみる

Last updated at Posted at 2022-06-25

Node.js(Express)とMySQLを使ってDBと連携するアプリをHeroku上で動かします。
HerokuはPostgresが公式サポートのようですが、MySQLの方が触ったことがあったり職場で使ってたりシェアも大きいしってことでそっちをチョイスしました。

やってみた感想

特にクセもなく実装できるかなーと思いました。
MySQLも全然普通に使えるしHeroku便利だわ。

環境

Win10+VSC@1.68
Node.js@16.14.2+Express@4.18.1
MySQL@8.0(ローカル側DB)
ClearDB MySQL(Heroku側DB)

事前準備としてHello worldだけを出力するアプリをHeroku上にデプロイしておき、ローカル-GitHub-Herokuが繋がった環境を先に構築してあります。

やろうとしたこと

  • Herokuサービス上でNode.js(Express)アプリケーションを動かす
  • JSONデータをHerokuにデプロイしたサービスにPOST
  • POSTしたJSONデータをCrearDB(MySQL)に保存
  • DBに保存されているデータ一覧を取り出し&レスポンスとして返す

HerokuでMySQL(ClearDB)を使えるようにする

上記のサイトさんを参考にやっていきます。

まずはClearDBのアドオンを追加

Inkedheroku.jpg

HerokuにログインしてMySQLを使いたいアプリを開き、ResourcesのところからClearDB検索して追加します。
プランは無料で使えるIgniteプランを選択…と思ったらクレカ登録が必要らしいので登録しておきます。

ClearDBをHeroku上のアプリにアドオンとして追加したらdatabaseURLを取得します。
ここからはエディタ上の作業。

公式リファレンスを参考に
heroku config
のコマンドを叩きます。

=== [アプリ名] Config Vars
CLEARDB_DATABASE_URL: mysql://[ユーザID]:[パスワード]@us-cdbr-east-05.cleardb.net/[heroku_から始まるDBID]?reconnect=true

次にDBを作成

heroku addons:create cleardb:ignite
のコマントを叩きます。

Creating cleardb:ignite on ⬢ [アプリ名]... free
Created [DB名] as [DBのURLが格納されたHeroku上の環境変数]
Use heroku addons:docs cleardb to view documentation

DBに接続してみる

上記のサイトさんを参考にしつつやっていきます。
ログインパスなどなどはheroku configを叩いたときに出てきた

CLEARDB_DATABASE_URL: mysql://[ユーザID]:[パスワード]@us-cdbr-east-05.cleardb.net/[heroku_から始まるDBID]?reconnect=true

このdatabaseURLの[ユーザID]、[パスワード]、[heroku_から始まるDBID]らを使って

mysql -u [ユーザID] -p -h us-cdbr-east-05.cleardb.net [heroku_から始まるDBID]
Pass [パスワード]

こう入力していきます。

※ローカルでMySQLコマンドを打とうと思ったらインストールしてなかったのでインストールしておきます。

MySQLのインストールはここが詳しい。

ちなProgateさんは勉強するのにかなりよいサービスでした。
月額1000円でステップバイステップで勉強できるので結構、というかかなりよかった。

DBにログインしようとしたらエラーになったのでエディタに管理者権限でログインしたらOKでした。

ログインしたらテーブルを作成

SQL文は
create table characters (id int, name varchar(20), title varchar(10));
作ったらdesc charactersで確認します。

+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | YES  |     | NULL    |       |
| name  | varchar(20) | YES  |     | NULL    |       |
| title | varchar(10) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
3 rows in set (0.20 sec)

問題なくテーブルが作成されたので列を挿入していきます。

insert into characters values (1, 'Anya Forger', 'SPY Family');
insert into characters values (2, 'Loid Forger', 'SPY Family');
insert into characters values (3, 'Yor Forger', 'SPY Family');
insert into characters values (4, '煉獄杏寿郎', '鬼滅の刃');

SELECT * FROM characters;
+------+-------------+------------+
| id   | name        | title      |
+------+-------------+------------+
|    1 | Anya Forger | SPY Family |
|    2 | Loid Forger | SPY Family |
|    3 | Yor Forger  | SPY Family |
|    4 | 煉獄杏寿郎  | 鬼滅の刃   |
+------+-------------+------------+
4 rows in set (1.77 sec)

これでテーブルの準備ができました。

ついでにローカル側にも同じようにテーブルを作って確認用にします。
一旦Heroku側のDBからログアウトしてローカル側のMySQL DBにログイン。

CREATE DATABASE local_test;
USE local_test;
insert into characters values (1, 'Anya Forger', 'SPY Family');
insert into characters values (2, 'Loid Forger', 'SPY Family');
insert into characters values (3, 'Yor Forger', 'SPY Family');
insert into characters values (4, '煉獄杏寿郎', '鬼滅の刃');
SELECT * FROM characters;
+------+-------------+------------+
| id   | name        | title      |
+------+-------------+------------+
|    1 | Anya Forger | SPY Family |
|    2 | Loid Forger | SPY Family |
|    3 | Yor Forger  | SPY Family |
|    4 | 煉獄杏寿郎  | 鬼滅の刃   |
+------+-------------+------------+
4 rows in set (0.00 sec)

できました。

Node.js(Express)側の実装

データベースの準備ができたのでここからNode.js側の実装をやっていきます。
まずはローカル側のDBを使って

  • DBとの接続
  • DBへの書き込み
  • DBからの読み出しをやっていきます。
const express = require('express')
const mysql = require('mysql')
const app = express()
const PORT = process.env.PORT || 3000

const con = mysql.createConnection({
  host: 'localhost',
  user: '[ユーザ名]',
  password: '[パスワード]'
});

con.connect(function(err) {
  if (err) throw err;
  console.log('Connected');
});

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.get('/', function (req, res) {
  res.send(`HELLO WORLD!! PORT NUMBER is ${PORT}`)
})

app.post('/', function (req, res) {
    console.log(req.body)
    res.json({
        msg: 'These messages are JSON',
        name: req.body.name,
        data: req.body.age
  })
})

app.listen(PORT)
console.log(`Express Server Listen START at port=${PORT}`)

実行の前にnpm install mysqlをやっておきます。
で、実行。エラーが出ました。

ER_NOT_SUPPORTED_AUTH_MODE

認証方法がアカン…?
調べてみるとローカルDBはSQL8.0を入れていますが、さっきインストールしたmysqlパッケージが使っている認証プロトコルだと弾かれてしまうっぽい。

mysql2パッケージを入れて解決できるようだけど・・・ダメでした。
(後で考えてみたら先に入れたmysqlパッケージを削除してなかっただけ)
ので、mysqlにログインして

ALTER USER '[ユーザ名]'@'localhost' IDENTIFIED WITH mysql_native_password BY 'パスワード';

こうする。

Express Server Listen START at port=3000
Connected

これでDBにも接続OKの結果が出ました。

GETリクエスト部分の実装

SQL文の実行結果をクライアント側に返す実装をやっていきます。
流れとしては

  • クライアントからサーバへGETリクエスト
  • サーバ側でGET受信、DBへSQL文発行
  • DBからの結果をクライアントへ返す

createConnectionにDB名を加える&GET部分を書き換えて

*
const con = mysql.createConnection({
  host: 'localhost',
  user: '[ユーザ名]',
  password: 'パスワード'
  database: 'データベース名'
})

app.get('/', function (req, res) {
  const sql = "select * from characters"
  con.query(sql, function(err, result, fields){
    if(err) throw err
    res.json(result)
  })
})
*
結果
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 210
Connection: close
[
  {
    "id": 1,
    "name": "Anya Forger",
    "title": "SPY Family"
  },
  {
    "id": 2,
    "name": "Loid Forger",
    "title": "SPY Family"
  },
  {
    "id": 3,
    "name": "Yor Forger",
    "title": "SPY Family"
  },
  {
    "id": 4,
    "name": "煉獄杏寿郎",
    "title": "鬼滅の刃"
  }
]

ちゃんと結果が返ってきました。
ちなみにSQLを発行するタイミングで接続が行われるので
con.connect(function(err) {
if (err) throw err
console.log('Connected')
})
この部分は削除してもよくなります。

DBへの書き込みができるか試してみる

GETの受信とDBへのSQL文の発行ができたので、次にDBへの書き込みを行うSQL文を発行して書き込みができるかをチェックしてみます。また、DBへの書き込みを行った後にデーブルの内容を取得します。

*
app.get('/', function (req, res) {
  const sql_insert = "insert into characters(name, title) values('rin', 'Yurucamp')"
  const sql_display = "select * from characters"
  con.query(sql_insert, function(err, result, fields){
    if(err) throw err
    return
  })
  con.query(sql_display, function(err, result, fields){
    if(err) throw err
    res.json(result)
  })
})
*
結果
[
~省略
  {
    "id": null,
    "name": "rin",
    "title": "Yurucamp"
  }
]

リンちゃんのidがnullになってしまったのでid列に主キー設定、番号が自動的に入るようにします。

alter table characters modify id int not null primary key auto_increment;

GETリクエストを投げると

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 251
Connection: close
[
~省略
  {
    "id": 5,
    "name": "rin",
    "title": "Yurucamp"
  }
]

今度はOKです。

POSTリクエストのデータを使ってDBへ書き込み

次にGETリクエスト処理部分に書いたインサート文を、POST処理部分へと移動してあげます。

まずは受け取ったPOSTリクエストデータからSQLインサート分を作る前に、ベタ打ちでPOSTリクエスト処理部からDBへ書き込みできるかチェックします。例のごとく列を書き込んだらテーブルを全部出力。

*
app.post('/', function (req, res) {
  const sql_insert = "insert into characters set ?"
  const sql_display = "select * from characters"
  con.query(sql_insert, {name: 'nadeshiko', title: 'Yurucamp'}, function(err, result, fields){
    if(err) throw err
    return
  })
  con.query(sql_display, function(err, result, fields){
    if(err) throw err
    res.json(result)
  })
})
結果
[
~省略
  {
    "id": 6,
    "name": "nadeshiko",
    "title": "Yurucamp"
  }
]

OKです。
{name: 'nadeshiko', title: 'Yurucamp'}の部分をreq.bodyに置き換えてあげればPOSTリクエストに含まれるデータでSQL文が出来上がります。
なお、GETやPOSTリクエストはVSCのREST Clientプラグインを使ってます。
使い易くて便利。

REST Client POSTデータ
POST http://localhost:3000 HTTP/1.1
content-type: application/json

{
    "name": "inuko",
    "title": "Yurucamp"
}
結果
[
~省略
  {
    "id": 7,
    "name": "inuko",
    "title": "Yurucamp"
  }
]

結果もOKそうです。
これでJSONデータ入りのPOSTリクエストを送って、サーバからDBにデータを書き込み。
DBから全テーブルデータを取り出してJSONデータ形式でクライアントにレスポンスとして返すことができました。

Heroku上で動く形に整えていく

まずHerokuのDBに作ったcharactersテーブルのid列に主キー設定と自動インクリメント設定を付与しておきます。
テーブル作成した時に設定してませんでしたので…

alter table characters modify id int not null primary key auto_increment;

次にDBとのコネクション部分を整えていきます。
ClearDBのURL情報を参考に置き換え。
mysql://[ユーザID]:[パスワード]@us-cdbr-east-05.cleardb.net/[heroku_から始まるDBID]?reconnect=true

*
const con = mysql.createConnection({
  host: 'us-cdbr-east-05.cleardb.net',
  user: '[ユーザID]',
  password: '[パスワード]'
  database: '[heroku_から始まるDBID]'
})
*

これでHeroku上にpushします。
本当はパスワードやら何やらをベタ打ちしたソースコードをpushしたら絶対アカンので、それについては後で対策します。

続けてheroku open

0612_1.png

GETリクエスト処理部に書いておいたテーブルの中身を全部表示するSQL文が正しく実行されています。
と思って更新ボタンを押してみたらアプリケーションエラー画面へ…なぜ?

7d4c09b48d799cfabc379432fda679b9.png

heroku logs --tailでエラーを見てみると

PROTOCOL_CONNECTION_LOST

データベースとのコネクションが切れてるっぽいです。
MySQLの仕様上一定時間でDBとの接続が切れるらしい。

これについてはConnectionPoolを使って解決しました。
DBとのやり取りが終わったらconnection.release()を忘れずに。
コネクション情報の変数格納部分などを変更します。

まずはGETリクエスト処理部分。

*
const pool = mysql.createPool({
  host: 'us-cdbr-east-05.cleardb.net',
  user: '[ユーザID]',
  password: '[パスワード]'
  database: '[heroku_から始まるDBID]'
})

app.get('/', function (req, res) {
  const sql_display = "select * from characters"
  pool.getConnection(function(err, connection){
    pool.query(sql_display, function(err, result, fields){
      if(err) throw err
      res.json(result)
	  connection.release()
    })
  })
})
*

コネクション情報を格納していた変数conはpoolに置き換えています。
これで時間をおいたりリロードしても問題なく表示されます。

次にPOSTリクエスト処理部分をやっていきます。

*
app.post('/', function (req, res) {
  const sql_insert = "insert into characters set ?"
  const sql_display = "select * from characters"
  pool.getConnection(function(err, connection){
    pool.query(sql_insert, req.body, function(err, result, fields){
      if(err) throw err
      return
    })
    pool.query(sql_display, function(err, result, fields){
      if(err) throw err
      res.json(result)
	  connection.release()
    })
  })
})
*
REST Client POSTリクエスト
POST https://[アプリ名].herokuapp.com HTTP/1.1
content-type: application/json
{
    "name": "sakura",
    "title": "Yurucamp"
}
結果
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 255
[
~省略
  {
    "id": 14,
    "name": "sakura",
    "title": "Yurucamp"
  }
]

エラーもなくPOSTリクエストが通りました。
ただなぜかidが10ずつ増えるんですよね・・・

調べてみるとidが連番でないことはそんなに気にしなくてもよく、あくまで一意であることが重要とかなんとか。
逆に連番であることに依存している方が問題という意見を見つけた。そういうもんなのか?

ひとまずこれはおいておくことにする…

Herokuへデプロイするにあたってソースコードに書いたパスワードどうすんの問題

Heroku上でサービス公開をする上ではGitHub上にソースコードをアップする必要があるのですが、ソースコードにパスワードなどの認証情報を書いてバージョン管理なんてしたら明らかにダメです。

ということで下記を参考にしながら対策していきます。

Herokuの環境変数にDB接続情報を格納する

Heroku上で各アプリに環境変数を設定することができ、それを読みだして使う事ができます。
DB接続情報をその環境変数に登録しておいて、ソースコードからはそれら変数を読みだすようにします。

Herokuサイト上からアプリの詳細画面へ飛び、SettingタブからConfig Varsを選びます。
すでにDBのURLなんかが入っていますのでここにパスワードなどのDB接続情報を放り込みます。

HEROKUENV.jpg

ついでにローカル環境のDB接続にはローカル環境下用に.envファイルを参照する形にしてDB接続情報を読み込みます。
.envはgitignoreでコミット対象外にしておけばOK。

Herokuの環境変数の呼び出し方ですが、process.envを利用して環境変数にアクセスします。

.envファイルでの管理は上記を参考に。
dotenvをインストール&ルートに.envファイルを作成してその中に変数を放り込んでいきます。

.env
LOCAL_DB_USER=[ユーザ名]
LOCAL_DB_NAME=[データベース名]
LOCAL_DB_PASSWORD=[パスワード]
LOCAL_DB_HOST=[ホスト名]

変数とコネクション情報の格納はこんな感じ。てっぺんに.envの読み込みも書いておきます。

index.js
require('dotenv').config();
const env = process.env

const PORT = env.PORT || 3000
const DB_USER = env.HEROKUENV_DB_USER || env.LOCAL_DB_USER
const DB_PASSWORD = env.HEROKUENV_DB_PASSWORD || env.LOCAL_DB_PASSWORD
const DB_NAME = env.HEROKUENV_DB_NAME || env.LOCAL_DB_NAME
const DB_HOST = env.HEROKUENV_DB_HOST || env.LOCAL_DB_HOST

const pool = mysql.createPool({
  host: DB_HOST,
  user: DB_USER,
  password: DB_PASSWORD,
  database: DB_NAME
})

ルートに.gitignoreを追加して/.envを記載し.envファイルを追跡対象外にします。

最後に確認をしていきます。
まずはローカル上で動くか確認。ポートは3000です。

REST Client POSTリクエスト
POST http://localhost:3000 HTTP/1.1
content-type: application/json
{
    "name": "Sakura",
    "title": "Yurucamp"
}
結果
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 385

[
~省略
  {
    "id": 8,
    "name": "Sakura",
    "title": "Yurucamp"
  }
]

ちゃんと出力されるようなので、次にgitHub上にpushします。
.envがプッシュされてないかGitHubのページまで行って確認しておきます。
OKだったらHerokuにデプロイ。

Herokuの方でも動作確認をしていきます。まずは普通にアプリにアクセスして問題ない事を確認。
その次にPOSTを確認していきます。

REST Client POSTリクエスト
POST https://secret-lake-52496.herokuapp.com HTTP/1.1
content-type: application/json

{
    "name": "nadeshiko",
    "title": "Yurucamp"
}
結果
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 303

[
~省略
  {
    "id": 24,
    "name": "nadeshiko",
    "title": "Yurucamp"
  }
]

POSTも問題なさそうです。
idが飛ぶのは今後の課題ということで。
ローカルでは番号飛ばないしPool使わないようにしても解決はせず。

これで

  • Herokuサービス上でNode.js(Express)アプリケーションを動かす
  • JSONデータをHerokuにデプロイしたサービスにPOST
  • POSTしたJSONデータをCrearDB(MySQL)に保存
  • DBに保存されているデータ一覧を取り出し&レスポンスとして返す

これらをクリアするプログラムが作成できました。
ちなみにPOSTリクエスト処理部のところにテーブルの内容を取り出してくるSQL文を書かずに、POSTリクエストの処理をした後にGETリクエストを投げるHTMLページにリダイレクトしてあげればいいと思います。

最終的なソースコードは以下の通り。

index.js
require('dotenv').config()

const express = require('express')
const mysql = require('mysql')
const app = express()
const env = process.env

const PORT = env.PORT || 3000
const DB_HOST = env.HEROKUENV_DB_HOST || env.LOCAL_DB_HOST
const DB_USER = env.HEROKUENV_DB_USER || env.LOCAL_DB_USER
const DB_PASSWORD = env.HEROKUENV_DB_PASSWORD || env.LOCAL_DB_PASSWORD
const DB_NAME = env.HEROKUENV_DB_NAME || env.LOCAL_DB_NAME

app.use(express.json())
app.use(express.urlencoded({ extended: true }))

const pool = mysql.createPool({
  host: DB_HOST,
  user: DB_USER,
  password: DB_PASSWORD,
  database: DB_NAME
})

app.get('/', function (req, res) {
  const sql_display = "select * from characters"
  pool.getConnection(function(err, connection){
    pool.query(sql_display, function(err, result, fields){
      if(err) throw err
      res.json(result)
      connection.release()
    })
  })
})

app.post('/', function (req, res) {
  const sql_insert = "insert into characters set ?"
  const sql_display = "select * from characters"
  pool.getConnection(function(err, connection){
    pool.query(sql_insert, req.body, function(err, result, fields){
      if(err) throw err
      pool.query(sql_display, function(err, result, fields){
        if(err) throw err
        res.json(result)
        connection.release()
      })
      return
    })
  })
})

app.listen(PORT)
console.log(`Express Server Listen START at port=${PORT}`)
.env
LOCAL_DB_USER=[ユーザ名]
LOCAL_DB_NAME=[データベース名]
LOCAL_DB_PASSWORD=[パスワード]
LOCAL_DB_HOST=[ホスト名]

お疲れ様でした。

※余談
そういえばnodeのindex.jsを更新する度にCtrl+Cでサーバを落として再度node index.jsをするのが面倒だったのでnodemonを導入しました。
npm install nodemonの後で{npx nodemon index.js`で起動。コード変更の度に勝手に再起動して変更を反映してくれるので非常にラク。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?