Trước khi nói về khái niệm mới, thì như dự kiến từ cuối bài viết trước là chúng ta sẽ thực hiện nốt procedure
truy xuất các bản ghi article
bằng category-id
và những procedure
cơ bản thuộc nhóm procedure/article
. Tuy nhiên đối với các procedure
cơ bản như insert
, select-by-id
, update
, delete-by-id
, thì hầu hết là chúng ta có thể copy/paste
code xử lý từ nhóm procedure/category
và chỉnh sửa lại đôi chút không đáng kể. Do đó ở đây mình nghĩ chúng ta chỉ cần làm nốt ví dụ về thao tác select-by-category-id
của nhóm procedure/article
thôi.
Một chút lưu ý về hiệu năng xử lý
Thủ tục select-by-category-id
ở đây có logic xử lý tổng quan là chúng ta sẽ lọc ra những bản ghi article
có category-id
khớp với tham số cung cấp. Mặc dù chỉ là một procedure
đơn giản nhưng phương thức mà chúng ta tiến hành cũng sẽ có một vài điểm đáng để đặt một chút suy nghĩ.
Để lọc ra các bản ghi phù hợp như đã nói, chúng ta có thể tiến thành theo hai cách -
- Đọc tất cả các bản ghi
article
và chuyển thành một mảng chứa cácobject
dữ liệu trong môi trường phần mềm; Sau đó tiến hành lặp qua mảng này để lọc ra những bản ghi phù hợp vớicategory-id
được cung cấp. - Đọc một bản ghi
article
đầu tiên và chuyển thành mộtobject
dữ liệu trong môi trường phần mềm; Sau đó tiến hành kiểm tra ngaycategory-id
để xem phù hợp không. Nếu phù hợp thì bổ sung vào mảng kết quả; Và cứ thế lặp lại tiến trình xử lý như vậy với tất cả các bản ghi còn lại.
Ở đây chúng ta thấy rõ ràng là trong trường hợp đầu tiên, sẽ có ít nhất một khoảnh khắc nào đó mà bộ nhớ máy tính sẽ phải phân bổ để lưu trữ 1001 object
dữ liệu mô tả các bản ghi article
. Việc lưu trữ sẽ phải duy trì cho đến khi thao tác lọc các bài viết phù hợp được thực hiện xong và procedure
kết thúc thì phần bộ nhớ lưu các object
không phù hợp mới được giải phóng.
Trong khi đó đối với cách thức thứ hai, khi một object
dữ liệu mô tả một article
được cho là không phù hợp với kết quả tìm kiếm thì nó sẽ được giải phóng ngay khỏi bộ nhớ máy tính. Đây là một lưu ý nhỏ nhưng khá quan trọng khi chúng ta viết code để làm việc với database
, bởi khi số lượng các bản ghi trong database
đủ nhiều thì chúng ta sẽ thấy sự khác biệt là có thể nhận biết được.
const readAllRecordIds = require("./sub-procedure/read-all-record-ids--async-throw");
const selectArticleById = require("../article/select-by-id--async-throw");
const Article = require("../../type/Article"); module.exports = async ( in_categoryId = "Infinity", out_matchedArticles = []
) => { var allRecordIds = []; await readAllRecordIds(allRecordIds); /* one-by-one select and check */ for (var recordId of allRecordIds) { var selected = new Article(); await selectArticleById(recordId, selected); /* collect the record if matched */ var selectedArticleIsMatched = (selected.get("category-id") == in_categoryId); if (selectedArticleIsMatched) out_matchedArticles.push(selected); else /* do nothing */; } // for
};
Như trong code ví dụ ở trên thì chúng ta đã thực hiện công việc thu thập tất cả các article-id
từ tên thư mục của 1001 bản ghi article
. Sau đó chúng ta thực hiện thao tác lặp và truy xuất từng bản ghi article
để kiểm tra category-id
, và quyết định lưu vào mảng kết quả hoặc bỏ object article
đó ngay.
const Article = require("./database/type/Article");
const Category = require("./database/type/Category");
const databaseManager = require("./database/manager");
const view = require("./database/view/article-left-join-category--all-join-name--async-throw");
const ArticleJoinCategory = require("./database/type/ArticleJoinCategory/all-join-name"); void async function() { var procedureName, id, selected; await databaseManager.execute( procedureName = "select-articles-by-category-id", id = "01", selected = [] ); console.log(selected);
} (); // void
CMD | Terminal
npm test [ Article(7) [Map] { '@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', 'markdown' => 'Nội dung của bài viết đầu tiên...' }, Article(7) [Map] { '@id' => '0002', 'title' => 'Cách Chèn Ảnh & Các Liên Kết', 'short-title' => 'Ảnh & Liên Kết', 'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'html', 'ảnh', 'liên kết' ], 'edited-datetime' => 'Sat, 16 Apr 2022 19:13:22 GMT', 'category-id' => '01', 'markdown' => 'Nội dung của bài viết thứ hai...' }
]
Thao tác truy vấn tổ hợp select-top
Thực tế thì trong bài viết trước, mình đã đề xuất việc tạo ra thủ tục procedure/article/select-by-category-id
nhằm kiểm tra trước khi thực hiện thao tác xóa một category
nhưng cốt bản là để lấy ví dụ về một thao tác truy vấn phức tạp hơn một chút và được xây dựng trên chất liệu là thao tác truy vấn cơ bản procedure/article/select-by-id
.
Còn về mặt ứng dụng thì procedure
này lại không phải là giải pháp tốt, vì nếu như chúng ta có nhiều bài viết thì việc lặp qua tất cả các bản ghi article
sẽ rất mất công.
Thay vào đó thì chúng ta chỉ nên lặp cho đến khi gặp bài viết đầu tiên có category-id
trùng hợp. Như vậy chúng ta có thể xem xét việc viết một procedure
khác để kiểm tra thao tác xóa category
hợp lệ. Ví dụ như procedure
để chọn ra một vài bản ghi article
mới nhất thuộc category
đó (nếu có) -
const readAllRecordIds = require("./sub-procedure/read-all-record-ids--async-throw");
const selectArticleById = require("./select-by-id--async-throw");
const Article = require("../../type/Article"); module.exports = async ( in_options = { numberOfRecords: 0, reverseOrder: false }, in_categoryId = "Infinity", out_selected = []
) => { /* prepare list of all record ids */ var allRecordIds = []; await readAllRecordIds(allRecordIds); if (in_options.reverseOrder == false) /* do nothing */; else allRecordIds = allRecordIds.reverse(); /* select each record to check */ for (var recordId of allRecordIds) { var selected = new Article(); await selectArticleById(recordId, selected); /* collect the record if matched */ if (selected.get("category-id") != in_categoryId) /* not matched */; else out_selected.push(selected); /* stop if found enough records */ if (out_selected.length < in_options.numberOfRecords) /* continue collecting */; else break; } // for
}; // module.exports
Với procedure
này thì chúng ta thực hiện lặp từ giá trị id
lớn nhất của các bản ghi article
trở lại tới bản ghi article
đầu tiên. Ngay khi gặp một bản ghi phù hợp thì thao tác lặp sẽ được break
để kết thúc procedure
ngay tại đó. Như vậy số thao tác mà máy tính phải thực hiện có khả năng thấp hơn rất nhiều so với việc sử dụng procedure
trước. Và code thủ tục xóa category
có thể được sửa lại như thế này.
const selectTopArticleByCategoryId = require("../article/select-top-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 selectTopArticleByCategoryId( { numberOfRecords: 1, reverseOrder: true }, in_recordId, selectedArticles ); var theCategoryIsEmpty = (selectedArticles.length == 0); if (theCategoryIsEmpty) await removeRecordFromDatabase(in_recordId, out_deleted); else throw new Error("Đang có bài viết thuộc danh mục này"); } catch (error) { throw error; }
}; // module.exports
Thao tác truy vấn select-top
như trên là rất phổ biến và thường được sử dụng khi chúng ta muốn chọn ra một vài bản ghi đầu tiên trong một tập kết quả lớn.
Thao tác truy vấn liên hợp join
Giả sử chúng ta đang cần dữ liệu để tạo ra một trang đơn bài viết. Lúc này ngoài các trường dữ liệu do bản ghi article
cung cấp thì chúng ta sẽ cần thêm trường name
của bản ghi category
tương ứng. Code sử dụng databaseManager
từ bên ngoài có thể xử lý bằng cách gọi thủ tục select-article-by-id
để có được bản ghi article
, rồi sau đó truy xuất category-id
và tiếp tục gọi thủ tục select-category-by-id
để lấy bản ghi category
tương ứng và sau đó truy xuất name
.
Một thao tác truy vấn liên hợp như thế này là rất phổ biến khi sử dụng relational database
và vì vậy nên các hệ quản trị relational database
thường cung cấp một phương thức có tên là join
giúp kết hợp hai bản ghi liên quan để tạo thành một kiểu dữ liệu liên hợp mới làm kết quả trả về.
Để viết code xử lý tương tự cho phần mềm quản lý database
đơn giản thì chúng ta có thể định nghĩa một kiểu dữ liệu liên hợp class ArticleJoinCategory
, và sau đó viết một procedure
thực hiện thao tác truy vấn liên hợp để trả về một object
kết quả thuộc kiểu dữ liệu đó.
const Article = require("./Article");
const Category = require("./Category"); const ArticleJoinCategory = class extends Map { constructor(...params) { super(...params); ArticleJoinCategory.initialize("@id", this) .initialize("title", this) .initialize("short-title", this) .initialize("keywords", this) .initialize("edited-datetime", this) .initialize("markdown", this) .initialize("category-id", this) .initialize("category-name", this); return this; } static initialize( in_key = "", out_article = new Article() ) { if (out_article.has(in_key)) /* do nothing */ ; else out_article.set(in_key, null); return Article; } static populate( in_article = new Article(), in_category = new Category(), out_joined = new ArticleJoinCategory() ) { var allArticleEntries = [ ...in_article ]; for (var entry of allArticleEntries) { var [key, value] = entry; out_joined.set(key, value); } // for var categoryName = in_category.get("name"); out_joined.set("category-name", categoryName); }
}; // ArticleJoinCategory module.exports = ArticleJoinCategory;
const ArticleJoinCategory = require("../../type/ArticleJoinCategory--all-join-name");
const Article = require("../../type/Article");
const Category = require("../../type/Category");
const selectArticleById = require("./select-by-id--async-throw");
const selectCategoryById = require("../category/select-by-id--async-throw"); module.exports = async ( in_articleId = "", out_selectedJoin = new ArticleJoinCategory()
) => { var selectedArticle = new Article(); await selectArticleById(in_articleId, selectedArticle); var categoryId = selectedArticle.get("category-id"); var selectedCategory = new Category(); await selectCategoryById(categoryId, selectedCategory); ArticleJoinCategory.populate(selectedArticle, selectedCategory, out_selectedJoin);
};
const storedProcedure = new Map(); /* other procedures ... */ storedProcedure.set( "select-article-by-id-join-category--all-join-name", require("./procedure/article-join-category--all-join-name/select-by-article-id--async-throw")
); exports.execute = async ( procedureName = "tên-thủ-tục", ...parameters
) => { await storedProcedure .get(procedureName) .call(null, ...parameters);
};
const databaseManager = require("./database/manager");
const ArticleJoinCategory = require("./database/type/ArticleJoinCategory--all-join-name"); void async function() { var procedureName, id, selected; await databaseManager.execute( procedureName = "select-article-by-id-join-category--all-join-name", id = "01", selected = new ArticleJoinCategory() ); console.log(selected);
} (); // void
CMD | Terminal
npm test ArticleJoinCategory(8) [Map] { '@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', 'markdown' => 'Nội dung của bài viết đầu tiên...', 'category-id' => '01', 'category-name' => 'html5'
}
Khởi tạo & Sử dụng view
Khái niệm view
- hay giao diện quan sát - trong quản lý database
cũng không khác so với những bối cảnh khác mà chúng ta đã thấy từ view
xuất hiện trước đó. Một view
là một giao diện trình bày thông tin cho người quan sát - và trong quản lý database
nói riêng thì một view
được xem là một giao diện bảng dữ liệu với các trường dữ liệu được thiết kế để đáp ứng nhu cầu truy vấn và sử dụng nhất định.
Xuất phát từ procedure
truy vấn liên hợp mà chúng ta mới viết ở trên; Kết quả mà chúng ta thu được là một object
mô tả một bản ghi liên hợp thuộc class ArticleJoinCategory
với các trường dữ liệu là @id
, title
, ..., category-name
. Nếu như chúng ta truy vấn tất cả các bản ghi article
liên hợp và đặt các object
kết quả lần lượt vào một mảng, thì chúng ta có thể xem mảng đó là một view
; Và view
này có thể được biểu thị ở dạng bảng như sau -
+------+--------------------+-----------+-------------+---------------+
| @id | title | ......... | category-id | category-name |
+------+--------------------+-----------+-------------+---------------+
| 0000 | Làm Thế Nào Để ... | ......... | 01 | html |
+------+--------------------+-----------+-------------+---------------+
| 0001 | Cách Chèn Ảnh ... | ......... | 01 | html |
+------+--------------------+-----------+-------------+---------------+
| .... | .................. | ......... | .. | .... |
+------+--------------------+-----------+-------------+---------------+
| 1001 | Hoàn Thành Series | ......... | .. | .... |
+------+--------------------+-----------+-------------+---------------+
Và dưới đây là biểu thị trong code quản lý database
-
[database]
. |
. +-----[data]
. +-----[procedure]
. | |
. | +-----[article]
. | +-----[category]
. | +-----[article-join-category--all-join-name]
. | |
. | +-----select-by-article-id--async-throw.js
. |
. +-----[type]
. +-----[view]
. |
. +-----article-join-category--all-join-name--async-throw.js
const readAllArticleIds = require("../procedure/article/sub-procedure/read-all-record-ids--async-throw");
const ArticleJoinCategory = require("../type/ArticleJoinCategory--all-join-name");
const selectArticleByIdJoinCategory = require("../procedure/article-join-category--all-join-name/select-by-article-id--async-throw"); const view = { indexData: async function* ( in_options = { reverseOrder: false } ) { var allArticleIds = []; await readAllArticleIds(allArticleIds); if (in_options.reverseOrder == false) /* do nothing */; else allArticleIds = allArticleIds.reverse(); for (var articleId of allArticleIds) { var joinedRecord = new ArticleJoinCategory(); await selectArticleByIdJoinCategory(articleId, joinedRecord); yield joinedRecord; } // for .. of } // indexData
}; // view module.exports = view;
Ở đây chúng ta có view
là một object
có chứa phương thức indexData
là một hàm generator
. Khi chúng ta gọi phương thức này thì kết quả trả về là một bộ dữ liệu trừu tượng chứa tất cả các bản ghi article
liên hợp - và cũng chính là bảng dữ liệu mà chúng ta đã nói đến ở trên. Bây giờ chúng ta sẽ lặp qua từng bản ghi trong bảng này và in ra console
.
const view = require("./database/view/article-join-category--all-join-name--async-throw"); void async function() { var allJoinedRecords = view.indexData({ reverseOrder: true }); for await (var record of allJoinedRecords) { console.log(record); }
} (); // void
CMD | Terminal
npm test ArticleJoinCategory(8) [Map] { '@id' => '0002', 'title' => 'Cách Chèn Ảnh & Các Liên Kết', 'short-title' => 'Ảnh & Liên Kết', 'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'html', 'ảnh', 'liên kết' ], 'edited-datetime' => 'Sat, 16 Apr 2022 19:13:22 GMT', 'markdown' => 'Nội dung của bài viết thứ hai...', 'category-id' => '01', 'category-name' => 'html5'
}
ArticleJoinCategory(8) [Map] { '@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', 'markdown' => 'Nội dung của bài viết đầu tiên...', 'category-id' => '01', 'category-name' => 'html5'
}
ArticleJoinCategory(8) [Map] { '@id' => '0000', 'title' => 'Nội dung bạn tìm kiếm không tồn tại', 'short-title' => 'Nội Dung Không Tồn Tại', 'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web' ], 'edited-datetime' => 'Sat, 16 Apr 2022 01:13:22 GMT', 'markdown' => 'Nội dung mà bạn đang tìm kiếm không tồn tại...', 'category-id' => '00', 'category-name' => 'unknown'
}
Như vậy, đối với các thao tác truy vấn khác liên quan đến kiểu bản ghi liên hợp ArticleJoinCategory
, chúng ta có thể truy vấn thông qua view
này. Ví dụ điển hình là khi chúng ta muốn chọn ra một vài bài viết mới nhất để bày các bản giới thiệu ngắn trên giao diện trang chủ. Lúc này chúng ta có thể viết một thủ thục select-top
sử dụng view
này như sau -
const view = require("../../view/article-join-category--all-join-name--async-throw"); module.exports = async ( in_options = { numberOfRecords: 0, reverseOrder: false }, out_selected = []
) => { var allJoinedRecords = view.indexData({ ...in_options }); for await (var record of allJoinedRecords) { if (out_selected.length < in_options.numberOfRecords) out_selected.push(record); else break; }
}; // module.exports
const storedProcedure = new Map(); /* other procedures ... */ storedProcedure.set( "select-top-articles-join-category--all-join-name", require("./procedure/article-join-category--all-join-name/select-top-by-category-id--async-throw")
); exports.execute = async ( procedureName = "tên-thủ-tục", ...parameters
) => { await storedProcedure .get(procedureName) .call(null, ...parameters);
};
const databaseManager = require("./database/manager"); void async function() { var procedure, options, selected; await databaseManager.execute( procedure = "select-top-articles-join-category--all-join-name", options = { numberOfRecords: 3, reverseOrder: true }, selected = [] ); console.log(selected);
} (); // void
CMD | Terminal
[ ArticleJoinCategory(8) [Map] { '@id' => '0002', 'title' => 'Cách Chèn Ảnh & Các Liên Kết', 'short-title' => 'Ảnh & Liên Kết', 'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web', 'html', 'ảnh', 'liên kết' ], 'edited-datetime' => 'Sat, 16 Apr 2022 19:13:22 GMT', 'markdown' => 'Nội dung của bài viết thứ hai...', 'category-id' => '01', 'category-name' => 'html5' }, ArticleJoinCategory(8) [Map] { '@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', 'markdown' => 'Nội dung của bài viết đầu tiên...', 'category-id' => '01', 'category-name' => 'html5' }, ArticleJoinCategory(8) [Map] { '@id' => '0000', 'title' => 'Nội dung bạn tìm kiếm không tồn tại', 'short-title' => 'Nội Dung Không Tồn Tại', 'keywords' => [ 'hướng dẫn cơ bản', 'lập trình web' ], 'edited-datetime' => 'Sat, 16 Apr 2022 01:13:22 GMT', 'markdown' => 'Nội dung mà bạn đang tìm kiếm không tồn tại...', 'category-id' => '00', 'category-name' => 'unknown' }
]
Kết thúc bài viết
Như vậy là chúng ta đã được biết sơ khai về khái niệm view
trong quản lý database
và một số phương thức truy vấn phức hợp phổ biến là select-top
và join
. Chúng ta sẽ được gặp lại những khái niệm này trong Sub-Series SQL
với nhiều đặc điểm chi tiết hơn. Còn bây giờ thì chúng ta sẽ tạm dừng Sub-Series Database
tại đây một thời gian ngắn để hoàn thiện trang blog đơn giản đang xây dựng như đã dự kiến trước đó. Hẹn gặp lại bạn trong những bài viết tiếp theo.
(Chưa đăng tải) [Database] Bài 8 - Từ từ để xem chúng ta cần học thêm cái gì đã. Học theo cách tự nhiên mà.