137 lines
3.5 KiB
Go
137 lines
3.5 KiB
Go
package cassandra
|
||
|
||
import (
|
||
"fmt"
|
||
"reflect"
|
||
"sync"
|
||
"unicode"
|
||
|
||
"github.com/scylladb/gocqlx/v2/table"
|
||
)
|
||
|
||
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)
|
||
if t.Kind() == reflect.Ptr {
|
||
t = t.Elem()
|
||
}
|
||
|
||
// 取得表名稱
|
||
tableName := doc.TableName()
|
||
if tableName == "" {
|
||
return table.Metadata{}, ErrMissingTableName
|
||
}
|
||
|
||
// 構建快取 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
|
||
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:"-" 則跳過
|
||
if tag := field.Tag.Get(DBFiledName); tag == "-" {
|
||
continue
|
||
}
|
||
// 取得欄位名稱
|
||
colName := field.Tag.Get(DBFiledName)
|
||
if colName == "" {
|
||
colName = toSnakeCase(field.Name)
|
||
}
|
||
columns = append(columns, colName)
|
||
// 若有 partition_key:"true" 標記,加入 PartKey
|
||
if field.Tag.Get(Pk) == "true" {
|
||
partKeys = append(partKeys, colName)
|
||
}
|
||
// 若有 clustering_key:"true" 標記,加入 SortKey
|
||
if field.Tag.Get(ClusterKey) == "true" {
|
||
sortKeys = append(sortKeys, colName)
|
||
}
|
||
}
|
||
if len(partKeys) == 0 {
|
||
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)),
|
||
}
|
||
copy(cachedMeta.columns, columns)
|
||
copy(cachedMeta.partKeys, partKeys)
|
||
copy(cachedMeta.sortKeys, sortKeys)
|
||
metadataCache.Store(cacheKey, cachedMeta)
|
||
|
||
// 組合並返回 Metadata(包含 keyspace)
|
||
meta := table.Metadata{
|
||
Name: fmt.Sprintf("%s.%s", keyspace, tableName),
|
||
Columns: columns,
|
||
PartKey: partKeys,
|
||
SortKey: sortKeys,
|
||
}
|
||
|
||
return meta, nil
|
||
}
|
||
|
||
// 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)
|
||
}
|