Trong bài viết này, chúng ta sẽ cùng bắt đầu thực hiện công việc viết code quản lý một database
đơn giản cho trang blog cá nhân mà chúng ta đang xây dựng. Tuy nhiên, mình muốn lưu ý một chút về convention
trong việc viết code xử lý database
trước khi bắt tay vào viết code.
Code quản lý database
sẽ được viết chủ đạo trên nền PP (Procedural Programming)
, với các thủ tục procedure
thao tác nhập/xuất
trên các tệp dữ liệu. Các kết quả thu được khi thực hiện các procedure
sẽ được trả về ở dạng hiệu ứng biên side-effect
- tức là thay đổi nội dung của một object
được truyền vào thay vì sử dụng lệnh return
.
Các tham số của procedure
sẽ được chia làm 2 nhóm là -
in_
- các tham số truyền dữ liệu vào và sẽ không bị thay đổi.out_
- các tham số nhận kết quả khiprocedure
được thực thi.
const selectCategoryById = async ( in_recordId = "Infinity", out_selected = new Map()
) => { // truy vấn dữ liệu từ các tệp... // gắn dữ liệu vào object kết quả out_selected.set("@id", in_recordId) out_selected.set("name", "html")
} var selected = new Map()
await selectCategoryById("01", selected)
console.log(selected) // Category(2) [Map] {
// '@id' => '01',
// 'name' => 'html'
// }
Do hầu hết các procedure
ở đây đều làm việc với các tệp nên và chúng ta đã thảo luận từ trước là sẽ dùng các thao tác async
, vì vậy nên khi nào nhìn thấy từ khóa async
thì chúng ta sẽ ngầm định là procedure
nhé. Trong trường hợp không phải là thao tác async
thì mình sẽ ghi /* procedure */
thay vào vị trí của từ khóa đó.
Các hàm function
(nếu cần sử dụng) - cũng sẽ được viết với cú pháp =>
và sử dụng /* function */
thay vào vị trí async
ở trên. Tuy nhiên, khi sử dụng khái niệm hàm, chúng ta sẽ không có các tham số in_
và out_
, mà tất cả đều sẽ được ngầm định là in_
và được áp dụng từng phần khi gọi hàm; Đồng thời, các hàm sẽ luôn luôn sử dụng lệnh return
để trả về kết quả và không tạo ra side-effect
nào đối với các yếu tố bên ngoài hàm.
const sumOf = /* function */ (a = 0) => (b = 0) => { var sum = a + b return sum } var nine = sumOf(1)(8)
console.log(nine) // 9
Cấu trúc thư mục database
Chúng ta sẽ khởi đầu với các nhóm dữ liệu bài viết article
và danh mục category
; Và thư mục database
của chúng ta sẽ có cấu trúc cơ bản như thế này -
[express-blog]
. |
. +-----[database]
. | |
. | +-----[data]
. | | |
. | | +-----[article]
. | | +-----[category]
. | |
. | +-----[procedure]
. | | |
. | | +-----[article]
. | | +-----[category]
. | |
. | +-----[type]
. | | |
. | | +-----Article.js
. | | +-----Category.js
. | |
. | +-----manager.js
. |
. +-----test.js
Các tệp dữ liệu như chúng ta vẫn quy ước trước đó là đặt trong thư mục data
, với các bản ghi được xếp thành các nhóm article
và category
. Khi các bản ghi được truy xuất vào môi trường vận hành code sẽ cần được chuyển thành các object
; Và do đó nên chúng ta có thêm các class
mô tả tương ứng được đặt trong thư mục type
.
Cuối cùng là các tệp code định nghĩa các thủ tục thao tác trong database
được đặt trong thư mục procedure
và sẽ được tổng kết tại manager.js
. Code ở bên ngoài sẽ chỉ sử dụng các phương thức do manager
cung cấp và các class
trong thư mục type
chứ không chạm vào bất kỳ thành phần nào khác trong thư mục database
.
Các bản ghi và cấu trúc các tệp dữ liệu
Đối với mỗi bản ghi thuộc bất kỳ kiểu dữ liệu nào - article
, category
, admin
, v.v... - sẽ có một thư mục đại diện với tên thư mục ở dạng id-1001
và bên trong thư mục này sẽ gồm một tệp header.json
và một tệp content.md
. Trong đó thì tệp header.json
sẽ chứa các thông tin dạng ngắn, còn tệp content.md
sẽ chứa nội dung văn bản dài của trang đơn mô tả cho bản ghi đó trên bề mặt web (nếu có).
[data]
. |
. +-----[article]
. | |
. | +-----[id-0000]
. | |
. | +-----header.json
. | +-----content.md
. |
. +-----[category]
. |
. +-----[id-00]
. |
. +-----header.json
. +-----content.md
Các bản ghi article
sẽ có tệp header.json
với nội dung dạng này -
{ "@id": "0001", "title": "Làm Thế Nào Để Tạo Ra Một Trang Web?", "short-title": "Giới Thiệu Mở Đầu", "keywords": [ "hướng dẫn cơ bản", "lập trình web", "html", "giới thiệu" ], "edited-datetime": "Sat, 16 Apr 2022 10:13:22 GMT", "category-id": "01"
}
Ở đây @id
là giá trị id
được lưu trong tên thư mục của bản ghi này. Nội dung của tệp content.md
thì chỉ đơn giản là văn bản dài có chứa mã markdown
của Github nên chúng ta không có gì để lưu ý. Khi bản ghi article
này được truy vấn đầy đủ
và đưa vào môi trường vận hành code thì chúng ta sẽ có một object
như sau -
var firstArticle = { "@id": "0001", "title": "Làm Thế Nào Để Tạo Ra Một Trang Web?", "short-title": "Giới Thiệu Mở Đầu", "keywords": [ "hướng dẫn cơ bản", "lập trình web", "html", "giới thiệu" ], "edited-datetime": "Sat, 16 Apr 2022 10:13:22 GMT", "category-id": "01", "category-name": "html", "markdown": "Đây là nội dung của bài viết đầu tiên..."
}
Ở đây category-name
sẽ được truy vấn ngay sau bước đọc tệp header.json
từ trị số category-id
và thêm vào object
mô tả bài viết. Còn nội dung của tệp content.md
được lưu vào khóa markdown
.
Còn đây là nội dung tệp header.json
của một danh mục category
-
{ "@id": "02", "name": "css", "keywords": [ "hướng dẫn cơ bản", "lập trình web" ]
}
Bạn có thể chuẩn bị trước nội dung ngắn gọn cho một vài bản ghi hoặc copy/paste
từ các liên kết dưới đây -
database/data/article
database/data/category
Các class
mô trả dữ liệu
Các procedure
về cơ bản là code thực hiện tương tác giữa môi trường vận hành và các tệp tĩnh; Do đó nên trước hết chúng ta sẽ cần chuẩn bị trước các class
mô tả các bản ghi trong môi trường phần mềm. Đối với mỗi nhóm các bản ghi thì chúng ta nên có tên class
riêng và vì vậy nên chúng ta sẽ có hai class
là - Article
và Category
.
Về cơ bản thì các class
này đều không có gì đặc biệt và chỉ đơn giản là được sử dụng tạo ra các object
chung chuyển dữ liệu. Đối với nhu cầu sử dụng như thế này thì chúng ta có class Map
đã được thiết kế sẵn với nhiều tính năng tiện ích phù hợp. Và đầu tiên là code cho class Article extends Map
-
class Article
extends Map { constructor(...params) { super(...params) this.initialize("@id") .initialize("title") .initialize("short-title") .initialize("keywords") .initialize("edited-datetime") .initialize("category-id") .initialize("category-name") .initialize("markdown") } initialize(in_key = "...") { if (this.has(in_key)) /* do nothing */ ; else this.set(in_key, null) return this }
} // class Article module.exports = Article
Giống với việc sử dụng các class
tự định nghĩa thông thường, sau khi kế thừa Map
chúng ta cần khởi tạo các thuộc tính
- hay các trường dữ liệu
- tương ứng với các bản ghi bằng cách tạo một phương thức có tên là initialize(key)
. Phương thức này sẽ kiểm tra sự tồn tại của các khóa key
và khởi tạo những thuộc tính còn thiếu khi code bên ngoài sử dụng new Article(...entries)
.
Rồi... như vậy là đã tạm đủ chất liệu cho các procedure
làm việc. Bây giờ chúng ta sẽ tiến hành viết code cho các procedure
; Khi nào cần bổ sung hoặc chỉnh sửa gì đó ở các class
này thì chúng ta sẽ quay lại xử lý thêm sau. Trong bài viết này thì chúng ta sẽ tập trung cho các procedure
làm việc trên các bản ghi category
trước. Lý do thì là vì các bản ghi article
có sự lệ thuộc vào các bản ghi category
như chúng ta đã nói trong bài trước.
Các thủ tục cơ bản trong database
Mặc dù mục đích sử dụng phần mềm server
ở lớp phía trên rất đa dạng. Nhưng khi tương tác với database
, về cơ bản thì chúng ta sẽ chỉ có 4 kiểu thao tác -
insert
- thêm một ghi mới vàodatabase
.select
- lấy ra một bản ghi để xem thông tin.update
- cập nhật dữ liệu của một ghi đã có.delete
- xóa một bản ghi trongdatabase
.
Và chúng ta sẽ khởi đầu với các procedure
tương ứng thực hiện thao tác trên một bản ghi đơn. Các thao tác phức tạp hơn (nếu cần thiết) - sẽ có thể sử dụng các procedure
này làm chất liệu.
[database]
. |
. +-----[procedure]
. | |
. | +-----[category]
. | |
. | +-----[sub-procedure]
. | |
. | +-----insert--async-throw.js
. | +-----select-by-id--async-throw.js
. | +-----update--async-throw.js
. | +-----delete-by-id--async-throw.js
. |
. +-----manager.js
Mình thường có thói quen ghi chú trong tên tệp một vài yếu tố mà mình quan tâm ở phía cuối; Vì vậy nên tên các tệp trong ví dụ mình ghi có hơi dài một chút. Bạn có thể đặt tên tệp theo cách hiểu của bạn là được, điểm này không quan trọng lắm nên bạn đừng bận tâm nhé.
Về cơ bản thì các procedure
đều phải thực hiện các thao tác nhập/xuất
liên quan tới các tệp nên thường sẽ là các thao tác async
, và nếu có ngoại lệ phát sinh khi tương tác với các tệp dữ liệu thì chúng ta sẽ throw
ra ngoài cho code xử lý server
. Bởi vì code quản lý database
về cơ bản là một phần mềm nhỏ plug-in
thụ động - được sử dụng bởi code logic của server
ở phía bên ngoài; Do đó nên việc xử lý các ngoại lệ thế nào để phản hồi cho trình duyệt web thì hiển nhiên là không thể xử lý ở tầng này được.
Do các procedure
của chúng ta đều phải thực hiện các thao tác có nhiều bước và chắc chắn sẽ cần chia thành các tác vụ nhỏ. Ở đây chúng ta sẽ tạo sẵn một thư mục sub-procedure
để lưu trữ code xử lý các tác vụ chia nhỏ và có thể được sử dụng chung cho các procedure
chính.
Bây giờ thì chúng ta sẽ khai báo đơn giản và tổng kết các procedure
này tại manager.js
để code bên ngoài có thể sử dụng được -
const Category = require("../../type/Category") module.exports = async ( in_submitted = new Category(), out_inserted = new Category()
) => { console.log("insert-category") console.log(`in_submitted: ${in_submitted}`) console.log(`out_inserted: ${out_inserted}`)
}
const Category = require("../../type/Category") module.exports = async ( in_recordId = "Infinity", out_selected = new Category()
) => { console.log("select-category-by-id") console.log(`in_recordId: ${in_recordId}`) console.log(`out_selected: ${out_selected}`)
}
const Category = require("../../type/Category") module.exports = async ( in_record = new Category(), out_updated = new Category()
) => { console.log("update-category") console.log(`in_record: ${in_record}`) console.log(`out_updated: ${out_updated}`)
}
const Category = require("../../type/Category") module.exports = async ( in_recordId = "Infinity", out_deleted = new Category()
) => { console.log("delete-category-by-id") console.log(`in_recordId: ${in_recordId}`) console.log(`out_deleted: ${out_deleted}`)
}
const storedProcedure = {} storedProcedure["insert-category"] = require("./procedure/category/insert--async-throw")
storedProcedure["select-category-by-id"] = require("./procedure/category/select-by-id--async-throw")
storedProcedure["update-category"] = require("./procedure/category/update--async-throw")
storedProcedure["delete-category-by-id"] = require("./procedure/category/delete-by-id--async-throw") exports.execute = async ( in_procedureName = "tên-thủ-tục", ...parameters
) => { await storedProcedure[in_procedureName].call(null, ...parameters)
}
Và viết một vài dòng trong test.js
và chạy lệnh npm test
để xem manager
đã được kết nối với các tệp procedure
ổn chưa -
const databaseManager = require("./database/manager") void async function() { console.log("==========") databaseManager.execute("insert-category", "a-new-category", "inserted") console.log("==========") databaseManager.execute("select-category-by-id", "id-00", "selected") console.log("==========") databaseManager.execute("update-category", "a-category", "updated") console.log("==========") databaseManager.execute("delete-category-by-id", "id-00", "deleted")
} () // void
Và bây giờ thì chúng ta sẽ bắt đầu viết code xử lý chi tiết cho từng procedure
. Tuy nhiên thì bài viết của chúng ta tới đây thực sự là đã hơi dài quá rồi, vì vậy nên... Trong bài viết tiếp theo, chúng ta sẽ cùng viết code xử lý chi tiết cho thao tác insert
.
[Database] Bài 5 - VIết Code Quản Lý Database Đơn Giản (Tiếp Theo)