[Database] Bài 4 – Viết Code Quản Lý Một Database Đơn Giản

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

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ả khi procedure được thực thi.
const selectCategoryById =async(
   in_recordId ="Infinity",
   out_selected =newMap())=>{// 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 =newMap()awaitselectCategoryById("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_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 articlecategory. 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 –

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à – ArticleCategory.

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

classArticleextendsMap{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 */;elsethis.set(in_key,null)returnthis}}// 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ào database.
  • 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 trong database.

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ẽ throwra 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 =newCategory(),
   out_inserted =newCategory())=>{
   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 =newCategory())=>{
   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 =newCategory(),
   out_updated =newCategory())=>{
   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 =newCategory())=>{
   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")voidasyncfunction(){
   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)

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ