1. Database Migration là gì?

Database Migration là quá trình quản lý và áp dụng những thay đổi đối với cấu trúc hoặc dữ liệu trong cơ sở dữ liệu theo từng phiên bản của ứng dụng.

Trong quá trình phát triển phần mềm, yêu cầu nghiệp vụ thường xuyên thay đổi. Một bảng có thể cần thêm cột mới, thay đổi kiểu dữ liệu, bổ sung chỉ mục hoặc thiết lập thêm ràng buộc giữa các bảng. Những thay đổi này cần được áp dụng vào database một cách có tổ chức để ứng dụng tiếp tục hoạt động đúng.

Ví dụ, ban đầu hệ thống có bảng products với hai cột id và name:

CREATE TABLE products (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);

Sau đó, ứng dụng cần lưu thêm giá sản phẩm. Lúc này, cấu trúc database cần được thay đổi để bổ sung cột price:

ALTER TABLE products
ADD COLUMN price NUMERIC(12, 2) NOT NULL DEFAULT 0;

Câu lệnh trên thay đổi cấu trúc bảng hiện có mà không cần xóa và tạo lại toàn bộ bảng.

Tuy nhiên, nếu chỉ chạy câu lệnh SQL thủ công, nhóm phát triển sẽ khó theo dõi ai đã thay đổi database, thay đổi nào đã được áp dụng và làm thế nào để tái tạo cùng cấu trúc trên các môi trường khác nhau.

Database Migration giải quyết vấn đề đó bằng cách lưu những thay đổi thành các phiên bản có thể theo dõi, kiểm soát và áp dụng theo thứ tự.

2. Tại sao cần Database Migration?

2.1. Quản lý sự thay đổi của cấu trúc dữ liệu

Khi ứng dụng phát triển, schema của database cũng cần thay đổi theo.

Ví dụ:

  • Phiên bản đầu tiên có bảng products.
  • Phiên bản tiếp theo bổ sung cột price.
  • Phiên bản sau đó thêm cột stock_quantity.
  • Phiên bản tiếp theo tạo chỉ mục để cải thiện hiệu năng truy vấn.

Mỗi thay đổi có thể được lưu trong một migration riêng. Nhờ đó, lịch sử thay đổi cấu trúc database trở nên rõ ràng và dễ kiểm tra hơn.

2.2. Đồng bộ database giữa các môi trường

Một dự án có thể có nhiều môi trường:

  • Development: môi trường phát triển.
  • Testing: môi trường kiểm thử.
  • Staging: môi trường tiền triển khai.
  • Production: môi trường đang phục vụ người dùng thực tế.

Nếu mỗi môi trường được sửa database thủ công theo cách riêng, chúng có thể có cấu trúc khác nhau. Điều này dễ dẫn đến tình trạng ứng dụng chạy được ở máy lập trình viên nhưng gặp lỗi khi triển khai.

Migration giúp áp dụng cùng một tập hợp thay đổi theo thứ tự xác định trên những môi trường khác nhau.

Tuy nhiên, để bảo đảm tính nhất quán, nhóm phát triển vẫn cần quản lý phiên bản migration, cấu hình môi trường và quy trình triển khai phù hợp.

2.3. Quản lý thay đổi khi làm việc nhóm

Migration có thể được lưu cùng mã nguồn trong Git.

Khi một lập trình viên bổ sung cột price, họ tạo migration tương ứng và đưa tệp đó vào repository. Những thành viên khác có thể lấy mã nguồn mới và chạy migration để cập nhật database của mình.

Nhờ vậy, việc thay đổi database trở thành một phần của quy trình phát triển phần mềm thay vì phụ thuộc vào những thao tác thủ công khó theo dõi.

3. Migration hoạt động như thế nào?

Một migration thường mô tả một thay đổi cụ thể đối với database. Công cụ migration sử dụng các tệp này để áp dụng thay đổi theo thứ tự và theo dõi những phiên bản đã được thực thi.

Một quy trình phổ biến gồm các bước:

  1. Xác định thay đổi cần thực hiện đối với database.
  2. Tạo migration mô tả thay đổi đó.
  3. Kiểm tra nội dung migration và tác động đến dữ liệu hiện có.
  4. Chạy migration trên môi trường phù hợp.
  5. Ghi nhận phiên bản đã áp dụng.
  6. Kiểm thử ứng dụng với cấu trúc database mới.

Ví dụ, một migration có thể được đặt tên:

202610170001_add_price_to_products.up.sql

Tên tệp này chỉ là quy ước minh họa, không phải định dạng bắt buộc của mọi công cụ migration.

Nội dung migration:

ALTER TABLE products
ADD COLUMN price NUMERIC(12, 2) NOT NULL DEFAULT 0;

Sau khi migration được áp dụng thành công, bảng products có thêm cột price.

Công cụ migration thường lưu phiên bản hiện tại trong một bảng quản lý riêng hoặc sử dụng cơ chế theo dõi tương đương. Nhờ đó, công cụ có thể xác định migration nào đã chạy và migration nào còn chưa được áp dụng.

Cần lưu ý rằng migration không nhất thiết phải chỉ thay đổi cấu trúc. Một migration cũng có thể cập nhật hoặc chuyển đổi dữ liệu để đáp ứng mô hình dữ liệu mới.

4. Các loại thay đổi thường gặp trong Migration

4.1. Thêm cột mới

Giả sử bảng products chưa có cột price. Có thể bổ sung cột bằng:

ALTER TABLE products
ADD COLUMN price NUMERIC(12, 2) NOT NULL DEFAULT 0;

Trong PostgreSQL, việc thêm cột NOT NULL vào bảng đã có dữ liệu cần được thiết kế cẩn thận. Giá trị mặc định giúp các bản ghi hiện có đáp ứng ràng buộc không được để trống.

Với bảng lớn, nên đánh giá ảnh hưởng của thao tác này đến khóa, thời gian thực thi và hoạt động của ứng dụng.

4.2. Thay đổi kiểu dữ liệu

Giả sử cột price ban đầu có kiểu INTEGER, nhưng ứng dụng cần hỗ trợ phần thập phân.

Có thể sử dụng:

ALTER TABLE products
ALTER COLUMN price TYPE NUMERIC(12, 2)
USING price::NUMERIC(12, 2);

Mệnh đề USING chỉ định cách chuyển đổi dữ liệu hiện có sang kiểu mới.

Việc đổi kiểu dữ liệu không phải lúc nào cũng đơn giản. Nếu dữ liệu cũ không thể chuyển đổi hoặc việc thay đổi làm mất thông tin, migration có thể thất bại hoặc gây sai lệch dữ liệu. Vì vậy, cần kiểm tra dữ liệu trước khi thực hiện.

4.3. Tạo chỉ mục

Nếu ứng dụng thường xuyên tìm sản phẩm theo tên, có thể cân nhắc tạo chỉ mục:

CREATE INDEX idx_products_name
ON products(name);

Chỉ mục có thể cải thiện hiệu năng của những truy vấn phù hợp, nhưng cũng làm tăng chi phí lưu trữ và các thao tác ghi dữ liệu.

Với PostgreSQL, nếu cần tạo chỉ mục trên bảng lớn trong môi trường production, có thể cân nhắc CREATE INDEX CONCURRENTLY để giảm ảnh hưởng đến các thao tác ghi. Tuy nhiên, câu lệnh này có những giới hạn riêng và không thể chạy bên trong một transaction thông thường.

4.4. Thêm ràng buộc

Ví dụ, muốn bảo đảm giá sản phẩm không âm:

ALTER TABLE products
ADD CONSTRAINT products_price_nonnegative
CHECK (price >= 0);

Ràng buộc CHECK yêu cầu các bản ghi phải đáp ứng điều kiện được chỉ định.

Nếu bảng đã có dữ liệu không hợp lệ, việc thêm ràng buộc có thể thất bại. Vì vậy, cần kiểm tra và xử lý dữ liệu hiện có trước khi áp dụng migration.

4.5. Xóa cột hoặc bảng

Ví dụ xóa cột price:

ALTER TABLE products
DROP COLUMN price;

Đây là thay đổi có nguy cơ mất dữ liệu. Sau khi cột bị xóa, dữ liệu lưu trong cột đó không còn khả dụng theo cách thông thường.

Trong môi trường production, nên xác nhận rằng ứng dụng không còn phụ thuộc vào cột này và có phương án sao lưu hoặc khôi phục phù hợp trước khi thực hiện.

5. Up Migration và Down Migration

Nhiều công cụ migration phân biệt hai hướng thay đổi.

Up Migration là migration áp dụng thay đổi để đưa database sang phiên bản mới.

Down Migration là migration hoàn tác một thay đổi, thường nhằm đưa database về trạng thái trước đó nếu việc hoàn tác được hỗ trợ và an toàn.

Ví dụ, Up Migration thêm cột price:

ALTER TABLE products
ADD COLUMN price NUMERIC(12, 2) NOT NULL DEFAULT 0;

Down Migration tương ứng có thể xóa cột đó:

ALTER TABLE products
DROP COLUMN price;

Tuy nhiên, hoàn tác về mặt cấu trúc không đồng nghĩa với khôi phục được dữ liệu đã mất. Nếu ứng dụng đã ghi dữ liệu vào cột price, việc xóa cột sẽ làm mất những dữ liệu đó.

Không phải migration nào cũng có thể được đảo ngược hoàn toàn. Chẳng hạn, việc xóa dữ liệu hoặc gộp nhiều trường thành một trường mới có thể làm mất thông tin ban đầu.

Vì vậy, cần xem xét tính an toàn của thao tác trước khi thiết kế Down Migration.

6. Database Migration trong Go

Trong hệ sinh thái Go, có nhiều công cụ hỗ trợ quản lý migration, chẳng hạn golang-migrate/migrate và pressly/goose.

Những công cụ này giúp tổ chức các tệp migration, áp dụng thay đổi theo thứ tự và theo dõi trạng thái migration.

6.1. Cài đặt công cụ

Ví dụ sử dụng thư viện golang-migrate trong dự án Go:

go get -tags 'postgres' github.com/golang-migrate/migrate/v4

Tùy cách sử dụng, bạn có thể tích hợp thư viện vào chương trình Go hoặc cài đặt và chạy công cụ CLI riêng.

6.2. Tạo tệp migration

Một migration có thể gồm hai tệp:

migrations/
├── 000001_create_products_table.up.sql
└── 000001_create_products_table.down.sql

Tệp up.sql tạo bảng:

CREATE TABLE products (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price NUMERIC(12, 2) NOT NULL DEFAULT 0
);

Tệp down.sql hoàn tác bằng cách xóa bảng:

DROP TABLE products;

Tệp Down Migration này chỉ là ví dụ minh họa. Trong môi trường có dữ liệu thực tế, xóa bảng có thể làm mất toàn bộ dữ liệu sản phẩm và cần được kiểm soát nghiêm ngặt.

6.3. Chạy migration

Khi sử dụng CLI của golang-migrate, có thể áp dụng các migration chưa chạy bằng lệnh tương tự:

migrate -path migrations \
  -database "postgres://postgres:your_password@localhost:5432/shop_db?sslmode=disable" \
  up

Để hoàn tác một phiên bản migration, công cụ hỗ trợ lệnh down với các tùy chọn tương ứng. Cần kiểm tra tài liệu của phiên bản công cụ đang dùng trước khi thực hiện vì lệnh và cách cài đặt có thể khác nhau.

Không nên đưa mật khẩu thật vào lịch sử shell hoặc mã nguồn. Trong môi trường production, hãy sử dụng biến môi trường hoặc hệ thống quản lý secrets thích hợp.

6.4. Migration và ORM có giống nhau không?

Migration và ORM có liên quan nhưng không phải cùng một khái niệm.

ORM là công cụ giúp chương trình thao tác với dữ liệu database thông qua model và API. Migration là cơ chế quản lý các thay đổi của database theo phiên bản.

Ví dụ, GORM cung cấp AutoMigrate() để tự động thực hiện một số thay đổi schema dựa trên model. Trong khi đó, Bun cung cấp hệ thống migration riêng thông qua package bun/migrate, và có thể kết hợp với các tệp SQL migration.

Với dự án sử dụng Bun, lập trình viên có thể viết SQL migration rõ ràng để kiểm soát những thay đổi cần thực hiện trên PostgreSQL.

Việc sử dụng ORM không đồng nghĩa với việc database sẽ tự động được cập nhật mỗi khi model thay đổi. Điều đó phụ thuộc vào tính năng và cách cấu hình của thư viện.

7. Những lưu ý quan trọng khi sử dụng Migration

7.1. Không chỉnh sửa tùy tiện migration đã chạy

Sau khi một migration đã được áp dụng trên môi trường dùng chung, việc sửa nội dung tệp cũ có thể khiến lịch sử migration giữa các môi trường không còn thống nhất.

Nếu cần thay đổi thêm, thông thường nên tạo một migration mới thay vì chỉnh sửa migration đã được triển khai.

7.2. Cẩn thận với thay đổi phá vỡ tương thích

Thay đổi schema có thể khiến phiên bản ứng dụng cũ không còn hoạt động nếu ứng dụng vẫn đang sử dụng cột hoặc kiểu dữ liệu đã bị thay đổi.

Một chiến lược triển khai an toàn thường chia thành nhiều bước. Ví dụ, khi đổi tên cột:

  1. Thêm cột mới.
  2. Cho ứng dụng ghi hoặc đọc dữ liệu theo cách tương thích với cả hai cột trong giai đoạn chuyển tiếp.
  3. Chuyển đổi dữ liệu hiện có.
  4. Triển khai phiên bản ứng dụng không còn phụ thuộc vào cột cũ.
  5. Xóa cột cũ trong một migration riêng khi đã xác nhận an toàn.

Cách triển khai này thường được gọi là chiến lược mở rộng rồi thu hẹp (expand-and-contract).

7.3. Kiểm tra migration trước khi triển khai

Migration cần được kiểm thử trên dữ liệu và cấu trúc gần với môi trường thực tế. Điều này đặc biệt quan trọng với các bảng lớn, thay đổi kiểu dữ liệu, ràng buộc và chỉ mục.

Ngoài ra, cần xem xét thời gian khóa bảng, khả năng rollback, sao lưu và kế hoạch khôi phục nếu triển khai thất bại.

7.4. Migration không thay thế việc sao lưu

Migration giúp quản lý thay đổi, nhưng không phải là giải pháp sao lưu dữ liệu. Một migration có thể thành công về mặt kỹ thuật nhưng vẫn gây mất dữ liệu nếu được thiết kế sai.

Với các thay đổi quan trọng, cần có phương án backup và recovery phù hợp.

Kết luận

Database Migration là một phần quan trọng trong quá trình phát triển và triển khai ứng dụng. Nó giúp quản lý các thay đổi của schema và dữ liệu theo phiên bản, đồng bộ cấu trúc database giữa các môi trường và hỗ trợ làm việc nhóm.

Tuy nhiên, migration không chỉ đơn giản là chạy một câu lệnh SQL. Mỗi thay đổi đều cần được xem xét về dữ liệu hiện có, khả năng tương thích với ứng dụng, thời gian thực thi và khả năng khôi phục.

Mục tiêu của Database Migration là biến những thay đổi của database thành một quy trình có thể theo dõi, kiểm thử và kiểm soát, thay vì phụ thuộc vào các thao tác sửa đổi thủ công.

Nguồn tham khảo

Bài viết khác

Redis & Caching là gì?

Redis & Caching là gì? Hướng dẫn sử dụng Redis trong Go 1. Caching là gì? Vì sao cần cache khi đã có database? Caching (bộ nhớ đệm) là kỹ thuật lưu tạm dữ liệu thường được truy cập để có thể lấy lại nhanh chóng khi cần, thay vì phải thực hiện lại toàn […]

Severity vs Priority(Độ nghiêm trọng vs Mức độ ưu tiên)

1.Severity và Priority là gì? Phân biệt ra sao? Trong bức tranh toàn cảnh của Vòng đời kiểm thử phần mềm (Software Testing Lifecycle – STLC), việc phát hiện ra lỗi (Bug/Defect) mới chỉ đóng góp 50% vào thành công của quy trình đảm bảo chất lượng. 50% còn lại phụ thuộc hoàn toàn vào […]

Bug Lifecycle & Bug Report

1.Bug Lifecycle & Bug Report là gì? Trong quản lý chất lượng phần mềm, Bug Lifecycle (Vòng đời của lỗi) và Bug Report (Báo cáo lỗi) là hai khái niệm cực kỳ quan trọng giúp kiểm thử viên (Tester) và lập trình viên (Developer) trao đổi, theo dõi và sửa lỗi một cách chuyên nghiệp. […]

Bun ORM là gì?

Bun ORM là gì? Hướng dẫn sử dụng Bun ORM với Go và PostgreSQL 1. Bun ORM là gì? Bun là một thư viện ORM dành cho ngôn ngữ Go, được thiết kế theo hướng SQL-first. Thay vì cố gắng che giấu hoàn toàn SQL phía sau các phương thức trừu tượng, Bun cho phép […]

Positive vs Negative Testing(Kiểm thử tích cực và Kiểm thử tiêu cực)

1.Positive vs Negative Testing là gì? Trong ngành công nghệ phần mềm, chất lượng sản phẩm không bao giờ là một khái niệm đơn chiều. Một ứng dụng được lập trình hoàn hảo trên lý thuyết vẫn có thể sụp đổ hoàn toàn ngay trong ngày đầu tiên phát hành nếu nó chỉ được chuẩn […]

Eloquent ORM

Eloquent ORM là gì? Hướng dẫn sử dụng Eloquent trong Laravel 1. Eloquent ORM là gì? Eloquent ORM là hệ thống Object-Relational Mapping (ORM) được tích hợp trong Laravel, một framework phát triển ứng dụng Web bằng PHP. Eloquent cho phép lập trình viên tương tác với cơ sở dữ liệu thông qua các class […]

Leave a Reply

Your email address will not be published. Required fields are marked *