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