func (c *CacheableDB) CreateCacheKey(structPtr Cacheable, fields ...string) (CacheKey, reflect.Value, error) {
	var t = reflect.TypeOf(structPtr)
	var typeName = t.String()
	if c.typeName != typeName {
		return emptyCacheKey, emptyValue, fmt.Errorf("CreateCacheKey(): unmatch Cacheable: want %s, have %s", c.typeName, typeName)
	}
	var (
		v        = reflect.ValueOf(structPtr).Elem()
		values   = make([]interface{}, 0, 2)
		cacheKey string
		isPriKey bool
	)
	if len(fields) == 0 {
		for _, idx := range c.priFieldsIndex {
			vv := v.Field(idx)
			values = append(values, vv.Interface())
		}
		bs, err := json.Marshal(values)
		if err != nil {
			return emptyCacheKey, emptyValue, errors.New("CreateCacheKey(): " + err.Error())
		}
		isPriKey = true
		cacheKey = c.cachePriKeyPrefix + goutil.BytesToString(bs)
	} else {
		for i, field := range fields {
			fields[i] = goutil.SnakeString(field)
			vv := v.FieldByName(goutil.CamelString(field))
			values = append(values, vv.Interface())
		}
		var err error
		cacheKey, err = c.CreateCacheKeyByFields(fields, values)
		if err != nil {
			return emptyCacheKey, emptyValue, err
		}
		if strings.HasPrefix(cacheKey, c.cachePriKeyPrefix+"[") {
			isPriKey = true
		}
	}
	return CacheKey{
		Key:         cacheKey,
		FieldValues: values,
		isPriKey:    isPriKey,
	}, v, nil
}
CreateCacheKey creates cache key and fields' values.
NOTE:
If fields is empty, auto-use primary fields.
package mysql
import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"reflect"
	"sort"
	"strings"
	"time"
	"github.com/henrylee2cn/goutil"
	"github.com/henrylee2cn/goutil/errors"
	"github.com/henrylee2cn/erpc/v6"
	"github.com/xiaoenai/tp-micro/v6/model/redis"
	"github.com/xiaoenai/tp-micro/v6/model/sqlx"
	"github.com/xiaoenai/tp-micro/v6/model/sqlx/reflectx"
)
type DB struct {
	*sqlx.DB
	Cache        *redis.Client
	dbConfig     *Config
	redisConfig  *redis.Config
	cacheableDBs map[string]*CacheableDB
}
func Connect(dbConfig *Config, redisConfig *redis.Config) (*DB, error) {
	var cache *redis.Client
	if !dbConfig.NoCache && redisConfig != nil {
		var err error
		cache, err = redis.NewClient(redisConfig)
		if err != nil {
			return nil, err
		}
	}
	db, err := sqlx.Connect("mysql", dbConfig.Source())
	if err != nil {
		return nil, err
	}
	db.SetMaxOpenConns(dbConfig.MaxOpenConns)
	db.SetMaxIdleConns(dbConfig.MaxIdleConns)
	db.SetConnMaxLifetime(time.Duration(dbConfig.ConnMaxLifetime) * time.Second)
	db.Mapper = reflectx.NewMapperFunc("json", goutil.SnakeString)
	return &DB{
		DB:           db,
		dbConfig:     dbConfig,
		Cache:        cache,
		redisConfig:  redisConfig,
		cacheableDBs: make(map[string]*CacheableDB),
	}, nil
}
type Cacheable interface {
	TableName() string
}
type CacheableDB struct {
	*DB
	tableName         string
	cachePriKeyPrefix string
	cols              []string
	priCols           []string
	cacheExpiration   time.Duration
	typeName          string
	priFieldsIndex    []int           
	fieldsIndexMap    map[string]int  
	module            *redis.Module
}
var ErrCacheNil = errors.New("*DB.Cache (redis) is nil")
func (d *DB) RegCacheableDB(ormStructPtr Cacheable, cacheExpiration time.Duration) (*CacheableDB, error) {
	tableName := ormStructPtr.TableName()
	if _, ok := d.cacheableDBs[tableName]; ok {
		return nil, fmt.Errorf("re-register cacheable table: %s", tableName)
	}
	if !d.dbConfig.NoCache && d.Cache == nil {
		return nil, ErrCacheNil
	}
	type Col struct {
		ColumnName string `json:"COLUMN_NAME" db:"COLUMN_NAME"`
		ColumnKey  string `json:"COLUMN_KEY" db:"COLUMN_KEY"`
	}
	var colsResult []*Col
	err := d.DB.Select(&colsResult, `SELECT COLUMN_NAME, COLUMN_KEY FROM information_schema.columns WHERE table_schema = ? AND table_name = ?;`, d.dbConfig.Database, tableName)
	if err != nil {
		return nil, fmt.Errorf("RegCacheableDB(): %s", err.Error())
	}
	priCols := make([]string, 0, 1)
	cols := make([]string, 0, len(colsResult))
	for _, col := range colsResult {
		cols = append(cols, col.ColumnName)
		if col.ColumnKey == "PRI" {
			priCols = append(priCols, col.ColumnName)
		}
	}
	if len(priCols) == 0 {
		return nil, fmt.Errorf("RegCacheableDB(): table '%s.%s' has no primary key", d.dbConfig.Database, tableName)
	}
	sort.Strings(priCols)
	t := reflect.TypeOf(ormStructPtr)
	var typeName = t.String()
	if t.Kind() != reflect.Ptr {
		return nil, fmt.Errorf("ormStructPtr must be *struct type: %s", typeName)
	}
	t = t.Elem()
	if t.Kind() != reflect.Struct {
		return nil, fmt.Errorf("ormStructPtr must be *struct type: %s", typeName)
	}
	structMap := d.Mapper.TypeMap(t)
	var fieldsIndexMap = make(map[string]int, len(structMap.Index))
	for i, idx := range structMap.Index {
		for _, col := range cols {
			if col == idx.Name {
				fieldsIndexMap[col] = i
				break
			}
		}
	}
	var priFieldsIndex = make([]int, len(priCols))
	for i, col := range priCols {
		priFieldsIndex[i] = fieldsIndexMap[col]
	}
	module := redis.NewModule(d.dbConfig.Database + ":" + tableName)
	c := &CacheableDB{
		DB:                d,
		tableName:         tableName,
		cachePriKeyPrefix: module.Key(strings.Join(priCols, "&")),
		cols:              cols,
		priCols:           priCols,
		cacheExpiration:   cacheExpiration,
		typeName:          typeName,
		priFieldsIndex:    priFieldsIndex,
		fieldsIndexMap:    fieldsIndexMap,
		module:            module,
	}
	d.cacheableDBs[tableName] = c
	return c, nil
}
func (d *DB) GetCacheableDB(tableName string) (*CacheableDB, error) {
	c, ok := d.cacheableDBs[tableName]
	if !ok {
		return nil, fmt.Errorf("has not called *DB.RegCacheableDB() to register: %s", tableName)
	}
	return c, nil
}
type CacheKey struct {
	Key         string
	FieldValues []interface{}
	isPriKey    bool
}
var emptyCacheKey = CacheKey{}
func (c *CacheableDB) CreateCacheKeyByFields(fields []string, values []interface{}) (string, error) {
	if len(fields) != len(values) {
		return "", errors.New("CreateCacheKeyByFields(): len(fields) != len(values)")
	}
	bs, err := json.Marshal(values)
	if err != nil {
		return "", errors.New("CreateCacheKeyByFields(): " + err.Error())
	}
	return c.module.Key(strings.Join(fields, "&") + goutil.BytesToString(bs)), nil
}
var emptyValue = reflect.Value{}