package cassandra import ( "context" "reflect" "github.com/gocql/gocql" "github.com/scylladb/gocqlx/v3/qb" "github.com/scylladb/gocqlx/v3/table" ) var qh = &queryHelper{} // Insert 依據 document 自動產生 INSERT 語句並執行 // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) Insert(ctx context.Context, document any, keyspace string) error { keyspace = getKeyspace(db, keyspace) metadata, err := GenerateTableMetadata(document, keyspace) if err != nil { return err } t := table.New(metadata) q := qh.withContextAndTimestamp(ctx, db.GetSession().Query(t.Insert()).BindStruct(document)) return q.ExecRelease() } // 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 的作用是將資料分布到不同節點(Node),Clustering Key 則是節點內排序資料用 // - 如果僅提供 Partition Key,會查到分區內的多筆資料,但由於 .Get() 預設加 LIMIT 1,僅會取得其中一筆(排序第一) // - 若想查詢特定欄位(如 name)但該欄位不是 Primary Key 組成部分,則無法使用 .Get() 查詢,也無法用該欄位直接篩選資料(會報錯) // - 解法是:1. 改變 table 結構使欲查欄位成為 PK,或 2. 建立額外 table 以該欄位為 Partition Key,或 3. 使用 ALLOW FILTERING(不建議) // Get 根據 struct 的 Primary Key 查詢單筆資料(Get ByPK) // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) Get(ctx context.Context, dest any, keyspace string) error { keyspace = getKeyspace(db, keyspace) metadata, err := GenerateTableMetadata(dest, keyspace) if err != nil { return err } t := table.New(metadata) q := qh.withContextAndTimestamp(ctx, db.GetSession().Query(t.Get()).BindStruct(dest)) err = q.GetRelease(dest) if err == gocql.ErrNotFound { return ErrNotFound.WithTable(metadata.Name) } else if err != nil { return ErrInvalidInput.WithTable(metadata.Name).WithError(err) } return nil } // Delete 依據 document 的主鍵產生 DELETE 語句並執行 // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) Delete(ctx context.Context, filter any, keyspace string) error { keyspace = getKeyspace(db, keyspace) metadata, err := GenerateTableMetadata(filter, keyspace) if err != nil { return err } t := table.New(metadata) stmt, names := t.Delete() q := qh.withContextAndTimestamp(ctx, db.GetSession().Query(stmt, names).BindStruct(filter)) return q.ExecRelease() } // Update 根據 document 欄位產生 UPDATE 語句並執行 // - 只會更新非零值或非 nil 的欄位(零值欄位會被排除) // - 主鍵欄位一定會保留,作為 WHERE 條件使用 // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) Update(ctx context.Context, document any, keyspace string) error { return db.UpdateSelective(ctx, document, keyspace, false) } // UpdateSelective 根據 document 欄位產生 UPDATE 語句並執行 // - includeZero: false 時只更新非零值欄位(等同於 Update),true 時更新所有欄位(包括零值) // - 主鍵欄位一定會保留,作為 WHERE 條件使用 // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) UpdateSelective(ctx context.Context, document any, keyspace string, includeZero bool) error { keyspace = getKeyspace(db, keyspace) 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() // 收集更新欄位與其值(根據 includeZero 決定是否包含零值,保留主鍵) 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 !includeZero && isZero(val) { continue } setCols = append(setCols, tag) setVals = append(setVals, val.Interface()) } if len(setCols) == 0 { return ErrNoFieldsToUpdate.WithTable(metadata.Name) } // Build UPDATE statement builder := qb.Update(metadata.Name).Set(setCols...) for _, col := range whereCols { builder = builder.Where(qb.Eq(col)) } stmt, names := builder.ToCql() setVals = append(setVals, whereVals...) q := qh.withContextAndTimestamp(ctx, db.GetSession().Query(stmt, names).Bind(setVals...)) return q.ExecRelease() } // UpdateAll 更新所有欄位(包括零值) // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) UpdateAll(ctx context.Context, document any, keyspace string) error { return db.UpdateSelective(ctx, document, keyspace, true) } // TODO: Cassandra 不支援 OFFSET 方式的分頁(例如查詢第 N 頁) // 原因:Cassandra 是分散式資料庫,設計上不允許像傳統 SQL 那樣用 OFFSET 跳頁,會導致效能極差 // ✅ 正確方式為使用 PagingState 做游標式(Cursor-based)分頁,一頁一頁往後翻 // ✅ 如果需要快取第 N 頁位置,應在應用層儲存每一頁的 PagingState 以供跳轉 // ❌ Cassandra 不適合直接實作全站排行榜或全表分頁查詢,除非搭配 ElasticSearch 或針對 Partition Key 分頁設計 // 若未來有特定分區(如 user_id)條件,可考慮實作分區內的分頁邏輯以提高效能 // GetAll 取得指定 struct 類型在 Cassandra 中的所有資料 // - filter:用來推斷 table 結構的範例物件(可為指標) // - result:要寫入的 slice 指標,如 *[]MyStruct // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) GetAll(ctx context.Context, filter any, result any, keyspace string) error { keyspace = getKeyspace(db, keyspace) 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 := qh.withContextAndTimestamp(ctx, db.GetSession().Query(stmt, names)) return q.SelectRelease(result) } // QueryBuilder executes a query with optional conditions on Cassandra table // keyspace 如果為空,則使用初始化時設定的預設 keyspace func (db *CassandraDB) QueryBuilder( ctx context.Context, tableStruct any, result any, keyspace string, opts ...QueryOption, ) error { keyspace = getKeyspace(db, keyspace) 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 := qh.withContextAndTimestamp(ctx, db.GetSession().Query(stmt, names).BindMap(bindMap)) return query.SelectRelease(result) }