Expressの続きです。前回はMySQLのデータをbody-parserでブラウザに返しました。今回はいよいよFetch APIとMySQLの連携してブラウザのフォームから入力したデータをCRUDします。それではいきましょう!
【目次】
※参考:前回記事
【Express】MySQLのデータをbody-parserでブラウザに返す - クモのようにコツコツと
※参考:Node.js / Expressを習得するためにやったことまとめ
qiita.com
前回までのおさらい
フォームから一句を送信するとMySQLのDBにあるフォームのデータが返されてアラート表示される。
ただし、フォームに入力したデータとは違う内容になっている。
※参考:【Express】MySQLのデータをbody-parserでブラウザに返す - クモのようにコツコツと
フォームとDBを連携するにはMySQLへのCRUD処理が必要。以前、ターミナルからMySQLにCURD処理した記事
※参考:【SQL】ターミナルからMAMPのMySQLにCRUDする - クモのようにコツコツと
ブラウザ側の処理はFetch APIを使う。以前、Fetch APIのデータとJSON Serverを連携してCRUDした記事
※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと
今回はここら辺の内容が合わさったような内容になる。ブラウザとDBの間でデータを受け渡す処理をExpressが行う。
その他、下記の記事も参考にさせていただいた(レスポンス後の処理は異なるがExpress部分がとても参考になった)
※参考:レスポンスをEJSで画面に反映させている事例
Express.js(node.js)からMySQLへの接続とCRUD操作 | アールエフェクト
※参考:レスポンスをReactで画面に反映させている事例
nodejs + mysql + React + ReduxでCRUDアプリを作る Part1 - Qiita
Expressのコード
ここからコードを見ていく。コード全体はGitHub参照
※参考:コード全体(GitHub)
GitHub - ryo-i/express_mysql_crud: ExpressでFetch APIとMySQLを連携してCRUDする
モジュールのインストール
まずは各種モジュールのインストールを行う。
package.json作成
npm init -y
Expressをインストール
npm install express --save
body-parserをインストール
npm install body-parser
mysql(モジュール)をインストール
npm install mysql
package.jsonでインストールを確認
"dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", "mysql": "^2.18.1" }
各種設定のコード
まずindex.jsの冒頭に設定関係のコードを書く。
各種モジュールのインポート
const express = require('express'); const mysql = require('mysql'); const bodyParser = require('body-parser');
変数app
でExpressの有効化設定
const app = express(); app.use(express.static('public'));
変数connection
にMAMPのMySQLデータベースの情報を設定
// MySQL設定 const connection = mysql.createConnection({ host: 'localhost', user: 'root', password: 'root', database: 'ikku', port: 8889 });
変数jsonParser
にbody-parserでフォームのJSONデータを受け取る設定
// Fetch API設定 const jsonParser = bodyParser.json();
Create:データ追加
Create部分のコード全体
// Create app.post('/fetch', jsonParser, (req, res) => { const body = req.body; const ikku = req.body.ikku; const insertSql = "INSERT INTO ikkulist SET ?" const selectSql = 'SELECT * FROM ikkulist WHERE ikku = ?'; connection.query(insertSql, body, (err, result) => { if (err) throw err; connection.query(selectSql, ikku, (err, result) => { if (err) throw err; console.log(result[0]); res.send(result[0]); }); }); });
app.post()
でPOST通信実行、第一引数はパスで/fetch
にアクセスがあったら動く
第二引数はjsonParser
でJSONデータを受け取る
第三引数は無名関数で引数はリクエストreq
とレスポンスres
- 変数
body
でreq
のbody
情報全体を取得 - 変数
ikku
でreq
のikku
の値を取得 - 変数
insertSql
はINSERT文を実行、テーブルikkulist
にデータを追加 - 変数
selectSql
はSELECT文を実行、ikkulist
のikku
のデータを取得 connection.query()
でDB操作、第一引数でinsertSql
を実行
第二引数で?
にbody
を代入、第三引数は無名関数で引数はerr
とresult
- もしエラーだったら処理を抜ける
connection.query()
でDB操作、selectSql
を実行し、?
にikku
を代入- もしエラーだったら処理を抜ける
res.send()
で配列result
の一つ目[0]
の内容をブラウザに返す
app.post()
POST通信実行。詳細はこちらの記事を参照。
※参考:【Express】body-parserでFetch API(およびForm)のPOST送信を受け取る - クモのようにコツコツと
第二引数にbody perserの設定jsonParser
を入れることでブラウザのフォームからのJSONデータを扱えるようになる。
変数insertSql
のINSERT文の値の部分は?
と書いている。
const insertSql = "INSERT INTO ikkulist SET ?"
connection.query()
の第一引数をinsertSql
、第二引数を?
に入れたい変数body
にする。
connection.query(insertSql, body, (err, result) => { // 中略 });
こうするとbody
の値が?
に代入されて追加された!
この?
の使い方はこちらの記事を参考にした。
insert文で直接Valuesの中に値を入れていましたが、?を使うこともできます。
※参考:Express.js(node.js)からMySQLへの接続とCRUD操作 | アールエフェクト
MySQLのリファレンスで?
について調べると「プレースホルダ」というものらしい。
準備済みステートメント内では、? プレースホルダマーカーを使用して LIMIT パラメータを指定できます。
準備済みステートメントでは、プレースホルダを使用できます。次のステートメントは、tbl テーブルの 1 行を返します。
※参考:MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.2.9 SELECT 構文
プレースホルダの意味。
プレースホルダ(英:place holder)とは
正式な値が入るまで一時的に場所を確保しておく措置のこと
※参考:https://wa3.i-3-i.info/word118.html
WHERE句は以前、MySQLのCRUDの時にデータ取得の絞り込みに使った。
※参考:【SQL】ターミナルからMAMPのMySQLにCRUDする - クモのようにコツコツと
今回の例では変数selectSql
の部分
const selectSql = 'SELECT * FROM ikkulist WHERE ikku = ?';
SELECT文で、カラムは*
で全てを対象にする。レコードはテーブルikkulist
の全件ではなくikku
の?
と一致したデータのみを取得。
connection.query()
の第一引数をselectSql
、第二引数を?
に入れたい変数ikku
にする。
connection.query(selectSql, ikku, (err, result) => { if (err) throw err; console.log(result[0]); res.send(result[0]); });
こうするとikku
の値が?
に代入されて、その値と一致したデータを絞り込んで取得された!
res.send()
でデータをブラウザに返す。result[0]
と配列の一つ目だけを返すのは複数のデータが一致しても返すのは一つだけにするため。
Read:データ取得
次はRead部分。
// Read app.get('/fetch', jsonParser, (req, res) => { const selectSql = "SELECT * FROM ikkulist"; connection.query(selectSql, (err, result) => { if (err) throw err; console.log(result); res.send(result); }); });
app.get()
でGET通信実行、/fetch
のアクセスで動く- 変数
selectSql
でSELECT文を実行、テーブルikkulist
のデータを取得 connection.query()
でDB操作、第一引数でselectSql
実行res.send()
でresult
を返す
app.get()
はGET通信を実行できる。
Returns the value of name app setting, where name is one of the strings in the app settings table.
name app settingの値を返します。nameはアプリ設定テーブル内の文字列の1つです。
変数selectSql
はSELECT文。今回はテーブルikkulist
の全件カラム、全レコードを取得するので?
はない。
const selectSql = "SELECT * FROM ikkulist";
connection.query()
の第一引数をselectSql
にする。
connection.query(selectSql, (err, result) => { // 中略 res.send(result); });
処理部分はres.send(result)
で、result
の全件をブラウザに返す。
Update:データ修正
Update部分のコード全体
// Update app.put('/fetch', jsonParser, (req, res) => { const id = req.body.id; const ikku = req.body.ikku; const updateSql = 'UPDATE ikkulist SET ikku = ? WHERE id = ?'; const selectSql = 'SELECT * FROM ikkulist WHERE id = ?'; connection.query(updateSql, [ikku, id], (err, result) => { if (err) throw err; connection.query(selectSql, id, (err, result) => { if (err) throw err; console.log(result[0]); res.send(result[0]); }); }); });
app.put()
でPUT通信実行、/fetch
のアクセスで動く- 変数
id
でbody
情報のid
を、変数ikku
でbody
情報のikku
を取得 - 変数
updateSql
でSELECT文を実行、テーブルikkulist
のid
のikku
を取得 connection.query()
でDB操作、第一引数でupdateSql
実行、第二引数でikku, id
を代入connection.query()
でDB操作、第一引数でselectSql
実行、第二引数でid
を代入res.send()
で配列result
の一つ目[0]
の内容をブラウザに返す
app.put()
はPUT通信を実行できる。
Routes HTTP PUT requests to the specified path with the specified callback functions.
指定されたコールバック関数を使用して、HTTP PUT要求を指定されたパスにルーティングします。
変数updateSql
はUPDATE文でテーブルikkulist
のid
の?
に一致するレコードのikku
カラムを?
に更新する。
const updateSql = 'UPDATE ikkulist SET ikku = ? WHERE id = ?';
connection.query()
の第一引数をupdateSql
、第二引数は?
に入れたい内容だが?
が二つあるため、配列にする。変数ikku
と変数id
。
connection.query(updateSql, [ikku, id], (err, result) => { // 中略 });
変数selectSql
はSELECT文でテーブルikkulist
の中からid
が?
と一致するレコードの全カラムを取得する。
const selectSql = 'SELECT * FROM ikkulist WHERE id = ?';
connection.query()
の第一引数をselectSql
、第二引数は?
に入れたい内容で変数id
。
connection.query(selectSql, id, (err, result) => { // 中略 res.send(result[0]); });
id
と一致するデータを取得されるので、それをres.send(result[0])
でブラウザに返す。1件のみを返すのでresult
の配列の一つ目[0]
。
Delete:データ削除
Delete部分のコード全体
// delete app.delete('/fetch', jsonParser, (req, res) => { const id = req.body.id; const deleteSql = 'DELETE FROM ikkulist WHERE id = ?'; connection.query(deleteSql, id, (err, result) => { if (err) throw err; res.json({ "id": Number(id), "ikku": "deleted" }); }); });
app.delete()
でDELETE通信実行、/fetch
のアクセスで動く- 変数
id
でbody
情報のid
を取得 - 変数
deleteSql
でDELETE文を実行、ikkulist
のid
のデータを削除する connection.query()
でDB操作、第一引数でdeleteSql
実行、第二引数でid
を代入res.json()
でJSON形式のデータを返す、id
キーにはid
を、ikku
キーには削除済みdeleted
を返す
app.delete()
はDELETE通信を実行できる。
Routes HTTP DELETE requests to the specified path with the specified callback functions.
指定されたコールバック関数を使用して、HTTP DELETE要求を指定されたパスにルーティングします。
変数deleteSql
はDELETE文で、テーブルikkulist
の中からid
が?
と一致したレコードを削除する。
const deleteSql = 'DELETE FROM ikkulist WHERE id = ?';
connection.query()
の第一引数をdeleteSql
、第二引数は?
に入れたい内容で変数id
。
connection.query(deleteSql, id, (err, result) => { // 中略 res.json({ "id": Number(id), "ikku": "deleted" }); });
削除したデータはもうSELECT文で取得できないため、ブラウザには削除したid
の番号を返す。
これまでと同じJSON形式で返したいのでres.json()
で新たに作る。id
キーにNumber()
で数値値したid
。ikku
キーにはdeleted
というテキストを入れる。削除が成功したよーって意味。
サーバ起動
最後にapp.listen()
で Nodeサーバを起動、ポート番号は3000
app.listen(3000, () => { console.log('Start server port:3000'); });
ここはこれまでと変更なし。
Fetch APIのコード
基本的に下記のコードの準ずるが、細かいところを少し変えている。
※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと
HTMLコード
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ExpressでMySQLとFetch APIを連携してCRUD</title> <link rel="stylesheet" href="css/fetch_crud.css"> </head> <body> <section> <h1>今日の一句</h1> <section> <h2>一句詠む</h2> <input type="text" name="ikku" size="30" maxlength="30" class="postIkku"> <input type="button" value="送信" class="postBtn"> </section> <section> <h2>過去の一句</h2> <ul class="ikkuList"></ul> </section> </section> <script src="js/fetch_crud.js"></script> </body> </html>
headタグのtitleタグを今回の内容変えた。bodyタグ以下は変更なし。
CSS設定もあるが変更ないし最低限の内容のため割愛。詳細は前回の記事を参照。
※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと
グローバル設定
ここからは「fetch_crud.js」のJSコード。
まずはグローバル変数の設定。
const postIkku = document.querySelector('.postIkku'); const postBtn = document.querySelector('.postBtn'); const ikkulist = document.querySelector('.ikkuList'); const url = '/fetch';
最初の3つはDOM取得で変更なし。変数url
はパスを/fetch
に変更。
Create:データ追加
Create部分のコード全体
// Create const createFetch = () => { const data = { ikku: postIkku.value }; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then((response) => { if(!response.ok) { console.log('Create error!'); throw new Error('error'); } console.log('Create ok!'); return response.json(); }).then((data) => { appendList(data); }).catch((error) => { console.log(error); }); }; postBtn.addEventListener('click', createFetch, false);
Criate部分は特に変更なし。
Read:データ取得
Read部分のコード全体
// Read const readFetch = () => { fetch(url).then((response) => { if(!response.ok) { console.log('Read error!'); throw new Error('error'); } console.log('Read ok!'); return response.json(); }).then((data) => { for (let i = 0; i < data.length; i++) { const thisData = data[i]; appendList(thisData); } }).catch((error) => { console.log(error); }); }; readFetch();
Read部分も特に変更なし
Update:データ修正
Update部分のコード全体
// Update const updateFetch = (thisLi) => { const thisId = thisLi.dataset.id; const updateArea = thisLi.querySelector('.updateArea'); const updateIkku = thisLi.querySelector('.updateIkku').value; const data = { id: thisId, ikku: updateIkku }; fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then((response) => { if(!response.ok) { console.log('Update error!'); throw new Error('error'); } console.log('Update ok!'); return response.json(); }).then((data) => { thisLi.firstChild.textContent = data.ikku; thisLi.removeChild(updateArea); }).catch((error) => { console.log(error); }); }; document.addEventListener('click', (e) => { if (e.target.className !=='updateBtn') { return; } const thisLi = e.target.closest('li'); updateFetch(thisLi); }, false);
Update部分は少し変わっている。
前回は変数updateUrl
でurl + '/' + thisId
というパスを作っていた。パスの中に変数thisId
でid番号の情報を含んでいた。
今回はパスは変えずにthisId
は変数data
の中のid
キーとしてFetch APIのデータ情報で送る。
const data = { id: thisId, ikku: updateIkku };
fetch()
メソッドの第一引数は他と同じurl
に。
fetch(url, { // 中略 ).then( // 後略
これでURLのパスやパラメータに頼らず、純粋にUpdate送信だけでサーバとやり取りできるかを確かめたい。
Delete:データ削除
Delete部分のコード全体。
// Delete const deleteFetch = (thisLi) => { const thisId = thisLi.dataset.id; const data = { id: thisId }; fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then((response) => { if(!response.ok) { console.log('Delete error!'); throw new Error('error'); } console.log('Delete ok!'); return response.json(); }).then((data) => { const deleteId = data.id; console.log('Delete ID->' + deleteId) thisLi.remove(); }).catch((error) => { console.log(error); }); }; document.addEventListener('click', (e) => { if (e.target.className !=='doDelete') { return; } const thisLi = e.target.closest('li'); deleteFetch(thisLi); }, false);
Delete部分も少し違う。
こちらも前回は変数updateUrl
でurl + '/' + thisId
とid情報を含むパスに送っていたが、今回は変数data
のid
キーでデータ情報として送る。
const data = { id: thisId };
fetch()
部分
fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then( // 後略
- headers情報にJSONデータを送ることを記載
- body情報に
data
をJSON形式で送る
1番目のthen()
// 前略 }).then((response) => { if(!response.ok) { console.log('Delete error!'); throw new Error('error'); } console.log('Delete ok!'); return response.json(); }).then((data) => { // 後略
前回はデータを返さなかったが、今回はretuen response.json()
でJSON形式のデータを次のthen()
に返している
2番目のthen()
// 前略 }).then((data) => { const deleteId = data.id; console.log('Delete ID->' + deleteId) thisLi.remove(); }).catch((error) => { // 後略
- 変数
deleteId
でdata
のid
キーの値を取得 - コンソールで削除した一句の
id
を表示
これでリクエストしたid
とレスポンスのid
が合っているかをコンソールで確認する。
前回書いた処理内容の詳細はこちらを参照。
※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと
DOM操作
DOM操作部分は前回と変更なし。
// Append Button const appendBtn = (className, text) => { const btn = document.createElement('button'); btn.className = className; btn.innerHTML = text; return btn; }; // Append List const appendList = (thisData) => { const li = document.createElement('li'); li.dataset.id = thisData.id; li.innerHTML = thisData.ikku; const updateBtn = appendBtn('doUpdate', '修正'); li.appendChild(updateBtn); const deleteBtn = appendBtn('doDelete', '削除'); li.appendChild(deleteBtn); ikkulist.appendChild(li); }; // Append Update Area const appendUpdateInput = (thisIkku) => { const input = document.createElement('input'); input.type = 'text'; input.name = 'updateIkku'; input.size = '30'; input.maxlength = '30px'; input.className = 'updateIkku'; input.value = thisIkku; return input; }; const appendUpdateBtn = () => { const btn = document.createElement('input'); btn.type = 'button'; btn.value = '送信'; btn.className = 'updateBtn'; return btn; }; const appendUpdateArea = (thisLi) => { const thisIkku = thisLi.firstChild.textContent; const appendDiv = document.createElement('div'); appendDiv.className = 'updateArea'; appendDiv.appendChild(appendUpdateInput(thisIkku)); appendDiv.appendChild(appendUpdateBtn()); thisLi.appendChild(appendDiv); }; document.addEventListener('click', (e) => { if (e.target.className !=='doUpdate') { return; } const thisLi = e.target.closest('li'); if (thisLi.querySelector('.updateArea') === null) { appendUpdateArea(thisLi); } }, false);
処理内容の詳細は前回の記事を参照。
※参考:【JS】Fetch APIを使ってJSON ServerにCRUDする - クモのようにコツコツと
※参考:コード全体(GitHub)
GitHub - ryo-i/express_mysql_crud: ExpressでFetch APIとMySQLを連携してCRUDする
ブラウザ動作
さあ上記のコードによってブラウザでどのような動作になるか!
データの読み込み
事前に下記を行う作業
- MAMPのMySQLを起動→ブラウザでphpMyAdminを表示(DBの状態の確認用)
- ターミナルで
cd
コマンドでフォルダに移動→nodemonでExpress起動nodemon index.js
Expressを起動するとブラウザのlocalhost:3000が開く。 「過去の一句」にDB上の一句が一覧表示されてる!
Dev-toolsのConsoleパネルを見ると「Read ok!」と表示されてる!
NetworkパネルのHeaders情報、GET送信が成功(304)している。
ステータスコード304は「変更なし」でキャッシュを返す。
※参考:HTTP レスポンスステータスコード - HTTP | MDN
NetworkパネルのRrespons情報を見ると、データ一式がJSON形式で返ってきている。
なお、localhost:3000/fetch
をブラウザで開くとここでもデータ一式が確認できる。
データの追加
データを追加してみる。「HTML ああHTML HTML」と詠んでみる。
送信ボタンを押すと「過去の一句」に追加された!
Dev-toolsのConsoleパネルを見ると「Create ok!」と表示されてる!
NetworkパネルのHeaders情報、POST送信が成功(200)している。
Headers情報の下の方、「HTML〜」の一句がJSON形式でリクエスト送信されている。
Responseを見るとレスポンスでid番号70としてデータが追加されたことがわかる。
データの修正
「HTML ああHTML HTML」…ちょっと字余りで語呂が悪いので修正しよう。
修正ボタンを押すと修正用のフォームが表示される。
「HTM LああH TML」と修正。ちょっとトリッキーな区切り方だがw
送信ボタンを押すと修正が反映された!
Dev-toolsのConsole、「Update ok!」と表示されてる!
NetworkパネルのHeaders情報、PUT送信が成功(200)している。
Headers情報の下の方、id70番の一句の修正版データがJSON形式で送信されている。
Response情報、レスポンスでid70番の修正されたデータが返ってきている。
データの削除
HTMLの一句、やはりトリッキーすぎてイマイチだな。削除しようw
削除ボタンを押すとHTMLの一句が削除された!
Dev-toolsのConsoleパネル。「Delete ok!」と「Delete ID -> 70」でid70番のデータが削除されたことがわかる。
NetworkパネルのHeaders情報、DELETE送信が成功(200)している。
Headers情報の下の方、id70番のデータの削除をリクエストしている。
Response情報、レスポンスでid70番のデータを削除(deleted)したことが返ってくる。 これを先程のConsoleに表示していた。
ノード環境とデータベース
ターミナルにはサーバ(Node環境)のExpressが実行した処理の結果がconsole.log()
によって表示されている。
また、MAMPのphpMyAdminでも、データベースのテーブルikkulist
の処理の結果が反映されている。
※参考:コード全体(GitHub)
GitHub - ryo-i/express_mysql_crud: ExpressでFetch APIとMySQLを連携してCRUDする
最後に
というわけで、ようやくブラウザのFetch APIとMySQLのデータベースをExpressで連携してCRUD処理することができました。フロントエンドを開発しているとバックエンド側がどんなことをしているのか見えにくいですが、ExpressでAPIを自作することでイメージをすることができました!
Express側とFetch API側でURLとデータの通信タイプが一致したらちゃんと処理が実行されたので感動した!あと、フォームではあまり馴染みがないPUT通信やDELETE通信もちゃんと成功したのも感動!!
あとデータベースはMongoDBなどのJSON型のNoSQLの方がとっつきやすいイメージがあったんだけど、MySQLなどのRDBでもCRUD処理自体はJSONデータで行うため、そんなにとっつきにくさは感じなかったです♪
さて、今後のテーマは3つほど思いついてて、どの方向でいくか検討中です。
- ローカル環境:ExpressでFetch APIとMongoDB(NoSQL)を連携してCRUD処理する
- クラウド環境:ExpressでFetch APIとHerokuのPostgreSQL(RDB)を連携してCRUD処理する
- クラウド環境:Fetch APIとFirebaseのデータベースを連携してサーバレスなCRUD処理を体験
MongoDBやPostgreSQLは今回と似た感じでいけそうならばトライしたいですが、あまり似てないならばExpressについては一区切りにして、FirebaseのサーバレスなCRUD処理を体験してみたく思います。
それではまた!
※参考:Node.js / Expressを習得するためにやったことまとめ
qiita.com