Cài đặt golang & viết, run database migration trong golang

EminelEminel
9 min read

Golang (Go) ngày càng trở nên phổ biến trong phát triển ứng dụng backend nhờ hiệu suất cao, cú pháp đơn giản và hệ sinh thái phong phú. Một trong những tác vụ quan trọng khi xây dựng ứng dụng là quản lý database migration – giúp kiểm soát phiên bản của cơ sở dữ liệu một cách linh hoạt và an toàn.

Trong bài viết này, chúng ta sẽ tìm hiểu cách cài đặt Golang, thiết lập môi trường lập trình, và hướng dẫn cách viết, chạy database migration trong Golang. Bài viết sẽ cung cấp những hướng dẫn thực tế để bạn có thể áp dụng ngay vào dự án của mình.

Install Golang

Để cài đặt golang, chúng ta truy cập vào trang chủ của golang để chọn phiên bản cần cài đặt. Trong bài viết này tôi sử dụng phiên bản 1.23.7 linux/amd64 trên ubuntu 20. Trong series này tôi chỉ hướng dẫn làm việc với ubuntu nhé.

Tiếp theo tải version golang bằng curl.

curl -OL https://golang.org/dl/go1.23.7.linux-amd64.tar.gz

Để xác minh tính toàn vẹn của tệp bạn đã tải xuống, hãy chạy lệnh SHA256SUM để check.

sha256sum go1.23.7.linux-amd64.tar.gz
// Kết quả
6924efde5de86fe277676e929dc9917d466efa02fb934197bc2eba35d5680971  go1.23.7.linux-amd64.tar.gz

Giải nén và chỉ định thư mục giải nén

sudo tar -C /usr/local -xvf go1.23.7.linux-amd64.tar.gz

Mặc dù /usr/local/go là vị trí được khuyến nghị để cài đặt Go, các bạn cũng có thể sử dụng đường dẫn khác cũng được.

Tiếp theo, thiết lập biến môi trường cho Go bằng cách chỉnh sửa tệp .profile trong thư mục home của người dùng.

sudo nano ~/.profile

Sau đó, thêm thông tin sau vào cuối tệp của bạn:

// content .profile
. . .
export PATH=$PATH:/usr/local/go/bin

Tiếp theo, hãy refresh .profile của bằng cách chạy lệnh sau:

source ~/.profile

Kiêm tra phiên bản của golang.

go version

Kết quả như sau:

Ngoài ra chúng ta có thể dùng gvm để cài đặt và quản lý version Golang. Đây là link tham khảo cách cài đặt của gvm nhé: https://github.com/moovweb/gvm

Install golang-migrate

golang-migrate là một thư viện và công cụ dòng lệnh (CLI) dùng để quản lý database migrations trong các dự án sử dụng Go. Nó giúp bạn tạo, áp dụng, và hoàn tác (rollback) các thay đổi trong cơ sở dữ liệu một cách có kiểm soát.

Các tính năng chính:

  • Hỗ trợ nhiều database drivers như PostgreSQL, MySQL, SQLite, MongoDB...

  • Hỗ trợ up (nâng cấp) và down (hạ cấp) migrations.

  • Có thể sử dụng dưới dạng CLI hoặc nhúng vào code Go với package migrate.

  • Hỗ trợ tải migration từ nhiều nguồn khác nhau (tệp cục bộ, Amazon S3, GitHub...).

Đây là tài liệu tích hợp, tôi sử dụng ubuntu nên sẽ sử dụng các lệnh sau

curl -L https://packagecloud.io/golang-migrate/migrate/gpgkey | apt-key add -
echo "deb https://packagecloud.io/golang-migrate/migrate/ubuntu/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/migrate.list
apt-get update
apt-get install -y migrate

Create a new migration

Bây giờ tôi sẽ tạo một thư mục mới cho dự án simple_bank . Và bên trong, tôi sẽ tạo một thư mục db/migration để lưu trữ tất cả các file migration.

mkdir simple_bank
cd simple_bank
mkdir -p db/migration

Sau đó, hãy tạo file migration đầu tiên để khởi tạo lược đồ cơ sở dữ liệu của simple_bank.

migrate create -ext sql -dir db/migration -seq init_schema

Lệnh trên được sử dụng để tạo một migration mới trong thư mục db/migration. Hãy phân tích từng phần của lệnh:

  • migrate create: Lệnh dùng để tạo file migration mới.

  • -ext sql: Chỉ định phần mở rộng của file là .sql (tức là migration sẽ dùng SQL thuần).

  • -dir db/migration: Chỉ định thư mục lưu migration là db/migration.

  • -seq: Tạo tên file migration với số thứ tự tuần tự (ví dụ: 000001_init_schema.up.sql000001_init_schema.down.sql).

  • init_schema: Tên của migration, giúp xác định mục đích của nó (ở đây là khởi tạo schema ban đầu).

Up/down migration

Như các bạn có thể thấy, 2 file migration đã được tạo ra.

Nói đơn giản, đây là một best practice khi viết migration cho database.

  • Up script: Dùng để thay đổi schema theo hướng tiến (ví dụ: tạo bảng, thêm cột...).

  • Down script: Dùng để hoàn tác thay đổi của up script (ví dụ: xóa bảng, xóa cột...).

Khi chạy lệnh migrate up, các file up script trong thư mục db/migration sẽ được thực thi tuần tự theo thứ tự phiên bản được đặt ở phần tiền tố của tên file.

Ngược lại, khi chạy lệnh migrate down, các file down script trong thư mục db/migration sẽ được thực thi tuần tự nhưng theo thứ tự ngược lại của phần tiền tố phiên bản.

Bây giờ, hãy mở file simple_bank.sql mà chúng ta đã tạo trước đó. Tôi sẽ sao chép toàn bộ nội dung của file này và dán vào file init_schema.up.sql.

CREATE TABLE "accounts" (
  "id" bigserial PRIMARY KEY,
  "owner" varchar NOT NULL,
  "balance" bigint NOT NULL,
  "currency" varchar NOT NULL,
  "created_at" timestamp NOT NULL DEFAULT (now())
);

CREATE TABLE "entries" (
  "id" bigserial PRIMARY KEY,
  "account_id" bigint NOT NULL,
  "amount" bigint NOT NULL,
  "created_at" timestamp NOT NULL DEFAULT (now())
);

CREATE TABLE "transfers" (
  "id" bigserial PRIMARY KEY,
  "from_account_id" bigint NOT NULL,
  "to_account_id" bigint NOT NULL,
  "amount" bigint NOT NULL,
  "created_at" timestamp DEFAULT (now())
);

CREATE INDEX ON "accounts" ("owner");

CREATE INDEX ON "entries" ("account_id");

CREATE INDEX ON "transfers" ("from_account_id");

CREATE INDEX ON "transfers" ("to_account_id");

CREATE INDEX ON "transfers" ("from_account_id", "to_account_id");

COMMENT ON COLUMN "entries"."amount" IS 'can be negative or positive';

COMMENT ON COLUMN "transfers"."amount" IS 'must be positive';

ALTER TABLE "entries" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id");

ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id") REFERENCES "accounts" ("id");

ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id") REFERENCES "accounts" ("id");

Đối với file init_schema.down.sql, chúng ta cần hoàn tác các thay đổi do up script thực hiện. Trong trường hợp này, up script đã tạo 3 bảng: accounts, transfersentries, vì vậy down script sẽ xóa tất cả bằng câu lệnh DROP TABLE.

DROP TABLE IF EXISTS entries;
DROP TABLE IF EXISTS transfers;
DROP TABLE IF EXISTS accounts;

Ở đây, chúng ta xóa bảng entriestransfers trước khi xóa bảng accounts vì hai bảng này có ràng buộc foreign key tham chiếu đến accounts.

OK, bây giờ các script migration đã sẵn sàng. Hãy thử chạy chúng bằng cách đọc các phần sau từ từ và bình tĩnh nhé.

Truy cập vào postgres container

Sử dụng lệnh sau để truy cập vào container.

docker run --name postgres16 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=Abc@12345678 -p 54322:5432 -d postgres:16-alpine

Sau khi start được server database thì chúng ta có thể tiến hành truy cập vào container

docker exec -it postgres16 bin/bash
// or
docker exec -it postgres16 bin/sh

Vì đây là một container PostgreSQL, nó cũng cung cấp một số lệnh CLI để tương tác trực tiếp với máy chủ PostgreSQL từ shell.

  1. Tạo/ Xóa database bên trong postgres container

psql -U root

Để tạo mới hoặc xóa database chúng ta sử dụng lệnh sau

create database simple_bank;
drop database simple_bank;

  1. Tạo/ Xóa database bên ngoài postgres container

Bây giờ, từ bên ngoài container, chúng ta cũng có thể chạy trực tiếp lệnh tạo/xóa database bằng lệnh docker exec

docker exec -it postgres16 psql -U root -c "CREATE DATABASE simple_bank;"
docker exec -it postgres16 psql -U root -c "DROP DATABASE simple_bank;"

Và tất nhiên cũng có thể truy cập vào database bằng lệnh docker exec

 docker exec -it postgres16 psql -U root simple_bank

Viết Makefile

1️⃣ Makefile là gì?

  • Makefile là một tập tin chứa các lệnh để tự động hóa các tác vụ trong dự án, thường được sử dụng với lệnh make.

  • Giúp đơn giản hóa quá trình thiết lập và chạy dự án bằng cách gom nhóm các lệnh dài thành các lệnh ngắn gọn hơn.

2️⃣ Cách tạo và sử dụng Makefile

Tạo một file Makefile trong thư mục dự án và thêm các lệnh sau:

DB_NAME=simple_bank
DB_USER=postgres
DB_PASSWORD=Abc@12345678
DB_CONTAINER=postgres16
CONTAINER_DB_PORT=5432
HOST_PORT=54322


postgres:
    docker run --name $(DB_CONTAINER) -e POSTGRES_USER=$(DB_USER) -e POSTGRES_PASSWORD=$(DB_PASSWORD) -p $(HOST_PORT):$(CONTAINER_DB_PORT) -d postgres:16-alpine


createdb:
    docker exec -it $(DB_CONTAINER) bash -c "until pg_isready -U $(DB_USER); do sleep 1; done && psql -U $(DB_USER) -c 'CREATE DATABASE $(DB_NAME);'"


dropdb:
    docker exec -it $(DB_CONTAINER) psql -U $(DB_USER) -c "DROP DATABASE $(DB_NAME);"

.PHONY: postgres createdb dropdb
  • Khởi chạy PostgreSQL: Dùng Docker để chạy container postgres16 với user, password, và cổng được cấu hình.

  • Tạo database: Kiểm tra PostgreSQL sẵn sàng rồi tạo database simple_bank.

  • Xóa database: Xóa database simple_bank trong container.

  • .PHONY: Đảm bảo các target luôn chạy, không bị nhầm với file cùng tên.

Tiến hành dừng các container đang chạy và test hoạt động của Makefile

docker stop postgres16
docker container prune

Vậy giờ tất cả các container đã dừng và được xóa, hãy thử chạy file make nhé.

make postgres
make createdb

Sử dụng gui để kiểm tra xem database mới đã được tạo chưa

Okay, có vể mọi thứ đã hoạt động rồi chúng ta cùng nhau qua bước tiếp theo là viết các lệnh migrate vào Makefile

migrateup:
    migrate -path db/migration -database "postgresql://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_PORT)/$(DB_NAME)?sslmode=disable" -verbose up
  • migrate: Lệnh chạy công cụ migrate.

  • -path db/migration: Chỉ định thư mục chứa các file migration (SQL hoặc Go script)

  • -database "postgresql://$(DB_USER):$(DB_PASSWORD)@localhost:$(DB_PORT)/$(DB_NAME)?sslmode=disable": Kết nối đến database PostgreSQL, vì chạy trên local nên tôi sẽ sslmode=disable

  • -verbose: Hiển thị log chi tiết.

  • up: Áp dụng tất cả các migration mới chưa được thực thi.

Đây là Makefile sau khi thêm các câu lệnh migration updown

DB_NAME=simple_bank
DB_USER=postgres
DB_PASSWORD=Abc@12345678
DB_CONTAINER=postgres16
CONTAINER_DB_PORT=5432
HOST_PORT=54322


postgres:
    docker run --name $(DB_CONTAINER) -e POSTGRES_USER=$(DB_USER) -e POSTGRES_PASSWORD=$(DB_PASSWORD) -p $(HOST_PORT):$(CONTAINER_DB_PORT) -d postgres:16-alpine


createdb:
    docker exec -it $(DB_CONTAINER) bash -c "until pg_isready -U $(DB_USER); do sleep 1; done && psql -U $(DB_USER) -c 'CREATE DATABASE $(DB_NAME);'"


dropdb:
    docker exec -it $(DB_CONTAINER) psql -U $(DB_USER) -c "DROP DATABASE $(DB_NAME);"


migrateup:
    migrate -path db/migration -database "postgresql://$(DB_USER):$(DB_PASSWORD)@localhost:$(HOST_PORT)/$(DB_NAME)?sslmode=disable" -verbose up

migratedown:
    migrate -path db/migration -database "postgresql://$(DB_USER):$(DB_PASSWORD)@localhost:$(HOST_PORT)/$(DB_NAME)?sslmode=disable" -verbose down


.PHONY: postgres createdb dropdb migrateup migratedown

Okay, cùng nhau test nhé, bắt đầu thử chạy lệnh make migratedown, sử dụng dbeaver để kiểm tra, tất cả các bảng đã biến mất ngoại trừ bảng schema_migrations

Tiếp tục thử lại với lệnh make migrateup

Okay, có vẻ mọi thứ hoạt động ổn định, bài viết hôm nay về migration database xin kết thúc tại đây. Cám ơn các bạn đã đọc và hẹn gặp lại trong các bài viết lần sau.

Tài liệu tham khảo: https://dev.to/techschoolguru/how-to-write-run-database-migration-in-golang-5h6g

0
Subscribe to my newsletter

Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Eminel
Eminel

Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.