第12話 予約をデータベースへ格納する(これで予約処理完了!)
こんにちはKenです!!
今回は予約をデータベース(Postgres)へ格納する超重要な回です。データベースの操作は非同期処理ですのでそれを学習いただけたらと思います。
イベントデータの確認
前回は予約確認をリプライするconfirmation( )関数を作成しました。
では返ってきたイベントデータの中身をいつものように確認しましょう。
ev: {
type: 'postback',
replyToken: 'xxxxxxxxxxxxxxxxxx',
source: { userId: 'yyyyyyyyyyyyyyyyyyy', type: 'user' },
timestamp: 1601720974565,
mode: 'active',
postback: { data: 'yes&4&2020-09-30&10' },
}
これは「はい」を選択した場合のイベントデータです。「はい」ですから、お客様はこのメニュー、日時で予約したいとのことなので、これをデータベースへ格納する処理を行います。
handlePostbackEventの処理
handlePostbackEvent内はsplitData[0]によって処理を条件分岐してますので、以下のように'yes'の場合の処理を記述します。
else if(splitData[0] === 'yes'){
const orderedMenu = splitData[1];
const selectedDate = splitData[2];
const selectedTime = splitData[3];
const startTimestamp = timeConversion(selectedDate,selectedTime);
console.log('その1');
const treatTime = calcTreatTime(ev.source.userId,orderedMenu);
const endTimestamp = startTimestamp + treatTime*60*1000;
console.log('その4');
console.log('endTime:',endTimestamp);
}else if(splitData[0] === 'no'){
// あとで何か入れる
}
解説していきます。
timeConversion関数は自分で作った関数です。日付、時刻を引数として渡し、タイムスタンプ形式へ変換する関数です。今後、タイムスタンプへ変換する処理が何回かあるため、関数として外出ししてます。
timeConversion関数は簡単で以下のコードです。
const timeConversion = (date,time) => {
const selectedTime = 9 + parseInt(time) - 9;
return new Date(`${date} ${selectedTime}:00`).getTime();
}
new DateやgetTimeについては以下を参考にしてください。初めの9は9時開店なので、9を起点にする意味での9です。後ろの-9は、Node.jsだからなのかわかりませんが、new Date( )で日本標準時間へ変換するために勝手に+9時間分のミリ秒が足されてしまうため、-9時間しているという意味です。この辺に詳しい方ぜひ教えてください。
次にcalcTreatTimeを見ていきましょう。これはその名の通り、「施術時間を計算する」関数です。
今まで、来店希望時間は施術開始時間(startTimestamp)です。施術終了時間(endTimestamp)を計算するために、施術にかかる時間が必要となります。
calcTreatTimeを実装していきますが、ここでは「非同期処理」を使います。「非同期処理」は重要ですが、わかりにくいため、最初はあえて間違ったコーディングをし、「なぜそうなるのか」を理解した後に正しいコーディングへ直したいと思います。
calcTreatTime関数の実装
施術にかかる時間はグローバル変数に次のように定義しました。
const INITIAL_TREAT = [20,10,40,15,30,15,10]; //施術時間初期値(min)
カット→20分、シャンプー→10分、カラーリング→40分、ヘッドスパ→15分、マッサージ&パック→30分、眉整え→15分、顔そり→10分といった感じです。このうち、カット、シャンプー、カラーリング、ヘッドスパだけは、人によって施術時間が変わるため、顧客データベースの項目に入れてあとで変更できるようにしました。以下を復習してみてください。
そのため、選ばれたメニューがカット、シャンプー、カラーリング、ヘッドスパの場合はデータベースから施術時間を取ってこなければなりません。
calcTreatTimeのコードは次のようになります。
const calcTreatTime = (id,menu) => {
console.log('その2');
const selectQuery = {
text: 'SELECT * FROM users WHERE line_uid = $1;',
values: [`${id}`]
};
connection.query(selectQuery)
.then(res=>{
console.log('その3');
if(res.rows.length){
const info = res.rows[0];
const treatArray = [info.cuttime,info.shampootime,info.colortime,info.spatime,INITIAL_TREAT[4],INITIAL_TREAT[5],INITIAL_TREAT[6]];
const menuNumber = parseInt(menu);
const treatTime = treatArray[menuNumber];
return treatTime;
}else{
console.log('LINE IDに一致するユーザーが見つかりません。');
return;
}
})
.catch(e=>console.log(e));
}
まずSQLのクエリ文で、line_idがidに一致するデータを抽出しています。idは何かと言うと元はev.source.userId、つまり予約操作をしている人のLINE IDですね。
connection.query( )によってクエリを実行していますが、.then( )で繋がれているため、Promiseであることがわかります。そして返ってきたresオブジェクトの中で、データベースから取ってきたデータが含まれているのが、res.rows[0]となります。この辺はconsoleでresの中身を確認すると良いと思います。
なお、該当データがない場合、res.rowsの中には何もありません。つまり、データベースに登録されたユーザーではないことを意味します。その判断をするために、if( res.rows.length )でres.rowsが存在するかどうかを判定してます。このように、配列が存在するかどうかは、配列の長さがあるかどうかで判定します。
そしてtreatArrayという新たな配列を作っております。メニューごとの施術時間ですが、人によって施術時間が変わるものはデータベースに登録された時間、変わらないものはINITIAL_TREATをそのまま使ってます。
そして引数のmenuは選択されたメニュー番号(例えばシャンプーなら'1')を表しますが、文字列型のため、整数型へパースしてます。
そしてそのパースされた数字をtreatArrayのindexに使ってあげれば、選択されたメニューの施術時間が得られるわけです。それをtreatTime変数へ格納し、returnしてあげてます。
endTimestampの計算と確認
handlePostbackEvent関数に戻り、施術終了時間のタイムスタンプを計算します。
const endTimestamp = startTimestamp + treatTime*60*1000;
何をしているかというと、開始時間+施術時間をしてあげれば終了時間となりますが、calcTreatTime関数から返ってきたtreatTimeの単位は分ですので、これをミリ秒へ変換する必要があります。そのための*60*1000です。
ここまでできたら、herokuへデプロイしましょう。
$ heroku logs --tail
によって、コンソールが見れるようにします。LINEで「はい」をタップしたらコンソールに何が出力されたでしょうか。
endTime: NaN となったかと思います。
あえて解説してませんでしたが、この原因を探るため、コードの中にはconsole.log('その1')を'その4'まで仕込んであります。これもコンソールに出力されていると思いますので、確認してみてください。
私たちは、その1→その2→その3→その4と出力されると思い込んでコードを書いていると思いますが、実際はどうでしょうか。
その1→その2→その4→その3
となっていませんか。その3はどこにあるかというと、calcTreatTime関数内のconnection.query( ).then( )内にあります。つまりこの中の処理に時間がかかっているため、その終了を待たずにプログラムはその4の処理を先に実行したということです。これをJavaScriptの非同期処理と呼び、このおかげでJavaScriptは高速に動いているとも言えます。
ただし、その4はその3の処理の結果を使うので、その3の終了を待たなければなりません。
そこで登場するのがPromiseです。次のページが参考になるかと思います。
この中の言葉を引用させてもらうと次のようになります。
Promiseは処理が実行中の処理を監視し、処理が問題なく完了すればresolve、反対に問題があればrejectを呼び出してメッセージを表示します。
Promiseの適用
では、calcTreatTime関数を書き換えてみます。
const calcTreatTime = (id,menu) => {
return new Promise((resolve,reject)=>{
console.log('その2');
const selectQuery = {
text: 'SELECT * FROM users WHERE line_uid = $1;',
values: [`${id}`]
};
connection.query(selectQuery)
.then(res=>{
console.log('その3');
if(res.rows.length){
const info = res.rows[0];
const treatArray = [info.cuttime,info.shampootime,info.colortime,info.spatime,INITIAL_TREAT[4],INITIAL_TREAT[5],INITIAL_TREAT[6]];
const menuNumber = parseInt(menu);
const treatTime = treatArray[menuNumber];
resolve(treatTime);
}else{
console.log('LINE IDに一致するユーザーが見つかりません。');
return;
}
})
.catch(e=>console.log(e));
});
}
return new Promiseによってプロミスオブジェクトを返してあげてます。つまり、calcTreatTimeが完了(resolve)したら、treatTimeを返しています。
では、handlePostbackEvent内関数はどう変更すれば良いでしょうか。
const treatTime = calcTreatTime(ev.source.userId,orderedMenu);
const treatTime = await calcTreatTime(ev.source.userId,orderedMenu);
としてあげます。awaitはプロミスの結果が返ってくるまで、async関数内の処理を一時停止し、待機するものです。
ここまでできたら、herokuへデプロイし、heroku logs --tailでコンソールを立ち上げ、LINEで「はい」をタップした時の出力を確認してみましょう。
今度はその1→その2→その3→その4となり、endtimeの値もしっかり出力されたのではないでしょうか。
予約データベースの作成
以前は顧客データベースを作成しましたが、予約データベースはまだ作成していませんでしたね。
まず顧客データベースと同様に予約データベースを作成しましょう。
const create_reservationTable = {
text:'CREATE TABLE IF NOT EXISTS reservations (id SERIAL NOT NULL, line_uid VARCHAR(255), name VARCHAR(100), scheduledate DATE, starttime BIGINT, endtime BIGINT, menu VARCHAR(50));'
};
connection.query(create_reservationTable)
.then(()=>{
console.log('table users created successfully!!');
})
.catch(e=>console.log(e));
頭の方のusersテーブルを作成した下あたりに記述すると良いでしょう。データベースを構成する要素は次の項目になります。
■ id: 連続するユニークな値
■line_uid: 予約者のLINE ID
■name: 予約者の名前(LINEの表示名)
■scheduledate: 予約日
■starttime: 予約開始時間のタイムスタンプ
■endtime: 予約終了時間のタイムスタンプ
■menu: メニュー番号('0'など)
これで、プログラムが立ち上げられた際に、もしreservationsというテーブルが存在しない場合は、それを作成します。
データベースへの予約データの格納
それでは、予約データを格納するコードを書いていきましょう。
予約が確定するタイミングはLINEで「はい」がタップされ、postbackイベントが返ってきた時なので、handlePostbackEvent関数内のyes処理内に記述することにします。
else if(splitData[0] === 'yes'){
const orderedMenu = splitData[1];
const selectedDate = splitData[2];
const selectedTime = splitData[3];
const startTimestamp = timeConversion(selectedDate,selectedTime);
const treatTime = await calcTreatTime(ev.source.userId,orderedMenu);
const endTimestamp = startTimestamp + treatTime*60*1000;
const insertQuery = {
text:'INSERT INTO reservations (line_uid, name, scheduledate, starttime, endtime, menu) VALUES($1,$2,$3,$4,$5,$6);',
values:[ev.source.userId,profile.displayName,selectedDate,startTimestamp,endTimestamp,orderedMenu]
};
connection.query(insertQuery)
.then(res=>{
console.log('データ格納成功!');
client.replyMessage(ev.replyToken,{
"type":"text",
"text":"予約が完了しました。"
});
})
.catch(e=>console.log(e));
データの挿入なのでクエリ文はINSERT INTOを使います。
データの格納に成功すると、「予約が完了しました」というリプライを返してあげています。
予約がされたかの確認
では最後に確認してきましょう。いつものようにherokuへデプロイしheroku logs --tailでログを立ち上げましょう。
こんな感じでメッセージが返って来れば成功です。
では、実際にデータベースに書き込まれているか確認してみましょう。
$ heroku pg:psql
コンソール上でpostgresを立ち上げます。
DATABASE => select * from reservations;
によりreservationsテーブルの全データを表示させます。
こんな感じに表示されれば成功です!!
さて、今回は非同期処理やデータベースの処理など非常に重要な内容が多かったと思います。これをマスターできれば、他のプログラムも同様に書いていくことができると思います。
少しでも参考になりましたら「スキ」をいただけると嬉しいです。
最後までお読みいただき、ありがとうございました。
MENTA でLINEBOT開発サポートをしております。お気軽にご相談ください。