เนื้อหาตอนต่อไปนี้ เราจะมาดูในเรื่องของการใช้งานฐานข้อมูล
ใน Express หลังจากที่เราได้จำลองการทำ RESTful API
ไปแล้วในตอนที่ผ่านๆ มา ซึ่งในตอนนั้น เราใช้ข้อมูลที่เป็น Array object
แล้วจำลองการเพิ่ม ลบ แก้ไข และดึงข้อมูลมาแสดง ด้วยคำสั่ง JavaScript ที่
จัดการเกี่ยวกับ array อย่างง่าย
ฐานข้อมูล MySQL เป็นการจัดเก็บข้อมูลแบบถาวรรูปแบบหนึ่ง ที่สามารถใช้งานร่วมกับ
Express ได้ เราสามารถเลือกใช้งาน การจ้ดการกับข้อมูล ด้วยฐานข้อมูลแบบอื่นๆ ได้ตามต้องการ
เช่น Oracle, PostgreSQL, Redis, SQL Server, SQLite หรือ MongoDB เป็นต้น
ดูการใช้งาน Express ร่วมกับฐานข้อมูลอื่นๆ เพิ่มเติมได้ที่ Database integration
การติดตั้ง MySQL Module
npm install mysql --save
หลังจากติดตั้ง เรียบร้อยแล้ว ตอนนี้เราพร้อมทำการเชื่อมต่อ และใช้งาน MySQL แล้ว
เราจะจัดการฐานข้อมูลเบื้องต้นผ่าน phpMyadmin ใน Server จำลอง XAMPP หรือใครเป็นตัวอื่น
ก็จะสามารถเข้าไปดำเนินการได้ตามปกติ
ในที่นี้เราจะใช้ฐานข้อมูลชื่อ "testdb" จากนั้นสร้างตาราง "tbl_users" ด้วยรูปแบบ โครงสร้างคำสั่ง SQL เป็นดังนี้
-- -- Table structure for table `tbl_users` -- CREATE TABLE `tbl_users` ( `id` int(11) NOT NULL, `name` varchar(30) NOT NULL, `email` varchar(150) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- -- Indexes for table `tbl_users` -- ALTER TABLE `tbl_users` ADD PRIMARY KEY (`id`); -- -- AUTO_INCREMENT for table `tbl_users` -- ALTER TABLE `tbl_users` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
เราจะใช้โครงสร้างข้อมูล เหมือนที่ใช้งานในตัวจำลอง ซึ่งจะมี id , name และ email
ในเนื้อหาบทความที่เกี่ยวกับ RESTful API ตอนแรก http://niik.in/913
เรากำหนด USERS API ไว้ในโฟลเดอร์ "routes' ในไฟล์ users.js ในที่นี้เราจะเปลี่ยนไปกำหนดในโฟลเดอร์เฉพาะ
ที่เกี่ยวกับ API หรือที่ใช้งานสำหรับ RESTful API ส่วนไฟล์ users.js ในโฟลเดอร์ "routes" เราจะใช้เสมือนเป็นเพจหน้า
users ดังนั้นให้แก้ไขไฟล์ users.js [routes/users.js] เป็นดังนี้
ไฟล์ users.js [routes/users.js]
const express = require('express') const router = express.Router() router.get('/', function(req, res, next) { res.send('Users Page') }) module.exports = router
ต่อไปเราสร้างโฟลเดอร์ชื่อ api สำหรับเก็บการจัดการต่างๆ เกี่ยวกับ RESTful API โดยในบทความนี้ เราจะสร้าง
สองไฟล์ประกอบเนื้อหา คือ users.js และ provinces.js
ไฟล์ users.js [api/users.js]
const express = require('express') const router = express.Router() const { validation, schema } = require('../validator/users') router.route('/users?') .get((req, res, next) => { // แสดงข้อมูลทั้งหมด return res.json({}) }) .post(validation(schema),(req, res, next) => { // เพิ่มข้อมูลใหม่เข้าไปในฐานข้อมูล และแสดงข้อมูลใหม่ที่เพิ่งเพิ่ม return res.json({}) }) router.route('/user/:id') .all((req, res, next) => { // ตรวจสอบว่า id ข้อมูลที่ส่งมา หรืออยู่ในฐานข้อมูลหรือไม่ // ถ้ามีส่งต่อไปดึงมาแสดง / แก้ไข / ลบ next() }) .get((req, res, next) => { // แสดงรายการข้อมูลจากฐานข้อมูลของ id ข้อมูลที่อต้กงาร return res.json({}) }) .put(validation(schema),(req, res, next) => { // ทำการแก้ไขรายการข้อมูลของ id ข้อมูลที่ต้องการ จากฐานข้อมูล แล้วแสดงรายการข้อมูลที่แก้ไข return res.json({}) }) .delete((req, res, next) => { // ทำการลบช้อมูลของ id ข้อมูลที่ต้องการ จากฐานข้อมูล แล้วแสดงข้อมูลที่เพิ่งลบ return res.json({}) }) module.exports = router
โค้ดข้างต้น เราจะวางให้เห็นโครงสร้างคร่าวๆ ของ USERS API ซึ่งมีเค้าโครงมาจากรูปแบบเดิมที่ใช้ข้อมูลจำลอง
เรายังมีการใช้งานการ ตรวจสอบความถูกต้องของข้อมูล หรือ validator เพิ่มเติมที่เนื้อหา http://niik.in/914
ต่อไปให้เราไปแก้ไขที่ไฟล์ app.js เป็นดังนี้
const express = require('express') // ใช้งาน module express const app = express() // สร้างตัวแปร app เป็น instance ของ express const path = require('path') // เรียกใช้งาน path module const createError = require('http-errors') // เรียกใช้งาน http-errors module const port = 3000 // port // ส่วนของการใช้งาน router module ต่างๆ const indexRouter = require('./routes/index') const userRouter = require('./routes/users') const userApi = require('./api/users') // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); app.set('view options', {delimiter: '?'}); // app.set('env','production') app.use(express.json()) app.use(express.urlencoded({ extended: false })) app.use(express.static(path.join(__dirname, 'public'))) // เรียกใช้งาน indexRouter app.use('/', indexRouter) app.use('/user', userRouter) app.use('/api', [userApi]) // ทำงานทุก request ที่เข้ามา app.use(function(req, res, next) { var err = createError(404) next(err) }) // ส่วนจัดการ error app.use(function (err, req, res, next) { // กำหนด response local variables res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} // กำหนด status และ render หน้า error page res.status(err.status || 500) // ถ้ามี status หรือถ้าไม่มีใช้เป็น 500 res.render('error') }) app.listen(port, function() { console.log(`Example app listening on port ${port}!`) })
สังเกตในส่วนของการเรียกใช้งาน router module
const userRouter = require('./routes/users') const userApi = require('./api/users')
เรากำหนดแยกชัดเจนระหว่าง routes สำหรับ api อยู่ในโฟลเดอร์ "api" และ routes สำหรับเว็บเพจ อยู่ใน
โฟลเดอร์ "routes"
และในการกำหนด routes ให้กับแต่ละ module เราก็แยก path ออกจากกันชัดเจน เป็น
app.use('/user', userRouter) // http://localhost:3000/user app.use('/api', [userApi]) // http://localhost:3000/api/users
สังเกตตรงการกำหนด router ให้กับ path: "/api" เราใช้งานในรูปแบบ array ในเครื่องหมาย [] ทั้งนี้เพราะ เดี๋ยว
เราจะมีการเพิ่ม api ของส่วนอื่นเข้ามา คือ provinceApi เราก็สามารถกำหนดเป็น [userApi, provinceApi]
นั่นก็คือ เราจะสามารถเรียกใช้งาน path: "/api/provinces โดยไม่ต้องไปกำหนดแยกอีกบรรทัด เกี่ยวกับ provinceApi
จะพูดถึงในหัวข้อต่อไปตามลำดับ
ตอนนี้เราเตรียมโครงสร้างไฟล์ และโฟลเดอร์ของโปรเจ็คเบื้องต้นเรียบร้อยแล้ว จะไปต่อในเรื่องของ MySQL
การเชื่อมต่อกับ MySQL
ต่อไปเราจะสร้างไฟล์ module สำหร้บทำการเชื่อมต่อกับฐานข้อมูล MySQL ให้เราสร้างโฟลเดอร์ชื่อ config จากนั้น
สร้างไฟล์ db.js และกำหนดโค้ดเบื้องต้นเป็นดังนี้
ไฟล์ db.js [config/db.js]
const mysql = require('mysql') // เรียกใช้งาน MySQL module // กำหนดการเชื่อมต่อฐานข้อมูล const db = mysql.createConnection({ host : 'localhost', user : 'root', password : '', database : 'testdb' }) // ทำการเชื่อมต่อกับฐานข้อมูล db.connect((err) =>{ if(err){ // กรณีเกิด error console.error('error connecting: ' + err.stack) return } console.log('connected as id ' + db.threadId) }) // ปิดการเชื่อมต่อฐานข้อมูล MySQL ในที่นี้เราจะไม่ให้ทำงาน // db.end() module.exports = db
จะเห็นว่า ถ้าเรามีการเชื่อมต่อไปยัง MySQL server และไม่เกิดข้อผิดพลาดขึ้น จะขึ้น "connected as id 686"
นั่นคือเชื่อมต่อสำเร็จ เราจะใช้การเชื่อมต่อที่ id นี้ในการำงานร่วมกับฐานข้อมูล MySQL
ในขั้นตอนการพัฒนา app ค่่า id จะเปลี่ยนแปลงไปเรื่อยๆ เมื่อเราทำการ restart nodejs หรือทำการแก้ไขไฟล์
เหตุผลที่ปิดการทำงานของ db.end() ไม่ให้ทำงานหรือไม่ให้ทำการปิดการเชื่อมต่อกับ MySQL ที่การเชื่อมต่อ id ที่เกิดขึ้น
นั้น ก็เพื่อ ให้ใช้การเชื่อมต่อนี้ในการทำ คำสั่ง SQL ต่างๆ โดยไม่ต้องทำการเชื่อมต่อใหม่ทุกๆ ครั้ง ยกตัวอย่างเช่น เรา
ทำการเชื่อมต่อฐานข้อมูล เสร็จแล้วเราทำการเพิ่มข้อมูลเข้าไปในฐานข้อมูล จากนั้น ถ้าเราใช้คำสั่ง db.end() จะกลายเป็นว่า
การเชื่อมต่อทั้งหมดสิ้นสุดลง เราจะใช้คำสั่งแสดงข้อมูลที่เพิ่งไปต่อในทันทีอีกไม่ได้ ต้องมาทำการเชื่อมต่อใหม่ทุกครั้ง
แทนที่จะใช้การเชื่อมต่อเดิม ทำงานต่างๆ ให้เสร็จ เข้าใจอย่างง่าย คือเราจะใช้คำสั่ง db.end() เมื่อไม่ใช้งาน app แล้ว
อย่างไรก็ตาม สามารถศึกษารูปแบบการเชื่อมต่อกับ MySQL เพิ่มเติมได้ที่ MySQL module
การทำการ Query ข้อมูล MySQL
ก่อนจะไปต่อที่การใช้งาน MySQL เรามาดูว่า MySQL module มีรูปแบบการทำงานต่างๆ เกี่ยวกับการคิวรี่ข้อมูล
ด้วยคำสั่งอย่างไรบ้าง
คำสั่ง query()
ในการทำคำสั่ง SQL จะทำผ่านคำสั่ง query() ในรูปแบบ db.query() โดยตัว db เป็น instance การเชื่อมต่อกับ
ฐานข้อมูล เราจะใช้เป็นตัวแปรอะไรก็ได้เช่น connection.query() แต่ในที่นี้เรากำหนดในไฟล์ db.js ใช้
// กำหนดการเชื่อมต่อฐานข้อมูล const db = mysql.createConnection({ host : 'localhost', user : 'root', password : '', database : 'testdb' })
คำสั่ง query() มีลักษณะการทำงานง่ายๆ หลักๆ ก็คือการโยนคำสั่ง SQL เข้าไป ดังนี้
การแสดงข้อมูล (SELECT)
let sql = ' SELECT * FROM tbl_users ' db.query(sql, (error, results, fields) =>{ // error คือ ข้อผิดพลาดของคำสั่ง sql นั้นๆ if (error) throw error; // results เป็น array ของข้อมูลผลลัพธ์ // fields เป็นรายละเอียดของฟิลด์ของตาราง tbl_users ปกติเราจะไม่ได้ใช้ค่านี้เท่าไหร่ })
ตัวอย่าง
let sql = ' SELECT * FROM tbl_users ' db.query(sql,(error, results, fields)=>{ if(error) { throw error } res.json(results) })
ในคำสั่ง sql เราสามารถกำหนดค่าที่ต้องการเข้าไปได้เลย เช่น
let sql = ' SELECT * FROM tbl_users WHERE id = 1 ' // แบบกำหนดค่าโดยตรง let sql = ' SELECT * FROM tbl_users WHERE id = '+ req.params.id // แบบกำหนดค่าจากตัวแปร // แบบป้องกัน SQL Injection let sql = ' SELECT * FROM tbl_users WHERE id = '+ db.escape(req.params.id)
แต่วิธีที่นิยมจะกำหนดค่าใน parameter ตัวที่สองของคำสั่ง query()
let sql = ' SELECT * FROM tbl_users WHERE id = ? ' db.query(sql, [req.params.id], (error, results, fields) =>{ // error คือ ข้อผิดพลาดของคำสั่ง sql นั้นๆ if (error) throw error; // results เป็น array ของข้อมูลผลลัพธ์ // fields เป็นรายละเอียดของฟิลด์ของตาราง tbl_users ปกติเราจะไม่ได้ใช้ค่านี้เท่าไหร่ })
ซึ่งเป็นลักษณะคล้าย prepared statements ในคำสั่ง MySQL ที่รองรับการป้องกัน SQL Injection แต่ก็ไม่เหมือน
กันเสียทีเดียว เพราะค่าบางค่าที่กำหนด้วย ? จะถูกแทนที่ด้วยรูปแบบข้อมูลแตกต่างกันไป เช่น ค่าที่เป็น boolean จะ
ถูกแทนค่าเป็น true / false ค่าที่เป็น array เช่น ['a', 'b'] จะถูกแปลงเป็น 'a', 'b' เป็นต้น เหล่านี้เป็นขั้นตอนการทำงาน
ของ MySQL Module เราอาจไม่ต้องสนใจเท่าไหร่
let table = 'tbl_users' let columns = ['name','email'] let id = 1 let sql = ' SELECT ?? FROM ?? WHERE id = ? ' db.query(sql, [columns, table, id],(error, results, fields)=>{ console.log(error) if(error) { throw error } console.log(results) console.log(fields) res.json(results) })
นอกจากนั้น เรายังสามารถใช้ ?? แทนชื่อที่เป็นส่วนของฟิลด์หรือชื่อตาราง และใช้ ? แทนค่าของข้อมูล
คำสั่ง SQL ข้างต้นจะถูกแปลงเป็น
SELECT `name`, `email` FROM `tbl_users` WHERE id = 1
การเพิ่มข้อมูล (INSERT)
let user = { "name": 'William Smith', "email": 'will.smith@gmail.com' } let sql = ' INSERT INTO tbl_users SET ? ' db.query(sql, user, (error, results, fields)=>{ console.log(error) if(error) { throw error } console.log(results.insertId) console.log(results) console.log(fields) res.json(results) })
ในรูปแบบการเพิ่มข้อมูล เราจะกำหนดชื่อฟิลด์ และค่าของข้อมูลในรูป Object ค่าที่สำคัญค่าหนึ่งเมื่อมีการเพิ่ม
ข้อมูลคือ results.insertId หรือ id ที่เป็น AUTO INCREMENT ที่เราสามารถนำไปใช้งานต่อได้
การอัพเดทข้อมูล (UPDATE)
let user = { "name": 'Jennifer Lee', "email": 'jennifer.lee@gmail.com' } let id = 2 let sql = ' UPDATE tbl_users SET ? WHERE id = ? ' db.query(sql, [user ,id], (error, results, fields)=>{ console.log(error) if(error) { throw error } if(results.affectedRows > 0) { console.log('updaed new value') } console.log(results) console.log(fields) res.json(results) })
ในรูปแบบการอัพเดทข้อมูล จะกำหนดชื่อฟิลด์และข้อมูลที่อัพเดทเหมือนกับการกำหนดในขั้นตอนการเพิ่มข้อมูล
นอกจากนั้นในขั้นตอนการอัพเดท เราต้องส่ง id เงื่อนไขฟิลด์ที่จะอัพเดทเพิ่มเข้าไปด้วย ค่าที่ระบุจำนวนรายการ
ที่มีการอัพเดท results.affectedRows เป็นค่าที่จะบอกว่า มีการแก้ไขข้อมูลเป็นค่าใหม่ จำนวนกี่รายการ หากเราทำการ
อัพเดทข้อมูล แต่เป็นการส่งค่าเดิมไปทำการอัพเดท ดังนั้นจึงไม่มีผลต่อการเปลี่ยนแปลงข้อมูล results.affectedRows
ก็จะเท่ากับ 0
การลบข้อมูล (DELETE)
let id = 2 let sql = ' DELETE FROM tbl_users WHERE id = ? ' db.query(sql, [id], (error, results, fields)=>{ console.log(error) if(error) { throw error } if(results.affectedRows > 0) { console.log('deleted') } console.log(results) console.log(fields) res.json(results) })
ในรูปแบบการลบข้อมูล จะส่ง id เข้าไปในคำสังคิวรี่ เพื่อยืนยันว่ามีการลบเกิดขึ้นจริงๆ เราสามารถเช็คได้จาก
ค่า results.affectedRows ว่ามีข้อมูลหรือแถวรายการที่ถูกลบไปกี่รายการ ปกติก็จะเท่ากับ 1 รายการ
บล๊อกของคำสั่ง SQL (Transaction)
db.beginTransaction((err) => { if (err) { throw err } let user = { "name": 'William Smith', "email": 'will.smith@gmail.com' } let sql = ' INSERT INTO tbl_users SET ? ' db.query(sql, user, (error, results, fields)=>{ if (error) { return db.rollback(function() { throw error }) } let user = { "name": 'Jennifer Lee', "email": 'jennifer.lee@gmail.com' } let id = results.insertId let sql = ' UPDATE tbl_users SET ? WHERE id = ? ' db.query(sql, [user ,id], (error, results, fields)=>{ if (error) { return db.rollback(function() { throw error }) } db.commit((err) => { if (err) { return db.rollback(function() { throw err }) } console.log('success!') res.json(results) }) // end commit }) // end update }) // end insert }) // end beginTransaction
การทำคำสั่ง SQL ที่เป็นบล็อกคำสั่งอาจจะมีคำสั่งเดียวหรือหลายคำสั่งก็ได้ ที่เรียกว่า transaction อย่างในตัวอย่าง
ด้านบน เป็นการเพิ่มข้อมูลใหม่เข้าไปในฐานข้อมูล จากนั้นทำการอัพเดทข้อมูลนั้น โดยอ้างอิงค่า id ข้อมูลจาก
results.insertId เพื่อไปทำการอัพเดทข้อมูลเป็นค่าใหม่ จะเห็นว่า รูปแบบข้างต้นมีการใช้งานคำสั่ง
db.beginTransaction() > db.rollback() > db.commit() ไปตามลำดับ คือ เริ่มต้น beginTransaction()
ถ้าเกิด error ก็คือย้อนกลับด้วย rollback() และสุดท้ายจบที่ commit() ทำคำสั่งสุดท้าย
การใช้งาน MySQL ร่วมกับ RESTful API
เมื่อเราเข้าใจรูปแบบการใช้งาน MySQL module เพื่อทำงานร่วมกับฐานข้อมูล MySQL เบื้องต้นแล้ว ต่อไปเราก็จะนำ
มาประยุกต์ใช้งานกับ USERS API ของเราในไฟล์ users.js [api/users.js] จะได้เป็นดังนี้
ไฟล์ users.js [api/users.js]
const express = require('express') const router = express.Router() const { validation, schema } = require('../validator/users') const db = require('../config/db') // เรียกใช้งานเชื่อมกับ MySQL router.route('/users?') .get((req, res, next) => { // ทำการแสดงข้อมูลทั้งหมด let sql = ' SELECT * FROM tbl_users ' db.query(sql,(error, results, fields)=>{ // เกิด error ในคำสั่ง sql if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) // แสดงข้อมูลกร๊ไม่เกิด error const result = { "status": 200, "data": results } return res.json(result) }) }) .post(validation(schema),(req, res, next) => { // เตรียมฟิดล์และค่าของข้อมูลที่จะเพิ่ม let user = { "name": req.body.name, "email": req.body.email } let sql = ' INSERT INTO tbl_users SET ? ' db.query(sql, user, (error, results, fields)=>{ // เกิด error ในคำสั่ง sql if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) // เพื่อไม่ต้องไปดึงข้อมูลที่เพิ่งเพิม มาแสดง ให้เราใช้เฉพาะ id ข้อมูลใหม่ที่เพิ่งเพิม // รวมกับชุดข้อมูลที่เพิ่งเพิ่ม เป็น ข้อมูลที่ส่งกลับออกมา user = [{'id':results.insertId, ...user}] const result = { "status": 200, "data": user } return res.json(result) }) }) router.route('/user/:id') .all((req, res, next) => { // ตรวจสอบว่า id user ที่ส่งเข้ามาเพื่อดูข้อมูล / แก้ไข / ลบ มีอยู่ในฐานข้อมูลหรือไม่ let sql = ' SELECT * FROM tbl_users WHERE id = ? ' db.query(sql, [req.params.id], (error, results, fields)=>{ // เกิด error ในคำสั่ง sql if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) // กรณีไม่พบข้อมูล if(results.length ===0) return res.status(400).json({ "status": 400, "message": "Not found user with the given ID" }) res.user = results // ถ้ามี id นั้นอยู่ในฐานข้อมูล ส่งข้อมูลผู้ใช้นั้นไปทำงานต่อ next() }) }) .get((req, res, next) => { // ถ้าเป็นการแสดงข้อมูลของ ผู้ใช้ id ใด ก็นำค่าที่ถูกส่งมาไปแสดง const result = { "status": 200, "data": res.user } return res.json(result) }) .put(validation(schema),(req, res, next) => { // ถ้าเป็นการแก้ไขข้อมูล ก็เตรียมรูปแบบข้อมูลที่รับมาสำหรับแก้ไข let user = { "name": req.body.name, "email": req.body.email } // ทำการอัพเดท โดยอิง id ที่พบ let sql = ' UPDATE tbl_users SET ? WHERE id = ? ' db.query(sql, [user, req.params.id], (error, results, fields)=>{ // เกิด error ในคำสั่ง sql if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) // ถ้ามีการแก้ไขค่าใหม่ if(results.affectedRows > 0) { // เอาค่าฟิลด์ทีได้ทำการอัพเดท ไปอัพเดทกับข้อมูลทั้งหมด user = Object.assign(res.user[0], user) }else{ // มีการอัพเดท แต่เป็นค่าเดิม user = res.user } // ส่งรายการข้อมูลที่อัพเทกลับไป const result = { "status": 200, "data": user } return res.json(result) }) }) .delete((req, res, next) => { // ถ้าเป็นการลบข้อมูล จาก id ใดๆ let sql = ' DELETE FROM tbl_users WHERE id = ? ' db.query(sql, [req.params.id],(error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) // ใช้ค่าข้อมูลถ้าค้นเจอ ก่อนลบ ส่งออกไป เป็นการบอกว่า รายการนี้คือรายการที่ถูกลบไปแล้ว const result = { "status": 200, "data": res.user } return res.json(result) }) }) module.exports = router
ในการใช้งาน RESTful API ร่วมกับ MySQL ข้างต้น เรามีการเรียกใช้งาน db module ด้านบน ซึ่งเป็นการเชื่อมต่อ
กับฐานข้อมูล MySQL
ในกรณี error ใดๆ ที่เกิดขึ้นจากคำสั่ง SQL เราจะส่งค่ากลับมาเป็น 500 (Internal Server Error)
// เกิด error ในคำสั่ง sql if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage })
แต่ในขั้นตอนการพัฒนา เราสาารถใช้ค่า error.sqlMessage เพื่อดูคำสั่ง SQL ที่มีปัญหา หรือเพื่อ debug ได้
ในการดึงข้อมูลมีการระบุ id เช่นการแสดงรายการ user id ใดๆ การลอัพเดท และการลบข้อมูล หากทำรายการสำเร็จ
เราจะทำการส่งกลับรายการข้อมูลนั้นออกมา เพื่อนำไปใช้งานต่อ อย่างเช่น การลบข้อมูล ถึงแม้ว่าข้อมูลนั้นถูกลบไปแล้ว
แต่ค่าที่ส่งกลับมาจะเป็นค่าข้อมูลก่อนที่จะถูกลบ เราอาจจะเอาค่าไปแสดงหรือแจ้งว่า ข้อมูลดังกล่าวตามรายละเอียดนี้
ถูกลบไปเรียบร้อยแล้ว เหล่านี้เป็นต้น
อย่างไรก็ตาม จะไม่ขอสาธิตการทำงานเพิ่มเติม เพื่อไม่ให้เนื้อหายาวจนเกินไป
RESTful API รองรับการแบ่งหน้า
จะขอเพิ่มเติมตัวอย่างการสร้าง RESTful API ที่รองรับการแบ่งหน้า ทั้งนี้เพราะ หากข้อมูลมีจำนวนมากๆ หรือ
หากเราต้องการนำข้อมูลไปแสดง แบบรองรับการแบ่งหน้า ก็จะเป็นต้องปรับส่วนของการแสดงผล รวมทั้งรองรับการ
รับค่าที่ระบุว่า กำลังแสดงข้อมูลหน้าไหนอยู่ หลักการทำงานก็จะคล้ายๆเดิม เราจะใช้ข้อมูลตาราง tbl_provinces
เป็นฐานข้อมูลจังหวัดในประเทศไทย ดูคำสั่ง SQL ตารางข้อมูลได้ที่ http://niik.in/que_2398_6277
สิ่งที่เราจะสร้างมีด้วยกัน 2 ไฟล์คือ
1.ไฟล์ provinces.js ในโฟลเดอร์ validator
2.ไฟล์ provinces.js ในโฟลเดอร์ api
ตามลำดับดังนี้
ไฟล์ provinces.js [validator/provinces.js]
const Joi = require('@hapi/joi') const validation = (schema) =>{ return ((req, res, next) => { // ทำการตรวจสอบความถูกต้องของข้อมูล req.body ที่ส่งมา Joi.validate(req.body, schema, function (error, value) { // กรณีเกิด error ข้อมูลไม่ผ่านการตรวจสอบ if(error) return res.status(400).json({ "status": 400, "message": error.details[0].message }) if(!error) next() }) }) } // กำหนดชุดรูปแบบ schema const schema = Joi.object().keys({ province_name: Joi.string().min(3).max(30).required(), province_name_eng: Joi.string().min(3).max(30).required() }) module.exports = { validation, schema }
ไฟล์ provinces.js [api/provinces.js]
const express = require('express') const router = express.Router() const { validation, schema } = require('../validator/provinces') const db = require('../config/db') router.route('/provinces?') .get((req, res, next) => { // เราจะดึงข้อมูลท้งหมดตามเงื่อนไขแบ่งหน้า กับดึงข้อมูลจำนวนรายการทั้งหมด ในฟิลด์ total let sql = ' SELECT a.*, (SELECT COUNT(b.province_id) FROM tbl_provinces b) AS total FROM tbl_provinces a ' let per_page = 10 // กำหนดรายการที่จะแสดงในแต่ละหน้า let page = 1 // หน้าแรก let offset = 0 // เริ่มแสดงที่ offset let total = 0 // จำนวนรายการถั้งหมด if(req.query.page){ // ถ้ามีการแสดงหน้าที่จะแสดงเข้ามา เช่น ?page=1 page = req.query.page // หน้า ที่จะแสดง offset = (page-1) * per_page // กำหนด offset เริ่มต้นกรณีแบ่งหน้า sql += ' LIMIT '+ offset +','+ per_page +' ' // ต่อคำสั่ง sql เพิ่มการแบ่งหน้า } db.query(sql,(error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) total = results.length // ถ้าไม่มีการแบ่งหน้า จำนวนทั้งหมด จะนับจากจำนวนของรายการ // ถ้ามีการแบ่งหน้า จำนวนทั้งหมดจะใช้จากค่าฟิลด์ total ที่เราคิวรี่เริ่มด้ววคำสั่ง COUNT() if(req.query.page && total>0) total = results[0].total // รูปแบบข้อมูลที่ส่งกลับ กรณีรองรับการแบ่างหน้า const result = { "status": 200, "total":total, // จำนวนรายการทั้งหมด "current_page":page, // หน้าที่กำลังแสดงอยู่ "total_page":Math.ceil(total/per_page), // จำนวนหน้าทั้งหมด "data": results // รายการข้อมูล } return res.json(result) }) }) .post(validation(schema),(req, res, next) => { let province = { "province_name": req.body.province_name, "province_name_eng": req.body.province_name_eng } let sql = ' INSERT INTO tbl_provinces SET ? ' db.query(sql, province, (error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) province = [{'province_id':results.insertId, ...province}] const result = { "status": 200, "data": province } return res.json(result) }) }) router.route('/province/:id') .all((req, res, next) => { let sql = ' SELECT * FROM tbl_provinces WHERE province_id = ? ' db.query(sql, [req.params.id], (error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) if(results.length ===0) return res.status(400).json({ "status": 400, "message": "Not found user with the given ID" }) res.province = results next() }) }) .get((req, res, next) => { const result = { "status": 200, "data": res.province } return res.json(result) }) .put(validation(schema),(req, res, next) => { let province = { "province_name": req.body.province_name, "province_name_eng": req.body.province_name_eng } let sql = ' UPDATE tbl_provinces SET ? WHERE province_id = ? ' db.query(sql, [province, req.params.id], (error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) if(results.affectedRows > 0) { province = Object.assign(res.province[0], province) }else{ province = res.province } const result = { "status": 200, "data": province } return res.json(result) }) }) .delete((req, res, next) => { let sql = ' DELETE FROM tbl_provinces WHERE province_id = ? ' db.query(sql, [req.params.id],(error, results, fields)=>{ if(error) return res.status(500).json({ "status": 500, "message": "Internal Server Error" // error.sqlMessage }) const result = { "status": 200, "data": res.province } return res.json(result) }) }) module.exports = router
ในไฟล์ PROVINCS API นี้ส่วนอื่นๆ จะคล้ายๆ กับ USERS API แต่ส่วนที่เพิ่มเข้ามาคือในขั้นตอนการแสดงข้อมูล
หรือใน path: "/api/provinces" เราจะมีการจัดการเกียวกับการแบ่งหน้า ในลักษณะการทำงานดังนี้คือ
ถ้ามีการระบุ page เข้ามาในรูปแบบ query string ต่อท้าย path เข้ามาเช่น "/api/provinces?page=8"
เราจะไม่ดึงข้อมูลทั้งหมดมาแสดง แต่จะดึงข้อมูลเฉพาะหน้าที่ 8 ซึ่งเป็นหน้าสุดท้าย เนื่องจากจังหวัดในประเทศ
ไทยมีทั้งหมด 77 จังหวัด ดังนั้นหน้าสุดท้าย แสดงสูงสุดไม่เกินหน้าละ 10 หน้าสุดท้ายจึงแสดง 7 รายการสุดท้าย
ซึ่งคำสั่ง SQL ที่ถูกสร้างเมื่อเรียกให้แสดงหน้าที่ 8 คือ
SELECT a.*, (SELECT COUNT(b.province_id) FROM tbl_provinces b) AS total FROM tbl_provinces a LIMIT 70,10
เมื่อเราสร้าง PROVINCES API เพิ่มเข้ามาแล้ว ให้เราเข้าไปเพิ่มการเรียกใช้งานในไฟล์ app.js โดยเพิ่มบรรทัดของการ
ใช้งาน api module
const provinceApi = require('./api/provinces')
และส่วนของการใช้งานร่วมกับ routes path
app.use('/api', [userApi, provinceApi])
การใช้งาน MySQL ร่วมกับ Express และแนวทางการประยุกต์ใช้งานสร้าง RESTful API เบื้องต้นก็จะประมาณนี้ ทั้งหมดนี้
ด้วยความเข็มข้นของเนื้อหา ที่มีค่อนข้างมาก จะลดทอนการอธิบายซ้ำในบางจุด สามารถทำความเข้าใจเพิ่มเติมได้ที่
คอมเม้นที่กำกับไว้ในโค้ด
นอกจาก MySQL แล้ว ยังมี MongoDB อักหนึ่งรูปแบบฐานข้อมูล ที่เราอาจจะได้ทำความรู้จักในลำดับต่อๆ ไป