blockchain/internal/lib/cassandra/crud.go

183 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cassandra
import (
"context"
"errors"
"fmt"
"reflect"
"time"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx/v3/qb"
"github.com/scylladb/gocqlx/v3/table"
)
var ErrNotFound = fmt.Errorf("not found")
// Insert 依據 document 自動產生 INSERT 語句並執行
func (db *CassandraDB) Insert(ctx context.Context, document any, keyspace string) error {
metadata, err := GenerateTableMetadata(document, keyspace)
if err != nil {
return err
}
t := table.New(metadata)
q := db.GetSession().Query(t.Insert()).BindStruct(document).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3)
err = q.ExecRelease()
return err
}
// Get 根據 struct 的 Primary Key 查詢單筆資料Get ByPK
// - filter 為目標資料 struct其欄位需對應表格的 Primary Key 欄位Partition Key + Clustering Key
// - Cassandra 中 Primary Key 是由 Partition Key 與 Clustering Key 組成的整體,作為唯一識別一筆資料的 key
// - Cassandra 並不保證 Partition Key 或 Clustering Key 單獨具有唯一性,只有整個 Primary Key 才是唯一
// - Partition Key 的作用是將資料分布到不同節點NodeClustering Key 則是節點內排序資料用
// - 如果僅提供 Partition Key會查到分區內的多筆資料但由於 .Get() 預設加 LIMIT 1僅會取得其中一筆排序第一
// - 若想查詢特定欄位(如 name但該欄位不是 Primary Key 組成部分,則無法使用 .Get() 查詢,也無法用該欄位直接篩選資料(會報錯)
// - 解法是1. 改變 table 結構使欲查欄位成為 PK或 2. 建立額外 table 以該欄位為 Partition Key或 3. 使用 ALLOW FILTERING不建議
func (db *CassandraDB) Get(ctx context.Context, dest any, keyspace string) error {
metadata, err := GenerateTableMetadata(dest, keyspace)
if err != nil {
return err
}
t := table.New(metadata)
q := db.GetSession().Query(t.Get()).BindStruct(dest).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3)
err = q.GetRelease(dest)
if errors.Is(err, gocql.ErrNotFound) {
return ErrNotFound
} else if err != nil {
return err
}
return nil
}
// Delete 依據 document 的主鍵產生 DELETE 語句並執行
func (db *CassandraDB) Delete(ctx context.Context, filter any, keyspace string) error {
metadata, err := GenerateTableMetadata(filter, keyspace)
if err != nil {
return err
}
t := table.New(metadata)
stmt, names := t.Delete()
q := db.GetSession().Query(stmt, names).BindStruct(filter).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3)
return q.ExecRelease()
}
// Update 根據 document 欄位產生 UPDATE 語句並執行
// - 只會更新非零值或非 nil 的欄位(零值欄位會被排除)
// - 主鍵欄位一定會保留,作為 WHERE 條件使用
// Update 根據 document 產生 UPDATE 語句並執行(只更新非零值欄位,保留主鍵)
func (db *CassandraDB) Update(ctx context.Context, document any, keyspace string) error {
metadata, err := GenerateTableMetadata(document, keyspace)
if err != nil {
return err
}
v := reflect.ValueOf(document)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
typ := v.Type()
// 收集更新欄位與其值(排除零值,保留主鍵)
setCols := make([]string, 0)
setVals := make([]any, 0)
whereCols := make([]string, 0)
whereVals := make([]any, 0)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("db")
if tag == "" || tag == "-" {
continue
}
val := v.Field(i)
if !val.IsValid() {
continue
}
if contains(metadata.PartKey, tag) || contains(metadata.SortKey, tag) {
whereCols = append(whereCols, tag)
whereVals = append(whereVals, val.Interface())
continue
}
if isZero(val) {
continue
}
setCols = append(setCols, tag)
setVals = append(setVals, val.Interface())
}
if len(setCols) == 0 {
return fmt.Errorf("no non-zero update fields provided")
}
// Build UPDATE statement
builder := qb.Update(metadata.Name).Set(setCols...)
for _, col := range whereCols {
builder = builder.Where(qb.Eq(col))
}
stmt, names := builder.ToCql()
args := append(setVals, whereVals...)
q := db.GetSession().Query(stmt, names).Bind(args...).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3)
return q.ExecRelease()
}
// TODO: Cassandra 不支援 OFFSET 方式的分頁(例如查詢第 N 頁)
// 原因Cassandra 是分散式資料庫,設計上不允許像傳統 SQL 那樣用 OFFSET 跳頁,會導致效能極差
// ✅ 正確方式為使用 PagingState 做游標式Cursor-based分頁一頁一頁往後翻
// ✅ 如果需要快取第 N 頁位置,應在應用層儲存每一頁的 PagingState 以供跳轉
// ❌ Cassandra 不適合直接實作全站排行榜或全表分頁查詢,除非搭配 ElasticSearch 或針對 Partition Key 分頁設計
// 若未來有特定分區(如 user_id條件可考慮實作分區內的分頁邏輯以提高效能
// GetAll 取得指定 struct 類型在 Cassandra 中的所有資料
// - structInstance用來推斷 table 結構的範例物件(可為指標)
// - result要寫入的 slice 指標,如 *[]MyStruct
func (db *CassandraDB) GetAll(ctx context.Context, filter any, result any, keyspace string) error {
metadata, err := GenerateTableMetadata(filter, keyspace)
if err != nil {
return err
}
t := table.New(metadata)
stmt, names := qb.Select(t.Name()).Columns(metadata.Columns...).ToCql()
q := db.GetSession().Query(stmt, names).WithContext(ctx).WithTimestamp(time.Now().UnixNano() / 1e3)
return q.SelectRelease(result)
}
// QueryBuilder executes a query with optional conditions on Cassandra table
func (db *CassandraDB) QueryBuilder(
ctx context.Context,
tableStruct any,
result any,
keyspace string,
opts ...QueryOption,
) error {
metadata, err := GenerateTableMetadata(tableStruct, keyspace)
if err != nil {
return err
}
tbl := table.New(metadata)
builder := qb.Select(tbl.Name()).Columns(metadata.Columns...)
bindMap := qb.M{}
for _, opt := range opts {
opt(builder, bindMap)
}
stmt, names := builder.ToCql()
query := db.GetSession().Query(stmt, names).WithContext(ctx).BindMap(bindMap).WithTimestamp(time.Now().UnixNano() / 1e3)
return query.SelectRelease(result)
}