[Database] Bài 6 – Viết Code Quản Lý Một Database Đơn Giản (Tiếp Theo)

Tram Ho

Tới bây giờ thì chúng ta đã khá quen thuộc với cấu trúc thư mục và việc phân chia các tác vụ nhỏ sub-procedure rồi; Do đó nên trong bài viết này chúng ta sẽ đi nhanh qua các procedure cơ bản còn lại là select, update, và delete. Hãy cùng bắt đầu với thủ tục select.

## select

Chính xác hơn thì là select-by-id – đây là thủ tục select cơ bản nhất và có thể được sử dụng để làm chất liệu cho các procedure khác phức tạp hơn. Về cơ bản thì ở đây chúng ta chỉ cần thực hiện một vài bước như sau – tìm đường dẫn tới thư mục bản ghi tương ứng với id được cung cấp, sau đó đọc dữ liệu từ các tệp header.jsoncontent.md để nạp vào object kết quả thuộc class Category.

procedure/category/select-by-id--async-throw.js
const findRecordFolderPathById = require("./sub-procedure/find-record-folder-path-by-id--async-throw");
const readRecordHeader = require("./sub-procedure/read-record-header--async-throw");
const readRecordContent = require("./sub-procedure/read-record-content--async-throw");
const Category = require("../../type/Category");

module.exports = async (
in_recordId = "Infinity",
out_selected = new Category()
) => {
try {
/* find record's folder path */
var found = { recordFolderPath: "..." };
await findRecordFolderPathById(in_recordId, found);

/* read record's header and content */
await readRecordHeader(found.recordFolderPath, out_selected);
await readRecordContent(found.recordFolderPath, out_selected);
}
catch (error) {
throw error;
}
}; // module.exports

Việc tìm kiếm đường dẫn tới thư mục bản ghi tương ứng thì chỉ đơn giản là chúng ta đọc tên của tất cả các thư mục và lọc ra kết quả phù hợp thôi. Để thu thập đường dẫn thư mục của tất cả các bản ghi thì chúng ta có thể sử dụng lại một sub-procedure đã viết trước đó. Còn thao tác lọc ra kết quả phù hợp thì chúng ta sẽ sử dụng vòng lặp for để phù hợp với tinh thần PP thay vì sử dụng phương thức lặp .forEach của mảng.

procedure/category/sub-procedure/find-record-folder-path-by-id--async-throw.js
const readAllRecordFolderNames = require("./read-all-record-folder-names--async-throw");
const path = require("path");

module.exports = async (
in_recordId = "Infinity",
out_found = { recordFolderPath: "" }
) => {
try {
/* collect all records' folder names */
var allRecordFolderNames = [];
await readAllRecordFolderNames(allRecordFolderNames);

/* search for matched folder name */
var matchedFolderName = "";
for (var folderName of allRecordFolderNames) {
if (folderName.includes(in_recordId))
matchedFolderName = folderName;
else
/* do nothing */ ;
} // for

/* populate output path if found matched */
if (matchedFolderName == "")
/* do nothing */ ;
else
out_found.recordFolderPath = path.join(
__dirname, "../../../data/category", matchedFolderName
); // out_found
}
catch (error) {
throw error;
}
}; // module.exports

Việc viết code chạy thử các sub-procedure thì mình sẽ lược giản bớt nhé. Ở đây chúng ta sẽ chỉ viết code chạy thử các procedure chính thôi. Như vậy chúng ta sẽ có thể duy trì nội dung bài viết gọn gàng hơn và không quá dài.

Sau khi đã tìm được đường dẫn path tới thư mục của bản ghi tương ứng, chúng ta tiến hành đọc các tệp dữ liệu và nạp vào object kết quả thôi. Tuy nhiên đối với các thao tác đọc nội dung từ các tệp, chúng ta cần đảm bảo rằng kết quả thu được sẽ ở dạng văn bản với mã encodingutf-8.

procedure/category/sub-procedure/read-record-header--async-throw.js
const Category = require("../../../type/Category");
const fsPromises = require("fs/promises");
const path = require("path");

module.exports = async (
in_recordFolderPath = "",
out_record = new Category()
) => {
try {
var headerFilePath = path.join(in_recordFolderPath, "header.json");
var headerText = await fsPromises.readFile(headerFilePath, {encoding: "utf-8"});
var headerJSON = JSON.parse(headerText);
var headerEntries = Object.entries(headerJSON);

for (var entry of headerEntries) {
var [key, value] = entry;
out_record.set(key, value);
} // for
}
catch (error) {
throw error;
}
}; // module.exports

procedure/category/sub-procedure/read-record-content--async-throw.js
const Category = require("../../../type/Category");
const fsPromises = require("fs/promises");
const path = require("path");

module.exports = async (
in_recordFolderPath = "",
out_record = new Category()
) => {
try {
var contentFilePath = path.join(in_recordFolderPath, "content.md");
var recordContent = await fsPromises.readFile(contentFilePath, {encoding: "utf-8"});
out_record.set("markdown", recordContent);
}
catch (error) {
throw error;
}
}; // module.exports

Và bây giờ thì chúng ta đã có thể viết code chạy thử thủ tục select-by-id.

express-blog/test.js
const Category = require("./database/type/Category");
const databaseManager = require("./database/manager");

void async function() {
var selected = new Category();
await databaseManager.execute("select-category-by-id", "01", selected);

console.log(selected);
} (); // void

CMD | Terminal


npm test

Category(4) [Map] {
'@id' => '01',
'name' => 'html',
'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'html' ],
'markdown' => 'Nội dung của trang đơn mô tả danh mục HTML...'
}

## update

Trường hợp sử dụng của update là khi chúng ta mở một bài viết đã đăng tải trước đó để chỉnh sửa nội dung và sau đó lưu lại. Lúc này thông tin được gửi về server sẽ là đầy đủ các cặp key/value của một object Category. Code xử lý của route tương ứng sẽ tạo ra một object bản ghi thuộc class Category và gọi thủ tục update.

procedure/category/update--async-throw.js
const writeRecordToDataFolder = require("./sub-procedure/write-record-to-data-folder--async-throw");
const Category = require("../../type/Category");

module.exports = async (
in_record = new Category(),
out_updated = new Category()
) => {
try {
await writeRecordToDataFolder(in_record);
Category.clone(in_record, out_updated);
}
catch (error) {
throw error;
}
}; // module.exports

So với insert thì ở đây chúng ta không cần thực hiện thao tác khởi tạo giá trị id mới. Tuy nhiên ở đây chúng ta sẽ cần sửa lại sub-procedure ghi dữ liệu vào thư mục bản ghi mà chúng ta đã định nghĩa trước đó. Trường hợp lúc này là chúng ta đã có thư mục tương ứng với bản ghi cần cập nhật và thao tác fsPromises.mkdir(recordPath) trong code mà chúng ta đã viết dưới đây sẽ báo lỗi là thư mục đã tồn tại.

procedure/category/sub-procedure/write-record-to-data-folder--async-throw.js
const path = require("path");
const fsPromises = require("fs/promises");
const Category = require("../../../type/Category");
const writeRecordHeaderToFile = require("./write-record-header-to-file--async-throw");
const writeRecordContentToFile = require("./write-record-content-to-file--async-throw");

module.exports = async (
in_record = new Category()
) => {
try {
/* prepare path to record's data folder */
var categoryFolderPath = path.join(__dirname, "../../../data/category");
var recordFolderName = "id-" + in_record.get("@id");
var recordFolderPath = path.join(categoryFolderPath, recordFolderName);

/* create folder for new record */
await fsPromises.mkdir(recordFolderPath);

/* write record's data to files */
await writeRecordHeaderToFile(in_record, recordFolderPath);
await writeRecordContentToFile(in_record, recordFolderPath);
}
catch (error) {
throw error;
}
}; // module.exports

Các thao tác ghi dữ liệu vào các tệp thì chắc chắn sẽ không có vấn đề gì, bởi vì theo tài liệu của NodeJS cung cấp thì thao tác fsPromises.writeFile(filePath) sẽ tự động thay thế tệp đã tồn tại bằng tệp mới. Do đó nên chúng ta chỉ cần thêm điều kiện kiểm tra xem thư mục bản ghi đã tồn tại chưa trước khi quyết định khởi tạo đường dẫn cho thư mục mới và chạy lệnh fsPromises.mkdir(recordPath).

procedure/category/sub-procedure/write-record-to-data-folder--async-throw.js
const Category = require("../../../type/Category");
const findRecordFolderPathById = require("./find-record-folder-path-by-id--async-throw");
const path = require("path");
const fsPromises = require("fs/promises");
const writeRecordHeaderToFile = require("./write-record-header-to-file--async-throw");
const writeRecordContentToFile = require("./write-record-content-to-file--async-throw");

module.exports = async (
in_record = new Category()
) => {
try {
var found = { recordFolderPath: "" };
await findRecordFolderPathById(in_record.get("@id"), found);
var recordExists = (found.recordFolderPath != "");

if (recordExists) {
await writeRecordHeaderToFile(in_record, found.recordFolderPath);
await writeRecordContentToFile(in_record, found.recordFolderPath);
}
else {
/* prepare path to new record's data folder */
var categoryFolderPath = path.join(__dirname, "../../../data/category");
var newRecordFolderName = "id-" + in_record.get("@id");
var newRecordFolderPath = path.join(categoryFolderPath, newRecordFolderName);

/* create folder for new record */
await fsPromises.mkdir(newRecordFolderPath);

/* write new record's data to files */
await writeRecordHeaderToFile(in_record, newRecordFolderPath);
await writeRecordContentToFile(in_record, newRecordFolderPath);
} // else
}
catch (error) {
throw error;
}
}; // module.exports

Ở đây mình chọn viết lặp lại các lời gọi thủ tục writeRecord... cho mỗi trường hợp của kết quả tìm kiếm thư mục bản ghi tương ứng với id. Bạn có thể xử lý theo cách khác là tạo ra một biến lưu đường dẫn thư mục ở đầu tiên và thay đổi giá trị của biến đó bằng khối điều kiện if ... else ...; Rồi sau đó sử dụng biến chứa đường dẫn thư mục đó để gọi các thủ tục ghi dữ liệu vào các tệp một lần duy nhất ở cuối cùng.

express-blog/test.js
const Category = require("./database/type/Category");
const databaseManager = require("./database/manager");

void async function() {
var selected = new Category();
var updated = new Category();

await databaseManager.execute("select-category-by-id", "01", selected);
selected.set("name", "html5");
await databaseManager.execute("update-category", selected, updated);
console.log(updated);

await databaseManager.execute("select-category-by-id", "02", selected);
selected.set("name", "css3");
await databaseManager.execute("update-category", selected, updated);
console.log(updated);
} (); // void

CMD | Terminal


npm test

Category(4) [Map] {
'@id' => '01',
'name' => 'html5',
'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'html' ],
'markdown' => 'Nội dung của trang đơn mô tả danh mục HTML...'
}
Category(4) [Map] {
'@id' => '02',
'name' => 'css3',
'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'css' ],
'markdown' => 'Nội dung của trang đơn mô tả danh mục CSS...'
}

## delete

Trường hợp sử dụng của delete là khi chúng ta chọn nút nhấn xóa một danh mục trên giao diện web quản lý các danh mục bài viết. Dữ liệu được gửi về server thường sẽ chỉ cần duy nhất thành phần định danh của danh mục đó là id.

Thao tác mà chúng ta cần xử lý đầu tiên là – kiểm tra xem có bài viết article nào đang thuộc danh mục này không; Nếu có thì cần thông báo ngoại lệ, còn nếu không thì chúng ta có thể tiến hành xóa bản ghi category tương ứng và trả về kết quả là thông tin của bản ghi đã được xóa khỏi database.

procedure/category/delete-by-id--async-throw.js
const selectArticlesByCategoryId = require("../article/select-by-category-id--async-throw");
const removeRecordFromDatabase = require("./sub-procedure/remove-record-from-database--async-throw");
const Category = require("../../type/Category");

module.exports = async (
in_recordId = "Infinity",
out_deleted = new Category()
) => {
try {
var selectedArticles = [];
await selectArticlesByCategoryId(in_recordId, selectedArticles);
var theCategoryContainsSomeArticles = (selectedArticles.length != 0);

if (theCategoryContainsSomeArticles)
throw new Error("Đang có bài viết thuộc danh mục này");
else
await removeRecordFromDatabase(in_recordId, out_deleted);
}
catch (error) {
throw error;
}
}; // module.exports

Để kiểm tra xem có bài viết article nào đang thuộc danh mục chỉ định hay không thì chúng ta sẽ tìm trong số tất cả các bản ghi article để lọc ra các bản ghi có category-id tương ứng. Đây cũng là procedure đầu tiên mà chúng ta tạo ra cho nhóm procedure/article. Tuy nhiên chúng ta hãy cứ tạm giả định là không có bài viết nào thuộc danh mục cần xóa và để dành procedure này cho phần thảo luận sau cùng nhé.

procedure/article/select-by-category-id--async-throw.js
module.exports = async (
in_categoryId = "Infinity",
out_matchedArticles = []
) => {
/* do nothing */ ;
};

Và trong trường hợp không có bài viết nào đang thuộc danh mục chỉ định, thì việc xóa các tệp dữ liệu và thư mục của bản ghi category này sẽ được ủy thác cho một sub-procedure có tên removeRecord... như trên.

procedure/category/sub-procedure/remove-record-from-database--async-throw.js
const Category = require("../../../type/Category");
const selectRecordById = require("../select-by-id--async-throw");
const findRecordFolderPathById = require("./find-record-folder-path-by-id--async-throw");
const fsPromises = require("fs/promises");
const path = require("path");

module.exports = async (
in_recordId = "Infinity",
out_deleted = new Category()
) => {
try {
await selectRecordById(in_recordId, out_deleted);

var found = { recordFolderPath: "" };
await findRecordFolderPathById(in_recordId, found);
found.headerFilePath = path.join(found.recordFolderPath, "header.json");
found.contentFilePath = path.join(found.recordFolderPath, "content.md");

await fsPromises.rm(found.headerFilePath);
await fsPromises.rm(found.contentFilePath);
await fsPromises.rmdir(found.recordFolderPath);
}
catch (error) {
console.error(error);
}
}; // module.exports

Bây giờ chúng ta cứ viết code chạy thử delete-by-id cho trường hợp danh mục không chứa bài viết nào đã. Trường hợp còn lại cứ để tính sau đi. 😀

express-blog/test.js
const Category = require("./database/type/Category");
const databaseManager = require("./database/manager");

void async function() {
var deleted = new Category();
await databaseManager.execute("delete-category-by-id", "02", deleted)
console.log(deleted);
} (); // void

CMD | Terminal


npm test

Category(4) [Map] {
'@id' => '02',
'name' => 'css3',
'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'css' ],
'markdown' => 'Nội dung của trang đơn mô tả danh mục CSS...'
}

## Kết thúc bài viết 😀

Mình xin lỗi nhưng có lẽ là chúng ta sẽ không mang một thủ tục của nhóm procedure/article vào để thảo luận trong bài viết này, nhằm mục đích duy trì trọng tâm của bài viết xoay quanh các procedure cơ bản đối với nhóm category. Ở thời điểm hiện tại thì mình tin chắc chắn rằng bạn đã có thể tự hoàn thành code truy vấn tất cả các bản ghi article và lọc ra những bản ghi có category-id tương ứng.

Chúng ta đã khá quen với các thao tác làm việc với các thư mục và đọc dữ liệu từ các tệp rồi, vì vậy nên những procedure phát sinh do nhu cầu thiết kế blog cá nhân của bạn hiển nhiên cũng sẽ không thể làm khó bạn được nữa. Trong bài viết tiếp theo, chúng ta sẽ lướt nhanh qua thao tác select-by-category-id của nhóm procedure/article, và sau đó sẽ cùng thảo luận về một chủ đề khác trong việc sử dụng database.

[**[Database] Bài 7 – Một Số Procedure Khác & Khái Niệm View**](https://viblo.asia/p/gDVK2r4eKLj)

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo