MySQL Document Store: Dùng MySQL như NoSQL với X DevAPI — Không cần đổi database

MySQL tutorial - IT technology blog
MySQL tutorial - IT technology blog

Vấn đề: Schema cứng nhắc khi dữ liệu thay đổi liên tục

Mình gặp cái này lúc build tính năng product attributes cho một e-commerce app. Mỗi loại sản phẩm có cấu trúc thuộc tính hoàn toàn khác nhau — laptop thì có RAM/CPU/SSD, áo quần thì có size/màu/chất liệu, thực phẩm thì có hạn dùng/thành phần. Cách truyền thống là tạo bảng EAV (Entity-Attribute-Value) với cặp key/value, hoặc tạo bảng riêng cho từng loại sản phẩm.

Cả hai đều không ổn. Bảng EAV thì query phức tạp, JOIN nhiều tầng, khó debug. Còn tạo bảng riêng thì mỗi lần thêm loại sản phẩm mới lại phải viết migration — với team nhỏ không có DBA chuyên trách thì khá mệt.

Ai đó trong team đề xuất chuyển phần này sang MongoDB. Nghe có vẻ hợp lý, nhưng toàn bộ hệ thống đang chạy MySQL — transaction, foreign key, báo cáo đều dựa vào đó. Thêm một database engine vào stack là thêm complexity không cần thiết: monitoring hai chỗ, backup hai chỗ, on-call cũng phải biết hai chỗ.

Nguyên nhân: Chưa biết MySQL đã có tính năng này từ lâu

Thực ra không phải MySQL không làm được — mà là mình chưa biết MySQL có tính năng này. Từ MySQL 5.7 đã có native JSON type. Và từ MySQL 8.0, họ đưa thêm MySQL Document Store kết hợp với X DevAPI vào core.

MySQL Document Store cho phép tạo các Collection — giống collection trong MongoDB — lưu JSON documents mà không cần định nghĩa schema trước. Phía dưới vẫn là InnoDB, vẫn có ACID đầy đủ, nhưng giao diện query có thêm document-oriented API. X DevAPI chạy trên port 33060 (song song với port 3306 thông thường), hỗ trợ cả SQL truyền thống lẫn CRUD document-style trong cùng một session.

Các cách giải quyết

Cách 1: JSON column trong bảng SQL thông thường

Cách đơn giản nhất — thêm cột attributes JSON vào bảng products rồi query bằng JSON_EXTRACT() hoặc operator ->>. Ổn nếu chỉ cần đọc/ghi JSON đơn giản. Nhưng khi cần index nhiều nested field, hay khi code backend muốn dùng document-style API thay vì nối chuỗi SQL, thì bắt đầu cồng kềnh.

Cách 2: MySQL Document Store với X DevAPI

Trước tiên, cài MySQL Shell — đây là client hỗ trợ X Protocol:

# Ubuntu/Debian
sudo apt install mysql-shell

# Hoặc cài Python connector hỗ trợ X DevAPI
pip install mysql-connector-python

Kết nối bằng X Protocol (port 33060) và tạo collection:

# Kết nối MySQL Shell với X Protocol
mysqlsh root@localhost:33060 --js
// Trong MySQL Shell (JavaScript mode)
var db = session.createSchema('product_catalog');
var products = db.createCollection('products');

// Thêm document — không cần định nghĩa schema trước
products.add({
  name: "Laptop Dell XPS 15",
  category: "laptop",
  attributes: {
    cpu: "Intel Core i7-13700H",
    ram: "32GB DDR5",
    storage: "1TB NVMe SSD"
  },
  price: 35000000,
  in_stock: true
}).execute();

// Document khác loại — schema hoàn toàn khác, không lỗi
products.add({
  name: "Áo Thun Basic",
  category: "clothing",
  attributes: {
    sizes: ["S", "M", "L", "XL"],
    colors: ["đen", "trắng", "xám"],
    material: "100% cotton"
  },
  price: 250000,
  in_stock: true
}).execute();

// Query document
products.find("category = 'laptop' AND price > 30000000")
  .fields("name", "price")
  .order_by("price DESC")
  .execute();

// Tìm theo nested field
products.find("attributes.ram = '32GB DDR5'").execute();

Dùng từ Python backend thực tế:

import mysqlx

# Kết nối X DevAPI
session = mysqlx.get_session({
    'host': 'localhost',
    'port': 33060,
    'user': 'root',
    'password': 'your_password'
})

db = session.get_schema('product_catalog')
products = db.get_collection('products')

# Thêm document
result = products.add({
    "name": "MacBook Pro M3",
    "category": "laptop",
    "attributes": {
        "chip": "Apple M3 Pro",
        "ram": "18GB Unified Memory",
        "storage": "512GB SSD"
    },
    "price": 55000000
}).execute()

print(f"Added ID: {result.get_generated_ids()}")

# Query có filter + sort
docs = products.find("in_stock = true AND category = :cat") \
    .bind('cat', 'laptop') \
    .fields("name", "price", "attributes") \
    .order_by("price DESC") \
    .limit(10) \
    .execute()

for doc in docs.fetch_all():
    print(f"{doc['name']} — {doc['price']:,} VND")

session.close()

Tạo index để tránh slow query

Mình học được bài này khi table users vượt 10 triệu row — slow query bắt đầu xuất hiện và phải lần mò tối ưu index. Document Store cũng không tránh khỏi vấn đề tương tự nếu không index đúng từ đầu:

// Index cho field category (dùng nhiều trong WHERE)
products.createIndex('category_idx', {
  fields: [{
    field: '$.category',
    type: 'TEXT(50)'
  }]
});

// Index cho numeric field price
products.createIndex('price_idx', {
  fields: [{
    field: '$.price',
    type: 'DECIMAL(15,2)'
  }]
});

// Kiểm tra index đã tạo
session.sql('SHOW INDEX FROM product_catalog.products').execute();

Cách tốt nhất: Kết hợp relational và document trong cùng database

Sau khi thử qua các approach, mình thấy cách hiệu quả nhất không phải chọn một trong hai — mà dùng cả hai trong cùng một MySQL instance.

Phân chia theo đặc điểm dữ liệu:

  • Table SQL thông thường: orders, users, payments — cấu trúc cố định, cần JOIN, cần transaction toàn vẹn.
  • Document Collection: products, product_reviews, user_preferences — schema thay đổi theo loại, không cần JOIN phức tạp.

Trong cùng một session MySQL Shell, mix SQL và Document API hoàn toàn bình thường:

// SQL cho orders (relational — cần JOIN)
session.sql(`
  SELECT o.id, o.total, u.email
  FROM orders o
  JOIN users u ON o.user_id = u.id
  WHERE o.status = 'pending'
`).execute();

// Document API cho product catalog (flexible schema)
var catalog = session.getSchema('product_catalog');
catalog.getCollection('products')
  .find("in_stock = true")
  .limit(20)
  .execute();

Một số lưu ý thực tế

  • Port 33060: X Protocol dùng port riêng. Kiểm tra bằng SHOW VARIABLES LIKE 'mysqlx_port'. Nhớ mở firewall nếu app server ở máy khác.
  • Document ID: MySQL tự sinh _id dạng string 28 ký tự (không phải ObjectId như MongoDB). Có thể tự set ID khi add document nếu muốn.
  • Transaction: X DevAPI hỗ trợ transaction đầy đủ — session.startTransaction()session.commit(). Đây là điểm mạnh so với MongoDB khi cần ACID trên nhiều collection.
  • Backup/monitoring: Dùng chung với MySQL hiện tại — mysqldump, xtrabackup, Prometheus MySQL exporter đều hoạt động bình thường, không cần setup thêm gì.

Điểm mình thấy tiện nhất sau khi chuyển sang approach này: không phải maintain thêm infrastructure, không phải học thêm query language mới từ đầu, team đã quen MySQL rồi thì chỉ cần học thêm X DevAPI syntax là xong. Khi dữ liệu lớn dần và cần tối ưu, dùng đúng tool quen thuộc — thay vì phải debug cả hai hệ thống song song.

Share: