File upload and DB transaction
- ติดตั้ง multer
> npm install multer
- สร้าง Instance สำหรับเรียกใช้ dependencies Multer
const multer = require('multer')
- กำหนด config สำหรับ multer
var storage = multer.diskStorage({
destination: function (req, file, callback) {
callback(null, './static/uploads') // path to save file
},
filename: function (req, file, callback) {
// set file name
callback(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
}
})
- กำหนดตัวแปรสำหรับเรียกใช้งาน
const upload = multer({ storage: storage })
// Require multer for file upload
const multer = require('multer')
// SET STORAGE
var storage = multer.diskStorage({
destination: function (req, file, callback) {
callback(null, './static/uploads')
},
filename: function (req, file, callback) {
callback(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
}
})
const upload = multer({ storage: storage })
- สร้าง Router
/blogs
ใหม่ในไฟล์/routes/blog.js
router.post('/blogs', async function (req, res, next) {
// create code here
}
- แทรก
upload.single('myimage')
ไว้ระหว่าง path กับ callback ของ router โดยตั้งชื่อ key ว่า'myimage'
จะได้โค้ดตามนี้
// >> insert code here <<
router.post('/blogs', upload.single('myImage'), async function (req, res, next) {
// create code here
}
- สร้างตัวแปรสำหรับเก็บข้อมูลของ file ที่อาจจะอัปโหลดแนบมาด้วย
const file = req.file;
- เช็กว่ามีไฟล์แนบมากับ Request หรือไม่ ถ้าไม่มีรูปให้แสดง Error สถานะ 400 ออกไป
if (!file) {
const error = new Error("Please upload a file");
error.httpStatusCode = 400;
return next(error);
}
- สร้างตัวแปรมารับค่าจาก request
const title = req.body.title;
const content = req.body.content;
const status = req.body.status;
const pinned = req.body.pinned;
- สร้าง transaction ขึ้นมา
const conn = await pool.getConnection()
await conn.beginTransaction();
- สร้าง try catch
- ใน try เพิ่มข้อมูลตาราง blogs และประกาศตัวแปร results มารับค่า
let results = await conn.query(
"INSERT INTO blogs(title, content, status, pinned, `like`,create_date) VALUES(?, ?, ?, ?, 0,CURRENT_TIMESTAMP);",
[title, content, status, pinned]
)
- เอาค่า id ของ blog จาก results
const blogId = results[0].insertId;
- เพิ่มข้อมูลในตารางรูปโดยระบุ blog_id (ใช้บอกว่ารูปนี้เป็นของ blog ไหน)
file.path.substr(6) - เพื่อตัดคำว่า static ออกจาก path เพื่อเหมาะสำหรับการทำไปแสดงผล
await conn.query("INSERT INTO images(blog_id, file_path) VALUES(?, ?);",[blogId, file.path.substr(6)])
- Commit Transaction
conn.commit()
- Response
res.send("success!");
- หากการ Create เกิด Error ขึ้นมาขั้นตอนใดขั้นตอนหนึ่ง ให้ทำการ Rollback Transaction และ Return error ออกมา
await conn.rollback();
return next(error)
// Require multer for file upload
const multer = require('multer')
// SET STORAGE
var storage = multer.diskStorage({
destination: function (req, file, callback) {
callback(null, './static/uploads')
},
filename: function (req, file, callback) {
callback(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname))
}
})
const upload = multer({ storage: storage })
// POST - create new blog with single upload file
router.post('/blogs', upload.single('myImage'), async function (req, res, next) {
const file = req.file;
if (!file) {
const error = new Error("Please upload a file");
error.httpStatusCode = 400;
return next(error);
}
const title = req.body.title;
const content = req.body.content;
const status = req.body.status;
const pinned = req.body.pinned;
const conn = await pool.getConnection()
// Begin transaction
await conn.beginTransaction();
try {
let results = await conn.query(
"INSERT INTO blogs(title, content, status, pinned, `like`,create_date) VALUES(?, ?, ?, ?, 0,CURRENT_TIMESTAMP);",
[title, content, status, pinned]
)
const blogId = results[0].insertId;
await conn.query(
"INSERT INTO images(blog_id, file_path) VALUES(?, ?);",
[blogId, file.path.substr(6)])
await conn.commit()
res.send("success!");
} catch (err) {
await conn.rollback();
next(err);
} finally {
console.log('finally')
conn.release();
}
});
- ทดสอบยิง API โดยใช้
Postman
- สร้าง Router
/blogs/:id
ใหม่ในไฟล์/routes/blog.js
router.put('/blogs/:id', upload.single('myImage'), (req, res, next) => {
// update blog code here
}
- สร้าง transaction ขึ้นมา
const conn = await pool.getConnection()
await conn.beginTransaction();
- สร้าง try catch
- ใน try ให้สร้างตัวแปรสำหรับเก็บข้อมูลของ file ที่อาจจะอัปโหลดแนบมาด้วย
const file = req.file;
- ในกรณีที่มีไฟล์รูปภาพอัปโหลดขึ้นมาด้วย แสดงว่าจะต้องอัปเดทรูปภาพด้วย โดยให้สร้าง if มาดักไว้ และทำการ Update Path ของรูปที่อยู่ในตาราง images
if (file) {
await conn.query("UPDATE images SET file_path=? WHERE id=?",[file.path, req.params.id])
}
- ต่อมาก็ทำการอัปเดทข้อมูลอื่นในตาราง blogs
await conn.query('UPDATE blogs SET title=?,content=?, pinned=?, blogs.like=?, create_by_id=? WHERE id=?', [req.body.title, req.body.content, req.body.pinned, req.body.like, null, req.params.id])
- Commit Transaction
conn.commit()
- ถ้า Update สำเร็จให้ Response ออกมา
res.json({ message: "Update Blog id " + req.params.id + " Complete" })
- หากการ Update เกิด Error ขึ้นมาขั้นตอนใดขั้นตอนหนึ่ง ให้ทำการ Rollback Transaction และ Return error ออกมา
await conn.rollback();
return next(error)
router.put('/blogs/:id', upload.single('myImage'), async (req, res, next) => {
const conn = await pool.getConnection()
await conn.beginTransaction();
try {
const file = req.file;
if (file) {
await conn.query(
"UPDATE images SET file_path=? WHERE id=?",
[file.path, req.params.id])
}
await conn.query('UPDATE blogs SET title=?,content=?, pinned=?, blogs.like=?, create_by_id=? WHERE id=?', [req.body.title, req.body.content, req.body.pinned, req.body.like, null, req.params.id])
conn.commit()
res.json({ message: "Update Blog id " + req.params.id + " Complete" })
} catch (error) {
await conn.rollback();
return next(error)
} finally {
console.log('finally')
conn.release();
}
});
- ทดสอบยิง API โดยใช้
Postman
เงื่อนไข: ในการจะลบแต่ละ Blog จะต้องทำการเช็กว่า Blog นั้นมี comment หรือไม่ หากมี comment อยู่จะต้องแสดง message ว่า "Can't Delete. This Blog has a comment" แต่ถ้า Blog นั้น ไม่มี Comment ก็จะลบข้อมูลออกจากตาราง blogs และลบข้อมูลรูปภาพออกจากตาราง images ด้วย
- สร้าง Router
/blog/:id
ใหม่ในไฟล์/routes/blog.js
router.delete('/blogs/:id', function (req, res) {
// delete blog code here
});
- สร้าง Transaction ขึ้นมา
const conn = await pool.getConnection()
await conn.beginTransaction();
- สร้าง try catch
- ใน try ให้เลือก Comment ที่มี
blog_id
เท่ากับparams
ที่รับเข้ามา และเก็บผลลัพท์อยู่ในตัวแปรที่ชื่อว่าcomments
let comments = await conn.query('SELECT * FROM comments WHERE blog_id=?', [req.params.id])
- เช็กว่าถ้าเกิด
comments
ที่ได้ ถ้ามีมากกว่า 0 แสดงว่า blog นั้นมี comment อยู่ ให้ Response เป็นสถานะ 409 และมีข้อความว่า "Can't Delete because this blog has comment!!!"
if (comments[0].length > 0) {
res.status(409).json({ message: "Can't Delete because this blog has comment!!!" })
} else {
// continue delete ...
}
- ถ้า post นั้นไม่มี comment ก็ให้ลบข้อมูลที่อยู่ในตาราง blogs และ images
await conn.query('DELETE FROM blogs WHERE id=?;', [req.params.id]) // delete blog
await conn.query('DELETE FROM images WHERE blog_id=?;', [req.params.id]) // delete image
- Commit Transaction
conn.commit()
- ถ้า Delete สำเร็จให้ Response ออกมา
res.json({ message: 'Delete Blog id ' + req.params.id + ' complete' })
- หากการ Delete เกิด Error ขึ้นมาขั้นตอนใดขั้นตอนหนึ่ง ให้ทำการ Rollback Transaction และ Return error ออกมา
await conn.rollback();
return next(error)
router.delete('/blogs/:id', async (req, res, next) => {
const conn = await pool.getConnection()
await conn.beginTransaction();
try {
// check blog has comment?
let comments = await conn.query('SELECT * FROM comments WHERE blog_id=?', [req.params.id])
if (comments[0].length > 0) {
res.status(409).json({ message: "Can't Delete because this blog has comment!!!" })
} else {
await conn.query('DELETE FROM blogs WHERE id=?;', [req.params.id]) // delete blog
await conn.query('DELETE FROM images WHERE blog_id=?;', [req.params.id]) // delete image
await conn.commit()
res.json({ message: 'Delete Blog id ' + req.params.id + ' complete' })
}
} catch (error) {
await conn.rollback();
next(error);
} finally {
console.log('finally')
conn.release();
}
});
- ทดสอบยิง API โดยใช้
Postman
- ปรับแก้ไข route GET
/blogs/:id/
ซึ่งดึงข้อมูลรายละเอียดของ blog มาแสดง โดย ณ ตอนนี้ยังไม่มีการดึงข้อมูลรูปภาพของ blog จากตารางimages
มาแสดง
router.get("/blogs/:id", function (req, res, next) {
const promise1 = pool.query("SELECT * FROM blogs WHERE id=?", [
req.params.id,
]);
const promise2 = pool.query("SELECT * FROM comments WHERE blog_id=?", [
req.params.id,
]);
Promise.all([promise1, promise2])
.then((results) => {
const blogs = results[0];
const comments = results[1];
res.render("blogs/detail", {
blog: blogs[0][0],
comments: comments[0],
error: null,
});
})
.catch((err) => {
return next(err);
});
});
- ต่อเนื่องจากข้อ 1 ปรับแก้ไขหน้า detail.ejs ให้นำรูปภาพจากตาราง
images
มาแสดง
- สร้าง form สำหรับ create new comment โดยใช้ ejs โดย form จะต้องสามารถอัพโหลดไฟล์รูปภาพได้ (ไฟล์ detail.ejs line 90 - 99)
<form method="POST" action="..." enctype="...">
<div class="field">
<label class="label">Add Comment</label>
<div class="control">
<textarea class="textarea" name="comment" placeholder="Add Comment Here"></textarea>
</div>
</div>
<input name="myImage" type="file">
<input class="button is-primary" type="submit" value="Submit">
</form>
อย่าลืม enctype="multipart/form-data"
- แก้ไข code ของ route สำหรับ create new comment ที่ทำใน WEEK09 ให้รับไฟล์ image ที่ถูกอัพโหลดและเก็บไฟล์ไว้ในโฟลเดอร์ static/uploads จากนั้นทำการบันทึกข้อมูลลงในตาราง
comments
และ ตารางimages
- จะต้องสามารถ upload ไฟล์จาก form ในข้อ 3 ได้ - ลองปรับปรุงโจทย์ในข้อ 4 โดยใช้ database transactions เพราะมีการ insert 2 ตาราง
- แก้ไข detail.ejs ในส่วนที่แสดงผล comment โดยให้แสดงรูปภาพของ comment ด้วย (detail.ejs line 62 - 89)
หมายเหตุ 1: ให้ทุกคนทำการ generate ตัว database schema webpro ขึ้นมาใหม่ โดยใช้ file 256503-webpro.sql ที่ให้มา (ผมมีการเพิ่ม column comment_id
ในตาราง images
ที่เป็น FK ไปหาตาราง comments
)
หมายเหตุ 2: ให้ทุกคนนำ file comment.js ที่ได้ทำไปใน week9-Exercise มาประยุกต์ใช้ต่อในสัปดาห์นี้
หมายเหตุ 3: การทำงานในส่วน frontend ของ Lab นี้ ให้ทำในรูปแบบของ ejs + Vue.js หรือจะใช้ ejs อย่างเดียวก็ได้