blockchain/internal/lib/cassandra/crud.go

207 lines
6.9 KiB
Go
Raw Normal View History

2025-08-05 23:41:29 +00:00
package cassandra
import (
"context"
"errors"
"fmt"
"github.com/gocql/gocql"
"github.com/scylladb/gocqlx/v3/qb"
"github.com/scylladb/gocqlx/v3/table"
"reflect"
"time"
)
// Deprecated: is deprecate please use new func Query.InsertOne()
func (db *DB) 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不建議
// Deprecated: is deprecate please use new func Query.Get()
func (db *DB) 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 語句並執行
// Deprecated: is deprecate please use new func Query.Delete()
func (db *DB) 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()
}
// 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
// Deprecated: is deprecate please use new func Query.GetAll()
func (db *DB) 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 *DB) 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)
}
// Update 根據 document 欄位產生 UPDATE 語句並執行
// - 只會更新非零值或非 nil 的欄位(零值欄位會被排除)
// - 主鍵欄位一定會保留,作為 WHERE 條件使用
// Update 根據 document 產生 UPDATE 語句並執行(只更新非零值欄位,保留主鍵)
// Deprecated: is deprecate please use new func Query.Update()
func (db *DB) 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())
}
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
}
// 只要 pointer 不為 nil就 update不管是不是 zero value
if field.Type.Kind() == reflect.Ptr && !val.IsNil() {
setCols = append(setCols, tag)
setVals = append(setVals, val.Elem().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()
}