# Cassandra Database Client for Go with Advanced CRUD Operations and Transaction Support 一套功能完備的 Go 語言 Apache Cassandra 客戶端,支援進階 CRUD 操作、Batch 交易、分散式鎖機制、SAI (Storage-Attached Indexing) 索引與 Fluent API 鏈式查詢介面,讓你用最簡潔的程式碼玩轉 Cassandra! ## 特色 * Go struct 自動生成 Table Metadata * 批次操作與原子性交易支援(含 rollback) * 內建分散式鎖 (基於唯一索引) * 支援 SAI 二級索引 * 類 GORM 流暢式(Fluent API)查詢體驗 * 單筆/多筆操作自動處理 * 完善的連線管理與組態選項 ## 專案結構 ``` . ├── batch.go # Batch 批次操作/交易 ├── client.go # Cassandra 連線管理主體 ├── crud.go # 基本 CRUD 操作 ├── ez_transaction.go # 支援 rollback 的交易系統 ├── lock.go # 分散式鎖實作 ├── metadata.go # 由 struct 產生 Table metadata ├── option.go # 組態與查詢選項 ├── table.go # Table 操作、查詢組合 ├── utils.go # 工具函式 └── tests/ # 全面測試 ``` ## 安裝方式 ```bash go get gitlab.supermicro.com/infra/infra-core/storage/cassandra ``` ## 快速開始 ### 1. 初始化 Client ```go import "gitlab.supermicro.com/infra/infra-core/storage/cassandra" // 基本初始化(使用預設 keyspace) client, err := cassandra.NewCassandraDB( []string{"localhost"}, cassandra.WithPort(9042), cassandra.WithKeyspace("my_keyspace"), cassandra.WithAuth("username", "password"), // 可選 ) if err != nil { log.Fatal(err) } defer client.Close() // 使用預設 keyspace 時,後續操作可以省略 keyspace 參數 // 如果傳入空字串 "",會自動使用初始化時設定的預設 keyspace ``` ### 2. 定義資料模型 ```go type User struct { ID gocql.UUID `db:"id" partition_key:"true"` Name string `db:"name" clustering_key:"true" sai:"true"` Email string `db:"email"` CreatedAt time.Time `db:"created_at"` } func (u *User) TableName() string { return "users" } ``` ### 3. 基本 CRUD 操作 ```go // 新增(keyspace 為空時使用預設 keyspace) user := &User{ ID: gocql.TimeUUID(), Name: "John Doe", Email: "john@example.com", CreatedAt: time.Now(), } err = client.Insert(ctx, user, "") // 使用預設 keyspace // 或明確指定 keyspace err = client.Insert(ctx, user, "my_keyspace") // 查詢 result := &User{ID: user.ID} err = client.Get(ctx, result, "") if cassandra.IsNotFound(err) { // 處理記錄不存在的情況 log.Println("User not found") } // 更新(只更新非零值欄位) result.Email = "newemail@example.com" err = client.Update(ctx, result, "") // 更新所有欄位(包括零值) result.Email = "" err = client.UpdateAll(ctx, result, "") // 選擇性更新(可控制是否包含零值) err = client.UpdateSelective(ctx, result, "", false) // false = 排除零值 // 刪除 err = client.Delete(ctx, result, "") ``` ### 4. 進階:Batch 與補償式交易操作 ```go // Batch 操作(原子性批次操作) // Batch 是 Cassandra 原生的批次操作,保證原子性 batch := client.NewBatch(ctx, "") // 使用預設 keyspace batch.Insert(user1) batch.Insert(user2) batch.Update(user3) err := batch.Commit() // 補償式交易(Compensating Transaction) // 注意:這不是真正的 ACID 交易,而是基於補償操作的模式 // 適用於最終一致性場景,可以確保「要嘛全成功,要嘛全失敗」 tx := cassandra.NewCompensatingTransaction(ctx, "", client) // 或使用向後相容的別名 // tx := cassandra.NewEZTransaction(ctx, "", client) tx.Insert(user1) tx.Update(user2) if err := tx.Commit(); err != nil { // 如果 Commit 失敗,執行 Rollback 進行補償操作 if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Printf("Rollback failed: %v", rollbackErr) } return err } ``` **Batch vs CompensatingTransaction 的區別:** - **Batch**: Cassandra 原生的原子性批次操作,所有操作要嘛全部成功,要嘛全部失敗。但無法跨表操作,且不支援條件操作。 - **CompensatingTransaction**: 基於補償操作的交易模式,可以跨表操作,支援複雜的業務邏輯。透過記錄操作日誌,在失敗時執行補償操作來實現「要嘛全成功,要嘛全失敗」的語義。 ### 5. 錯誤處理 ```go import "gitlab.supermicro.com/infra/infra-core/storage/cassandra" // 統一的錯誤處理 result := &User{ID: userID} err := client.Get(ctx, result, "") if err != nil { // 檢查特定錯誤類型 if cassandra.IsNotFound(err) { // 處理記錄不存在 log.Println("User not found") } else if cassandra.IsLockFailed(err) { // 處理獲取鎖失敗 log.Println("Failed to acquire lock") } else { // 處理其他錯誤 log.Printf("Error: %v", err) } } // 錯誤類型包含詳細資訊 var cassandraErr *cassandra.Error if errors.As(err, &cassandraErr) { log.Printf("Error Code: %s", cassandraErr.Code) log.Printf("Error Message: %s", cassandraErr.Message) log.Printf("Table: %s", cassandraErr.Table) if cassandraErr.Err != nil { log.Printf("Underlying Error: %v", cassandraErr.Err) } } ``` ### 6. IN 操作 ```go // 使用 QueryBuilder 進行 IN 查詢 where := []qb.Cmp{qb.In("id")} args := map[string]any{"id": uuids} var result []User err := client.QueryBuilder( ctx, &User{}, &result, "", // 使用預設 keyspace cassandra.WithWhere(where, args), ) ``` --- ## Fluent API 鏈式查詢 (GORM 風格) 支援類 GORM 直覺式鏈式呼叫查詢方式,快速進行 CRUD、條件過濾、排序、分頁、單筆查詢、更新、刪除等操作: ```go type TestUser struct { ID gocql.UUID `db:"id" partition_key:"true"` Name string `db:"name" sai:"true"` Age int64 `db:"age"` } func (TestUser) TableName() string { return "test_user" } // 新增單筆 user := TestUser{ID: gocql.TimeUUID(), Name: "Alice", Age: 20} err := db.Model(ctx, TestUser{}, keyspace).InsertOne(user) // 批量新增 users := []TestUser{{...}, {...}} err := db.Model(ctx, TestUser{}, keyspace).InsertMany(users) // 查詢所有 var got []TestUser err := db.Model(ctx, TestUser{}, keyspace).GetAll(&got) // 查詢某些欄位 var got []TestUser err := db.Model(ctx, TestUser{}, ""). // 使用預設 keyspace Select("name").GetAll(&got) // 條件查詢 + 排序 + 分頁 var result []TestUser err := db.Model(ctx, TestUser{}, ""). Where(qb.Eq("name"), map[string]any{"name": "Alice"}). OrderBy("age", qb.DESC). Limit(10). Scan(&result) // IN 操作 var result []TestUser err := db.Model(ctx, TestUser{}, ""). Where(qb.In("name"), map[string]any{"name": []string{"Alice", "Bob"}}). Scan(&result) // 單筆查詢 var user TestUser err := db.Model(ctx, TestUser{}, ""). Where(qb.Eq("id"), map[string]any{"id": userID}). Take(&user) // 更新欄位(必須提供 partition_key 或 sai indexed 欄位在 WHERE 中) err := db.Model(ctx, TestUser{}, ""). Where(qb.Eq("id"), map[string]any{"id": userID}). Set("age", 30). Update() // 刪除(必須提供所有 partition keys) err := db.Model(ctx, TestUser{}, ""). Where(qb.Eq("id"), map[string]any{"id": userID}). Delete() // 計數 count, err := db.Model(ctx, TestUser{}, ""). Where(qb.Eq("name"), map[string]any{"name": "Alice"}). Count() ``` ### 常用查詢語法總結 | 操作 | 用法範例 | | ---- | --------------------------------------------- | | 條件查詢 | .Where(qb.Eq("欄位"), map\[string]any{"欄位": 值}) | | 指定欄位 | .Select("id", "name") | | 排序 | .OrderBy("age", qb.DESC) | | 分頁 | .Limit(10) | | 查單筆 | .Take(\&result) | | 更新欄位 | .Set("age", 25).Update() | | 刪除 | .Delete() | | 計數 | .Count() | --- ## 完整 API 參考 ### 初始化選項 ```go // 連線選項 cassandra.WithPort(port int) cassandra.WithKeyspace(keyspace string) cassandra.WithAuth(username, password string) cassandra.WithConsistency(consistency gocql.Consistency) cassandra.WithConnectTimeoutSec(timeout int) cassandra.WithNumConns(numConns int) cassandra.WithMaxRetries(maxRetries int) cassandra.WithRetryMinInterval(duration time.Duration) cassandra.WithRetryMaxInterval(duration time.Duration) cassandra.WithReconnectInitialInterval(duration time.Duration) cassandra.WithReconnectMaxInterval(duration time.Duration) cassandra.WithCQLVersion(version string) ``` ### 基本 CRUD 方法 ```go // 插入 func (db *CassandraDB) Insert(ctx context.Context, document any, keyspace string) error // 查詢(根據 Primary Key) func (db *CassandraDB) Get(ctx context.Context, dest any, keyspace string) error // 更新(只更新非零值欄位) func (db *CassandraDB) Update(ctx context.Context, document any, keyspace string) error // 選擇性更新(可控制是否包含零值) func (db *CassandraDB) UpdateSelective(ctx context.Context, document any, keyspace string, includeZero bool) error // 更新所有欄位(包括零值) func (db *CassandraDB) UpdateAll(ctx context.Context, document any, keyspace string) error // 刪除 func (db *CassandraDB) Delete(ctx context.Context, filter any, keyspace string) error // 查詢所有 func (db *CassandraDB) GetAll(ctx context.Context, filter any, result any, keyspace string) error // 查詢構建器 func (db *CassandraDB) QueryBuilder(ctx context.Context, tableStruct any, result any, keyspace string, opts ...QueryOption) error ``` ### Fluent API 方法 ```go // 創建查詢構建器 func (db *CassandraDB) Model(ctx context.Context, document any, keyspace string) *Query // Query 方法 func (q *Query) Where(cmp qb.Cmp, args map[string]any) *Query func (q *Query) Select(cols ...string) *Query func (q *Query) OrderBy(column string, order qb.Order) *Query func (q *Query) Limit(limit uint) *Query func (q *Query) Set(col string, val any) *Query func (q *Query) Scan(dest any) error func (q *Query) Take(dest any) error func (q *Query) GetAll(dest any) error func (q *Query) Count() (int64, error) func (q *Query) InsertOne(data any) error func (q *Query) InsertMany(documents any) error func (q *Query) Update() error func (q *Query) Delete() error ``` ### Batch 操作 ```go // 創建 Batch func (db *CassandraDB) NewBatch(ctx context.Context, keyspace string) *Batch // Batch 方法 func (tx *Batch) Insert(doc any) error func (tx *Batch) Delete(doc any) error func (tx *Batch) Update(doc any) error func (tx *Batch) Commit() error ``` ### 補償式交易 ```go // 創建補償式交易 func NewCompensatingTransaction(ctx context.Context, keyspace string, db *CassandraDB) CompensatingTransaction // 向後相容的別名(已棄用) func NewEZTransaction(ctx context.Context, keyspace string, db *CassandraDB) CompensatingTransaction // Transaction 方法 func (tx CompensatingTransaction) Insert(ctx context.Context, document any) error func (tx CompensatingTransaction) Delete(ctx context.Context, filter any) error func (tx CompensatingTransaction) Update(ctx context.Context, document any) error func (tx CompensatingTransaction) Commit() error func (tx CompensatingTransaction) Rollback() error ``` ### 分散式鎖 ```go // 嘗試獲取鎖 func (db *CassandraDB) TryLock(ctx context.Context, document any, keyspace string, opts ...LockOption) error // 釋放鎖 func (db *CassandraDB) UnLock(ctx context.Context, filter any, keyspace string) error // 鎖選項 func WithLockTTL(d time.Duration) LockOption func WithNoLockExpire() LockOption ``` ### 錯誤處理 ```go // 錯誤類型 type Error struct { Code string Message string Table string Err error } // 預定義錯誤 var ErrNotFound var ErrAcquireLockFailed var ErrInvalidInput var ErrNoPartitionKey var ErrMissingTableName var ErrNoFieldsToUpdate var ErrMissingWhereCondition var ErrMissingPartitionKey // 錯誤檢查函數 func IsNotFound(err error) bool func IsLockFailed(err error) bool ``` ## 注意事項 1. **Keyspace 處理**: 如果方法參數中的 `keyspace` 為空字串 `""`,會自動使用初始化時設定的預設 keyspace。 2. **WHERE 條件限制**: Cassandra 的 WHERE 條件只能使用: - Partition Key 欄位 - 有 SAI 索引的欄位 - Clustering Key 欄位(在 Partition Key 之後) 3. **Update 方法**: - `Update()`: 只更新非零值欄位 - `UpdateAll()`: 更新所有欄位(包括零值) - `UpdateSelective()`: 可控制是否包含零值 4. **補償式交易**: 這不是真正的 ACID 交易,而是基於補償操作的模式,適用於最終一致性場景。 5. **錯誤處理**: 建議使用 `IsNotFound()` 和 `IsLockFailed()` 等輔助函數來檢查特定錯誤類型。 ---