diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index c8a51945d..579130e12 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -9,10 +9,8 @@ package gdb import ( "context" "fmt" - "github.com/gogf/gf/v2/util/gconv" - "time" - "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/text/gstr" ) @@ -45,8 +43,7 @@ type Model struct { distinct string // Force the query to only return distinct results. lockInfo string // Lock for update or in shared lock. cacheEnabled bool // Enable sql result cache feature. - cacheDuration time.Duration // Cache TTL duration (< 1 for removing cache, >= 0 for saving cache). - cacheName string // Cache name for custom operation. + cacheOption CacheOption // Cache option for query statement. unscoped bool // Disables soft deleting features when select/delete operations. safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index d505abc3b..d5637518e 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -11,26 +11,32 @@ import ( "time" ) +type CacheOption struct { + // Duration is the TTL for the cache. + // If the parameter `Duration` < 0, which means it clear the cache with given `Name`. + // If the parameter `Duration` = 0, which means it never expires. + // If the parameter `Duration` > 0, which means it expires after `Duration`. + Duration time.Duration + + // Name is an optional unique name for the cache. + // The Name is used to bind a name to the cache, which means you can later control the cache + // like changing the `duration` or clearing the cache with specified Name. + Name string + + // Force caches the query result whatever the result is nil or not. + // It is used to avoid Cache Penetration. + Force bool +} + // 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. // -// If the parameter `duration` < 0, which means it clear the cache with given `name`. -// If the parameter `duration` = 0, which means it never expires. -// If the parameter `duration` > 0, which means it expires after `duration`. -// -// The optional parameter `name` is used to bind a name to the cache, which means you can -// later control the cache like changing the `duration` or clearing the cache with specified -// `name`. -// // Note that, the cache feature is disabled if the model is performing select statement // on a transaction. -func (m *Model) Cache(duration time.Duration, name ...string) *Model { +func (m *Model) Cache(option CacheOption) *Model { model := m.getModel() - model.cacheDuration = duration - if len(name) > 0 { - model.cacheName = name[0] - } + model.cacheOption = option model.cacheEnabled = true return model } @@ -38,9 +44,9 @@ func (m *Model) Cache(duration time.Duration, name ...string) *Model { // checkAndRemoveCache checks and removes the cache in insert/update/delete statement if // cache feature is enabled. func (m *Model) checkAndRemoveCache() { - if m.cacheEnabled && m.cacheDuration < 0 && len(m.cacheName) > 0 { + if m.cacheEnabled && m.cacheOption.Duration < 0 && len(m.cacheOption.Name) > 0 { var ctx = m.GetCtx() - _, err := m.db.GetCache().Remove(ctx, m.cacheName) + _, err := m.db.GetCache().Remove(ctx, m.cacheOption.Name) if err != nil { intlog.Error(ctx, err) } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 70f449d44..098f514fb 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -437,7 +437,7 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Retrieve from cache. if m.cacheEnabled && m.tx == nil { - cacheKey = m.cacheName + cacheKey = m.cacheOption.Name if len(cacheKey) == 0 { cacheKey = sql + ", @PARAMS:" + gconv.String(args) } @@ -461,16 +461,16 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Cache the result. if cacheKey != "" && err == nil { - if m.cacheDuration < 0 { + if m.cacheOption.Duration < 0 { if _, err := cacheObj.Remove(ctx, cacheKey); err != nil { intlog.Error(m.GetCtx(), err) } } else { // In case of Cache Penetration. - if result == nil { + if result.IsEmpty() && m.cacheOption.Force { result = Result{} } - if err := cacheObj.Set(ctx, cacheKey, result, m.cacheDuration); err != nil { + if err := cacheObj.Set(ctx, cacheKey, result, m.cacheOption.Duration); err != nil { intlog.Error(m.GetCtx(), err) } } diff --git a/database/gdb/gdb_z_mysql_model_basic_test.go b/database/gdb/gdb_z_mysql_model_basic_test.go index c1e7496ef..2d055a3fc 100644 --- a/database/gdb/gdb_z_mysql_model_basic_test.go +++ b/database/gdb/gdb_z_mysql_model_basic_test.go @@ -2479,7 +2479,11 @@ func Test_Model_Cache(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") @@ -2489,63 +2493,107 @@ func Test_Model_Cache(t *testing.T) { t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") time.Sleep(time.Second * 2) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_100") }) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_2") - r, err := db.Model(table).Data("passport", "user_200").Cache(-1, "test2").WherePri(2).Update() + r, err := db.Model(table).Data("passport", "user_200").Cache(gdb.CacheOption{ + Duration: -1, + Name: "test2", + Force: false, + }).WherePri(2).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_200") }) // transaction. gtest.C(t, func(t *gtest.T) { // make cache for id 3 - one, err := db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") - r, err := db.Model(table).Data("passport", "user_300").Cache(time.Second, "test3").WherePri(3).Update() + r, err := db.Model(table).Data("passport", "user_300").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { - one, err := tx.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_300") return nil }) t.AssertNil(err) - one, err = db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") }) gtest.C(t, func(t *gtest.T) { // make cache for id 4 - one, err := db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4") - r, err := db.Model(table).Data("passport", "user_400").Cache(time.Second, "test3").WherePri(4).Update() + r, err := db.Model(table).Data("passport", "user_400").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2553,12 +2601,20 @@ func Test_Model_Cache(t *testing.T) { err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // Cache feature disabled. - one, err := tx.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_400") // Update the cache. r, err := tx.Model(table).Data("passport", "user_4000"). - Cache(-1, "test4").WherePri(4).Update() + Cache(gdb.CacheOption{ + Duration: -1, + Name: "test4", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2567,7 +2623,11 @@ func Test_Model_Cache(t *testing.T) { }) t.AssertNil(err) // Read from db. - one, err = db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4000") })