backend/pkg/library/cassandra/README.md

759 lines
16 KiB
Markdown
Raw Normal View History

2025-11-19 05:33:06 +00:00
# Cassandra Client Library
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
一個基於 Go Generics 的 Cassandra 客戶端庫,提供類型安全的 Repository 模式和流暢的查詢構建器 API。
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
## 功能特色
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
- **類型安全**: 使用 Go Generics 提供編譯時類型檢查
- **Repository 模式**: 簡潔的 CRUD 操作介面
- **流暢查詢**: 鏈式查詢構建器,支援條件、排序、限制
- **分散式鎖**: 基於 Cassandra 的 IF NOT EXISTS 實現分散式鎖
- **批次操作**: 支援批次插入、更新、刪除
- **SAI 索引支援**: 完整的 SAI (Storage-Attached Indexing) 索引管理功能
- **Option 模式**: 靈活的配置選項
- **錯誤處理**: 統一的錯誤處理機制
- **高效能**: 內建連接池、重試機制、Prepared Statement 快取
## 安裝
```bash
go get github.com/scylladb/gocqlx/v2
go get github.com/gocql/gocql
```
2025-11-18 09:45:38 +00:00
## 快速開始
2025-11-19 05:33:06 +00:00
### 1. 定義資料模型
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
package main
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
import (
"time"
"github.com/gocql/gocql"
"backend/pkg/library/cassandra"
2025-11-18 09:45:38 +00:00
)
2025-11-19 05:33:06 +00:00
// User 定義用戶資料模型
type User struct {
ID gocql.UUID `db:"id" partition_key:"true"`
Name string `db:"name"`
Email string `db:"email"`
Age int `db:"age"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
// TableName 實現 Table 介面
func (u User) TableName() string {
return "users"
}
```
### 2. 初始化資料庫連接
```go
package main
import (
"context"
"fmt"
"log"
"backend/pkg/library/cassandra"
"github.com/gocql/gocql"
)
func main() {
// 創建資料庫連接
db, err := cassandra.New(
cassandra.WithHosts("127.0.0.1"),
cassandra.WithPort(9042),
cassandra.WithKeyspace("my_keyspace"),
cassandra.WithAuth("username", "password"),
cassandra.WithConsistency(gocql.Quorum),
)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 創建 Repository
userRepo, err := cassandra.NewRepository[User](db, "my_keyspace")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// 使用 Repository...
_ = userRepo
}
```
## 詳細範例
### CRUD 操作
#### 插入資料
```go
// 插入單筆資料
user := User{
ID: gocql.TimeUUID(),
Name: "Alice",
Email: "alice@example.com",
Age: 30,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
err := userRepo.Insert(ctx, user)
if err != nil {
log.Printf("插入失敗: %v", err)
}
// 批次插入
users := []User{
{ID: gocql.TimeUUID(), Name: "Bob", Email: "bob@example.com"},
{ID: gocql.TimeUUID(), Name: "Charlie", Email: "charlie@example.com"},
}
err = userRepo.InsertMany(ctx, users)
2025-11-18 09:45:38 +00:00
if err != nil {
2025-11-19 05:33:06 +00:00
log.Printf("批次插入失敗: %v", err)
2025-11-18 09:45:38 +00:00
}
```
2025-11-19 05:33:06 +00:00
#### 查詢資料
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 根據主鍵查詢
userID := gocql.TimeUUID()
user, err := userRepo.Get(ctx, userID)
if err != nil {
if cassandra.IsNotFound(err) {
log.Println("用戶不存在")
} else {
log.Printf("查詢失敗: %v", err)
}
return
2025-11-18 09:45:38 +00:00
}
2025-11-19 05:33:06 +00:00
fmt.Printf("用戶: %+v\n", user)
```
#### 更新資料
```go
// 更新資料(只更新非零值欄位)
user.Name = "Alice Updated"
user.Email = "alice.updated@example.com"
err = userRepo.Update(ctx, user)
if err != nil {
log.Printf("更新失敗: %v", err)
}
// 更新所有欄位(包括零值)
user.Age = 0 // 零值也會被更新
err = userRepo.UpdateAll(ctx, user)
if err != nil {
log.Printf("更新失敗: %v", err)
2025-11-18 09:45:38 +00:00
}
```
2025-11-19 05:33:06 +00:00
#### 刪除資料
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 刪除資料
err = userRepo.Delete(ctx, userID)
if err != nil {
log.Printf("刪除失敗: %v", err)
}
```
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
### 查詢構建器
#### 基本查詢
```go
// 查詢所有符合條件的記錄
var users []User
err := userRepo.Query().
Where(cassandra.Eq("age", 30)).
OrderBy("created_at", cassandra.DESC).
Limit(10).
Scan(ctx, &users)
if err != nil {
log.Printf("查詢失敗: %v", err)
2025-11-18 09:45:38 +00:00
}
2025-11-19 05:33:06 +00:00
// 查詢單筆記錄
user, err := userRepo.Query().
Where(cassandra.Eq("email", "alice@example.com")).
One(ctx)
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
if err != nil {
if cassandra.IsNotFound(err) {
log.Println("用戶不存在")
} else {
log.Printf("查詢失敗: %v", err)
}
}
```
#### 條件查詢
```go
// 等於條件
userRepo.Query().Where(cassandra.Eq("name", "Alice"))
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
// IN 條件
userRepo.Query().Where(cassandra.In("id", []any{id1, id2, id3}))
// 大於條件
userRepo.Query().Where(cassandra.Gt("age", 18))
// 小於條件
userRepo.Query().Where(cassandra.Lt("age", 65))
// 組合多個條件
userRepo.Query().
Where(cassandra.Eq("status", "active")).
Where(cassandra.Gt("age", 18))
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
#### 排序和限制
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 按建立時間降序排列,限制 20 筆
2025-11-18 09:45:38 +00:00
var users []User
2025-11-19 05:33:06 +00:00
err := userRepo.Query().
OrderBy("created_at", cassandra.DESC).
Limit(20).
Scan(ctx, &users)
// 多欄位排序
err = userRepo.Query().
OrderBy("status", cassandra.ASC).
OrderBy("created_at", cassandra.DESC).
Scan(ctx, &users)
```
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
#### 選擇特定欄位
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
```go
// 只查詢特定欄位
var users []User
err := userRepo.Query().
Select("id", "name", "email").
Where(cassandra.Eq("status", "active")).
Scan(ctx, &users)
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
#### 計數查詢
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 計算符合條件的記錄數
count, err := userRepo.Query().
Where(cassandra.Eq("status", "active")).
Count(ctx)
if err != nil {
log.Printf("計數失敗: %v", err)
} else {
fmt.Printf("活躍用戶數: %d\n", count)
}
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
### 分散式鎖
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 獲取鎖(預設 30 秒 TTL
lockUser := User{ID: userID}
err := userRepo.TryLock(ctx, lockUser)
if err != nil {
if cassandra.IsLockFailed(err) {
log.Println("獲取鎖失敗,資源已被鎖定")
} else {
log.Printf("鎖操作失敗: %v", err)
}
return
2025-11-18 09:45:38 +00:00
}
2025-11-19 05:33:06 +00:00
// 執行需要鎖定的操作
defer func() {
// 釋放鎖
if err := userRepo.UnLock(ctx, lockUser); err != nil {
log.Printf("釋放鎖失敗: %v", err)
}
}()
// 執行業務邏輯...
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
#### 自訂鎖 TTL
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 設定鎖的 TTL 為 60 秒
err := userRepo.TryLock(ctx, lockUser, cassandra.WithLockTTL(60*time.Second))
// 永不自動解鎖
err := userRepo.TryLock(ctx, lockUser, cassandra.WithNoLockExpire())
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
### 複雜主鍵
#### 複合主鍵Partition Key + Clustering Key
2025-11-18 09:45:38 +00:00
```go
2025-11-19 05:33:06 +00:00
// 定義複合主鍵模型
type Order struct {
UserID gocql.UUID `db:"user_id" partition_key:"true"`
OrderID gocql.UUID `db:"order_id" clustering_key:"true"`
ProductID string `db:"product_id"`
Quantity int `db:"quantity"`
Price float64 `db:"price"`
CreatedAt time.Time `db:"created_at"`
}
func (o Order) TableName() string {
return "orders"
}
// 查詢時需要提供完整的主鍵
order, err := orderRepo.Get(ctx, Order{
UserID: userID,
OrderID: orderID,
})
2025-11-18 09:45:38 +00:00
```
2025-11-19 05:33:06 +00:00
#### 多欄位 Partition Key
```go
type Message struct {
ChatID gocql.UUID `db:"chat_id" partition_key:"true"`
MessageID gocql.UUID `db:"message_id" clustering_key:"true"`
UserID gocql.UUID `db:"user_id" partition_key:"true"`
Content string `db:"content"`
CreatedAt time.Time `db:"created_at"`
}
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
func (m Message) TableName() string {
return "messages"
}
// 查詢時需要提供所有 Partition Key
message, err := messageRepo.Get(ctx, Message{
ChatID: chatID,
UserID: userID,
MessageID: messageID,
})
```
## 配置選項
### 連接選項
```go
db, err := cassandra.New(
// 主機列表
cassandra.WithHosts("127.0.0.1", "127.0.0.2", "127.0.0.3"),
// 連接埠
cassandra.WithPort(9042),
// Keyspace
cassandra.WithKeyspace("my_keyspace"),
// 認證
cassandra.WithAuth("username", "password"),
// 一致性級別
cassandra.WithConsistency(gocql.Quorum),
// 連接超時
cassandra.WithConnectTimeout(10 * time.Second),
// 每個節點的連接數
cassandra.WithNumConns(10),
// 重試次數
cassandra.WithMaxRetries(3),
// 重試間隔
cassandra.WithRetryInterval(100*time.Millisecond, 1*time.Second),
// 重連間隔
cassandra.WithReconnectInterval(1*time.Second, 10*time.Second),
// CQL 版本
cassandra.WithCQLVersion("3.0.0"),
)
```
## 錯誤處理
### 錯誤類型
```go
// 檢查是否為特定錯誤
if cassandra.IsNotFound(err) {
// 記錄不存在
}
if cassandra.IsConflict(err) {
// 衝突錯誤(如唯一鍵衝突)
}
if cassandra.IsLockFailed(err) {
// 獲取鎖失敗
}
// 使用 errors.As 獲取詳細錯誤資訊
var cassandraErr *cassandra.Error
if errors.As(err, &cassandraErr) {
fmt.Printf("錯誤代碼: %s\n", cassandraErr.Code)
fmt.Printf("錯誤訊息: %s\n", cassandraErr.Message)
fmt.Printf("資料表: %s\n", cassandraErr.Table)
}
```
### 錯誤代碼
- `NOT_FOUND`: 記錄未找到
- `CONFLICT`: 衝突(如唯一鍵衝突、鎖獲取失敗)
- `INVALID_INPUT`: 輸入參數無效
- `MISSING_PARTITION_KEY`: 缺少 Partition Key
- `NO_FIELDS_TO_UPDATE`: 沒有欄位需要更新
- `MISSING_TABLE_NAME`: 缺少 TableName 方法
- `MISSING_WHERE_CONDITION`: 缺少 WHERE 條件
## 最佳實踐
### 1. 使用 Context
```go
// 所有操作都應該傳入 context以便支援超時和取消
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := userRepo.Get(ctx, userID)
```
### 2. 錯誤處理
```go
user, err := userRepo.Get(ctx, userID)
if err != nil {
if cassandra.IsNotFound(err) {
// 處理不存在的情況
return nil, ErrUserNotFound
}
// 處理其他錯誤
return nil, fmt.Errorf("查詢用戶失敗: %w", err)
}
```
### 3. 批次操作
```go
// 對於大量資料,使用批次插入
const batchSize = 100
for i := 0; i < len(users); i += batchSize {
end := i + batchSize
if end > len(users) {
end = len(users)
}
err := userRepo.InsertMany(ctx, users[i:end])
if err != nil {
log.Printf("批次插入失敗 (索引 %d-%d): %v", i, end, err)
}
}
```
### 4. 使用分散式鎖
```go
// 在需要保證原子性的操作中使用鎖
err := userRepo.TryLock(ctx, lockUser, cassandra.WithLockTTL(30*time.Second))
if err != nil {
return fmt.Errorf("獲取鎖失敗: %w", err)
}
defer userRepo.UnLock(ctx, lockUser)
// 執行需要原子性的操作
```
### 5. 查詢優化
```go
// 只選擇需要的欄位
var users []User
err := userRepo.Query().
Select("id", "name", "email"). // 只選擇需要的欄位
Where(cassandra.Eq("status", "active")).
Scan(ctx, &users)
// 使用適當的限制
err = userRepo.Query().
Where(cassandra.Eq("status", "active")).
Limit(100). // 限制結果數量
Scan(ctx, &users)
```
2025-11-18 09:45:38 +00:00
2025-11-19 09:06:44 +00:00
## SAI 索引管理
### 建立 SAI 索引
```go
// 檢查是否支援 SAI
if !db.SaiSupported() {
log.Fatal("SAI is not supported in this Cassandra version")
}
// 建立標準索引
err := db.CreateSAIIndex(ctx, "my_keyspace", "users", "email", "users_email_idx", nil)
if err != nil {
log.Printf("建立索引失敗: %v", err)
}
// 建立全文索引(不區分大小寫)
opts := &cassandra.SAIIndexOptions{
IndexType: cassandra.SAIIndexTypeFullText,
IsAsync: false,
CaseSensitive: false,
}
err = db.CreateSAIIndex(ctx, "my_keyspace", "posts", "content", "posts_content_ft_idx", opts)
```
### 查詢 SAI 索引
```go
// 列出資料表的所有 SAI 索引
indexes, err := db.ListSAIIndexes(ctx, "my_keyspace", "users")
if err != nil {
log.Printf("查詢索引失敗: %v", err)
} else {
for _, idx := range indexes {
fmt.Printf("索引: %s, 欄位: %s, 類型: %s\n", idx.Name, idx.Column, idx.Type)
}
}
// 檢查索引是否存在
exists, err := db.CheckSAIIndexExists(ctx, "my_keyspace", "users_email_idx")
if err != nil {
log.Printf("檢查索引失敗: %v", err)
} else if exists {
fmt.Println("索引存在")
}
```
### 刪除 SAI 索引
```go
// 刪除索引
err := db.DropSAIIndex(ctx, "my_keyspace", "users_email_idx")
if err != nil {
log.Printf("刪除索引失敗: %v", err)
}
```
### SAI 索引類型
- **SAIIndexTypeStandard**: 標準索引(等於查詢)
- **SAIIndexTypeCollection**: 集合索引(用於 list、set、map
- **SAIIndexTypeFullText**: 全文索引
### SAI 索引選項
```go
opts := &cassandra.SAIIndexOptions{
IndexType: cassandra.SAIIndexTypeFullText, // 索引類型
IsAsync: false, // 是否異步建立
CaseSensitive: true, // 是否區分大小寫
}
```
2025-11-18 09:45:38 +00:00
## 注意事項
2025-11-19 05:33:06 +00:00
### 1. 主鍵要求
- `Get``Delete` 操作必須提供完整的主鍵(所有 Partition Key 和 Clustering Key
- 單一主鍵值只適用於單一 Partition Key 且無 Clustering Key 的情況
### 2. 更新操作
- `Update` 只更新非零值欄位
- `UpdateAll` 更新所有欄位(包括零值)
- 更新操作必須包含主鍵欄位
### 3. 查詢限制
- Cassandra 的查詢必須包含所有 Partition Key
- 排序只能按 Clustering Key 進行
- 不支援 JOIN 操作
### 4. 分散式鎖
- 鎖使用 IF NOT EXISTS 實現,預設 30 秒 TTL
- 獲取鎖失敗時會返回 `CONFLICT` 錯誤
- 釋放鎖時會自動重試,最多 3 次
### 5. 批次操作
- 批次操作有大小限制(建議不超過 1000 筆)
- 批次操作中的所有操作必須屬於同一個 Partition Key
### 6. SAI 索引
- SAI 索引需要 Cassandra 4.0.9+ 版本(建議 5.0+
- 建立索引前請先檢查 `db.SaiSupported()`
- 索引建立是異步操作,可能需要一些時間
- 刪除索引時使用 `IF EXISTS`,避免索引不存在時報錯
2025-11-19 09:06:44 +00:00
- 使用 SAI 索引可以大幅提升非主鍵欄位的查詢效能
- 全文索引支援不區分大小寫的搜尋
2025-11-19 05:33:06 +00:00
## 完整範例
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
```go
package main
import (
"context"
"fmt"
"log"
"time"
"backend/pkg/library/cassandra"
"github.com/gocql/gocql"
)
type User struct {
ID gocql.UUID `db:"id" partition_key:"true"`
Name string `db:"name"`
Email string `db:"email"`
Age int `db:"age"`
Status string `db:"status"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
func (u User) TableName() string {
return "users"
}
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
func main() {
// 初始化資料庫連接
db, err := cassandra.New(
cassandra.WithHosts("127.0.0.1"),
cassandra.WithPort(9042),
cassandra.WithKeyspace("my_keyspace"),
)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 創建 Repository
userRepo, err := cassandra.NewRepository[User](db, "my_keyspace")
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// 插入用戶
user := User{
ID: gocql.TimeUUID(),
Name: "Alice",
Email: "alice@example.com",
Age: 30,
Status: "active",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := userRepo.Insert(ctx, user); err != nil {
log.Printf("插入失敗: %v", err)
return
}
// 查詢用戶
foundUser, err := userRepo.Get(ctx, user.ID)
if err != nil {
log.Printf("查詢失敗: %v", err)
return
}
fmt.Printf("查詢到的用戶: %+v\n", foundUser)
// 更新用戶
user.Name = "Alice Updated"
user.Email = "alice.updated@example.com"
if err := userRepo.Update(ctx, user); err != nil {
log.Printf("更新失敗: %v", err)
return
}
// 查詢活躍用戶
var activeUsers []User
if err := userRepo.Query().
Where(cassandra.Eq("status", "active")).
OrderBy("created_at", cassandra.DESC).
Limit(10).
Scan(ctx, &activeUsers); err != nil {
log.Printf("查詢失敗: %v", err)
return
}
fmt.Printf("活躍用戶數: %d\n", len(activeUsers))
// 使用分散式鎖
if err := userRepo.TryLock(ctx, user, cassandra.WithLockTTL(30*time.Second)); err != nil {
if cassandra.IsLockFailed(err) {
log.Println("獲取鎖失敗")
} else {
log.Printf("鎖操作失敗: %v", err)
}
return
}
defer userRepo.UnLock(ctx, user)
// 執行需要鎖定的操作
fmt.Println("執行需要鎖定的操作...")
// 刪除用戶
if err := userRepo.Delete(ctx, user.ID); err != nil {
log.Printf("刪除失敗: %v", err)
return
}
fmt.Println("操作完成")
}
```
## 測試
套件包含完整的測試覆蓋,包括:
- 單元測試table-driven tests
- 集成測試(使用 testcontainers
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
運行測試:
```bash
go test ./pkg/library/cassandra/...
```
查看測試覆蓋率:
```bash
go test ./pkg/library/cassandra/... -cover
```
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
## 授權
2025-11-18 09:45:38 +00:00
2025-11-19 05:33:06 +00:00
本專案遵循專案的主要授權協議。
2025-11-18 09:45:38 +00:00