21
19

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 5 years have passed since last update.

SE1年目のJavaScript Webアプリケーションフレームワーク道...Expressで簡易メモ帳アプリ(2)

Posted at

JavaScriptのWebアプリケーションフレームワーク道

SEとしての1年目も終わりに差し掛かり、個人的にやってきたJavaScriptのWebアプリ作りで学んできたことを整理して、アウトプットするために記事にしました。

今回はExpressからDB(MySQL)への接続についてです。
前回に引き続き、メモ帳アプリを作っていきます。

目標

JavaScriptのWebアプリケーションフレームワークの中で最もシンプルなのが"Express"。
Expressでメモ帳アプリを作成することを目標にします。
記事が長くなりそうだったので、基本編とDB接続編の2本を立てです。
この記事は、DB接続編として前回までに作った部分にCRUD処理を追加していきます。

前回の基本編→SE1年目のJavaScript Webアプリケーションフレームワーク道...Expressで簡易メモ帳アプリ(1)

基本編とDB接続編をまとめた完成版コードはGitHubに置いてありますので、ご自由に御覧ください♪ → sasaken555/expMemo

##やること

  • Webアプリケーションフレームワークの利用
  • DB(今回はMySQL)との通信を含むCRUD処理

##やらないこと

  • ES6の記法 → SQLを書く中でバッククオートだけ使ってます。
  • ORM (O/R マッパー) の利用
  • NoSQLDB(MongoDBとか)の利用

実装する要件

基本的なCRUD処理ができれば良いなと。具体的には以下のような感じです。

  1. メモの一覧表示
    1. 最初に表示
    2. 各メモは作成・更新時間も分かるようにする
    3. 各メモは詳細も見れるものとする
  2. メモの登録
    1. タイトルと本文のみ入力する
  3. メモの更新
    1. これもタイトルと本文のみ入力する
  4. メモの削除
    1. 消す前の確認画面を入れる

#実行環境
動作確認はWindows10、GoogleChrome から実施しています。
以下、メインで使う環境たちです。

名称 バージョン 内容
Node.js 7.4.0 JavaScriptのランタイム
NPM 3.10.9 パッケージを管理する
Express 4.14.0 Webアプリケーションフレームワーク
MySQL 5.7 リレーショナルデータベース

前提
Node.js と MySQLはローカルにインストールしています。

#実装

NodejsとMySQLの連携

こちらの記事を参考にさせていただきました。
Node.jsでMySQLを使うメモ

NodejsとMySQLを連携させるには、node-mysqlを使います。
参考記事と、node-mysqlの公式のREADMEを読みつつ進めます。

Express側の準備

まずはnode-mysqlを手に入れるところから。
公式の通り、ターミナルから以下1行を叩けばOK!

npm install --save mysql

これで後はNodejs側からrequireで読み込めばMySQLと接続する処理を行えます。
DBからメモ全件のデータを主として、メモ一覧に表示する処理を書いてみます。

routes/index.js
var express = require('express');
var router = express.Router();

// DB接続定義
var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'EditUser',
  password: 'EditYourPass',
  database: 'memopad',
  port: 3306,
  dateStrings: true,
  timezone: 'jst'
});


/* 一覧画面表示処理 */
router.get('/', function(req, res, next) {
  // メモ一覧をDBから取り出してmemoItemsに格納
  var  memoItems = [];

  // DB接続処理
  connection.connect();
  var sql = 'SELECT * FROM memo ORDER BY id ASC;'
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    memoItems = results;
    res.render('index', { pageTitle: 'メモ一覧', memoItems: memoItems });
  });
  connection.end();
});

createConnectionの部分でMySQLとの接続の準備を行い、
connect() で接続、end() で切断します。
これでOK!

DB側の準備

連携先のデータベースとテーブルを以下のような形で準備します。

データベース名:memopad
テーブル名:memo

項目名 データ型 Null Key その他
id int NO PRI AUTO_INCREMENT
title varchar(50) NO
memo varchar(280) YES
created datetime NO
modified datetime YES

Node.js側ではメモのidを基準に処理を行うので、NOT NULL, PRIMARY KEYに設定しておきます。
DBの準備ができたら、後は5件ほど適当なデータを入れておきます。

一覧の稼働確認

一旦ここまでで動くか確認します。

一覧画面_v2.PNG

上手くいけました!
ただし、ここで1つ罠がありました。

Connectionの罠

ここで何度かリロードしたり、一度閉じてもう一度アクセスすると。
接続拒否に。。。

コンソール上には以下のようなエラーが吐かれていました。

Error: Cannot enqueue Handshake after already enqueuing a Handshake.

要は『Connectionが作られているから、2度Connectionを作ってるんじゃねえ』ってことみたいですね。じゃあ『end()を書かなければ1つのConnectionなんでは?』と思い、connection.end(); を削除してみると...

Error: Cannot enqueue Query after invoking quit.

次は『接続切ったのに、接続試みるのやめてもらえます?』と怒られてしまいました。どっちをしてほしいんでしょうか...
接続切らずに、2度のconnectionを作成しないようにするには、以下の記事に同様の現象の対処が載っていました。→Node+Express+MySQL備忘録

結論から言えば、connection.connect() connection.end()をどちらも書かないが正解みたいです。もう一度公式を見たら、connect()しなくてもquery()で接続を確立する旨が書いてありましたね。

Establishing connections
However, a connection can also be implicitly established by invoking a query:

なので書き直すとしたらこうなるでしょうか。

routes/index.js
/* 一覧画面表示処理 */
router.get('/', function(req, res, next) {
  // メモ一覧をDBから取り出してsampleMemosに格納
  var  memoItems = [];

  // DB接続処理
  // connection(), end()はそれぞれエラーが出るため記述しない
  // query()で接続している
  var sql = 'SELECT * FROM memo ORDER BY id ASC;'
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    memoItems = results;
    res.render('index', { pageTitle: 'メモ一覧', memoItems: memoItems });
  });
});

これでエラーが出なくなりました(*'ω'*)

CRUD書き直し

一覧がうまく表示されたところで、他の処理も書いてしまいます。
基本は一覧処理と同じですが、気を付けるべきは時間の処理でした。

routes/index.js
/* 詳細画面表示処理 */
// 正規表現(\\d)で /new とルーティングを区別している
router.get('/:memoId(\\d+)', function(req, res, next) {
  var memoId = req.params.memoId;
  var targetMemo = [];

  var sql = `SELECT * FROM memo WHERE id = ${memoId};`
  console.log(sql);
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    targetMemo = results[0];
    res.render('memoDetail', { pageTitle: 'メモ詳細', targetMemo: targetMemo });
  });
});
routes/delete.js
var express = require('express');
var router = express.Router();

// DB接続定義
var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'EditUser',
  password: 'EditYourPass',
  database: 'memopad',
  port: 3306,
  dateStrings: true,
  timezone: 'jst'
});


/* 削除確認画面表示処理 */
router.get('/:memoId', function(req, res, next) {
  var memoId = req.params.memoId;
  var targetMemo = [];

  var sql = `SELECT * FROM memo WHERE id = ${memoId};`
  console.log(sql);
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    targetMemo = results[0];
    res.render('deleteMemo', { pageTitle: 'メモ削除', targetMemo: targetMemo });
  });
});


/* 削除処理 */
router.post('/:memoId', function(req, res, next) {
  var memoId = req.params.memoId;

  // SQLの作成
  var sql = `DELETE FROM memo WHERE id = ${memoId}`;

  // 削除実行
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    res.redirect('/');
  });
});

module.exports = router;

詳細表示と削除処理は、表示する部分がほぼ共通しているので書くのは困りませんね。
SQLを作る際にES6のバッククオート記法を使っています。DELETE FROM memo WHERE id = ${memoId} のところです。こうやって${}で変数を直接埋め込めるので便利ですね。
ES6については、かなりNode.jsが対応してきているみたいなので、別の機会にフルES6のNode.jsアプリなんか書いてみたいですね。→Node.jsのES6対応状況

続いて新規作成と更新処理を書きます。
時間の処理だけ気を付けていきます。

routes/new.js
var express = require('express');
var router = express.Router();

// DB接続定義
var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'EditUser',
  password: 'EditYourPass',
  database: 'memopad',
  port: 3306,
  dateStrings: true,
  timezone: 'jst'
});


/*
  日本時間返却処理
  mysqlJpTime(date)
  date: Date型のUTC時間の値
*/
function mysqlJpTime() {
  // UTC時間
  var date = new Date();
  // UTC時間を日本時間に修正
  date.setTime(date.getTime() + 32400000);
  // JSのDate型をMySQLのDateTime型に変換
  var mysqlJpTime = date.toISOString().slice(0, 19).replace('T', ' ');
  return mysqlJpTime;
}


/* 新規登録画面表示処理 */
router.get('/', function(req, res, next) {
  res.render('newMemo', { pageTitle: '新規メモ作成' });
});


/* 新規登録処理 */
router.post('/', function(req, res, next) {
  var title = req.body.title;
  var memo = req.body.memo;
  var createdTime = mysqlJpTime();

  var post = {title: title, memo: memo, created: createdTime};
  var sql = `INSERT INTO memo SET ?`
  connection.query(sql, post, function(error, results, fields) {
    if (error) throw error;
    res.redirect('/');
  });
});

module.exports = router;
routes/edit.js
var express = require('express');
var router = express.Router();

// DB接続定義
var mysql = require('mysql');
var connection = mysql.createConnection({
  host: 'localhost',
  user: 'EditUser',
  password: 'EditYourPass',
  database: 'memopad',
  port: 3306,
  dateStrings: true,
  timezone: 'jst'
});


/*
  日本時間返却処理
  mysqlJpTime(date)
  date: Date型のUTC時間の値
*/
function mysqlJpTime() {
  // UTC時間
  var date = new Date();
  // UTC時間を日本時間に修正
  date.setTime(date.getTime() + 32400000);
  // JSのDate型をMySQLのDateTime型に変換
  var mysqlJpTime = date.toISOString().slice(0, 19).replace('T', ' ');
  return mysqlJpTime;
}


/* 更新画面表示処理 */
router.get('/:memoId', function(req, res, next) {
  var memoId = req.params.memoId;
  var targetMemo = [];

  var sql = `SELECT * FROM memo WHERE id = ${memoId};`
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    targetMemo = results[0];
    res.render('editMemo', { pageTitle: 'メモ更新', targetMemo: targetMemo });
  });
});


/* 更新処理 */
router.post('/:memoId', function(req, res, next) {
  // 更新する値
  var title = req.body.title;
  var memo = req.body.memo;
  var modifiedTime = mysqlJpTime();
  var memoId = req.params.memoId;

  // SQLの作成
  var sql = 'UPDATE memo SET title = ?, memo = ?, modified = ? WHERE id = ?';
  var post = [title, memo, modifiedTime, memoId];
  sql = mysql.format(sql, post);

  // 更新実行
  connection.query(sql, function(error, results, fields) {
    if (error) throw error;
    res.redirect('/');
  });
});

module.exports = router;

ここで引っかかったのは、JavaScriptの時間の処理。以下2つの問題への対処が必要です。ややこしい処理はひとまとめにして目的の時間を返すmysqlJpTime(date) を定義しています。

  • new DateはUTC時間
    new Date で現在時刻を取得すると、UTC時間になっているようですね。
    日本時間に直すためには、9時間分遅らせなきゃいけません。
    右の記事を参考にさせていただいて、以下のように書きました。
    node.jsで現在の時刻を自動で取得する
date.setTime(date.getTime() + 32400000);
  • MySQLのdatetimeにJavaScriptのDateを合わせる
    また、MySQLをdatetime型でテーブル定義してしまいましたので、これまた変換しなければいけません。下の参考記事の中で最もシンプルなものを採用です。
var mysqlJpTime = date.toISOString().slice(0, 19).replace('T', ' ');

これですべての実装が完了!

稼働確認

いつも通り、npm start からの localhost:3000で一覧から確認。

一覧画面
一覧画面_v2.PNG

詳細画面
詳細画面_v2.PNG

新規登録画面
新規登録画面_v2.PNG

更新画面
更新画面_v2.PNG

削除確認画面
削除確認画面_v2.PNG

何とか全ての処理ができたようです(*'ω'*)

#まとめ
2回に渡ってやってきたExpressを使ったメモ帳アプリの作成から、体感は以下の感じです。

  • ExpressはWebアプリ作成の骨となるルーティングや画面表示までは容易にできる。
    • しかも強力なコード自動生成で雛形を作ってくれるので、必要な部分に手を加えればOK
  • DB等の別の機能との連携まで手を伸ばすと、別途Express外の機能の理解が必要。
    • 裏を返せば、他の機能との連携に制約がなく、自由にカスタマイズ可能とも言えそう。

今回はExpressを扱ってみましたが、他のWebアプリケーションフレームワークも触って比較してみたいと思います。それはまた別の機会に。

以上

21
19
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
21
19

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?