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:
- Xác định thay đổi cần thực hiện đối với database.
- Tạo migration mô tả thay đổi đó.
- Kiểm tra nội dung migration và tác động đến dữ liệu hiện có.
- Chạy migration trên môi trường phù hợp.
- Ghi nhận phiên bản đã áp dụng.
- 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:
- Thêm cột mới.
- 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.
- Chuyển đổi dữ liệu hiện có.
- Triển khai phiên bản ứng dụng không còn phụ thuộc vào cột cũ.
- 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.








Khoá học lập trình game con rắn cho trẻ em




