July 10, 2019

Migrate to NoSql

Một điều được nhắc đến rất nhiệu khi chuyển từ sql sang nosql là cách để lưu trữ quan hệ giữa các đối tượng trong database. Một đối tượng thường rất ít khi tồn tại độc lâp trong một hệ thống mà nắm giữ 1 phần thông tin được sử dụng để tạo ra một đối tượng khác.

Các dạng quan hệ chính giữa các đối tượng có thể lưu được trong cơ sở dữ liệu quan hệ bao gồm

  • One-to-One
  • One-to-N
  • N-to-N

Tất nhiên, trong các loại đã được liệt kê phía trên, quan hệ N-to-N thường ít thấy do được chuyển thành 2 quan hệ One-to-N ( và tốt nhất là nó nên được chuyển như thế :smile: ), quan hệ One-to-One thì khá cơ bản và không gặp vấn đề khi chuyển từ sql sang nosql. Vậy vấn đề chính cần giải quyết là tìm câu trả lời cho việc thế hiện quan hệ One-to-N một cách hiệu quả trong nosql.

Để phủ đầu mọi tranh luận, câu trả lời ngắn gọn cho câu trả lời trên là: không có phương pháp cố định nào để áp dụng khi chuyển quan hệ One-to-N từ sql sang nosql! Nó phụ thuộc vào một số trường hợp cụ thể, cũng như yêu cầu của hệ thống và đặc điểm của dữ liệu để người thiết kế có thể quyết định cách nào là hiệu quả.

Để đưa ra thiết kế phù hợp cho dữ liệu có quan hệ One-to-N, một câu hỏi được đặt ra: Cardinality ( lực lượng hay số số phần tử của một tập hợp ) của tập N trong mối quan hệ đố là bao nhiêu? hay một cách hỏi khác: cụ thể quan hệ One-to-N đó là One-to-few, One-to-many, One-to-squillions? Tuỳ thuộc vào câu trả lời mà cách thiết kế trong nosql tương ứng sẽ khác nhau.

One-to-few

Lưu trữ dư thừa ( hay lặp lại ) để giảm bớt việc join khi cần lấy dữ liệu là một đặc tính của nosql. Trong trường hợp quan hệ One-to-N là One-to-few, một cách hợp lý để lưu dữ liệu là lưu trực tiếp các thành phần dữ liệu N trong dữ liệu One

> db.person.findOne()
{
  name: 'khanhtc',
  dob: '1994-01-01'
  addresses : [
    { city: 'Tokyo', cc: 'JPN' },
    { city: 'Hanoi', cc: 'VNM' }
  ]
}

Dễ thấy số lượng dữ liệu địa chỉ là nhỏ và khi tính đến việc query 2 bảng hoặc join bảng để lấy dữ liệu, việc lưu trữ trực tiếp thông tin địa chỉ dưới dạng mảng hiệu quả hơn.

One-to-many

Khi lực lượng (cardinality) của tập N trong quan hệ One-to-N đủ lớn ( cỡ hàng trăm chẳng hạn ), việc lưu trữ lặp lại hàng trăm dữ liệu của tập N trong mỗi document dữ liệu của tập One là dư thừa quá lớn. Một cách đơn giản, giống như trog cơ sở dữ liệu quan hệ, lưu trữ key trỏ đến dữ liệu của kiểu N trong kiểu One là một cách hợp lý để giải quyết vấn đề này.

Ví dụ một product như oto chẳng hạn, được tạo thành từ vài trăm chi tiết khác nhau.

> db.parts.findOne()
{
    _id : ObjectID('AAAA'),
    partno : '123-aff-456',
    name : '#4 grommet',
    qty: 94,
    cost: 0.94,
    price: 3.99
}
> db.cars.findOne()
{
    name : 'mini cooper',
    manufacturer : 'BMW',
    catalog_number: 1234,
    parts : [     // array of references to Part documents
        ObjectID('AAAA'),    // reference to the #4 grommet above
        ObjectID('F17C'),    // reference to a different Part
        ObjectID('D2AA'),
        // etc
    ]
}

Để query dữ liệu, ta cần 2 query riêng biệt cho trường hợp này

// Fetch the Cars document identified by this catalog number
> product = db.cars.findOne({catalog_number: 1234});
// Fetch all the Parts that are linked to this Car
> product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

Một điểm cần chú ý là khi dữ liệu của tập N thường xuyên bị thay đổi, việc lưu riêng dữ liệu kiểu N thành 1 collections giúp cho việc update bớt tốn kém hơn ( tương tự như khi lưu dữ liệu trong sql ).

One-to-squillions

Một ví dụ điển hình của kiểu dữ liệu dạng này là dữ liệu log hệ thống, chẳng hạn log trạng thái của các host trong hệ thống.

> db.hosts.findOne()
{
    _id : ObjectID('AAAB'),
    name : 'goofy.example.com',
    ipaddr : '127.66.66.66'
}
> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
....
}

Bất cứ dữ liệu kiểu dạng log nào cũng đều rất lớn và tăng cực nhanh, một cách hiệu quả để thể hiện mỗi quan hệ One-to-N giữ host và log thuộc về nó là để dữ liệu kiểu N (log) giữ relation đến dữ liệu kiểu One (host).

> db.logmsg.findOne()
{
time : ISODate("2014-03-28T09:42:41.382Z"),
message : 'cpu is on fire!',
host: ObjectID('AAAB')       // Reference to the Host document
}

Khi đó query dữ liệu log theo host sẽ trở thành

// find the parent ‘host’ document
> host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // assumes unique index
// find the most recent 5000 log message documents linked to that host
> last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

to be continue…

© khanhtc 2019