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 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-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
|
|
|
|
|