Chuyển đến nội dung chính

Hướng dẫn Mô hình hóa Dữ liệu MongoDB cho Ứng dụng Blog

Tìm hiểu một số khả năng mô hình hóa dữ liệu, bao gồm tài liệu lồng nhau, khi thiết kế hệ quản trị nội dung (CMS) hoặc ứng dụng blog.
· 7 phút đọc

Khám phá cùng AI

Vậy là bạn muốn tự xây dựng hệ thống quản trị nội dung (CMS) của riêng mình, đôi khi còn gọi là blog? Đây là ví dụ kinh điển khi học cách sử dụng cơ sở dữ liệu, dù là hệ quản trị cơ sở dữ liệu quan hệ (RDBMS) hay cơ sở dữ liệu NoSQL, vì nó khai thác một lượng dữ liệu tiềm năng lớn cũng như các mối quan hệ trong dữ liệu đó. Ví dụ về ứng dụng blog cũng chuyển dịch tốt sang các nhu cầu mô hình hóa dữ liệu khác.

Trong bài viết này, chúng ta sẽ khám phá một số điều nên và không nên làm khi thiết kế tài liệu NoSQL trong MongoDB. Tuy nhiên, chúng ta sẽ không thực sự phát triển một ứng dụng blog, mà chỉ nhìn nhận mọi thứ dưới góc độ dữ liệu.

Các thành phần của một ứng dụng blog

Trước khi thử thiết kế mô hình tài liệu cho một blog, sẽ là ý hay nếu lùi lại một bước và nghĩ về mọi thành phần có thể gắn dữ liệu với nó.

Một blog điển hình có thể có các tính năng sau:

  • Nhiều người dùng hoặc tác giả
  • Mỗi tác giả có nhiều bài viết
  • Mỗi bài viết có nhiều bình luận

Tất nhiên, danh sách tính năng đó có thể dài và phức tạp hơn tùy theo nhu cầu kinh doanh của blog bạn. Để đơn giản, chúng ta sẽ mô hình hóa dữ liệu dựa trên những điều trên.

Những gì bạn có thể làm nhưng không nên làm

Suy nghĩ đầu tiên khi nhìn vào các tính năng trên có thể là coi mỗi gạch đầu dòng như một tài liệu riêng. Đây là cách tiếp cận điển hình trong cơ sở dữ liệu quan hệ như Postgres. Trong MongoDB, nó có thể trông như sau:

{
	"_id": "author-1",
	"name": "Nic Raboy",
	"description": "I'm some dude named Nic."
}

Ở trên có thể là một biểu diễn tối giản của một tác giả. Tất nhiên, bạn có thể có nhiều trường khác như email và các thứ khác, nhưng điều đó không thực sự quan trọng ở đây.

Sau đó, chúng ta có thể có tài liệu cho các bài viết như sau:

{
	"_id": "blog-1",
	"author_id": "author-1",
	"title": "MongoDB is Awesome!",
	"content": "This is some blog content..."
}

Cũng như trên, tài liệu blog có thể có các trường khác như tags và bất cứ gì bạn nghĩ ra. Điểm mấu chốt ở đây là nó có tham chiếu id giữa hai tài liệu. Chút nữa sẽ nói thêm về điều đó.

Mô hình tài liệu cuối cùng trong kịch bản này sẽ là phần bình luận:

{
	"_id": "comment-1",
	"blog_id": "blog-1",
	"username": "Anonymous User",
	"content": "Hey great article, it really helped a lot!"
}

Một lần nữa, tài liệu bình luận trong kịch bản này có mối quan hệ tham chiếu dựa trên giá trị id giữa tài liệu bình luận và tài liệu blog. Tất cả điều này rất phổ biến trong cơ sở dữ liệu quan hệ, và thực tế là nó cũng hoạt động ổn trong MongoDB. Tuy nhiên, cách này sẽ không mang lại trải nghiệm tốt nhất với MongoDB.

Một lý do bạn không muốn làm như vậy trong MongoDB là vấn đề hiệu năng.

Để nối mỗi tài liệu trong ba tài liệu này, bạn sẽ muốn dùng thao tác $lookup trong pipeline tổng hợp. Các thao tác $lookup không rẻ và có thể ảnh hưởng tiêu cực đến hiệu năng khi cơ sở dữ liệu của bạn mở rộng, đặc biệt khi bạn có nhiều hơn một thao tác $lookup trong pipeline.

Có những cách tốt hơn để hoàn thành công việc trong MongoDB.

Cách tiếp cận mô hình hóa dữ liệu tốt hơn, thân thiện với MongoDB hơn

Một trong những quy tắc đầu tiên của MongoDB là dữ liệu được truy cập cùng nhau nên được lưu trữ cùng nhau. Lưu mỗi tính năng trong một tài liệu hoặc bộ sưu tập riêng phá vỡ quy tắc đó vì chúng ta không còn thực hiện một thao tác lấy dữ liệu duy nhất cho nội dung hiển thị cho người dùng.

Hãy thử lại vấn đề, mô hình hóa các tài liệu cho blog của chúng ta như sau:

{
	"_id": "author-1",
	"name": "Nic Raboy",
	"description": "I'm some dude named Nic.",
	"posts": [
		{
			"title": "MongoDB is Awesome!",
			"content": "This is some blog content...",
			"comments": [
				{
					"username": "Anonymous User",
					"content": "Hey great article, it really helped a lot!"
				}
			]
		}
	]
}

Được rồi, vậy là một trong những quy tắc chính của MongoDB đã được đáp ứng trong mô hình trên. Chúng ta có một tài liệu duy nhất dùng lồng nhau để lưu toàn bộ bài viết và bình luận cùng với tác giả.

Mặc dù có thể hoạt động, nhưng có lẽ không phải ý hay khi mô hình hóa tài liệu ứng dụng blog của bạn như vậy.

MongoDB có giới hạn kích thước tài liệu, và khi bạn có các mảng không bị ràng buộc trong những tài liệu đó, bạn có nguy cơ vượt quá các giới hạn này. Chưa kể bạn có thể gặp vấn đề hiệu năng khi mảng trở nên lớn. Trong trường hợp này, chúng ta có thể có số lượng bài viết không giới hạn và số lượng bình luận không giới hạn cho mỗi bài viết. Đến một lúc nào đó sẽ trở nên lộn xộn.

Điều có thể phù hợp hơn trong ví dụ này là kết hợp hai chiến lược.

Hãy nhớ rằng, tác giả, bài viết và bình luận có thể có bất kỳ số lượng trường nào đi kèm. Trong ví dụ này, chúng ta giữ ở mức tối thiểu. Với điều đó, chúng ta biết rằng dữ liệu được truy cập thường xuyên nhất sẽ là bản thân bài blog. Hãy xem thiết kế mới:

{
	"_id": "blog-1",
	"author": {
		"_id": "author-1",
		"name": "Nic Raboy",
	},
	"title": "MongoDB is Awesome!",
	"content": "This is some blog content...",
	"comments": [
		{
			"username": "Anonymous User",
			"content": "Hey great article, it really helped a lot!"
		}
	]
}

Tài liệu thứ hai trong ví dụ này vẫn sẽ là tài liệu gốc cho thông tin tác giả:

{
	"_id": "author-1",
	"name": "Nic Raboy",
	"description": "I'm some dude named Nic."
}

Chúng ta biết rằng một phần thông tin tác giả nên được đưa vào khi hiển thị một bài blog, nhưng không nhất thiết cần toàn bộ thông tin. Ta có thể nhúng phần thông tin tác giả cần thiết kèm theo _id ngay trong bài viết, và nếu người dùng muốn tìm hiểu thêm về tác giả, họ có thể đi sâu hơn trong ứng dụng. Thao tác $lookup không diễn ra mỗi lần, nhờ đó cải thiện hiệu năng.

Giờ đây, chúng ta có vấn đề về số lượng bình luận tiềm tàng vô hạn cho mỗi bài viết đang được đính kèm dưới dạng một mảng.

Có vài cách tiếp cận để xử lý và cuối cùng là tùy bạn:

  • Bạn có thể chọn cách ngây thơ là giữ chúng trong một mảng, nhưng có lẽ không phải ý hay.
  • Bạn có thể đưa bình luận trở lại thiết kế ban đầu là mỗi bình luận một tài liệu, tất cả đều có id tham chiếu đến bài blog và dùng thao tác $lookup để đưa chúng vào.
  • Bạn có thể dùng chiến lược lưu trữ, giữ một số lượng cố định bình luận trong mảng bên trong bài viết đồng thời tạo tài liệu bình luận riêng cho từng bình luận, cho phép bạn chỉ truy vấn thêm khi cần.
  • Bạn có thể khám phá mẫu bucketing, nhóm bình luận thành các "bucket" khoảng 100 bình luận mỗi tài liệu, truy vấn số bucket bình luận bạn cần tại mỗi thời điểm. Bucketing tránh giới hạn kích thước tài liệu và cũng giúp phân trang. Bạn cũng có thể tăng hiệu năng bằng cách tạo chỉ mục trên blog_id để nhanh chóng lấy bình luận cho bất kỳ bài viết nào. Thông tin thêm về lập chỉ mục có thể tìm trong tài liệu MongoDB.
  • Kết hợp linh hoạt và hơn thế nữa...

Trong trường hợp mẫu bucketing, tài liệu bình luận của bạn có thể trông như sau:

{
	"_id": "comment-bucket-1",
	"blog_id": "blog-1",
	"comments": [
		{
			"username": "Anonymous User",
			"content": "Hey great article, it really helped a lot!"
		},
		// More comments here...
	],
	"comment_count": 57
}

Trong kịch bản này, khi có bình luận mới, bạn truy vấn một bucket chưa đầy. Bạn thêm bình luận mới vào mảng và tăng giá trị đếm. Mặc dù chiến lược này phụ thuộc nhiều hơn vào logic ở cấp ứng dụng để duy trì các bucket, nó sẽ mang lại hiệu năng tốt hơn. Nếu blog của bạn có cơ chế cuộn vô hạn, càng tốt vì khi đó bạn có thể phân trang tốt hơn bằng các bucket.

Mặc dù nằm ngoài phạm vi của bài viết này, nếu bạn muốn hình dung cách thêm bình luận mới, bạn sẽ lọc bộ sưu tập bucket bình luận (xem truy vấn ví dụ bên dưới) để tìm các bucket khớp với blog_id và có comment_count nhỏ hơn giá trị bạn chọn.

Đối với bucket khớp, bạn sẽ dùng toán tử $push trong tiêu chí cập nhật và toán tử $inc để tăng giá trị đếm bình luận. Tất cả có thể thực hiện trong một thao tác, và nếu bạn dùng cờ upsert, bạn có thể tạo bucket bình luận mới nếu chưa tồn tại bucket nào khớp tiêu chí.

Ví dụ truy vấn cập nhật: 

db.commentBuckets.updateOne(
  { blog_id: "blog-1", comment_count: { $lt: 100 } },
  { $push: { comments: newComment }, $inc: { comment_count: 1 } },
  { upsert: true }
)

Kết luận

Sẽ thật tuyệt nếu có thể nói bạn nên lưu mọi thứ về blog trong một tài liệu lồng nhau duy nhất, nhưng có lẽ đó không phải là ý hay. Cũng như có lẽ không phải ý hay khi triển khai một mô hình dữ liệu vốn phù hợp với cơ sở dữ liệu quan hệ nhưng không nhất thiết phù hợp với cơ sở dữ liệu tài liệu như MongoDB.

Với MongoDB, bạn muốn giảm thiểu tối đa các mối quan hệ tham chiếu để đạt hiệu năng tốt nhất có thể.

Hãy ghi nhớ những điều sau khi bạn mở rộng quy mô:

  • Nếu bạn dự đoán sẽ có nhiều bình luận, hãy cân nhắc mẫu bucket hoặc kết hợp nó với việc nhúng một phần trong tài liệu blog.
  • Nhu cầu phân trang của bạn là gì?
  • Lập chỉ mục các bộ sưu tập dựa trên cách bạn dự định truy vấn trong ứng dụng.

Nhắc lại, hãy nghĩ về cách bạn sẽ trình bày dữ liệu blog ở frontend. Bạn có thực sự cần toàn bộ thông tin tác giả trong mỗi yêu cầu hay chỉ cần một phần là đủ? Bạn có thực sự muốn tải một triệu bình luận cùng bài viết hay chỉ cần tải một phần nhỏ? Khi suy nghĩ kỹ, bạn sẽ ngạc nhiên về số lượng thao tác join mà mình không cần.

Nếu bạn mới làm quen với MongoDB, tôi khuyên bạn nên xem khóa học Introduction to MongoDB in Python.

Câu hỏi thường gặp

Tôi có thể chuyển mô hình dữ liệu ứng dụng blog từ cơ sở dữ liệu quan hệ sang MongoDB không?

Bạn có thể tạo các tài liệu phẳng với tham chiếu id và nó sẽ hoạt động, nhưng bạn sẽ không tận dụng được hiệu năng tối đa mà MongoDB có thể mang lại.

Tại sao không lưu mọi thứ trong một tài liệu duy nhất?

MongoDB có các giới hạn kích thước và khi bạn có mảng không bị ràng buộc, bạn có thể dễ dàng chạm đến các giới hạn đó.

Đã nhắc đến mẫu bucket, nhưng còn mẫu nào khác không?

Có những mẫu khác có thể phù hợp, nhưng mẫu bucket là một lựa chọn tốt khi xử lý các mảng lớn.

Giới hạn kích thước tài liệu của MongoDB là bao nhiêu?

Tài liệu MongoDB có kích thước tối đa là 16MB.

Tôi xử lý cập nhật dữ liệu lồng nhau thế nào (ví dụ: chỉnh sửa bình luận)?

Với dữ liệu nhúng, bạn có thể dùng các toán tử cập nhật như $set, và với bình luận được bucket, bạn có thể xác định bucket và cập nhật bình luận cụ thể trong đó.

Nic Raboy

Tác giả

Nic Raboy

Nic Raboy là Trưởng bộ phận Quan hệ Nhà phát triển tại MongoDB, nơi anh dẫn dắt một đội ngũ các nhà phát triển Python, Java, C# và PHP tạo ra nội dung xuất sắc giúp các nhà phát triển triển khai MongoDB thành công trong dự án của mình. Anh có kinh nghiệm với Golang và JavaScript, và thường xuyên viết về nhiều hành trình phát triển của bản thân.

Chủ đề

Liên quan