Collections và Schemas trong Meteor

MongoDB collections trong Meteor

Trong bất kỳ một ứng dụng web nào đều cung cấp cho người dùng giao diện và cách thức chỉnh sửa dữ liệu được lưu trữ trong DB. Có thể nói khi thao tác với một danh sách công việc cần làm, thứ tự xe đưa đón nhân viên... trong một ứng dụng đơn giản, chúng ta cũng sẽ tương tác với dữ liệu và thay đổi chúng ít nhiều. (data layer)

Trong Meteor, lớp data layer được lưu trữ trong MongoDB. Một tập hợp các dữ liệu có liên quan trong MongoDB được gọi là một collection. Trong Meteor bạn truy cập tới dữ liệu lưu trữ trong MongoDB thông qua collection.

Tuy nhiên, với collection chúng ta có rất nhiều cách để lưu và lấy dữ liệu. Lý do là bởi, meteor muốn đưa tới người dùng những trải nghiệm của một ứng dụng theo cách tốt nhất, dễ cài đặt và sử dụng nhất.

Trong bài viết này chúng ta sẽ đi tìm hiểu kỹ hơn cách mà collection hoạt động trong Meteor Framework.

Server-side collections

Khi bạn tạo 1 collection trên Server:

Todos = new Mongo.Collection('Todos');

Với câu lệnh trên, chúng ta sẽ tạo ra:

  • Một collection bên trong MongoDB
  • Một interface tới collection để sử dụng trên server. Đó là một layer nằm bên dưới Node MongoDB driver, cung cấp cho chúng ta API đồng bộ dữ liệu:
// This line won't complete until the insert is done
Todos.insert({_id: 'my-todo'});
// So this line will return something
const todo = Todos.findOne({_id: 'my-todo'});
// Look ma, no callbacks!
console.log(todo);

Client-side collections

Trên Client, khi bạn viết dòng code sau:

Todos = new Mongo.Collection('Todos');

Có vẻ như giống với đoạn code mà chúng ta đã viết trên server, nhưng cách thức hoạt động của chúng ko giống nhau (chao)

Trên Client, không có kết nối trực tiếp đến các cơ sở dữ liệu MongoDB, và trong thực tế không có một API đồng bộ nào cả. Thay vào đó, Collection trên Client là một bộ nhớ cache trên trình duyệt. Điều này có được nhờ vào một thư viện nổi tiếng mang tên Minimongo. Khi bạn code:

// This line is changing an in-memory Minimongo data structure
Todos.insert({_id: 'my-todo'});
// And this line is querying it
const todo = Todos.findOne({_id: 'my-todo'});
// So this happens right away!
console.log(todo);

Đó là cách chúng ta di chuyển dữ liệu từ server (MongoDB-backed) collection sang client (in-memory) collection. Nói cách khác, đó là khi chúng ta gọi lệnh subscribe tới một publication (pushes data từ server tới client). Có thể giả định rằng client chứa một tập con mới nhất của tập đầy đủ MongoDB collection trên server.

Local collections

Có một cách thứ ba để sử dụng một collection trên Meteor. Trên Client hoặc Server, nếu bạn tạo ra một collection nhưng pass cho nó giá trị tên là NULL:

SelectedTodos = new Mongo.Collection(null);

Đoạn mã này tạo ra một local collection. Đây là một collection bên trong Minimongo mà không có kết nối tới cơ sở dữ liệu (thường là một collection có tên hoặc là sẽ được kết nối trực tiếp đến các cơ sở dữ liệu trên server, hoặc thông qua một subscription trên client).

Local collection chính là cách thuận tiện nhất để sử dụng toàn bộ sức mạnh của thư viện Minimongo nhằm lưu trữ dữ liệu trong bộ nhớ. Ví dụ, bạn có thể sử dụng nó thay vì sử dụng một mảng nếu bạn cần thực hiện các truy vấn phức tạp trên dữ liệu của bạn. Hoặc bạn có thể muốn tận dụng khả năng tương tác của nó trên client với giao diện người dùng theo cách tự nhiên trong Meteor.

Defining a schema

Mặc dù MongoDB là một cơ sở dữ liệu schema-less, tạo ra sự linh hoạt tối đa trong cấu trúc dữ liệu, nhưng nếu bạn ko xác định rõ format của dữ liệu đầu vào, validate cẩn thận thì dữ liệu được insert vào DB có thể không được nhất quán, điều này dẫn tới việc phát sinh bug trong chương trình. Do đó, thay vì insert trực tiếp data và cơ sở dữ liệu, chúng ta sẽ sử dụng schema để định hình lại chúng trước:

Để viết một schema sử dụng simple-schema, bạn có thể tạo một instance mới của SimpleSchema class:

Lists.schema = new SimpleSchema({
  name: {type: String},
  incompleteCount: {type: Number, defaultValue: 0},
  userId: {type: String, regEx: SimpleSchema.RegEx.Id, optional: true}
});

Trong ví dụ trên, ta định nghĩa các rules sau cho một list-item:

  • name field của một list thì bắt buộc phải có kiểu string.
  • incompleteCount có kiểu number, và giá trị mặc định là 0.
  • userId là giá trị không bắt buộc, có kiểu string giống như ID của document.

Validating against a schema

Bây giờ chúng ta có một schema, làm thế nào để chúng ta sử dụng nó? Khá đơn giản, để validate một document vs schema. Chúng ta viết:

const list = {
  name: 'My list',
  incompleteCount: 3
};

Lists.schema.validate(list);

Trong trường hợp này, khi list hợp lệ vs schema, validate() function sẽ chạy qua mà ko có vấn đề. Tuy nhiên nếu ta viết:

const list = {
  name: 'My list',
  incompleteCount: 3,
  madeUpField: 'this should not be here'
};

Lists.schema.validate(list);

Function validate() sẽ throw một ValidationError, chưa chi tiết về các lỗi của list document.

ValidationError

ValidationError là gì ? Nó là một lỗi đặc biệt được sử dụng trong Meteor nhằm chỉ ra các lỗi trong user-input khi chỉnh sửa collection tương ứng. Thông thường, chi tiết trong ValidationError được sử dụng để chỉ cho người dùng thấy họ nhập sai dữ liệu gì trên form.

Thiết kế Schema

Có một số điểm cần lưu ý khi thiết kế Meteor data schema. Quan trọng nhất liên quan tới DDP (Một giao thức trao đổi dữ liệu trong Meteor). DDP sẽ gửi các thay đổi tới documents ở mức top-level là fields. Điều đó có nghĩa là nếu bạn có một tập document với số lương fields lớn và phức tạp thì DDB có thể gửi các thay đổi không cần thiết trên network.

Ví dụ, trong “pure” MongoDB, ta có thể thiết kế schema để mỗi list-document có một field được gọi todos(mảng của todo-items):

Lists.schema = new SimpleSchema({
  name: {type: String},
  todos: {type: [Object]}
});

Như đã đề cập ở trên về hành vi của DDP, mỗi thay đổi với bất kỳ todo-item nào trong list cũng sẽ yêu cầu gửi toàn bộ các todos qua mạng.

References