@vincent119/go-copilot-rules 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,412 @@
1
+ ---
2
+ name: go-database
3
+ description: |
4
+ Go Database Migration 與 ORM 規範:Migration 工具選擇(golang-migrate/goose)、
5
+ 命名慣例、版本控制、CI/CD 整合、最佳實務(pt-online-schema-change, gh-ost)。
6
+
7
+ **適用場景**:設計資料庫遷移策略、實作 Migration、管理 Schema 版本、處理大型表變更、
8
+ 配置 CI/CD Pipeline、避免 AutoMigrate、GORM 使用規範。
9
+
10
+ **關鍵字**:database migration, golang-migrate, goose, schema, version control,
11
+ pt-online-schema-change, gh-ost, CI/CD, migrations, up/down, rollback, GORM, ORM
12
+ ---
13
+
14
+ # Go Database Migration 與 ORM 規範
15
+
16
+ > **相關 Skills**:本規範建議搭配 `go-ddd`(Repository Pattern)
17
+
18
+ ---
19
+
20
+ ## Migration 工具選擇
21
+
22
+ ### 推薦工具
23
+
24
+ 1. **[golang-migrate/migrate](https://github.com/golang-migrate/migrate)** - 最流行、支援多種資料庫
25
+ 2. **[pressly/goose](https://github.com/pressly/goose)** - 支援 Go 與 SQL 混合 Migration
26
+
27
+ **原則**:
28
+ - 選擇後**全專案統一**,禁止混用
29
+ - **禁止**使用 `gorm.AutoMigrate()` 於生產環境(僅限本地開發)
30
+
31
+ ### 安裝
32
+
33
+ ```bash
34
+ # golang-migrate
35
+ go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
36
+
37
+ # goose
38
+ go install github.com/pressly/goose/v3/cmd/goose@latest
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 命名慣例
44
+
45
+ ### 檔案結構
46
+
47
+ ```
48
+ migrations/
49
+ ├── 20260108120000_create_users_table.up.sql
50
+ ├── 20260108120000_create_users_table.down.sql
51
+ ├── 20260108130000_add_email_index.up.sql
52
+ ├── 20260108130000_add_email_index.down.sql
53
+ ├── 20260109100000_alter_orders_add_status.up.sql
54
+ └── 20260109100000_alter_orders_add_status.down.sql
55
+ ```
56
+
57
+ ### 命名格式
58
+
59
+ **格式**:`YYYYMMDDHHMMSS_<description>.<up|down>.sql`
60
+
61
+ **規則**:
62
+ - 時間戳:使用 UTC 時間,確保唯一性與排序
63
+ - 描述:使用**蛇形命名法**(snake_case),簡潔描述變更
64
+ - 必須成對:每個 `.up.sql` 都有對應的 `.down.sql`
65
+
66
+ **範例**:
67
+ ```bash
68
+ # 正確
69
+ 20260108120000_create_users_table.up.sql
70
+ 20260108120000_create_users_table.down.sql
71
+
72
+ # 錯誤
73
+ 2026-01-08-CreateUsers.sql # 格式錯誤
74
+ create_users.up.sql # 缺少時間戳
75
+ 20260108120000_AddUserEmail.up.sql # 應使用 snake_case
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Migration 內容範例
81
+
82
+ ### CREATE TABLE
83
+
84
+ **`20260108120000_create_users_table.up.sql`**:
85
+ ```sql
86
+ CREATE TABLE users (
87
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
88
+ email VARCHAR(255) NOT NULL UNIQUE,
89
+ name VARCHAR(100) NOT NULL,
90
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
91
+ updated_at TIMESTAMP NOT NULL DEFAULT NOW()
92
+ );
93
+
94
+ CREATE INDEX idx_users_email ON users(email);
95
+ ```
96
+
97
+ **`20260108120000_create_users_table.down.sql`**:
98
+ ```sql
99
+ DROP INDEX IF EXISTS idx_users_email;
100
+ DROP TABLE IF EXISTS users;
101
+ ```
102
+
103
+ ### ALTER TABLE
104
+
105
+ **`20260109100000_alter_orders_add_status.up.sql`**:
106
+ ```sql
107
+ -- 新增欄位(帶預設值避免 NULL 問題)
108
+ ALTER TABLE orders
109
+ ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending';
110
+
111
+ -- 新增索引
112
+ CREATE INDEX idx_orders_status ON orders(status);
113
+ ```
114
+
115
+ **`20260109100000_alter_orders_add_status.down.sql`**:
116
+ ```sql
117
+ DROP INDEX IF EXISTS idx_orders_status;
118
+
119
+ ALTER TABLE orders
120
+ DROP COLUMN IF EXISTS status;
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 版本控制
126
+
127
+ ### 規範
128
+
129
+ 1. Migration 檔案**必須**納入 Git
130
+ 2. **禁止**修改已執行的 migration(新增新檔案修正)
131
+ 3. 復原(down)**必須**與 up 對應,確保可回退
132
+ 4. 團隊共享:所有開發者使用相同的 migration 版本
133
+
134
+ ### 驗證 Migration 一致性
135
+
136
+ **錯誤做法**:
137
+ ```sql
138
+ -- ❌ 修改已執行的 migration
139
+ -- 20260108120000_create_users_table.up.sql
140
+ CREATE TABLE users (
141
+ id UUID PRIMARY KEY,
142
+ email VARCHAR(255) NOT NULL,
143
+ -- 後來加上的欄位(不應修改已執行的 migration)
144
+ phone VARCHAR(20) NOT NULL
145
+ );
146
+ ```
147
+
148
+ **正確做法**:
149
+ ```sql
150
+ -- ✅ 新增一個新的 migration
151
+ -- 20260110150000_add_phone_to_users.up.sql
152
+ ALTER TABLE users
153
+ ADD COLUMN phone VARCHAR(20);
154
+ ```
155
+
156
+ ---
157
+
158
+ ## CI/CD 整合
159
+
160
+ ### 執行時機
161
+
162
+ **原則**:
163
+ - Migration 應在**應用啟動前**執行(init container 或 pre-deploy hook)
164
+ - **禁止**在應用程式 `main()` 中執行 migration(避免多副本競爭)
165
+
166
+ ### Kubernetes 範例(Init Container)
167
+
168
+ ```yaml
169
+ # deployment.yaml
170
+ apiVersion: apps/v1
171
+ kind: Deployment
172
+ metadata:
173
+ name: myapp
174
+ spec:
175
+ template:
176
+ spec:
177
+ initContainers:
178
+ - name: db-migration
179
+ image: myapp:latest
180
+ command:
181
+ - /bin/sh
182
+ - -c
183
+ - |
184
+ migrate -path /migrations \
185
+ -database "${DATABASE_URL}" \
186
+ up
187
+ env:
188
+ - name: DATABASE_URL
189
+ valueFrom:
190
+ secretKeyRef:
191
+ name: db-secret
192
+ key: url
193
+ containers:
194
+ - name: app
195
+ image: myapp:latest
196
+ # ...
197
+ ```
198
+
199
+ ### GitHub Actions 範例
200
+
201
+ ```yaml
202
+ # .github/workflows/deploy.yml
203
+ name: Deploy
204
+
205
+ on:
206
+ push:
207
+ branches: [main]
208
+
209
+ jobs:
210
+ migrate:
211
+ runs-on: ubuntu-latest
212
+ steps:
213
+ - uses: actions/checkout@v3
214
+
215
+ - name: Run migrations
216
+ run: |
217
+ migrate -path ./migrations \
218
+ -database "${{ secrets.DATABASE_URL }}" \
219
+ up
220
+
221
+ - name: Deploy application
222
+ run: |
223
+ # ...部署應用
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 最佳實務
229
+
230
+ ### 大型表變更
231
+
232
+ **問題**:ALTER TABLE 在大型表(百萬行級別)會鎖表,影響線上服務。
233
+
234
+ **解決方案**:使用 **pt-online-schema-change** 或 **gh-ost**
235
+
236
+ #### pt-online-schema-change(Percona Toolkit)
237
+
238
+ ```bash
239
+ # 範例:新增欄位到大型表
240
+ pt-online-schema-change \
241
+ --alter "ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending'" \
242
+ --execute \
243
+ D=mydb,t=orders,h=localhost,u=root,p=password
244
+ ```
245
+
246
+ **特性**:
247
+ - 建立影子表(shadow table)
248
+ - 透過 trigger 同步資料
249
+ - 最後瞬間切換表名(最小化鎖表時間)
250
+
251
+ #### gh-ost(GitHub 開發)
252
+
253
+ ```bash
254
+ # 範例
255
+ gh-ost \
256
+ --user="root" \
257
+ --password="password" \
258
+ --host="localhost" \
259
+ --database="mydb" \
260
+ --table="orders" \
261
+ --alter="ADD COLUMN status VARCHAR(20) NOT NULL DEFAULT 'pending'" \
262
+ --execute
263
+ ```
264
+
265
+ **特性**:
266
+ - 無 trigger 設計(透過 binlog 同步)
267
+ - 可即時暫停/繼續
268
+ - 適合 MySQL/MariaDB
269
+
270
+ ### 新增 NOT NULL 欄位
271
+
272
+ **問題**:直接新增 NOT NULL 欄位會失敗(舊資料無值)。
273
+
274
+ **正確步驟**:
275
+ 1. 新增欄位(允許 NULL 或給預設值)
276
+ 2. 回填資料
277
+ 3. 改為 NOT NULL
278
+
279
+ **Migration 範例**:
280
+
281
+ **Step 1**:`20260110100000_add_status_nullable.up.sql`
282
+ ```sql
283
+ ALTER TABLE orders
284
+ ADD COLUMN status VARCHAR(20) DEFAULT 'pending';
285
+ ```
286
+
287
+ **Step 2**:`20260110110000_backfill_status.up.sql`
288
+ ```sql
289
+ -- 回填舊資料
290
+ UPDATE orders
291
+ SET status = 'pending'
292
+ WHERE status IS NULL;
293
+ ```
294
+
295
+ **Step 3**:`20260110120000_make_status_not_null.up.sql`
296
+ ```sql
297
+ ALTER TABLE orders
298
+ ALTER COLUMN status SET NOT NULL;
299
+ ```
300
+
301
+ ### Migration 測試
302
+
303
+ **本地測試流程**:
304
+ ```bash
305
+ # 1. 執行 up
306
+ migrate -path ./migrations -database "postgres://localhost/testdb" up
307
+
308
+ # 2. 驗證 Schema
309
+ psql testdb -c "\d orders"
310
+
311
+ # 3. 執行 down(測試回退)
312
+ migrate -path ./migrations -database "postgres://localhost/testdb" down 1
313
+
314
+ # 4. 再次執行 up(確保可重複執行)
315
+ migrate -path ./migrations -database "postgres://localhost/testdb" up
316
+ ```
317
+
318
+ ---
319
+
320
+ ## GORM 使用規範
321
+
322
+ ### 禁止 AutoMigrate 於生產環境
323
+
324
+ ```go
325
+ // ❌ 禁止:生產環境使用 AutoMigrate
326
+ func main() {
327
+ db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})
328
+ db.AutoMigrate(&User{}, &Order{}) // 不可預測、缺少版本控制
329
+ }
330
+
331
+ // ✅ 正確:僅在本地開發使用
332
+ func setupTestDB(t *testing.T) *gorm.DB {
333
+ db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
334
+ db.AutoMigrate(&User{}, &Order{}) // 測試環境可用
335
+ return db
336
+ }
337
+ ```
338
+
339
+ ### Repository 層隔離 GORM 實作
340
+
341
+ **分離 Domain Entity 與 GORM Model**:
342
+
343
+ ```go
344
+ // internal/order/domain/order.go (Domain Entity)
345
+ package domain
346
+
347
+ type Order struct {
348
+ id string
349
+ customerID string
350
+ amount float64
351
+ }
352
+
353
+ // internal/order/infra/order_model.go (GORM Model)
354
+ package infra
355
+
356
+ type OrderModel struct {
357
+ ID string `gorm:"type:uuid;primaryKey"`
358
+ CustomerID string `gorm:"type:uuid;not null"`
359
+ Amount float64 `gorm:"type:decimal(10,2);not null"`
360
+ CreatedAt time.Time
361
+ UpdatedAt time.Time
362
+ }
363
+
364
+ func (OrderModel) TableName() string {
365
+ return "orders"
366
+ }
367
+
368
+ // 轉換函式
369
+ func toDomainOrder(m *OrderModel) *domain.Order {
370
+ return &domain.Order{
371
+ id: m.ID,
372
+ customerID: m.CustomerID,
373
+ amount: m.Amount,
374
+ }
375
+ }
376
+
377
+ func toGORMModel(o *domain.Order) *OrderModel {
378
+ return &OrderModel{
379
+ ID: o.ID(),
380
+ CustomerID: o.CustomerID(),
381
+ Amount: o.Amount(),
382
+ }
383
+ }
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 檢查清單
389
+
390
+ **Migration 檔案**
391
+ - [ ] 使用統一的 Migration 工具(golang-migrate 或 goose)
392
+ - [ ] 命名遵循 `YYYYMMDDHHMMSS_description.up/down.sql`
393
+ - [ ] 每個 `.up.sql` 都有對應的 `.down.sql`
394
+ - [ ] 描述使用 snake_case
395
+ - [ ] Migration 檔案納入 Git
396
+
397
+ **版本控制**
398
+ - [ ] 不修改已執行的 migration
399
+ - [ ] 新需求使用新 migration 修正
400
+ - [ ] down migration 能正確回退
401
+
402
+ **CI/CD**
403
+ - [ ] Migration 在應用啟動前執行(init container)
404
+ - [ ] 不在應用程式 `main()` 中執行 migration
405
+ - [ ] CI 階段驗證 migration 可執行
406
+
407
+ **最佳實務**
408
+ - [ ] 大型表使用 pt-online-schema-change 或 gh-ost
409
+ - [ ] 新增 NOT NULL 欄位分步驟執行(先 nullable → 回填 → not null)
410
+ - [ ] 本地測試 up/down 流程
411
+ - [ ] 生產環境禁用 `gorm.AutoMigrate()`
412
+ - [ ] Domain Entity 與 GORM Model 分離