2025-11-17 09:31:58 +00:00
|
|
|
|
package cassandra
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"reflect"
|
2025-11-18 09:45:38 +00:00
|
|
|
|
"sync"
|
|
|
|
|
|
"unicode"
|
2025-11-17 09:31:58 +00:00
|
|
|
|
|
2025-11-19 05:33:06 +00:00
|
|
|
|
"github.com/scylladb/gocqlx/v2/table"
|
2025-11-17 09:31:58 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-11-18 09:45:38 +00:00
|
|
|
|
var (
|
|
|
|
|
|
// metadataCache 快取已生成的 Metadata,避免重複反射解析
|
|
|
|
|
|
// key: tableName + ":" + structType (不包含 keyspace,因為同一個 struct 在不同 keyspace 結構相同)
|
|
|
|
|
|
metadataCache sync.Map
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
type cachedMetadata struct {
|
|
|
|
|
|
columns []string
|
|
|
|
|
|
partKeys []string
|
|
|
|
|
|
sortKeys []string
|
|
|
|
|
|
err error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// generateMetadata 根據傳入的 struct 產生 table.Metadata
|
|
|
|
|
|
// 使用快取機制避免重複反射解析,提升效能
|
|
|
|
|
|
func generateMetadata[T Table](doc T, keyspace string) (table.Metadata, error) {
|
|
|
|
|
|
// 取得型別資訊
|
|
|
|
|
|
t := reflect.TypeOf(doc)
|
2025-11-17 09:31:58 +00:00
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 09:45:38 +00:00
|
|
|
|
// 取得表名稱
|
|
|
|
|
|
tableName := doc.TableName()
|
|
|
|
|
|
if tableName == "" {
|
2025-11-17 09:31:58 +00:00
|
|
|
|
return table.Metadata{}, ErrMissingTableName
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-18 09:45:38 +00:00
|
|
|
|
// 構建快取 key: tableName:structType (不包含 keyspace)
|
|
|
|
|
|
cacheKey := fmt.Sprintf("%s:%s", tableName, t.String())
|
|
|
|
|
|
|
|
|
|
|
|
// 檢查快取
|
|
|
|
|
|
if cached, ok := metadataCache.Load(cacheKey); ok {
|
|
|
|
|
|
cachedMeta := cached.(cachedMetadata)
|
|
|
|
|
|
if cachedMeta.err != nil {
|
|
|
|
|
|
return table.Metadata{}, cachedMeta.err
|
|
|
|
|
|
}
|
|
|
|
|
|
// 從快取構建 metadata,動態加上 keyspace
|
|
|
|
|
|
meta := table.Metadata{
|
|
|
|
|
|
Name: fmt.Sprintf("%s.%s", keyspace, tableName),
|
|
|
|
|
|
Columns: make([]string, len(cachedMeta.columns)),
|
|
|
|
|
|
PartKey: make([]string, len(cachedMeta.partKeys)),
|
|
|
|
|
|
SortKey: make([]string, len(cachedMeta.sortKeys)),
|
|
|
|
|
|
}
|
|
|
|
|
|
copy(meta.Columns, cachedMeta.columns)
|
|
|
|
|
|
copy(meta.PartKey, cachedMeta.partKeys)
|
|
|
|
|
|
copy(meta.SortKey, cachedMeta.sortKeys)
|
|
|
|
|
|
return meta, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 快取未命中,生成 metadata
|
2025-11-17 09:31:58 +00:00
|
|
|
|
columns := make([]string, 0, t.NumField())
|
|
|
|
|
|
partKeys := make([]string, 0, t.NumField())
|
|
|
|
|
|
sortKeys := make([]string, 0, t.NumField())
|
|
|
|
|
|
|
|
|
|
|
|
// 遍歷所有 exported 欄位
|
|
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
|
|
field := t.Field(i)
|
|
|
|
|
|
// 跳過 unexported 欄位
|
|
|
|
|
|
if field.PkgPath != "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// 如果欄位有標記 db:"-" 則跳過
|
2025-11-19 05:33:06 +00:00
|
|
|
|
if tag := field.Tag.Get(DBFiledName); tag == "-" {
|
2025-11-17 09:31:58 +00:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// 取得欄位名稱
|
2025-11-19 05:33:06 +00:00
|
|
|
|
colName := field.Tag.Get(DBFiledName)
|
2025-11-17 09:31:58 +00:00
|
|
|
|
if colName == "" {
|
|
|
|
|
|
colName = toSnakeCase(field.Name)
|
|
|
|
|
|
}
|
|
|
|
|
|
columns = append(columns, colName)
|
2025-11-18 09:45:38 +00:00
|
|
|
|
// 若有 partition_key:"true" 標記,加入 PartKey
|
2025-11-19 05:33:06 +00:00
|
|
|
|
if field.Tag.Get(Pk) == "true" {
|
2025-11-17 09:31:58 +00:00
|
|
|
|
partKeys = append(partKeys, colName)
|
|
|
|
|
|
}
|
2025-11-18 09:45:38 +00:00
|
|
|
|
// 若有 clustering_key:"true" 標記,加入 SortKey
|
2025-11-19 05:33:06 +00:00
|
|
|
|
if field.Tag.Get(ClusterKey) == "true" {
|
2025-11-17 09:31:58 +00:00
|
|
|
|
sortKeys = append(sortKeys, colName)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(partKeys) == 0 {
|
2025-11-18 09:45:38 +00:00
|
|
|
|
err := ErrNoPartitionKey
|
|
|
|
|
|
// 快取錯誤結果
|
|
|
|
|
|
metadataCache.Store(cacheKey, cachedMetadata{err: err})
|
|
|
|
|
|
return table.Metadata{}, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 快取成功結果(只存結構資訊,不包含 keyspace)
|
|
|
|
|
|
cachedMeta := cachedMetadata{
|
|
|
|
|
|
columns: make([]string, len(columns)),
|
|
|
|
|
|
partKeys: make([]string, len(partKeys)),
|
|
|
|
|
|
sortKeys: make([]string, len(sortKeys)),
|
2025-11-17 09:31:58 +00:00
|
|
|
|
}
|
2025-11-18 09:45:38 +00:00
|
|
|
|
copy(cachedMeta.columns, columns)
|
|
|
|
|
|
copy(cachedMeta.partKeys, partKeys)
|
|
|
|
|
|
copy(cachedMeta.sortKeys, sortKeys)
|
|
|
|
|
|
metadataCache.Store(cacheKey, cachedMeta)
|
2025-11-17 09:31:58 +00:00
|
|
|
|
|
2025-11-18 09:45:38 +00:00
|
|
|
|
// 組合並返回 Metadata(包含 keyspace)
|
2025-11-17 09:31:58 +00:00
|
|
|
|
meta := table.Metadata{
|
2025-11-18 09:45:38 +00:00
|
|
|
|
Name: fmt.Sprintf("%s.%s", keyspace, tableName),
|
2025-11-17 09:31:58 +00:00
|
|
|
|
Columns: columns,
|
|
|
|
|
|
PartKey: partKeys,
|
|
|
|
|
|
SortKey: sortKeys,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return meta, nil
|
|
|
|
|
|
}
|
2025-11-18 09:45:38 +00:00
|
|
|
|
|
|
|
|
|
|
// toSnakeCase 將 CamelCase 字串轉換為 snake_case
|
|
|
|
|
|
func toSnakeCase(s string) string {
|
|
|
|
|
|
var result []rune
|
|
|
|
|
|
for i, r := range s {
|
|
|
|
|
|
if unicode.IsUpper(r) {
|
|
|
|
|
|
if i > 0 {
|
|
|
|
|
|
result = append(result, '_')
|
|
|
|
|
|
}
|
|
|
|
|
|
result = append(result, unicode.ToLower(r))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result = append(result, r)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return string(result)
|
|
|
|
|
|
}
|