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のアドオンを追加
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プラグインを使ってます。
使い易くて便利。
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
GETリクエスト処理部に書いておいたテーブルの中身を全部表示するSQL文が正しく実行されています。
と思って更新ボタンを押してみたらアプリケーションエラー画面へ…なぜ?
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()
})
})
})
*
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接続情報を放り込みます。
ついでにローカル環境のDB接続にはローカル環境下用に.envファイルを参照する形にしてDB接続情報を読み込みます。
.envはgitignoreでコミット対象外にしておけばOK。
Herokuの環境変数の呼び出し方ですが、process.env
を利用して環境変数にアクセスします。
.envファイルでの管理は上記を参考に。
dotenvをインストール&ルートに.envファイルを作成してその中に変数を放り込んでいきます。
LOCAL_DB_USER=[ユーザ名]
LOCAL_DB_NAME=[データベース名]
LOCAL_DB_PASSWORD=[パスワード]
LOCAL_DB_HOST=[ホスト名]
変数とコネクション情報の格納はこんな感じ。てっぺんに.envの読み込みも書いておきます。
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です。
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を確認していきます。
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ページにリダイレクトしてあげればいいと思います。
最終的なソースコードは以下の通り。
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}`)
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`で起動。コード変更の度に勝手に再起動して変更を反映してくれるので非常にラク。