fix issue in missing first result column when in select cache scenario

This commit is contained in:
John Guo
2022-03-31 15:42:12 +08:00
parent 21f48d3750
commit 372bae4799
14 changed files with 137 additions and 91 deletions

View File

@ -60,10 +60,11 @@ func (c *Core) Ctx(ctx context.Context) DB {
// GetCtx returns the context for current DB.
// It returns `context.Background()` is there's no context previously set.
func (c *Core) GetCtx() context.Context {
if c.ctx != nil {
return c.ctx
ctx := c.ctx
if ctx == nil {
ctx = context.TODO()
}
return c.injectInternalCtxData(context.TODO())
return c.injectInternalCtxData(ctx)
}
// GetCtxTimeout returns the context and cancel function for specified timeout type.
@ -241,7 +242,7 @@ func (c *Core) GetValue(ctx context.Context, sql string, args ...interface{}) (V
// GetCount queries and returns the count from database.
func (c *Core) GetCount(ctx context.Context, sql string, args ...interface{}) (int, error) {
// If the query fields do not contains function "COUNT",
// If the query fields do not contain function "COUNT",
// it replaces the sql string and adds the "COUNT" function to the fields.
if !gregex.IsMatchString(`(?i)SELECT\s+COUNT\(.+\)\s+FROM`, sql) {
sql, _ = gregex.ReplaceString(`(?i)(SELECT)\s+(.+)\s+(FROM)`, `$1 COUNT($2) $3`, sql)

View File

@ -158,6 +158,9 @@ func (tx *TX) transactionKeyForNestedPoint() string {
// Ctx sets the context for current transaction.
func (tx *TX) Ctx(ctx context.Context) *TX {
tx.ctx = ctx
if tx.ctx != nil {
tx.ctx = tx.db.GetCore().injectInternalCtxData(tx.ctx)
}
return tx
}

View File

@ -163,7 +163,7 @@ func (c *Core) sqlParsingHandler(ctx context.Context, in sqlParsingHandlerInput)
// DoCommit commits current sql and arguments to underlying sql driver.
func (c *Core) DoCommit(ctx context.Context, in DoCommitInput) (out DoCommitOutput, err error) {
// Inject internal data into ctx, just for double check.
// Inject internal data into ctx, especially for transaction creating.
ctx = c.injectInternalCtxData(ctx)
var (

View File

@ -8,9 +8,13 @@ package gdb
import (
"context"
"fmt"
"time"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/util/gconv"
)
type CacheOption struct {
@ -30,6 +34,11 @@ type CacheOption struct {
Force bool
}
type selectCacheItem struct {
Result Result
FirstResultColumn string
}
// Cache sets the cache feature for the model. It caches the result of the sql, which means
// if there's another same sql request, it just reads and returns the result from cache, it
// but not committed and executed into the database.
@ -43,12 +52,85 @@ func (m *Model) Cache(option CacheOption) *Model {
return model
}
// checkAndRemoveCache checks and removes the cache in insert/update/delete statement if
// checkAndRemoveSelectCache checks and removes the cache in insert/update/delete statement if
// cache feature is enabled.
func (m *Model) checkAndRemoveCache(ctx context.Context) {
func (m *Model) checkAndRemoveSelectCache(ctx context.Context) {
if m.cacheEnabled && m.cacheOption.Duration < 0 && len(m.cacheOption.Name) > 0 {
if _, err := m.db.GetCache().Remove(ctx, m.cacheOption.Name); err != nil {
intlog.Errorf(ctx, `%+v`, err)
}
}
}
func (m *Model) getSelectResultFromCache(ctx context.Context, sql string, args ...interface{}) (result Result, err error) {
if !m.cacheEnabled || m.tx != nil {
return
}
var (
ok bool
cacheItem *selectCacheItem
cacheKey = m.makeSelectCacheKey(sql, args...)
cacheObj = m.db.GetCache()
)
defer func() {
if cacheItem != nil {
if internalData := m.db.GetCore().getInternalCtxDataFromCtx(ctx); internalData != nil {
if internalData.FirstResultColumn == "" {
internalData.FirstResultColumn = cacheItem.FirstResultColumn
}
}
}
}()
if v, _ := cacheObj.Get(ctx, cacheKey); !v.IsNil() {
if cacheItem, ok = v.Val().(*selectCacheItem); ok {
// In-memory cache.
return cacheItem.Result, nil
} else if err = json.UnmarshalUseNumber(v.Bytes(), &cacheItem); err != nil {
// Other cache, it needs conversion.
return nil, err
}
}
return
}
func (m *Model) saveSelectResultToCache(ctx context.Context, result Result, sql string, args ...interface{}) (err error) {
if !m.cacheEnabled || m.tx != nil {
return
}
var (
cacheKey = m.makeSelectCacheKey(sql, args...)
cacheObj = m.db.GetCache()
)
if m.cacheOption.Duration < 0 {
if _, errCache := cacheObj.Remove(ctx, cacheKey); errCache != nil {
intlog.Errorf(ctx, `%+v`, errCache)
}
} else {
// In case of Cache Penetration.
if result.IsEmpty() && m.cacheOption.Force {
result = Result{}
}
var cacheItem = &selectCacheItem{
Result: result,
}
if internalData := m.db.GetCore().getInternalCtxDataFromCtx(ctx); internalData != nil {
cacheItem.FirstResultColumn = internalData.FirstResultColumn
}
if errCache := cacheObj.Set(ctx, cacheKey, cacheItem, m.cacheOption.Duration); errCache != nil {
intlog.Errorf(ctx, `%+v`, errCache)
}
}
return nil
}
func (m *Model) makeSelectCacheKey(sql string, args ...interface{}) string {
var cacheKey = m.cacheOption.Name
if len(cacheKey) == 0 {
cacheKey = fmt.Sprintf(
`GCache@Schema(%s):%s`,
m.db.GetSchema(),
gmd5.MustEncryptString(sql+", @PARAMS:"+gconv.String(args)),
)
}
return cacheKey
}

View File

@ -26,7 +26,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
}
defer func() {
if err == nil {
m.checkAndRemoveCache(ctx)
m.checkAndRemoveSelectCache(ctx)
}
}()
var (

View File

@ -228,7 +228,7 @@ func (m *Model) Save(data ...interface{}) (result sql.Result, err error) {
func (m *Model) doInsertWithOption(ctx context.Context, insertOption int) (result sql.Result, err error) {
defer func() {
if err == nil {
m.checkAndRemoveCache(ctx)
m.checkAndRemoveSelectCache(ctx)
}
}()
if m.data == nil {

View File

@ -13,11 +13,8 @@ import (
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/crypto/gmd5"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/internal/json"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
@ -521,33 +518,8 @@ func (m *Model) Having(having interface{}, args ...interface{}) *Model {
// doGetAllBySql does the select statement on the database.
func (m *Model) doGetAllBySql(ctx context.Context, queryType int, sql string, args ...interface{}) (result Result, err error) {
var (
ok bool
cacheKey = ""
cacheObj = m.db.GetCache()
)
// Retrieve from cache.
if m.cacheEnabled && m.tx == nil {
cacheKey = m.cacheOption.Name
if len(cacheKey) == 0 {
cacheKey = fmt.Sprintf(
`GCache@Schema(%s):%s`,
m.db.GetSchema(),
gmd5.MustEncryptString(sql+", @PARAMS:"+gconv.String(args)),
)
}
if v, _ := cacheObj.Get(ctx, cacheKey); !v.IsNil() {
if result, ok = v.Val().(Result); ok {
// In-memory cache.
return result, nil
}
// Other cache, it needs conversion.
if err = json.UnmarshalUseNumber(v.Bytes(), &result); err != nil {
return nil, err
} else {
return result, nil
}
}
if result, err = m.getSelectResultFromCache(ctx, sql, args...); err != nil || result != nil {
return
}
in := &HookSelectInput{
@ -562,25 +534,12 @@ func (m *Model) doGetAllBySql(ctx context.Context, queryType int, sql string, ar
Sql: sql,
Args: m.mergeArguments(args),
}
result, err = in.Next(ctx)
// Cache the result.
if cacheKey != "" && err == nil {
if m.cacheOption.Duration < 0 {
if _, errCache := cacheObj.Remove(ctx, cacheKey); errCache != nil {
intlog.Errorf(ctx, `%+v`, errCache)
}
} else {
// In case of Cache Penetration.
if result.IsEmpty() && m.cacheOption.Force {
result = Result{}
}
if errCache := cacheObj.Set(ctx, cacheKey, result, m.cacheOption.Duration); errCache != nil {
intlog.Errorf(ctx, `%+v`, errCache)
}
}
if result, err = in.Next(ctx); err != nil {
return
}
return result, err
err = m.saveSelectResultToCache(ctx, result, sql, args...)
return
}
func (m *Model) getFormattedSqlAndArgs(ctx context.Context, queryType int, limit1 bool) (sqlWithHolder string, holderArgs []interface{}) {

View File

@ -37,7 +37,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
}
defer func() {
if err == nil {
m.checkAndRemoveCache(ctx)
m.checkAndRemoveSelectCache(ctx)
}
}()
if m.data == nil {

View File

@ -26,7 +26,7 @@ type Operation struct {
Security *SecurityRequirements `json:"security,omitempty"`
Servers *Servers `json:"servers,omitempty"`
ExternalDocs *ExternalDocs `json:"externalDocs,omitempty"`
XExtensions `json:"-"`
XExtensions XExtensions `json:"-"`
}
func (oai *OpenApiV3) tagMapToOperation(tagMap map[string]string, operation *Operation) error {

View File

@ -28,7 +28,7 @@ type Parameter struct {
Example interface{} `json:"example,omitempty"`
Examples *Examples `json:"examples,omitempty"`
Content *Content `json:"content,omitempty"`
XExtensions `json:"-"`
XExtensions XExtensions `json:"-"`
}
func (oai *OpenApiV3) tagMapToParameter(tagMap map[string]string, parameter *Parameter) error {

View File

@ -19,21 +19,21 @@ import (
)
type Path struct {
Ref string `json:"$ref,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Connect *Operation `json:"connect,omitempty"`
Delete *Operation `json:"delete,omitempty"`
Get *Operation `json:"get,omitempty"`
Head *Operation `json:"head,omitempty"`
Options *Operation `json:"options,omitempty"`
Patch *Operation `json:"patch,omitempty"`
Post *Operation `json:"post,omitempty"`
Put *Operation `json:"put,omitempty"`
Trace *Operation `json:"trace,omitempty"`
Servers Servers `json:"servers,omitempty"`
Parameters Parameters `json:"parameters,omitempty"`
XExtensions `json:"-"`
Ref string `json:"$ref,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Connect *Operation `json:"connect,omitempty"`
Delete *Operation `json:"delete,omitempty"`
Get *Operation `json:"get,omitempty"`
Head *Operation `json:"head,omitempty"`
Options *Operation `json:"options,omitempty"`
Patch *Operation `json:"patch,omitempty"`
Post *Operation `json:"post,omitempty"`
Put *Operation `json:"put,omitempty"`
Trace *Operation `json:"trace,omitempty"`
Servers Servers `json:"servers,omitempty"`
Parameters Parameters `json:"parameters,omitempty"`
XExtensions XExtensions `json:"-"`
}
// Paths are specified by OpenAPI/Swagger standard version 3.0.

View File

@ -14,11 +14,11 @@ import (
// Response is specified by OpenAPI/Swagger 3.0 standard.
type Response struct {
Description string `json:"description"`
Headers Headers `json:"headers,omitempty"`
Content Content `json:"content,omitempty"`
Links Links `json:"links,omitempty"`
XExtensions `json:"-"`
Description string `json:"description"`
Headers Headers `json:"headers,omitempty"`
Content Content `json:"content,omitempty"`
Links Links `json:"links,omitempty"`
XExtensions XExtensions `json:"-"`
}
func (oai *OpenApiV3) tagMapToResponse(tagMap map[string]string, response *Response) error {

View File

@ -59,7 +59,7 @@ type Schema struct {
MaxProps *uint64 `json:"maxProperties,omitempty"`
AdditionalProperties *SchemaRef `json:"additionalProperties,omitempty"`
Discriminator *Discriminator `json:"discriminator,omitempty"`
XExtensions `json:"-"`
XExtensions XExtensions `json:"-"`
}
func (s Schema) MarshalJSON() ([]byte, error) {

View File

@ -194,8 +194,7 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string
var (
tempName string
elemFieldType reflect.StructField
elemTempValue reflect.Value
elemOriginKind reflect.Kind
elemFieldValue reflect.Value
elemType = pointerElemReflectValue.Type()
attrMap = make(map[string]string) // Attribute name to its check name which has no symbols.
)
@ -205,15 +204,17 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string
if !utils.IsLetterUpper(elemFieldType.Name[0]) {
continue
}
elemTempValue = pointerElemReflectValue.Field(i)
elemOriginKind = elemTempValue.Kind()
for elemOriginKind == reflect.Ptr || elemOriginKind == reflect.Interface {
elemTempValue = elemTempValue.Elem()
elemOriginKind = elemTempValue.Kind()
}
// Maybe it's struct/*struct embedded.
if elemFieldType.Anonymous && elemOriginKind == reflect.Struct {
if err = doStruct(paramsMap, pointerElemReflectValue.Field(i), mapping, priorityTag); err != nil {
if elemFieldType.Anonymous {
elemFieldValue = pointerElemReflectValue.Field(i)
// Ignore the interface attribute if it's nil.
if elemFieldValue.Kind() == reflect.Interface {
elemFieldValue = elemFieldValue.Elem()
if !elemFieldValue.IsValid() {
continue
}
}
if err = doStruct(paramsMap, elemFieldValue, mapping, priorityTag); err != nil {
return err
}
} else {