438 lines
13 KiB
Markdown
438 lines
13 KiB
Markdown
# 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()` 等輔助函數來檢查特定錯誤類型。
|
||
|
||
--- |