Compare commits

...

82 Commits

Author SHA1 Message Date
9706a9c768 version updates 2020-10-26 21:20:34 +08:00
cee67a8d4e improve urlencoding handling for parameters posted along with file uploading 2020-10-26 20:21:09 +08:00
87650557fd remove debugging codes from package gtime 2020-10-26 19:06:27 +08:00
d3bf52f12f fix issue in unit testing case for package gi18n 2020-10-26 19:00:11 +08:00
6b13a4849b improve package gi18n 2020-10-25 17:33:14 +08:00
8e380c0d9d improve package gtime/gconv for map converting 2020-10-25 11:33:30 +08:00
0caf4bfcec improve gconv.StructDeep 2020-10-25 10:47:47 +08:00
9c3b978b50 improve package ghttp and internal/structs 2020-10-22 15:16:31 +08:00
ab689a7792 improve gutil.Dump 2020-10-22 09:24:57 +08:00
846646d92d improve json validation rule for package gvalid 2020-10-22 09:11:38 +08:00
2eb2b89432 improve gconv.Struct* by doing the converting using json.Unmarshal if given params is json string/bytes 2020-10-21 14:09:16 +08:00
43441a8218 allow custom validation rule validate empty or nil values 2020-10-21 00:08:36 +08:00
561a541fa1 add custom CreatedAt/UpdatedAt/DeletedAt filed name configuration for package gdb 2020-10-20 21:01:39 +08:00
ffe9ecc141 improve package internal/empty 2020-10-20 14:07:01 +08:00
77f7884604 add function gutil.Try/g.Try;improve error string for gconv.Struct 2020-10-20 13:36:43 +08:00
0a203d1e22 fix issue in struct converting for ghttp.Request 2020-10-19 11:29:41 +08:00
f4f4550483 improve package gerror 2020-10-18 20:18:55 +08:00
e87226a092 improve package gerror 2020-10-18 11:29:09 +08:00
391a3ec9bd version update 2020-10-18 11:26:19 +08:00
dd5cd31ef5 add automatic data key to field name mapping feature for package gdb 2020-10-17 18:16:13 +08:00
de92e804fe Merge branch 'master' of https://github.com/gogf/gf 2020-10-17 17:17:27 +08:00
7725d9aaaf add Current/Next function for package gerror 2020-10-17 17:17:10 +08:00
bd3e25adea Merge pull request #946 from yuancjun/patch-1
Update ghttp_server_config.go
2020-10-15 20:41:07 +08:00
0b1d49af4b upgrade fsnotify to v1.4.9 2020-10-15 20:14:35 +08:00
7efa9e351e remove default initialization function and add lazy initialization feature for package gfsnotify 2020-10-15 20:07:21 +08:00
0b0141954b Update ghttp_server_config.go 2020-10-15 18:04:32 +08:00
82b531fbfb add domain support for domain of ghttp.Server 2020-10-14 21:18:11 +08:00
67fb626339 add configuration SessionCookieOutput for ghttp.Server 2020-10-14 20:52:26 +08:00
9044d5f05d fix issue in custom session id lost for ghttp.Request 2020-10-13 20:34:31 +08:00
47663aa1f1 fix issue of EOF in ghttp.Request when no data posted using form-data 2020-10-13 20:12:54 +08:00
63c0aab19c improve package gtime 2020-10-12 23:32:32 +08:00
261216f5e4 improve structure sql for package gdb 2020-10-12 23:22:56 +08:00
f88b799d67 add more unit testing case for package gdb 2020-10-11 22:21:03 +08:00
9c48dd172c add auto-json support for slice/struct attribute for data inserting of package gdb 2020-10-10 17:29:38 +08:00
09ce105eee improve gdb.Model.Fields/FieldsEx for package gdb 2020-10-10 14:00:10 +08:00
651aa895f8 improve function addWordBoundariesToNumbers for package gstr 2020-10-10 13:38:28 +08:00
0509e41198 recover number for unit testing case of package gconv 2020-10-10 00:04:55 +08:00
1b40d6a53a do tx.Rollback if there's panic in gdb.Transaction 2020-10-09 23:42:33 +08:00
f9189d48d1 remove type ValueFunc from package gcache 2020-10-09 23:36:39 +08:00
849874a247 improve adapter definition for package gcache 2020-10-09 20:59:49 +08:00
3f6510bae7 fix issue in gconv.Struct 2020-09-29 23:47:37 +08:00
a585a26c39 improve custom rule function type for package gvalid 2020-09-29 23:25:20 +08:00
9943966a86 improva function formatSql for package gdb 2020-09-29 22:53:44 +08:00
3617e51c01 improve gdb.Model.ScanList 2020-09-28 23:52:02 +08:00
c931032f08 comment update for gdb.Model.Unscoped 2020-09-27 23:37:40 +08:00
37b286eaa4 improve performance for gconv.Maps;comment update for package gcache 2020-09-27 22:35:02 +08:00
619287c273 improve cache feature for package gdb 2020-09-27 00:15:11 +08:00
aeb9b68298 improve adapter feature for package gcache 2020-09-26 22:44:07 +08:00
bae8f6315b comment updat 2020-09-26 21:13:09 +08:00
a6f70f8935 comment updat 2020-09-26 21:00:28 +08:00
acca6f4009 add adapter feature for package gcache 2020-09-26 20:47:29 +08:00
727fdd2391 improve unit testing case for association feature for package gdb 2020-09-25 08:33:22 +08:00
1d174e00c0 improve package internal/intlog 2020-09-24 23:46:19 +08:00
a5e3e2f5ba change g.SetDebug function to control the debugging information for framework 2020-09-24 23:40:44 +08:00
da43c2d52f improve example for package gview 2020-09-22 20:12:34 +08:00
262f27748c improve custom validation rule feature for package gvalid 2020-09-22 08:45:22 +08:00
a29aef7e1c improve package gvalid for custom rule 2020-09-21 23:51:30 +08:00
1671391195 comment updates 2020-09-21 22:56:48 +08:00
28b0d59c61 improve package gcfg/gjson for data loading 2020-09-21 21:30:58 +08:00
86433cef25 improve gtime.New, gconv.String 2020-09-18 23:59:49 +08:00
e96ccd5f71 v1.13.6 2020-09-17 23:49:03 +08:00
50a087bb3d Merge branch 'master' of https://github.com/gogf/gf 2020-09-17 23:41:23 +08:00
1ec049c52f improve gf server for routes check while starting 2020-09-17 23:27:10 +08:00
ec38805542 Merge pull request #908 from cai1111/patch-2
Update gdb_driver_mssql.go
2020-09-17 19:23:25 +08:00
4c8517d075 Update gdb_driver_mssql.go 2020-09-17 11:06:24 +08:00
edf06da6ea add struct which implements Interfaces function parameter support for gdb.Model.Insert/Save 2020-09-13 11:21:10 +08:00
eb43a2040e add ghttp.Client.RedirectLimit and gfile.ScanDirFileFunc 2020-09-09 22:26:46 +08:00
9d0ecc7d3e add more unit testing case for package gdb 2020-09-07 20:25:59 +08:00
ad943c5e02 improve gjson.New by using gconv.MapDeep for map/struct 2020-09-07 19:44:11 +08:00
bdb4fd0d25 donator updates 2020-09-07 19:10:04 +08:00
2440e05457 fix issue in limit operations for database driver oracle 2020-09-03 22:38:07 +08:00
1337c6c0d1 add sub query sql support for join functions 2020-09-03 21:57:58 +08:00
957689e07c up 2020-09-02 23:22:16 +08:00
3952d74f87 up 2020-09-02 21:36:21 +08:00
6dc4b81693 add max recursive depth for directory scanning for function gfile.Scan* 2020-09-02 21:25:02 +08:00
9cd953b7be improve function FieldsEx by filtering fields from custom fields specified by function Fields for package gdb 2020-09-02 20:37:02 +08:00
631810dda2 add function String for package gmap 2020-09-02 19:53:58 +08:00
8c12bc5506 change panic to internal logging for package glog 2020-09-01 21:22:19 +08:00
d4091a4826 improve some transaction operations by directly calling model operations, making their implements logic the same 2020-08-31 15:57:04 +08:00
a7c269886b improve support for dynamic database configurations in codes 2020-08-31 15:39:27 +08:00
f54593037b improve CURD functions for package gdb 2020-08-31 00:59:42 +08:00
0415cf6a08 fix issue in nil gtime attribute for model entity for package gdb 2020-08-31 00:39:12 +08:00
135 changed files with 3894 additions and 1510 deletions

View File

@ -1,10 +1,15 @@
# MySQL数据库配置
# MySQL.
[database]
debug = true
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?parseTime=true&loc=Local"
MaxOpen = 100
# Redis.
[redis]
default = "127.0.0.1:6379,0"
cache = "127.0.0.1:6379,1"
#[database]
# [[database.default]]
# type = "mysql"

View File

@ -3,6 +3,7 @@ package main
import (
"github.com/gogf/gf/database/gdb"
"github.com/gogf/gf/util/gutil"
"time"
)
func main() {
@ -10,7 +11,7 @@ func main() {
Host: "127.0.0.1",
Port: "3306",
User: "root",
Pass: "123456",
Pass: "12345678",
Name: "test",
Type: "mysql",
Role: "master",
@ -20,19 +21,20 @@ func main() {
if err != nil {
panic(err)
}
//db.GetCache().SetAdapter(adapter.NewRedis(g.Redis()))
// 开启调试模式以便于记录所有执行的SQL
db.SetDebug(true)
// 执行2次查询并将查询结果缓存3秒并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := db.Table("user").Cache(3, "vip-user").Where("uid=?", 1).One()
gutil.Dump(r.ToMap())
for i := 0; i < 3; i++ {
r, _ := db.Table("user").Cache(3000*time.Second).Where("id=?", 1).One()
gutil.Dump(r.Map())
}
// 执行更新操作,并清理指定名称的查询缓存
db.Table("user").Cache(-1, "vip-user").Data(gdb.Map{"name": "smith"}).Where("uid=?", 1).Update()
//db.Table("user").Cache(-1, "vip-user").Data(gdb.Map{"name": "smith"}).Where("id=?", 1).Update()
// 再次执行查询,启用查询缓存特性
r, _ := db.Table("user").Cache(3, "vip-user").Where("uid=?", 1).One()
gutil.Dump(r.ToMap())
//r, _ := db.Table("user").Cache(300000*time.Second, "vip-user").Where("id=?", 1).One()
//gutil.Dump(r.Map())
}

View File

@ -8,9 +8,6 @@ import (
func main() {
db := g.DB()
db.SetMaxIdleConnCount(10)
db.SetMaxOpenConnCount(10)
db.SetMaxConnLifetime(time.Minute)
// 开启调试模式以便于记录所有执行的SQL
db.SetDebug(true)
@ -19,7 +16,7 @@ func main() {
for i := 0; i < 10; i++ {
go db.Table("user").All()
}
time.Sleep(time.Second)
time.Sleep(time.Millisecond * 100)
}
}

View File

@ -2,25 +2,14 @@ package main
import (
"fmt"
"github.com/gogf/gf/i18n/gi18n"
"github.com/gogf/gf/frame/g"
)
func main() {
t := gi18n.New()
t.SetPath("/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/i18n/gi18n/i18n")
t.SetLanguage("en")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
t.SetLanguage("ja")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
t.SetLanguage("ru")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
fmt.Println(t.Translate(`hello`, "zh-CN"))
fmt.Println(t.Translate(`{#hello}{#world}!`, "zh-CN"))
var (
orderId = 865271654
orderAmount = 99.8
)
fmt.Println(g.I18n().Tfl(`en`, `{#OrderPaid}`, orderId, orderAmount))
fmt.Println(g.I18n().Tfl(`zh-CN`, `{#OrderPaid}`, orderId, orderAmount))
}

View File

@ -1,3 +1 @@
hello = "Hello"
world = "World"
OrderPaid = "You have successfully complete order #%d payment, paid amount: ¥%0.2f."

View File

@ -1,2 +1 @@
hello = "你好"
world = "世界"
OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。"

View File

@ -3,15 +3,21 @@ package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/os/glog"
)
type GetById struct {
Id *g.Var `p:"id" v:"required|integer#id不能为空|id必须为整数"`
}
func main() {
s := g.Server()
s.SetIndexFolder(true)
s.BindHandler("/", func(r *ghttp.Request) {
glog.Println(r.Header)
r.Response.Write("hello world")
var idInfo *GetById
if err := r.Parse(&idInfo); err != nil {
r.Response.Write(err)
}
r.Response.Write("ok")
})
s.SetPort(8999)
s.Run()

View File

@ -1,12 +1,13 @@
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func main() {
s := ghttp.GetServer()
s.EnablePProf()
s := g.Server()
s.Domain("localhost").EnablePProf()
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.Writeln("哈喽世界!")
})

View File

@ -2,19 +2,18 @@ package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/frame/gmvc"
"github.com/gogf/gf/net/ghttp"
)
type Controller struct {
gmvc.Controller
}
func (c *Controller) Test() {
c.View.Display("layout.html")
}
func main() {
s := g.Server()
s.BindControllerMethod("/", new(Controller), "Test")
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"header": "This is header",
"container": "This is container",
"footer": "This is footer",
})
})
s.SetPort(8199)
s.Run()
}

View File

@ -1,3 +1,3 @@
{{define "container"}}
<h1>CONTAINER</h1>
<h1>{{.container}}</h1>
{{end}}

View File

@ -1,3 +1,3 @@
{{define "footer"}}
<h1>FOOTER</h1>
<h1>{{.footer}}</h1>
{{end}}

View File

@ -1,3 +1,3 @@
{{define "header"}}
<h1>HEADER</h1>
<h1>{{.header}}</h1>
{{end}}

View File

@ -2,14 +2,14 @@
<html>
<head>
<title>GoFrame Layout</title>
{{template "header"}}
{{template "header" .}}
</head>
<body>
<div class="container">
{{template "container"}}
{{template "container" .}}
</div>
<div class="footer">
{{template "footer"}}
{{template "footer" .}}
</div>
</body>
</html>

View File

@ -9,11 +9,13 @@ func main() {
s := g.Server()
s.BindHandler("/main1", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"name": "smith",
"mainTpl": "main/main1.html",
})
})
s.BindHandler("/main2", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"name": "john",
"mainTpl": "main/main2.html",
})
})

View File

@ -11,7 +11,7 @@ func main() {
fmt.Println(1)
gutil.Throw("error")
fmt.Println(2)
}, func(err interface{}) {
}, func(err error) {
fmt.Println(err)
})
}

View File

@ -115,6 +115,10 @@ please note your github/gitee account in your payment bill. All the donations wi
|[hyuant](https://github.com/hyuant)|alipay|¥66.66|
|*庆|alipay|¥9.99| 支持一下gf越来越好
|**君|alipay|¥10.00| 加油
|向回走的闹钟|wechat|¥20.00| 越来越好
|金毛|wechat|¥100.00|
|莫失莫忘|wechat|¥100.00|
|**航|alipay|¥20.00|

View File

@ -457,6 +457,12 @@ func (m *AnyAnyMap) Merge(other *AnyAnyMap) {
}
}
// String returns the map as a string.
func (m *AnyAnyMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *AnyAnyMap) MarshalJSON() ([]byte, error) {
return json.Marshal(gconv.Map(m.Map()))

View File

@ -455,6 +455,12 @@ func (m *IntAnyMap) Merge(other *IntAnyMap) {
}
}
// String returns the map as a string.
func (m *IntAnyMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *IntAnyMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -426,6 +426,12 @@ func (m *IntIntMap) Merge(other *IntIntMap) {
}
}
// String returns the map as a string.
func (m *IntIntMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *IntIntMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -426,6 +426,12 @@ func (m *IntStrMap) Merge(other *IntStrMap) {
}
}
// String returns the map as a string.
func (m *IntStrMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *IntStrMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -451,6 +451,12 @@ func (m *StrAnyMap) Merge(other *StrAnyMap) {
}
}
// String returns the map as a string.
func (m *StrAnyMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *StrAnyMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -429,6 +429,12 @@ func (m *StrIntMap) Merge(other *StrIntMap) {
}
}
// String returns the map as a string.
func (m *StrIntMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *StrIntMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -429,6 +429,12 @@ func (m *StrStrMap) Merge(other *StrStrMap) {
}
}
// String returns the map as a string.
func (m *StrStrMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *StrStrMap) MarshalJSON() ([]byte, error) {
m.mu.RLock()

View File

@ -510,6 +510,12 @@ func (m *ListMap) Merge(other *ListMap) {
})
}
// String returns the map as a string.
func (m *ListMap) String() string {
b, _ := m.MarshalJSON()
return gconv.UnsafeBytesToStr(b)
}
// MarshalJSON implements the interface MarshalJSON for json.Marshal.
func (m *ListMap) MarshalJSON() ([]byte, error) {
return json.Marshal(gconv.Map(m.Map()))

View File

@ -128,6 +128,7 @@ type DB interface {
GetDryRun() bool
SetLogger(logger *glog.Logger)
GetLogger() *glog.Logger
GetConfig() *ConfigNode
SetMaxIdleConnCount(n int)
SetMaxOpenConnCount(n int)
SetMaxConnLifetime(d time.Duration)
@ -156,7 +157,7 @@ type DB interface {
// Internal methods.
// ===========================================================================
filterFields(schema, table string, data map[string]interface{}) map[string]interface{}
mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error)
convertValue(fieldValue interface{}, fieldType string) interface{}
rowsToResult(rows *sql.Rows) (Result, error)
}
@ -166,11 +167,12 @@ type Core struct {
DB DB // DB interface object.
group string // Configuration group name.
debug *gtype.Bool // Enable debug mode for the database.
cache *gcache.Cache // Cache manager.
cache *gcache.Cache // Cache manager, SQL result cache only.
schema *gtype.String // Custom schema for this object.
dryrun *gtype.Bool // Dry run.
prefix string // Table prefix.
logger *glog.Logger // Logger.
config *ConfigNode // Current config node.
maxIdleConnCount int // Max idle connection count.
maxOpenConnCount int // Max open connection count.
maxConnLifetime time.Duration // Max TTL for a connection.
@ -283,10 +285,10 @@ func Register(name string, driver Driver) error {
// New creates and returns an ORM object with global configurations.
// The parameter <name> specifies the configuration group name,
// which is DEFAULT_GROUP_NAME in default.
func New(name ...string) (db DB, err error) {
group := configs.group
if len(name) > 0 && name[0] != "" {
group = name[0]
func New(group ...string) (db DB, err error) {
groupName := configs.group
if len(group) > 0 && group[0] != "" {
groupName = group[0]
}
configs.RLock()
defer configs.RUnlock()
@ -294,16 +296,17 @@ func New(name ...string) (db DB, err error) {
if len(configs.config) < 1 {
return nil, errors.New("empty database configuration")
}
if _, ok := configs.config[group]; ok {
if node, err := getConfigNodeByGroup(group, true); err == nil {
if _, ok := configs.config[groupName]; ok {
if node, err := getConfigNodeByGroup(groupName, true); err == nil {
c := &Core{
group: group,
group: groupName,
debug: gtype.NewBool(),
cache: gcache.New(),
schema: gtype.NewString(),
dryrun: gtype.NewBool(),
logger: glog.New(),
prefix: node.Prefix,
config: node,
maxIdleConnCount: gDEFAULT_CONN_MAX_IDLE_COUNT,
maxConnLifetime: gDEFAULT_CONN_MAX_LIFE_TIME, // Default max connection life time if user does not configure.
}
@ -320,7 +323,7 @@ func New(name ...string) (db DB, err error) {
return nil, err
}
} else {
return nil, errors.New(fmt.Sprintf(`database configuration node "%s" is not found`, group))
return nil, errors.New(fmt.Sprintf(`database configuration node "%s" is not found`, groupName))
}
}
@ -438,11 +441,11 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
node = &n
}
// Cache the underlying connection pool object by node.
v := c.cache.GetOrSetFuncLock(node.String(), func() interface{} {
v, _ := gcache.GetOrSetFuncLock(node.String(), func() (interface{}, error) {
sqlDb, err = c.DB.Open(node)
if err != nil {
intlog.Printf("DB open failed: %v, %+v", err, node)
return nil
return nil, err
}
if c.maxIdleConnCount > 0 {
sqlDb.SetMaxIdleConns(c.maxIdleConnCount)
@ -461,7 +464,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
} else if node.MaxConnLifetime > 0 {
sqlDb.SetConnMaxLifetime(node.MaxConnLifetime * time.Second)
}
return sqlDb
return sqlDb, nil
}, 0)
if v != nil && sqlDb == nil {
sqlDb = v.(*sql.DB)

View File

@ -11,6 +11,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gogf/gf/text/gstr"
"reflect"
"strings"
@ -310,6 +311,11 @@ func (c *Core) Transaction(f func(tx *TX) error) (err error) {
return err
}
defer func() {
if err == nil {
if e := recover(); e != nil {
err = fmt.Errorf("%v", e)
}
}
if err != nil {
if e := tx.Rollback(); e != nil {
err = e
@ -334,7 +340,10 @@ func (c *Core) Transaction(f func(tx *TX) error) (err error) {
//
// The parameter <batch> specifies the batch operation count when given data is slice.
func (c *Core) Insert(table string, data interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoInsert(nil, table, data, gINSERT_OPTION_DEFAULT, batch...)
if len(batch) > 0 {
return c.Model(table).Data(data).Batch(batch[0]).Insert()
}
return c.Model(table).Data(data).Insert()
}
// InsertIgnore does "INSERT IGNORE INTO ..." statement for the table.
@ -347,7 +356,10 @@ func (c *Core) Insert(table string, data interface{}, batch ...int) (sql.Result,
//
// The parameter <batch> specifies the batch operation count when given data is slice.
func (c *Core) InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoInsert(nil, table, data, gINSERT_OPTION_IGNORE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(data).Batch(batch[0]).InsertIgnore()
}
return c.Model(table).Data(data).InsertIgnore()
}
// Replace does "REPLACE INTO ..." statement for the table.
@ -363,7 +375,10 @@ func (c *Core) InsertIgnore(table string, data interface{}, batch ...int) (sql.R
// If given data is type of slice, it then does batch replacing, and the optional parameter
// <batch> specifies the batch operation count.
func (c *Core) Replace(table string, data interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoInsert(nil, table, data, gINSERT_OPTION_REPLACE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(data).Batch(batch[0]).Replace()
}
return c.Model(table).Data(data).Replace()
}
// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the table.
@ -378,11 +393,14 @@ func (c *Core) Replace(table string, data interface{}, batch ...int) (sql.Result
// If given data is type of slice, it then does batch saving, and the optional parameter
// <batch> specifies the batch operation count.
func (c *Core) Save(table string, data interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoInsert(nil, table, data, gINSERT_OPTION_SAVE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(data).Batch(batch[0]).Save()
}
return c.Model(table).Data(data).Save()
}
// doInsert inserts or updates data for given table.
//
// This function is usually used for custom interface definition, you do not need call it manually.
// The parameter <data> can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
// Eg:
// Data(g.Map{"uid": 10000, "name":"john"})
@ -410,8 +428,14 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
switch reflectKind {
case reflect.Slice, reflect.Array:
return c.DB.DoBatchInsert(link, table, data, option, batch...)
case reflect.Map, reflect.Struct:
dataMap = DataToMapDeep(data)
case reflect.Struct:
if _, ok := data.(apiInterfaces); ok {
return c.DB.DoBatchInsert(link, table, data, option, batch...)
} else {
dataMap = ConvertDataForTableRecord(data)
}
case reflect.Map:
dataMap = ConvertDataForTableRecord(data)
default:
return result, errors.New(fmt.Sprint("unsupported data type:", reflectKind))
}
@ -432,7 +456,7 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
for k, _ := range dataMap {
// If it's SAVE operation,
// do not automatically update the creating time.
if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) {
if c.isSoftCreatedFiledName(k) {
continue
}
if len(updateStr) > 0 {
@ -465,35 +489,48 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
// BatchInsert batch inserts data.
// The parameter <list> must be type of slice of map or struct.
func (c *Core) BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoBatchInsert(nil, table, list, gINSERT_OPTION_DEFAULT, batch...)
if len(batch) > 0 {
return c.Model(table).Data(list).Batch(batch[0]).Insert()
}
return c.Model(table).Data(list).Insert()
}
// BatchInsert batch inserts data with ignore option.
// BatchInsertIgnore batch inserts data with ignore option.
// The parameter <list> must be type of slice of map or struct.
func (c *Core) BatchInsertIgnore(table string, list interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoBatchInsert(nil, table, list, gINSERT_OPTION_IGNORE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(list).Batch(batch[0]).InsertIgnore()
}
return c.Model(table).Data(list).InsertIgnore()
}
// BatchReplace batch replaces data.
// The parameter <list> must be type of slice of map or struct.
func (c *Core) BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoBatchInsert(nil, table, list, gINSERT_OPTION_REPLACE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(list).Batch(batch[0]).Replace()
}
return c.Model(table).Data(list).Replace()
}
// BatchSave batch replaces data.
// The parameter <list> must be type of slice of map or struct.
func (c *Core) BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) {
return c.DB.DoBatchInsert(nil, table, list, gINSERT_OPTION_SAVE, batch...)
if len(batch) > 0 {
return c.Model(table).Data(list).Batch(batch[0]).Save()
}
return c.Model(table).Data(list).Save()
}
// DoBatchInsert batch inserts/replaces/saves data.
// This function is usually used for custom interface definition, you do not need call it manually.
func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) {
table = c.DB.QuotePrefixTableName(table)
var (
keys []string
values []string
params []interface{}
listMap List
keys []string // Field names.
values []string // Value holder string array, like: (?,?,?)
params []interface{} // Values that will be committed to underlying database driver.
listMap List // The data list that passed from caller.
)
switch value := list.(type) {
case Result:
@ -518,10 +555,10 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
case reflect.Slice, reflect.Array:
listMap = make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
listMap[i] = DataToMapDeep(rv.Index(i).Interface())
listMap[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
case reflect.Map:
listMap = List{DataToMapDeep(value)}
listMap = List{ConvertDataForTableRecord(value)}
case reflect.Struct:
if v, ok := value.(apiInterfaces); ok {
var (
@ -529,11 +566,11 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
list = make(List, len(array))
)
for i := 0; i < len(array); i++ {
list[i] = DataToMapDeep(array[i])
list[i] = ConvertDataForTableRecord(array[i])
}
listMap = list
} else {
listMap = List{DataToMapDeep(value)}
listMap = List{ConvertDataForTableRecord(value)}
}
default:
return result, errors.New(fmt.Sprint("unsupported list type:", kind))
@ -566,7 +603,7 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
for _, k := range keys {
// If it's SAVE operation,
// do not automatically update the creating time.
if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) {
if c.isSoftCreatedFiledName(k) {
continue
}
if len(updateStr) > 0 {
@ -636,15 +673,11 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
// "age IN(?,?)", 18, 50
// User{ Id : 1, UserName : "john"}
func (c *Core) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) {
newWhere, newArgs := formatWhere(c.DB, condition, args, false)
if newWhere != "" {
newWhere = " WHERE " + newWhere
}
return c.DB.DoUpdate(nil, table, data, newWhere, newArgs...)
return c.Model(table).Data(data).Where(condition, args...).Update()
}
// doUpdate does "UPDATE ... " statement for the table.
// Also see Update.
// This function is usually used for custom interface definition, you do not need call it manually.
func (c *Core) DoUpdate(link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) {
table = c.DB.QuotePrefixTableName(table)
var (
@ -663,7 +696,7 @@ func (c *Core) DoUpdate(link Link, table string, data interface{}, condition str
case reflect.Map, reflect.Struct:
var (
fields []string
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
)
for k, v := range dataMap {
fields = append(fields, c.DB.QuoteWord(k)+"=?")
@ -704,15 +737,11 @@ func (c *Core) DoUpdate(link Link, table string, data interface{}, condition str
// "age IN(?,?)", 18, 50
// User{ Id : 1, UserName : "john"}
func (c *Core) Delete(table string, condition interface{}, args ...interface{}) (result sql.Result, err error) {
newWhere, newArgs := formatWhere(c.DB, condition, args, false)
if newWhere != "" {
newWhere = " WHERE " + newWhere
}
return c.DB.DoDelete(nil, table, newWhere, newArgs...)
return c.Model(table).Where(condition, args...).Delete()
}
// DoDelete does "DELETE FROM ... " statement for the table.
// Also see Delete.
// This function is usually used for custom interface definition, you do not need call it manually.
func (c *Core) DoDelete(link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) {
if link == nil {
if link, err = c.DB.Master(); err != nil {
@ -801,3 +830,22 @@ func (c *Core) HasTable(name string) (bool, error) {
}
return false, nil
}
// isSoftCreatedFiledName checks and returns whether given filed name is an automatic-filled created time.
func (c *Core) isSoftCreatedFiledName(fieldName string) bool {
if fieldName == "" {
return false
}
if config := c.DB.GetConfig(); config.CreatedAt != "" {
if utils.EqualFoldWithoutChars(fieldName, config.CreatedAt) {
return true
}
return gstr.InArray(append([]string{config.CreatedAt}, createdFiledNames...), fieldName)
}
for _, v := range createdFiledNames {
if utils.EqualFoldWithoutChars(fieldName, v) {
return true
}
}
return false
}

View File

@ -39,6 +39,9 @@ type ConfigNode struct {
DryRun bool // (Optional) Dry run, which does SELECT but no INSERT/UPDATE/DELETE statements.
Weight int // (Optional) Weight for load balance calculating, it's useless if there's just one node.
Charset string // (Optional, "utf8mb4" in default) Custom charset when operating on database.
CreatedAt string // (Optional) The filed name of table for automatic-filled created datetime.
UpdatedAt string // (Optional) The filed name of table for automatic-filled updated datetime.
DeletedAt string // (Optional) The filed name of table for automatic-filled updated datetime.
LinkInfo string `json:"link"` // (Optional) Custom link information, when it is used, configuration Host/Port/User/Pass/Name are ignored.
MaxIdleConnCount int `json:"maxidle"` // (Optional) Max idle connection configuration for underlying connection pool.
MaxOpenConnCount int `json:"maxopen"` // (Optional) Max open connection configuration for underlying connection pool.
@ -115,6 +118,14 @@ func GetDefaultGroup() string {
return configs.group
}
// IsConfigured checks and returns whether the database configured.
// It returns true if any configuration exists.
func IsConfigured() bool {
configs.RLock()
defer configs.RUnlock()
return len(configs.config) > 0
}
// SetLogger sets the logger for orm.
func (c *Core) SetLogger(logger *glog.Logger) {
c.logger = logger
@ -204,3 +215,8 @@ func (c *Core) SetSchema(schema string) {
func (c *Core) GetSchema() string {
return c.schema.Val()
}
// GetConfig returns the current used node configuration.
func (c *Core) GetConfig() *ConfigNode {
return c.config
}

View File

@ -7,6 +7,8 @@
package gdb
import (
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/util/gutil"
"strings"
"time"
@ -145,15 +147,50 @@ func (c *Core) convertValue(fieldValue interface{}, fieldType string) interface{
}
// filterFields removes all key-value pairs which are not the field of given table.
func (c *Core) filterFields(schema, table string, data map[string]interface{}) map[string]interface{} {
// It must use data copy here to avoid its changing the origin data map.
newDataMap := make(map[string]interface{}, len(data))
if fields, err := c.DB.TableFields(table, schema); err == nil {
for k, v := range data {
if _, ok := fields[k]; ok {
newDataMap[k] = v
func (c *Core) mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error) {
if fieldsMap, err := c.DB.TableFields(table, schema); err == nil {
fieldsKeyMap := make(map[string]interface{}, len(fieldsMap))
for k, _ := range fieldsMap {
fieldsKeyMap[k] = nil
}
// Automatic data key to table field name mapping.
var foundKey string
for dataKey, dataValue := range data {
if _, ok := fieldsKeyMap[dataKey]; !ok {
foundKey, _ = gutil.MapPossibleItemByKey(fieldsKeyMap, dataKey)
if foundKey != "" {
data[foundKey] = dataValue
delete(data, dataKey)
} else if !filter {
if schema != "" {
return nil, gerror.Newf(`no column of name "%s" found for table "%s" in schema "%s"`, dataKey, table, schema)
}
return nil, gerror.Newf(`no column of name "%s" found for table "%s"`, dataKey, table)
}
}
}
// Data filtering.
if filter {
for dataKey, _ := range data {
if _, ok := fieldsMap[dataKey]; !ok {
delete(data, dataKey)
}
}
}
}
return newDataMap
return data, nil
}
//// filterFields removes all key-value pairs which are not the field of given table.
//func (c *Core) filterFields(schema, table string, data map[string]interface{}) map[string]interface{} {
// // It must use data copy here to avoid its changing the origin data map.
// newDataMap := make(map[string]interface{}, len(data))
// if fields, err := c.DB.TableFields(table, schema); err == nil {
// for k, v := range data {
// if _, ok := fields[k]; ok {
// newDataMap[k] = v
// }
// }
// }
// return newDataMap
//}

View File

@ -15,6 +15,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gogf/gf/os/gcache"
"strconv"
"strings"
@ -198,47 +199,58 @@ func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`mssql_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`mssql_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`
SELECT a.name Field,
structureSql := fmt.Sprintf(`
SELECT
a.name Field,
CASE b.name
WHEN 'datetime' THEN 'datetime'
WHEN 'numeric' THEN b.name + '(' + convert(varchar(20),a.xprec) + ',' + convert(varchar(20),a.xscale) + ')'
WHEN 'char' THEN b.name + '(' + convert(varchar(20),a.length)+ ')'
WHEN 'varchar' THEN b.name + '(' + convert(varchar(20),a.length)+ ')'
ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END as TYPE,
case when a.isnullable=1 then 'YES'else 'NO' end as [Null],
case when exists(SELECT 1 FROM sysobjects where xtype='PK' and name in (
SELECT name FROM sysindexes WHERE indid in(
SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid
))) then 'PRI' else '' end AS [Key],
case when COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 then 'auto_increment'else '' end Extra,
isnull(e.text,'') as [Default],
WHEN 'numeric' THEN b.name + '(' + convert(varchar(20), a.xprec) + ',' + convert(varchar(20), a.xscale) + ')'
WHEN 'char' THEN b.name + '(' + convert(varchar(20), a.length)+ ')'
WHEN 'varchar' THEN b.name + '(' + convert(varchar(20), a.length)+ ')'
ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END AS Type,
CASE WHEN a.isnullable=1 THEN 'YES' ELSE 'NO' end AS [Null],
CASE WHEN exists (
SELECT 1 FROM sysobjects WHERE xtype='PK' AND name IN (
SELECT name FROM sysindexes WHERE indid IN (
SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid
)
)
) THEN 'PRI' ELSE '' END AS [Key],
CASE WHEN COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 THEN 'auto_increment' ELSE '' END Extra,
isnull(e.text,'') AS [Default],
isnull(g.[value],'') AS [Comment]
FROM syscolumns a
left join systypes b on a.xtype=b.xusertype
inner join sysobjects d on a.id=d.id and d.xtype='U' and d.name<>'dtproperties'
left join syscomments e on a.cdefault=e.id
left join sys.extended_properties g on a.id=g.major_id and a.colid=g.minor_id
left join sys.extended_properties f on d.id=f.major_id and f.minor_id =0
where d.name='%s'
order by a.id,a.colorder`, strings.ToUpper(table)))
FROM syscolumns a
LEFT JOIN systypes b ON a.xtype=b.xtype AND a.xusertype=b.xusertype
INNER JOIN sysobjects d ON a.id=d.id AND d.xtype='U' AND d.name<>'dtproperties'
LEFT JOIN syscomments e ON a.cdefault=e.id
LEFT JOIN sys.extended_properties g ON a.id=g.major_id AND a.colid=g.minor_id
LEFT JOIN sys.extended_properties f ON d.id=f.major_id AND f.minor_id =0
WHERE d.name='%s'
ORDER BY a.id,a.colorder`,
strings.ToUpper(table),
)
structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql))
result, err = d.DB.DoGetAll(link, structureSql)
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
fields[strings.ToLower(m["FIELD"].String())] = &TableField{
fields[strings.ToLower(m["Field"].String())] = &TableField{
Index: i,
Name: strings.ToLower(m["FIELD"].String()),
Type: strings.ToLower(m["TYPE"].String()),
Name: strings.ToLower(m["Field"].String()),
Type: strings.ToLower(m["Type"].String()),
Null: m["Null"].Bool(),
Key: m["Key"].String(),
Default: m["Default"].Val(),
@ -246,7 +258,7 @@ func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[st
Comment: m["Comment"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gregex"
"github.com/gogf/gf/text/gstr"
@ -102,21 +103,23 @@ func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.cache.GetOrSetFunc(
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`mysql_table_fields_%s_%s`, table, checkSchema),
func() interface{} {
var result Result
var link *sql.DB
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(
link,
fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.DB.QuoteWord(table)),
)
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -131,7 +134,7 @@ func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[st
Comment: m["Comment"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gstr"
"reflect"
"strconv"
@ -78,44 +79,45 @@ func (d *DriverOracle) HandleSqlBeforeCommit(link Link, sql string, args []inter
// parseSql does some replacement of the sql before commits it to underlying driver,
// for support of oracle server.
func (d *DriverOracle) parseSql(sql string) string {
patten := `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,\s*(\d+))`
if gregex.IsMatchString(patten, sql) == false {
var (
patten = `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,{0,1}\s*(\d*))`
allMatch, _ = gregex.MatchAllString(patten, sql)
)
if len(allMatch) == 0 {
return sql
}
res, err := gregex.MatchAllString(patten, sql)
if err != nil {
return ""
}
var (
index = 0
keyword = strings.ToUpper(strings.TrimSpace(res[index][0]))
keyword = strings.ToUpper(strings.TrimSpace(allMatch[index][0]))
)
index++
switch keyword {
case "SELECT":
if len(res) < 2 || (strings.HasPrefix(res[index][0], "LIMIT") == false &&
strings.HasPrefix(res[index][0], "limit") == false) {
if len(allMatch) < 2 || strings.HasPrefix(allMatch[index][0], "LIMIT") == false {
break
}
if gregex.IsMatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql) == false {
break
}
queryExpr, _ := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", sql)
if len(queryExpr) != 4 || strings.EqualFold(queryExpr[1], "SELECT") == false ||
if len(queryExpr) != 4 ||
strings.EqualFold(queryExpr[1], "SELECT") == false ||
strings.EqualFold(queryExpr[3], "LIMIT") == false {
break
}
first, limit := 0, 0
for i := 1; i < len(res[index]); i++ {
if len(strings.TrimSpace(res[index][i])) == 0 {
for i := 1; i < len(allMatch[index]); i++ {
if len(strings.TrimSpace(allMatch[index][i])) == 0 {
continue
}
if strings.HasPrefix(res[index][i], "LIMIT") || strings.HasPrefix(res[index][i], "limit") {
first, _ = strconv.Atoi(res[index][i+1])
limit, _ = strconv.Atoi(res[index][i+2])
if strings.HasPrefix(allMatch[index][i], "LIMIT") {
if allMatch[index][i+2] != "" {
first, _ = strconv.Atoi(allMatch[index][i+1])
limit, _ = strconv.Atoi(allMatch[index][i+2])
} else {
limit, _ = strconv.Atoi(allMatch[index][i+1])
}
break
}
}
@ -157,18 +159,24 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`oracle_table_fields_%s_%s`, table, checkSchema),
func() interface{} {
func() (interface{}, error) {
result := (Result)(nil)
result, err = d.DB.GetAll(fmt.Sprintf(`
SELECT COLUMN_NAME AS FIELD, CASE DATA_TYPE
WHEN 'NUMBER' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN 'FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, strings.ToUpper(table)))
structureSql := fmt.Sprintf(`
SELECT
COLUMN_NAME AS FIELD,
CASE DATA_TYPE
WHEN 'NUMBER' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN 'FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`,
strings.ToUpper(table),
)
structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql))
result, err = d.DB.GetAll(structureSql)
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -178,7 +186,7 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
Type: strings.ToLower(m["TYPE"].String()),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)
@ -188,24 +196,26 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
func (d *DriverOracle) getTableUniqueIndex(table string) (fields map[string]map[string]string, err error) {
table = strings.ToUpper(table)
v := d.DB.GetCache().GetOrSetFunc("table_unique_index_"+table, func() interface{} {
res := (Result)(nil)
res, err = d.DB.GetAll(fmt.Sprintf(`
v, _ := gcache.GetOrSetFunc(
"table_unique_index_"+table,
func() (interface{}, error) {
res := (Result)(nil)
res, err = d.DB.GetAll(fmt.Sprintf(`
SELECT INDEX_NAME,COLUMN_NAME,CHAR_LENGTH FROM USER_IND_COLUMNS
WHERE TABLE_NAME = '%s'
AND INDEX_NAME IN(SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME='%s' AND UNIQUENESS='UNIQUE')
ORDER BY INDEX_NAME,COLUMN_POSITION`, table, table))
if err != nil {
return nil
}
fields := make(map[string]map[string]string)
for _, v := range res {
mm := make(map[string]string)
mm[v["COLUMN_NAME"].String()] = v["CHAR_LENGTH"].String()
fields[v["INDEX_NAME"].String()] = mm
}
return fields
}, 0)
if err != nil {
return nil, err
}
fields := make(map[string]map[string]string)
for _, v := range res {
mm := make(map[string]string)
mm[v["COLUMN_NAME"].String()] = v["CHAR_LENGTH"].String()
fields[v["INDEX_NAME"].String()] = mm
}
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]map[string]string)
}
@ -231,7 +241,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio
case reflect.Map:
fallthrough
case reflect.Struct:
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
default:
return result, errors.New(fmt.Sprint("unsupported data type:", kind))
}
@ -351,12 +361,12 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{},
case reflect.Array:
listMap = make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
listMap[i] = DataToMapDeep(rv.Index(i).Interface())
listMap[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
case reflect.Map:
fallthrough
case reflect.Struct:
listMap = List{Map(DataToMapDeep(list))}
listMap = List{Map(ConvertDataForTableRecord(list))}
default:
return result, errors.New(fmt.Sprint("unsupported list type:", kind))
}

View File

@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gstr"
"strings"
@ -107,21 +108,28 @@ func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`pgsql_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`pgsql_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`
SELECT a.attname AS field, t.typname AS type FROM pg_class c, pg_attribute a
LEFT OUTER JOIN pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid,pg_type t
WHERE c.relname = '%s' and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid
ORDER BY a.attnum`, strings.ToLower(table)))
structureSql := fmt.Sprintf(`
SELECT a.attname AS field, t.typname AS type FROM pg_class c, pg_attribute a
LEFT OUTER JOIN pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid,pg_type t
WHERE c.relname = '%s' and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid
ORDER BY a.attnum`,
strings.ToLower(table),
)
structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql))
result, err = d.DB.DoGetAll(link, structureSql)
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
@ -132,7 +140,7 @@ func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[st
Type: m["type"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/os/gfile"
"github.com/gogf/gf/text/gstr"
"strings"
@ -98,17 +99,20 @@ func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[s
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`sqlite_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`sqlite_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table))
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -118,7 +122,7 @@ func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[s
Type: strings.ToLower(m["type"].String()),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/empty"
"github.com/gogf/gf/internal/json"
"github.com/gogf/gf/internal/utils"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/util/gutil"
@ -97,14 +98,56 @@ func GetInsertOperationByOption(option int) string {
return operator
}
// DataToMapDeep converts struct object to map type recursively.
// ConvertDataForTableRecord is a very important function, which does converting for any data that
// will be inserted into table as a record.
//
// The parameter <obj> should be type of *map/map/*struct/struct.
// It supports inherit struct definition for struct.
func ConvertDataForTableRecord(value interface{}) map[string]interface{} {
var (
rvValue reflect.Value
rvKind reflect.Kind
data = DataToMapDeep(value)
)
for k, v := range data {
rvValue = reflect.ValueOf(v)
rvKind = rvValue.Kind()
for rvKind == reflect.Ptr {
rvValue = rvValue.Elem()
rvKind = rvValue.Kind()
}
switch rvKind {
case reflect.Slice, reflect.Array, reflect.Map:
// It should ignore the bytes type.
if _, ok := v.([]byte); !ok {
// Convert the value to JSON.
data[k], _ = json.Marshal(v)
}
case reflect.Struct:
switch v.(type) {
case time.Time, *time.Time, gtime.Time, *gtime.Time:
continue
default:
// Use string conversion in default.
if s, ok := v.(apiString); ok {
data[k] = s.String()
} else {
// Convert the value to JSON.
data[k], _ = json.Marshal(v)
}
}
}
}
return data
}
// DataToMapDeep converts <value> to map type recursively.
// The parameter <value> should be type of *map/map/*struct/struct.
// It supports inherit struct definition for struct.
func DataToMapDeep(value interface{}) map[string]interface{} {
if v, ok := value.(apiMapStrAny); ok {
return v.MapStrAny()
}
var (
rvValue reflect.Value
rvField reflect.Value
@ -182,7 +225,7 @@ func DataToMapDeep(value interface{}) map[string]interface{} {
// The underlying driver supports time.Time/*time.Time types.
fieldValue := rvField.Interface()
switch fieldValue.(type) {
case time.Time, *time.Time:
case time.Time, *time.Time, gtime.Time, *gtime.Time:
data[name] = fieldValue
default:
// Use string conversion in default.
@ -317,8 +360,10 @@ func GetPrimaryKeyCondition(primary string, where ...interface{}) (newWhereCondi
return where
}
if len(where) == 1 {
rv := reflect.ValueOf(where[0])
kind := rv.Kind()
var (
rv = reflect.ValueOf(where[0])
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
@ -341,9 +386,10 @@ func GetPrimaryKeyCondition(primary string, where ...interface{}) (newWhereCondi
// The internal handleArguments function might be called twice during the SQL procedure,
// but do not worry about it, it's safe and efficient.
func formatSql(sql string, args []interface{}) (newSql string, newArgs []interface{}) {
sql = gstr.Trim(sql)
sql = gstr.Replace(sql, "\n", " ")
sql, _ = gregex.ReplaceString(`\s{2,}`, ` `, sql)
// DO NOT do this as there may be multiple lines and comments in the sql.
// sql = gstr.Trim(sql)
// sql = gstr.Replace(sql, "\n", " ")
// sql, _ = gregex.ReplaceString(`\s{2,}`, ` `, sql)
return handleArguments(sql, args)
}

View File

@ -12,13 +12,6 @@ import (
"github.com/gogf/gf/os/gtime"
)
// Unscoped disables the soft deleting feature.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
// Delete does "DELETE FROM ... " statement for the model.
// The optional parameter <where> is the same as the parameter of Model.Where function,
// see Model.Where.
@ -32,7 +25,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
}
}()
var (
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameDelete = m.getSoftFieldNameDeleted()
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
)
// Soft deleting.

View File

@ -24,49 +24,35 @@ func (m *Model) Filter() *Model {
}
// Fields sets the operation fields of the model, multiple fields joined using char ','.
func (m *Model) Fields(fields string) *Model {
model := m.getModel()
model.fields = fields
return model
func (m *Model) Fields(fields ...string) *Model {
if len(fields) > 0 {
model := m.getModel()
model.fields = gstr.Join(fields, ",")
return model
}
return m
}
// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
// Note that this function supports only single table operations.
func (m *Model) FieldsEx(fields string) *Model {
if gstr.Contains(m.tables, " ") {
panic("function FieldsEx supports only single table operations")
func (m *Model) FieldsEx(fields ...string) *Model {
if len(fields) > 0 {
model := m.getModel()
model.fieldsEx = gstr.Join(fields, ",")
return model
}
tableFields, err := m.db.TableFields(m.tables)
if err != nil {
panic(err)
}
if len(tableFields) == 0 {
panic(fmt.Sprintf(`empty table fields for table "%s"`, m.tables))
}
model := m.getModel()
model.fieldsEx = fields
fieldsExSet := gset.NewStrSetFrom(gstr.SplitAndTrim(fields, ","))
fieldsArray := make([]string, len(tableFields))
for k, v := range tableFields {
fieldsArray[v.Index] = k
}
model.fields = ""
for _, k := range fieldsArray {
if fieldsExSet.Contains(k) {
continue
}
if len(model.fields) > 0 {
model.fields += ","
}
model.fields += k
}
model.fields = model.db.QuoteString(model.fields)
return model
return m
}
// Deprecated, use GetFieldsStr instead.
// This function name confuses the user that it was a chaining function.
func (m *Model) FieldsStr(prefix ...string) string {
return m.GetFieldsStr(prefix...)
}
// FieldsStr retrieves and returns all fields from the table, joined with char ','.
// The optional parameter <prefix> specifies the prefix for each field, eg: FieldsStr("u.").
func (m *Model) FieldsStr(prefix ...string) string {
func (m *Model) GetFieldsStr(prefix ...string) string {
prefixStr := ""
if len(prefix) > 0 {
prefixStr = prefix[0]
@ -93,11 +79,17 @@ func (m *Model) FieldsStr(prefix ...string) string {
return newFields
}
// Deprecated, use GetFieldsExStr instead.
// This function name confuses the user that it was a chaining function.
func (m *Model) FieldsExStr(fields string, prefix ...string) string {
return m.GetFieldsExStr(fields, prefix...)
}
// FieldsExStr retrieves and returns fields which are not in parameter <fields> from the table,
// joined with char ','.
// The parameter <fields> specifies the fields that are excluded.
// The optional parameter <prefix> specifies the prefix for each field, eg: FieldsExStr("id", "u.").
func (m *Model) FieldsExStr(fields string, prefix ...string) string {
func (m *Model) GetFieldsExStr(fields string, prefix ...string) string {
prefixStr := ""
if len(prefix) > 0 {
prefixStr = prefix[0]

View File

@ -56,8 +56,10 @@ func (m *Model) Data(data ...interface{}) *Model {
case Map:
model.data = params
default:
rv := reflect.ValueOf(params)
kind := rv.Kind()
var (
rv = reflect.ValueOf(params)
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
@ -66,11 +68,11 @@ func (m *Model) Data(data ...interface{}) *Model {
case reflect.Slice, reflect.Array:
list := make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
list[i] = DataToMapDeep(rv.Index(i).Interface())
list[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
model.data = list
case reflect.Map:
model.data = DataToMapDeep(data[0])
model.data = ConvertDataForTableRecord(data[0])
case reflect.Struct:
if v, ok := data[0].(apiInterfaces); ok {
var (
@ -78,11 +80,11 @@ func (m *Model) Data(data ...interface{}) *Model {
list = make(List, len(array))
)
for i := 0; i < len(array); i++ {
list[i] = DataToMapDeep(array[i])
list[i] = ConvertDataForTableRecord(array[i])
}
model.data = list
} else {
model.data = DataToMapDeep(data[0])
model.data = ConvertDataForTableRecord(data[0])
}
default:
model.data = data[0]
@ -147,9 +149,9 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
}
var (
nowString = gtime.Now().String()
fieldNameCreate = m.getSoftFieldNameCreate()
fieldNameUpdate = m.getSoftFieldNameUpdate()
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameCreate = m.getSoftFieldNameCreated()
fieldNameUpdate = m.getSoftFieldNameUpdated()
fieldNameDelete = m.getSoftFieldNameDeleted()
)
// Batch operation.
if list, ok := m.data.(List); ok {
@ -170,10 +172,14 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
list[k] = v
}
}
newData, err := m.filterDataForInsertOrUpdate(list)
if err != nil {
return nil, err
}
return m.db.DoBatchInsert(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(list),
newData,
option,
batch,
)
@ -190,10 +196,14 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
data[fieldNameUpdate] = nowString
}
}
newData, err := m.filterDataForInsertOrUpdate(data)
if err != nil {
return nil, err
}
return m.db.DoInsert(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(data),
newData,
option,
)
}

View File

@ -6,7 +6,21 @@
package gdb
import "fmt"
import (
"fmt"
"github.com/gogf/gf/text/gstr"
)
// isSubQuery checks and returns whether given string a sub-query sql string.
func isSubQuery(s string) bool {
s = gstr.TrimLeft(s)
if p := gstr.Pos(s, " "); p != -1 {
if gstr.Equal(s[:p], "select") {
return true
}
}
return false
}
// LeftJoin does "LEFT JOIN ... ON ..." statement on the model.
// The parameter <table> can be joined table and its joined condition,
@ -14,19 +28,32 @@ import "fmt"
// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
func (m *Model) LeftJoin(table ...string) *Model {
model := m.getModel()
var (
model = m.getModel()
joinStr = ""
)
if len(table) > 0 {
if isSubQuery(table[0]) {
joinStr = "(" + table[0] + ")"
} else {
joinStr = m.db.QuotePrefixTableName(table[0])
}
}
if len(table) > 2 {
model.tables += fmt.Sprintf(
" LEFT JOIN %s AS %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2],
joinStr, m.db.QuoteWord(table[1]), table[2],
)
} else if len(table) == 2 {
model.tables += fmt.Sprintf(
" LEFT JOIN %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), table[1],
joinStr, table[1],
)
} else if len(table) == 1 {
model.tables += fmt.Sprintf(
" LEFT JOIN %s",
joinStr,
)
} else {
panic("invalid join table parameter")
}
return model
}
@ -37,19 +64,32 @@ func (m *Model) LeftJoin(table ...string) *Model {
// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
func (m *Model) RightJoin(table ...string) *Model {
model := m.getModel()
var (
model = m.getModel()
joinStr = ""
)
if len(table) > 0 {
if isSubQuery(table[0]) {
joinStr = "(" + table[0] + ")"
} else {
joinStr = m.db.QuotePrefixTableName(table[0])
}
}
if len(table) > 2 {
model.tables += fmt.Sprintf(
" RIGHT JOIN %s AS %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2],
joinStr, m.db.QuoteWord(table[1]), table[2],
)
} else if len(table) == 2 {
model.tables += fmt.Sprintf(
" RIGHT JOIN %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), table[1],
joinStr, table[1],
)
} else if len(table) == 1 {
model.tables += fmt.Sprintf(
" RIGHT JOIN %s",
joinStr,
)
} else {
panic("invalid join table parameter")
}
return model
}
@ -60,19 +100,32 @@ func (m *Model) RightJoin(table ...string) *Model {
// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
func (m *Model) InnerJoin(table ...string) *Model {
model := m.getModel()
var (
model = m.getModel()
joinStr = ""
)
if len(table) > 0 {
if isSubQuery(table[0]) {
joinStr = "(" + table[0] + ")"
} else {
joinStr = m.db.QuotePrefixTableName(table[0])
}
}
if len(table) > 2 {
model.tables += fmt.Sprintf(
" INNER JOIN %s AS %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), m.db.QuoteWord(table[1]), table[2],
joinStr, m.db.QuoteWord(table[1]), table[2],
)
} else if len(table) == 2 {
model.tables += fmt.Sprintf(
" INNER JOIN %s ON (%s)",
m.db.QuotePrefixTableName(table[0]), table[1],
joinStr, table[1],
)
} else if len(table) == 1 {
model.tables += fmt.Sprintf(
" INNER JOIN %s",
joinStr,
)
} else {
panic("invalid join table parameter")
}
return model
}

View File

@ -8,7 +8,10 @@ package gdb
import (
"fmt"
"github.com/gogf/gf/container/gset"
"github.com/gogf/gf/container/gvar"
"github.com/gogf/gf/internal/json"
"github.com/gogf/gf/text/gstr"
"github.com/gogf/gf/util/gconv"
"reflect"
)
@ -53,12 +56,13 @@ func (m *Model) doGetAll(limit1 bool, where ...interface{}) (Result, error) {
}
conditionWhere += softDeletingCondition
}
// DO NOT quote the m.fields where, in case of fields like:
// DISTINCT t.user_id uid
return m.doGetAllBySql(
fmt.Sprintf(
"SELECT %s FROM %s%s",
m.fields,
m.getFieldsFiltered(),
m.tables,
conditionWhere+conditionExtra,
),
@ -66,6 +70,53 @@ func (m *Model) doGetAll(limit1 bool, where ...interface{}) (Result, error) {
)
}
// getFieldsFiltered checks the fields and fieldsEx attributes, filters and returns the fields that will
// really be committed to underlying database driver.
func (m *Model) getFieldsFiltered() string {
if m.fieldsEx == "" {
// No filtering.
return m.fields
}
var (
fieldsArray []string
fieldsExSet = gset.NewStrSetFrom(gstr.SplitAndTrim(m.fieldsEx, ","))
)
if m.fields != "*" {
// Filter custom fields with fieldEx.
fieldsArray = make([]string, 0, 8)
for _, v := range gstr.SplitAndTrim(m.fields, ",") {
fieldsArray = append(fieldsArray, v[gstr.PosR(v, "-")+1:])
}
} else {
if gstr.Contains(m.tables, " ") {
panic("function FieldsEx supports only single table operations")
}
// Filter table fields with fieldEx.
tableFields, err := m.db.TableFields(m.tables)
if err != nil {
panic(err)
}
if len(tableFields) == 0 {
panic(fmt.Sprintf(`empty table fields for table "%s"`, m.tables))
}
fieldsArray = make([]string, len(tableFields))
for k, v := range tableFields {
fieldsArray[v.Index] = k
}
}
newFields := ""
for _, k := range fieldsArray {
if fieldsExSet.Contains(k) {
continue
}
if len(newFields) > 0 {
newFields += ","
}
newFields += k
}
return newFields
}
// Chunk iterates the query result with given size and callback function.
func (m *Model) Chunk(limit int, callback func(result Result, err error) bool) {
page := m.start
@ -381,23 +432,35 @@ func (m *Model) FindScan(pointer interface{}, where ...interface{}) error {
// doGetAllBySql does the select statement on the database.
func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, err error) {
cacheKey := ""
cacheObj := m.db.GetCache()
// Retrieve from cache.
if m.cacheEnabled && m.tx == nil {
cacheKey = m.cacheName
if len(cacheKey) == 0 {
cacheKey = sql + "/" + gconv.String(args)
cacheKey = sql + ", @PARAMS:" + gconv.String(args)
}
if v := m.db.GetCache().Get(cacheKey); v != nil {
return v.(Result), nil
if v, _ := cacheObj.GetVar(cacheKey); !v.IsNil() {
if result, ok := v.Val().(Result); ok {
// In-memory cache.
return result, nil
} else {
// Other cache, it needs conversion.
var result Result
if err = json.Unmarshal(v.Bytes(), &result); err != nil {
return nil, err
} else {
return result, nil
}
}
}
}
result, err = m.db.DoGetAll(m.getLink(false), sql, m.mergeArguments(args)...)
// Cache the result.
if cacheKey != "" && err == nil {
if m.cacheDuration < 0 {
m.db.GetCache().Remove(cacheKey)
cacheObj.Remove(cacheKey)
} else {
m.db.GetCache().Set(cacheKey, result, m.cacheDuration)
cacheObj.Set(cacheKey, result, m.cacheDuration)
}
}
return result, err

View File

@ -15,58 +15,82 @@ import (
"github.com/gogf/gf/util/gutil"
)
const (
gSOFT_FIELD_NAME_CREATE = "create_at"
gSOFT_FIELD_NAME_UPDATE = "update_at"
gSOFT_FIELD_NAME_DELETE = "delete_at"
var (
createdFiledNames = []string{"created_at", "create_at"} // Default filed names of table for automatic-filled created datetime.
updatedFiledNames = []string{"updated_at", "update_at"} // Default filed names of table for automatic-filled updated datetime.
deletedFiledNames = []string{"updated_at", "delete_at"} // Default filed names of table for automatic-filled deleted datetime.
)
// Unscoped disables the auto-update time feature for insert, update and delete options.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
// getSoftFieldNameCreate checks and returns the field name for record creating time.
// If there's no field name for storing creating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameCreate(table ...string) string {
func (m *Model) getSoftFieldNameCreated(table ...string) string {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_CREATE)
config := m.db.GetConfig()
if config.CreatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.CreatedAt}, createdFiledNames...))
}
return m.getSoftFieldName(tableName, createdFiledNames)
}
// getSoftFieldNameUpdate checks and returns the field name for record updating time.
// If there's no field name for storing updating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameUpdate(table ...string) (field string) {
func (m *Model) getSoftFieldNameUpdated(table ...string) (field string) {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_UPDATE)
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.UpdatedAt}, updatedFiledNames...))
}
return m.getSoftFieldName(tableName, updatedFiledNames)
}
// getSoftFieldNameDelete checks and returns the field name for record deleting time.
// If there's no field name for storing deleting time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameDelete(table ...string) (field string) {
func (m *Model) getSoftFieldNameDeleted(table ...string) (field string) {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_DELETE)
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.DeletedAt}, deletedFiledNames...))
}
return m.getSoftFieldName(tableName, deletedFiledNames)
}
// getSoftFieldName retrieves and returns the field name of the table for possible key.
func (m *Model) getSoftFieldName(table string, key string) (field string) {
func (m *Model) getSoftFieldName(table string, keys []string) (field string) {
fieldsMap, _ := m.db.TableFields(table)
if len(fieldsMap) > 0 {
field, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), key,
)
for _, key := range keys {
field, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), key,
)
if field != "" {
return
}
}
}
return
}
@ -86,8 +110,8 @@ func (m *Model) getConditionForSoftDeleting() string {
// Base table.
match, _ := gregex.MatchString(`(.+?) [A-Z]+ JOIN`, m.tables)
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1]))
// Multiple joined tables.
matches, _ := gregex.MatchAllString(`JOIN (.+?) ON`, m.tables)
// Multiple joined tables, exclude the sub query sql which contains char '(' and ')'.
matches, _ := gregex.MatchAllString(`JOIN ([^()]+?) ON`, m.tables)
for _, match := range matches {
conditionArray.Append(m.getConditionOfTableStringForSoftDeleting(match[1]))
}
@ -103,7 +127,7 @@ func (m *Model) getConditionForSoftDeleting() string {
return conditionArray.Join(" AND ")
}
// Only one table.
if fieldName := m.getSoftFieldNameDelete(); fieldName != "" {
if fieldName := m.getSoftFieldNameDeleted(); fieldName != "" {
return fmt.Sprintf(`%s IS NULL`, m.db.QuoteWord(fieldName))
}
return ""
@ -122,7 +146,7 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string {
} else {
table = array2[0]
}
field = m.getSoftFieldNameDelete(table)
field = m.getSoftFieldNameDeleted(table)
if field == "" {
return ""
}

View File

@ -42,9 +42,9 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
}
var (
updateData = m.data
fieldNameCreate = m.getSoftFieldNameCreate()
fieldNameUpdate = m.getSoftFieldNameUpdate()
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameCreate = m.getSoftFieldNameCreated()
fieldNameUpdate = m.getSoftFieldNameUpdated()
fieldNameDelete = m.getSoftFieldNameDeleted()
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
)
// Automatically update the record updating time.
@ -59,7 +59,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
}
switch refKind {
case reflect.Map, reflect.Struct:
dataMap := DataToMapDeep(m.data)
dataMap := ConvertDataForTableRecord(m.data)
gutil.MapDelete(dataMap, fieldNameCreate, fieldNameUpdate, fieldNameDelete)
if fieldNameUpdate != "" {
dataMap[fieldNameUpdate] = gtime.Now().String()
@ -73,10 +73,14 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
updateData = updates
}
}
newData, err := m.filterDataForInsertOrUpdate(updateData)
if err != nil {
return nil, err
}
return m.db.DoUpdate(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(updateData),
newData,
conditionWhere+conditionExtra,
m.mergeArguments(conditionArgs)...,
)

View File

@ -28,27 +28,33 @@ func (m *Model) getModel() *Model {
// filterDataForInsertOrUpdate does filter feature with data for inserting/updating operations.
// Note that, it does not filter list item, which is also type of map, for "omit empty" feature.
func (m *Model) filterDataForInsertOrUpdate(data interface{}) interface{} {
func (m *Model) filterDataForInsertOrUpdate(data interface{}) (interface{}, error) {
var err error
switch value := data.(type) {
case List:
for k, item := range value {
value[k] = m.doFilterDataMapForInsertOrUpdate(item, false)
value[k], err = m.doMappingAndFilterForInsertOrUpdateDataMap(item, false)
if err != nil {
return nil, err
}
}
return value
return value, nil
case Map:
return m.doFilterDataMapForInsertOrUpdate(value, true)
return m.doMappingAndFilterForInsertOrUpdateDataMap(value, true)
default:
return data
return data, nil
}
}
// doFilterDataMapForInsertOrUpdate does the filter features for map.
// doMappingAndFilterForInsertOrUpdateDataMap does the filter features for map.
// Note that, it does not filter list item, which is also type of map, for "omit empty" feature.
func (m *Model) doFilterDataMapForInsertOrUpdate(data Map, allowOmitEmpty bool) Map {
if m.filter {
data = m.db.filterFields(m.schema, m.tables, data)
func (m *Model) doMappingAndFilterForInsertOrUpdateDataMap(data Map, allowOmitEmpty bool) (Map, error) {
var err error
data, err = m.db.mappingAndFilterData(m.schema, m.tables, data, m.filter)
if err != nil {
return nil, err
}
// Remove key-value pairs of which the value is empty.
if allowOmitEmpty && m.option&OPTION_OMITEMPTY > 0 {
@ -103,7 +109,7 @@ func (m *Model) doFilterDataMapForInsertOrUpdate(data Map, allowOmitEmpty bool)
delete(data, v)
}
}
return data
return data, nil
}
// getLink returns the underlying database link object with configured <linkType> attribute.

View File

@ -115,7 +115,6 @@ func (tx *TX) GetScan(objPointer interface{}, sql string, args ...interface{}) e
default:
return fmt.Errorf("element type should be type of struct/slice, unsupported: %v", k)
}
return nil
}
// GetValue queries and returns the field value from database.
@ -154,7 +153,10 @@ func (tx *TX) GetCount(sql string, args ...interface{}) (int, error) {
//
// The parameter <batch> specifies the batch operation count when given data is slice.
func (tx *TX) Insert(table string, data interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoInsert(tx.tx, table, data, gINSERT_OPTION_DEFAULT, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(data).Batch(batch[0]).Insert()
}
return tx.Model(table).Data(data).Insert()
}
// InsertIgnore does "INSERT IGNORE INTO ..." statement for the table.
@ -167,7 +169,10 @@ func (tx *TX) Insert(table string, data interface{}, batch ...int) (sql.Result,
//
// The parameter <batch> specifies the batch operation count when given data is slice.
func (tx *TX) InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoInsert(tx.tx, table, data, gINSERT_OPTION_IGNORE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(data).Batch(batch[0]).InsertIgnore()
}
return tx.Model(table).Data(data).InsertIgnore()
}
// Replace does "REPLACE INTO ..." statement for the table.
@ -183,7 +188,10 @@ func (tx *TX) InsertIgnore(table string, data interface{}, batch ...int) (sql.Re
// If given data is type of slice, it then does batch replacing, and the optional parameter
// <batch> specifies the batch operation count.
func (tx *TX) Replace(table string, data interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoInsert(tx.tx, table, data, gINSERT_OPTION_REPLACE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(data).Batch(batch[0]).Replace()
}
return tx.Model(table).Data(data).Replace()
}
// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the table.
@ -198,31 +206,46 @@ func (tx *TX) Replace(table string, data interface{}, batch ...int) (sql.Result,
// If given data is type of slice, it then does batch saving, and the optional parameter
// <batch> specifies the batch operation count.
func (tx *TX) Save(table string, data interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoInsert(tx.tx, table, data, gINSERT_OPTION_SAVE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(data).Batch(batch[0]).Save()
}
return tx.Model(table).Data(data).Save()
}
// BatchInsert batch inserts data.
// The parameter <list> must be type of slice of map or struct.
func (tx *TX) BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoBatchInsert(tx.tx, table, list, gINSERT_OPTION_DEFAULT, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(list).Batch(batch[0]).Insert()
}
return tx.Model(table).Data(list).Insert()
}
// BatchInsert batch inserts data with ignore option.
// The parameter <list> must be type of slice of map or struct.
func (tx *TX) BatchInsertIgnore(table string, list interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoBatchInsert(tx.tx, table, list, gINSERT_OPTION_IGNORE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(list).Batch(batch[0]).InsertIgnore()
}
return tx.Model(table).Data(list).InsertIgnore()
}
// BatchReplace batch replaces data.
// The parameter <list> must be type of slice of map or struct.
func (tx *TX) BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoBatchInsert(tx.tx, table, list, gINSERT_OPTION_REPLACE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(list).Batch(batch[0]).Replace()
}
return tx.Model(table).Data(list).Replace()
}
// BatchSave batch replaces data.
// The parameter <list> must be type of slice of map or struct.
func (tx *TX) BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) {
return tx.db.DoBatchInsert(tx.tx, table, list, gINSERT_OPTION_SAVE, batch...)
if len(batch) > 0 {
return tx.Model(table).Data(list).Batch(batch[0]).Save()
}
return tx.Model(table).Data(list).Save()
}
// Update does "UPDATE ... " statement for the table.
@ -240,11 +263,7 @@ func (tx *TX) BatchSave(table string, list interface{}, batch ...int) (sql.Resul
// "age IN(?,?)", 18, 50
// User{ Id : 1, UserName : "john"}
func (tx *TX) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) {
newWhere, newArgs := formatWhere(tx.db, condition, args, false)
if newWhere != "" {
newWhere = " WHERE " + newWhere
}
return tx.db.DoUpdate(tx.tx, table, data, newWhere, newArgs...)
return tx.Model(table).Data(data).Where(condition, args...).Update()
}
// Delete does "DELETE FROM ... " statement for the table.
@ -259,9 +278,5 @@ func (tx *TX) Update(table string, data interface{}, condition interface{}, args
// "age IN(?,?)", 18, 50
// User{ Id : 1, UserName : "john"}
func (tx *TX) Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) {
newWhere, newArgs := formatWhere(tx.db, condition, args, false)
if newWhere != "" {
newWhere = " WHERE " + newWhere
}
return tx.db.DoDelete(tx.tx, table, newWhere, newArgs...)
return tx.Model(table).Where(condition, args...).Delete()
}

View File

@ -181,7 +181,7 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
}
}
if len(relationDataMap) > 0 && !relationValue.IsValid() {
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
switch attrKind {
case reflect.Array, reflect.Slice:
@ -196,7 +196,7 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
return fmt.Errorf(`relationKey should not be empty as field "%s" is slice`, attributeName)
@ -207,15 +207,25 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
if len(relationDataMap) > 0 {
relationField = relationValue.FieldByName(relationAttrName)
if relationField.IsValid() {
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
v := relationDataMap[gconv.String(relationField.Interface())]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
if err = gconv.Struct(r[i], e); err != nil {
v := r[i]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
}
@ -226,15 +236,25 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
if len(relationDataMap) > 0 {
relationField = relationValue.FieldByName(relationAttrName)
if relationField.IsValid() {
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
v := relationDataMap[gconv.String(relationField.Interface())]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
if err = gconv.Struct(r[i], e); err != nil {
v := r[i]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
}

View File

@ -17,7 +17,152 @@ import (
"github.com/gogf/gf/test/gtest"
)
func Test_Table_Relation(t *testing.T) {
func Test_Table_Relation_One(t *testing.T) {
var (
tableUser = "user_" + gtime.TimestampMicroStr()
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()
tableUserScores = "user_scores_" + gtime.TimestampMicroStr()
)
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
name varchar(45) NOT NULL,
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, tableUser)); err != nil {
gtest.Error(err)
}
defer dropTable(tableUser)
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
address varchar(45) NOT NULL,
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, tableUserDetail)); err != nil {
gtest.Error(err)
}
defer dropTable(tableUserDetail)
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
uid int(10) unsigned NOT NULL,
score int(10) unsigned NOT NULL,
course varchar(45) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, tableUserScores)); err != nil {
gtest.Error(err)
}
defer dropTable(tableUserScores)
type EntityUser struct {
Uid int `orm:"uid"`
Name string `orm:"name"`
}
type EntityUserDetail struct {
Uid int `orm:"uid"`
Address string `orm:"address"`
}
type EntityUserScores struct {
Id int `orm:"id"`
Uid int `orm:"uid"`
Score int `orm:"score"`
Course string `orm:"course"`
}
type Entity struct {
User *EntityUser
UserDetail *EntityUserDetail
UserScores []*EntityUserScores
}
// Initialize the data.
var err error
gtest.C(t, func(t *gtest.T) {
err = db.Transaction(func(tx *gdb.TX) error {
r, err := tx.Table(tableUser).Save(EntityUser{
Name: "john",
})
if err != nil {
return err
}
uid, err := r.LastInsertId()
if err != nil {
return err
}
_, err = tx.Table(tableUserDetail).Save(EntityUserDetail{
Uid: int(uid),
Address: "Beijing DongZhiMen #66",
})
if err != nil {
return err
}
_, err = tx.Table(tableUserScores).Save(g.Slice{
EntityUserScores{Uid: int(uid), Score: 100, Course: "math"},
EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"},
})
return err
})
t.Assert(err, nil)
})
// Data check.
gtest.C(t, func(t *gtest.T) {
r, err := db.Table(tableUser).All()
t.Assert(err, nil)
t.Assert(r.Len(), 1)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[0]["name"].String(), "john")
r, err = db.Table(tableUserDetail).Where("uid", r[0]["uid"].Int()).All()
t.Assert(err, nil)
t.Assert(r.Len(), 1)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[0]["address"].String(), `Beijing DongZhiMen #66`)
r, err = db.Table(tableUserScores).Where("uid", r[0]["uid"].Int()).All()
t.Assert(err, nil)
t.Assert(r.Len(), 2)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[1]["uid"].Int(), 1)
t.Assert(r[0]["course"].String(), `math`)
t.Assert(r[1]["course"].String(), `physics`)
})
// Entity query.
gtest.C(t, func(t *gtest.T) {
var user Entity
// SELECT * FROM `user` WHERE `name`='john'
err := db.Table(tableUser).Scan(&user.User, "name", "john")
t.Assert(err, nil)
// SELECT * FROM `user_detail` WHERE `uid`=1
err = db.Table(tableUserDetail).Scan(&user.UserDetail, "uid", user.User.Uid)
t.Assert(err, nil)
// SELECT * FROM `user_scores` WHERE `uid`=1
err = db.Table(tableUserScores).Scan(&user.UserScores, "uid", user.User.Uid)
t.Assert(err, nil)
t.Assert(user.User, EntityUser{
Uid: 1,
Name: "john",
})
t.Assert(user.UserDetail, EntityUserDetail{
Uid: 1,
Address: "Beijing DongZhiMen #66",
})
t.Assert(user.UserScores, []EntityUserScores{
{Id: 1, Uid: 1, Course: "math", Score: 100},
{Id: 2, Uid: 1, Course: "physics", Score: 99},
})
})
}
func Test_Table_Relation_Many(t *testing.T) {
var (
tableUser = "user_" + gtime.TimestampMicroStr()
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()

View File

@ -204,9 +204,9 @@ CREATE TABLE %s (
gtest.C(t, func(t *gtest.T) {
model := db.Table(table1)
gtest.Assert(model.getSoftFieldNameCreate(table2), "createat")
gtest.Assert(model.getSoftFieldNameUpdate(table2), "updateat")
gtest.Assert(model.getSoftFieldNameDelete(table2), "deleteat")
gtest.Assert(model.getSoftFieldNameCreated(table2), "createat")
gtest.Assert(model.getSoftFieldNameUpdated(table2), "updateat")
gtest.Assert(model.getSoftFieldNameDeleted(table2), "deleteat")
})
}
@ -292,14 +292,23 @@ CREATE TABLE %s (
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_Func_DataToMapDeep(t *testing.T) {
func Test_Func_ConvertDataForTableRecord(t *testing.T) {
type Test struct {
ResetPasswordTokenAt mysql.NullTime `orm:"reset_password_token_at"`
}
gtest.C(t, func(t *gtest.T) {
m := DataToMapDeep(new(Test))
m := ConvertDataForTableRecord(new(Test))
t.Assert(len(m), 1)
t.AssertNE(m["reset_password_token_at"], nil)
t.Assert(m["reset_password_token_at"], new(mysql.NullTime))
})
}
func Test_isSubQuery(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(isSubQuery("user"), false)
t.Assert(isSubQuery("user.uid"), false)
t.Assert(isSubQuery("u, user.uid"), false)
t.Assert(isSubQuery("select 1"), true)
})
}

View File

@ -9,6 +9,7 @@ package gdb_test
import (
"fmt"
"github.com/gogf/gf/container/garray"
"github.com/gogf/gf/encoding/gparser"
"testing"
"time"
@ -183,6 +184,119 @@ func Test_DB_Insert(t *testing.T) {
})
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_DB_Insert_WithStructAndSliceAttribute(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Password struct {
Salt string `json:"salt"`
Pass string `json:"pass"`
}
data := g.Map{
"id": 1,
"passport": "t1",
"password": &Password{"123", "456"},
"nickname": []string{"A", "B", "C"},
"create_time": gtime.Now().String(),
}
_, err := db.Insert(table, data)
t.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], data["create_time"])
t.Assert(one["nickname"], gparser.MustToJson(data["nickname"]))
})
}
func Test_DB_Insert_KeyFieldNameMapping(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Insert(table, data)
t.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_DB_Upadte_KeyFieldNameMapping(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_10",
Password: "pass_10",
Nickname: "name_10",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Update(table, data, "id=1")
t.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_DB_Insert_KeyFieldNameMapping_Error(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
NoneExistField string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Insert(table, data)
t.AssertNE(err, nil)
})
}
func Test_DB_InsertIgnore(t *testing.T) {
table := createInitTable()
defer dropTable(table)

View File

@ -11,6 +11,7 @@ import (
"fmt"
"github.com/gogf/gf/container/garray"
"github.com/gogf/gf/container/gmap"
"github.com/gogf/gf/encoding/gparser"
"github.com/gogf/gf/util/gutil"
"testing"
"time"
@ -52,20 +53,20 @@ func Test_Model_Insert(t *testing.T) {
t.Assert(n, 1)
type User struct {
Id int `gconv:"id"`
Uid int `gconv:"uid"`
Passport string `json:"passport"`
Password string `gconv:"password"`
Nickname string `gconv:"nickname"`
CreateTime string `json:"create_time"`
Id int `gconv:"id"`
Uid int `gconv:"uid"`
Passport string `json:"passport"`
Password string `gconv:"password"`
Nickname string `gconv:"nickname"`
CreateTime *gtime.Time `json:"create_time"`
}
// Model inserting.
result, err = db.Table(table).Filter().Data(User{
Id: 3,
Uid: 3,
Passport: "t3",
Password: "25d55ad283aa400af464c76d713c07ad",
Nickname: "name_3",
CreateTime: gtime.Now().String(),
Id: 3,
Uid: 3,
Passport: "t3",
Password: "25d55ad283aa400af464c76d713c07ad",
Nickname: "name_3",
}).Insert()
t.Assert(err, nil)
n, _ = result.RowsAffected()
@ -80,7 +81,7 @@ func Test_Model_Insert(t *testing.T) {
Passport: "t4",
Password: "25d55ad283aa400af464c76d713c07ad",
Nickname: "T4",
CreateTime: gtime.Now().String(),
CreateTime: gtime.Now(),
}).Insert()
t.Assert(err, nil)
n, _ = result.RowsAffected()
@ -94,7 +95,164 @@ func Test_Model_Insert(t *testing.T) {
n, _ = result.RowsAffected()
t.Assert(n, 3)
})
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_Model_Insert_WithStructAndSliceAttribute(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Password struct {
Salt string `json:"salt"`
Pass string `json:"pass"`
}
data := g.Map{
"id": 1,
"passport": "t1",
"password": &Password{"123", "456"},
"nickname": []string{"A", "B", "C"},
"create_time": gtime.Now().String(),
}
_, err := db.Table(table).Data(data).Insert()
t.Assert(err, nil)
one, err := db.Table(table).One("id", 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], data["create_time"])
t.Assert(one["nickname"], gparser.MustToJson(data["nickname"]))
})
}
func Test_Model_Insert_KeyFieldNameMapping(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Model(table).Data(data).Insert()
t.Assert(err, nil)
one, err := db.Model(table).FindOne(1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_Model_Update_KeyFieldNameMapping(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_10",
Password: "pass_10",
Nickname: "name_10",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Model(table).Data(data).WherePri(1).Update()
t.Assert(err, nil)
one, err := db.Model(table).FindOne(1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_Model_Insert_KeyFieldNameMapping_Error(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
NoneExistFiled string
}
data := User{
Id: 1,
Passport: "user_1",
Password: "pass_1",
Nickname: "name_1",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Model(table).Data(data).Insert()
t.AssertNE(err, nil)
})
}
func Test_Model_Insert_Time(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "t1",
"password": "p1",
"nickname": "n1",
"create_time": "2020-10-10 20:09:18.334",
}
_, err := db.Table(table).Data(data).Insert()
t.Assert(err, nil)
one, err := db.Table(table).One("id", 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], "2020-10-10 20:09:18")
t.Assert(one["nickname"], data["nickname"])
})
}
func Test_Model_BatchInsertWithArrayStruct(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
user := db.Table(table)
array := garray.New()
for i := 1; i <= SIZE; i++ {
array.Append(g.Map{
"id": i,
"uid": i,
"passport": fmt.Sprintf("t%d", i),
"password": "25d55ad283aa400af464c76d713c07ad",
"nickname": fmt.Sprintf("name_%d", i),
"create_time": gtime.Now().String(),
})
}
result, err := user.Filter().Data(array).Insert()
t.Assert(err, nil)
n, _ := result.LastInsertId()
t.Assert(n, SIZE)
})
}
func Test_Model_InsertIgnore(t *testing.T) {
@ -2243,6 +2401,19 @@ func Test_Model_DryRun(t *testing.T) {
})
}
func Test_Model_Join_SubQuery(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
subQuery := fmt.Sprintf("select * from `%s`", table)
r, err := db.Table(table, "t1").Fields("t2.id").LeftJoin(subQuery, "t2", "t2.id=t1.id").Array()
t.Assert(err, nil)
t.Assert(len(r), SIZE)
t.Assert(r[0], "1")
t.Assert(r[SIZE-1], SIZE)
})
}
func Test_Model_Cache(t *testing.T) {
table := createInitTable()
defer dropTable(table)

View File

@ -151,7 +151,7 @@ CREATE TABLE %s (
}
func Test_SoftUpdateTime(t *testing.T) {
table := "time_test_table"
table := "time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,

View File

@ -764,3 +764,31 @@ func Test_Transaction(t *testing.T) {
}
})
}
func Test_Transaction_Panic(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
err := db.Transaction(func(tx *gdb.TX) error {
if _, err := tx.Replace(table, g.Map{
"id": 1,
"passport": "USER_1",
"password": "PASS_1",
"nickname": "NAME_1",
"create_time": gtime.Now().String(),
}); err != nil {
t.Error(err)
}
panic("error")
return nil
})
t.AssertNE(err, nil)
if value, err := db.Table(table).Fields("nickname").Where("id", 1).Value(); err != nil {
gtest.Error(err)
} else {
t.Assert(value.String(), "name_1")
}
})
}

View File

@ -0,0 +1,49 @@
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gdb
import (
"github.com/gogf/gf/test/gtest"
"testing"
)
func Test_Oracle_parseSql(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
o := new(DriverOracle)
sql := `UPDATE user SET name='john'`
newSql := o.parseSql(sql)
t.Assert(newSql, sql)
})
gtest.C(t, func(t *gtest.T) {
o := new(DriverOracle)
sql := `SELECT * FROM user`
newSql := o.parseSql(sql)
t.Assert(newSql, sql)
})
gtest.C(t, func(t *gtest.T) {
o := new(DriverOracle)
sql := `SELECT * FROM user LIMIT 0, 10`
newSql := o.parseSql(sql)
t.Assert(newSql, `SELECT * FROM (SELECT GFORM.*, ROWNUM ROWNUM_ FROM (SELECT * FROM user ) GFORM WHERE ROWNUM <= 10) WHERE ROWNUM_ >= 0`)
})
gtest.C(t, func(t *gtest.T) {
o := new(DriverOracle)
sql := `SELECT * FROM user LIMIT 1`
newSql := o.parseSql(sql)
t.Assert(newSql, `SELECT * FROM (SELECT GFORM.*, ROWNUM ROWNUM_ FROM (SELECT * FROM user ) GFORM WHERE ROWNUM <= 1) WHERE ROWNUM_ >= 0`)
})
gtest.C(t, func(t *gtest.T) {
o := new(DriverOracle)
sql := `SELECT ENAME FROM USER_INFO WHERE ID=2 LIMIT 1`
newSql := o.parseSql(sql)
t.Assert(newSql, `SELECT * FROM (SELECT GFORM.*, ROWNUM ROWNUM_ FROM (SELECT ENAME FROM USER_INFO WHERE ID=2 ) GFORM WHERE ROWNUM <= 1) WHERE ROWNUM_ >= 0`)
})
}

View File

@ -68,6 +68,7 @@ func StackWithFilters(filters []string, skip ...int) string {
file[0:len(goRootForFilter)] == goRootForFilter {
continue
}
// Custom filtering.
filtered = false
for _, filter := range filters {
if filter != "" && strings.Contains(file, filter) {

View File

@ -55,8 +55,10 @@ func NewWithTag(data interface{}, tags string, safe ...bool) *Json {
}
}
default:
rv := reflect.ValueOf(data)
kind := rv.Kind()
var (
rv = reflect.ValueOf(data)
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
@ -72,9 +74,7 @@ func NewWithTag(data interface{}, tags string, safe ...bool) *Json {
}
case reflect.Map, reflect.Struct:
i := interface{}(nil)
// Note that it uses Map function implementing the converting.
// Note that it here should not use MapDeep function if you really know what it means.
i = gconv.Map(data, tags)
i = gconv.MapDeep(data, tags)
j = &Json{
p: &i,
c: byte(gDEFAULT_SPLIT_CHAR),
@ -207,6 +207,21 @@ func LoadContentType(dataType string, data interface{}, safe ...bool) (*Json, er
return doLoadContent(dataType, content, safe...)
}
// IsValidDataType checks and returns whether given <dataType> a valid data type for loading.
func IsValidDataType(dataType string) bool {
if dataType == "" {
return false
}
if dataType[0] == '.' {
dataType = dataType[1:]
}
switch dataType {
case "json", "js", "xml", "yaml", "yml", "toml", "ini":
return true
}
return false
}
// checkDataType automatically checks and returns the data type for <content>.
// Note that it uses regular expression for loose checking, you can use LoadXXX/LoadContentType
// functions to load the content for certain content type.
@ -215,8 +230,9 @@ func checkDataType(content []byte) string {
return "json"
} else if gregex.IsMatch(`^<.+>[\S\s]+<.+>$`, content) {
return "xml"
} else if (gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*\w+`, content)) ||
(gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*\w+`, content)) {
} else if !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, content) && !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, content) &&
((gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*\w+`, content)) ||
(gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*\w+`, content))) {
return "yml"
} else if !gregex.IsMatch(`^[\s\t\n\r]*;.+`, content) &&
!gregex.IsMatch(`[\s\t\n\r]+;.+`, content) &&

View File

@ -112,4 +112,22 @@ app_conf = ./config/app.ini
`)
t.Assert(checkDataType(data), "ini")
})
gtest.C(t, func(t *gtest.T) {
data := []byte(`
# API Server
[server]
address = ":8199"
# Jenkins
[jenkins]
url = "https://jenkins-swimlane.com"
nodeJsStaticBuildCmdTpl = """
npm i --registry=https://registry.npm.taobao.org
wget http://consul.infra:8500/v1/kv/app_{{.SwimlaneName}}/{{.RepoName}}/.env.qa?raw=true -O ./env.qa
npm run build:qa
"""
`)
t.Assert(checkDataType(data), "toml")
})
}

View File

@ -47,3 +47,53 @@ func Test_Load_NewWithTag(t *testing.T) {
t.Assert(j.Get("addr-json"), nil)
})
}
func Test_Load_New_CustomStruct(t *testing.T) {
type Base struct {
Id int
}
type User struct {
Base
Name string
}
user := new(User)
user.Id = 1
user.Name = "john"
gtest.C(t, func(t *gtest.T) {
j := gjson.New(user)
t.AssertNE(j, nil)
s, err := j.ToJsonString()
t.Assert(err, nil)
t.Assert(s == `{"Id":1,"Name":"john"}` || s == `{"Name":"john","Id":1}`, true)
})
}
func Test_Load_New_HierarchicalStruct(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Me struct {
Name string `json:"name"`
Score int `json:"score"`
Children []Me `json:"children"`
}
me := Me{
Name: "john",
Score: 100,
Children: []Me{
{
Name: "Bean",
Score: 99,
},
{
Name: "Sam",
Score: 98,
},
},
}
j := gjson.New(me)
t.Assert(j.Remove("children.0.score"), nil)
t.Assert(j.Remove("children.1.score"), nil)
t.Assert(j.MustToJsonString(), `{"children":[{"children":null,"name":"Bean"},{"children":null,"name":"Sam"}],"name":"john","score":100}`)
})
}

View File

@ -10,7 +10,7 @@ import (
"github.com/gogf/gf/encoding/gjson"
)
// New creates a Parser object with any variable type of <data>, but <data> should be a map or
// New creates a Parser object with any variable type of <data>, but <data> should be a map, struct or
// slice for data access reason, or it will make no sense.
//
// The parameter <safe> specifies whether using this Json object in concurrent-safe context, which

View File

@ -26,6 +26,12 @@ type ApiCause interface {
Cause() error
}
// ApiLevel is the interface for Current/Next feature.
type ApiLevel interface {
Current() error
Next() error
}
// New creates and returns an error which is formatted from given text.
func New(text string) error {
if text == "" {
@ -120,3 +126,27 @@ func Stack(err error) string {
}
return ""
}
// Current creates and returns the current level error.
// It returns nil if current level error is nil.
func Current(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ApiLevel); ok {
return e.Current()
}
return err
}
// Next returns the next level error.
// It returns nil if current level error or the next level error is nil.
func Next(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ApiLevel); ok {
return e.Next()
}
return nil
}

View File

@ -27,6 +27,7 @@ const (
var (
// goRootForFilter is used for stack filtering purpose.
// Mainly for development environment.
goRootForFilter = runtime.GOROOT()
)
@ -36,8 +37,11 @@ func init() {
}
}
// Error implements the interface of Error, it returns the error as string.
// Error implements the interface of Error, it returns all the error as string.
func (err *Error) Error() string {
if err == nil {
return ""
}
if err.text != "" {
if err.error != nil {
return err.text + ": " + err.error.Error()
@ -49,6 +53,9 @@ func (err *Error) Error() string {
// Cause returns the root cause error.
func (err *Error) Cause() error {
if err == nil {
return nil
}
loop := err
for loop != nil {
if loop.error != nil {
@ -66,8 +73,8 @@ func (err *Error) Cause() error {
// Format formats the frame according to the fmt.Formatter interface.
//
// %v, %s : Print the error string;
// %-v, %-s : Print current error string;
// %v, %s : Print all the error string;
// %-v, %-s : Print current level error string;
// %+s : Print full stack error list;
// %+v : Print the error string and full stack error list;
func (err *Error) Format(s fmt.State, verb rune) {
@ -120,6 +127,28 @@ func (err *Error) Stack() string {
return buffer.String()
}
// Current creates and returns the current level error.
// It returns nil if current level error is nil.
func (err *Error) Current() error {
if err == nil {
return nil
}
return &Error{
error: nil,
stack: err.stack,
text: err.text,
}
}
// Next returns the next level error.
// It returns nil if current level error or the next level error is nil.
func (err *Error) Next() error {
if err == nil {
return nil
}
return err.error
}
// formatSubStack formats the stack for error.
func formatSubStack(st stack, buffer *bytes.Buffer) {
index := 1

View File

@ -127,3 +127,31 @@ func Test_Stack(t *testing.T) {
//fmt.Printf("%+v", err)
})
}
func Test_Current(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := errors.New("1")
err = gerror.Wrap(err, "2")
err = gerror.Wrap(err, "3")
t.Assert(err.Error(), "3: 2: 1")
t.Assert(gerror.Current(err).Error(), "3")
})
}
func Test_Next(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := errors.New("1")
err = gerror.Wrap(err, "2")
err = gerror.Wrap(err, "3")
t.Assert(err.Error(), "3: 2: 1")
err = gerror.Next(err)
t.Assert(err.Error(), "2: 1")
err = gerror.Next(err)
t.Assert(err.Error(), "1")
err = gerror.Next(err)
t.Assert(err, nil)
})
}

View File

@ -39,8 +39,15 @@ func Throw(exception interface{}) {
gutil.Throw(exception)
}
// TryCatch does the try...catch... mechanism.
func TryCatch(try func(), catch ...func(exception interface{})) {
// Try implements try... logistics using internal panic...recover.
// It returns error if any exception occurs, or else it returns nil.
func Try(try func()) (err error) {
return gutil.Try(try)
}
// TryCatch implements try...catch... logistics using internal panic...recover.
// It automatically calls function <catch> if any exception occurs ans passes the exception as an error.
func TryCatch(try func(), catch ...func(exception error)) {
gutil.TryCatch(try, catch...)
}

View File

@ -10,17 +10,14 @@ import (
"github.com/gogf/gf/os/glog"
)
// SetDebug disables/enables debug level for logging component globally.
func SetDebug(debug bool) {
glog.SetDebug(debug)
}
// SetLogLevel sets the logging level globally.
// Deprecated, use functions of package glog or g.Log() instead.
func SetLogLevel(level int) {
glog.SetLevel(level)
}
// GetLogLevel returns the global logging level.
// Deprecated, use functions of package glog or g.Log() instead.
func GetLogLevel() int {
return glog.GetLevel()
}

View File

@ -6,7 +6,17 @@
package g
import "github.com/gogf/gf/net/ghttp"
import (
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/net/ghttp"
)
// SetEnabled enables/disables the GoFrame internal logging manually.
// Note that this function is not concurrent safe, be aware of the DATA RACE,
// which means you should call this function in your boot but not the runtime.
func SetDebug(enabled bool) {
intlog.SetEnabled(enabled)
}
// SetServerGraceful enables/disables graceful reload feature of http Web Server.
// This feature is disabled in default.

View File

@ -31,26 +31,26 @@ func Database(name ...string) gdb.DB {
}
instanceKey := fmt.Sprintf("%s.%s", gFRAME_CORE_COMPONENT_NAME_DATABASE, group)
db := instances.GetOrSetFuncLock(instanceKey, func() interface{} {
// If configuration file does not exist but the gdb configurations are already set.
if !Config().Available() && gdb.GetConfig(group) != nil {
db, err := gdb.Instance(group)
if err != nil {
panic(err)
}
return db
}
// Check the configuration file.
var m map[string]interface{}
var (
configMap map[string]interface{}
configNodeKey string
)
// It firstly searches the configuration of the instance name.
nodeKey, _ := gutil.MapPossibleItemByKey(Config().GetMap("."), gDATABASE_NODE_NAME)
if nodeKey == "" {
nodeKey = gDATABASE_NODE_NAME
if Config().Available() {
configNodeKey, _ = gutil.MapPossibleItemByKey(Config().GetMap("."), gDATABASE_NODE_NAME)
if configNodeKey == "" {
configNodeKey = gDATABASE_NODE_NAME
}
configMap = Config().GetMap(configNodeKey)
}
if m = Config().GetMap(nodeKey); len(m) == 0 {
if len(configMap) == 0 && !gdb.IsConfigured() {
panic(fmt.Sprintf(`database init failed: "%s" node not found, is config file or configuration missing?`, gDATABASE_NODE_NAME))
}
if len(configMap) == 0 {
configMap = make(map[string]interface{})
}
// Parse <m> as map-slice and adds it to gdb's global configurations.
for group, groupConfig := range m {
for g, groupConfig := range configMap {
cg := gdb.ConfigGroup{}
switch value := groupConfig.(type) {
case []interface{}:
@ -65,37 +65,51 @@ func Database(name ...string) gdb.DB {
}
}
if len(cg) > 0 {
intlog.Printf("%s, %#v", group, cg)
gdb.SetConfigGroup(group, cg)
if gdb.GetConfig(group) == nil {
intlog.Printf("add configuration for group: %s, %#v", g, cg)
gdb.SetConfigGroup(g, cg)
} else {
intlog.Printf("ignore configuration as it already exists for group: %s, %#v", g, cg)
intlog.Printf("%s, %#v", g, cg)
}
}
}
// Parse <m> as a single node configuration,
// which is the default group configuration.
if node := parseDBConfigNode(m); node != nil {
if node := parseDBConfigNode(configMap); node != nil {
cg := gdb.ConfigGroup{}
if node.LinkInfo != "" || node.Host != "" {
cg = append(cg, *node)
}
if len(cg) > 0 {
intlog.Printf("%s, %#v", gdb.DEFAULT_GROUP_NAME, cg)
gdb.SetConfigGroup(gdb.DEFAULT_GROUP_NAME, cg)
if gdb.GetConfig(group) == nil {
intlog.Printf("add configuration for group: %s, %#v", gdb.DEFAULT_GROUP_NAME, cg)
gdb.SetConfigGroup(gdb.DEFAULT_GROUP_NAME, cg)
} else {
intlog.Printf("ignore configuration as it already exists for group: %s, %#v", gdb.DEFAULT_GROUP_NAME, cg)
intlog.Printf("%s, %#v", gdb.DEFAULT_GROUP_NAME, cg)
}
}
}
// Create a new ORM object with given configurations.
if db, err := gdb.New(name...); err == nil {
// Initialize logger for ORM.
var m map[string]interface{}
m = Config().GetMap(fmt.Sprintf("%s.%s", nodeKey, gLOGGER_NODE_NAME))
if len(m) == 0 {
m = Config().GetMap(nodeKey)
}
if len(m) > 0 {
if err := db.GetLogger().SetConfigWithMap(m); err != nil {
panic(err)
if Config().Available() {
// Initialize logger for ORM.
var loggerConfigMap map[string]interface{}
loggerConfigMap = Config().GetMap(fmt.Sprintf("%s.%s", configNodeKey, gLOGGER_NODE_NAME))
if len(loggerConfigMap) == 0 {
loggerConfigMap = Config().GetMap(configNodeKey)
}
if len(loggerConfigMap) > 0 {
if err := db.GetLogger().SetConfigWithMap(loggerConfigMap); err != nil {
panic(err)
}
}
}
return db
} else {
// It panics often because it dose not find its configuration for given group.
panic(err)
}
return nil

3
go.mod
View File

@ -5,7 +5,7 @@ go 1.11
require (
github.com/BurntSushi/toml v0.3.1
github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28
github.com/fsnotify/fsnotify v1.4.7
github.com/fsnotify/fsnotify v1.4.9
github.com/go-sql-driver/mysql v1.5.0
github.com/gomodule/redigo v2.0.0+incompatible
github.com/gorilla/websocket v1.4.1
@ -15,7 +15,6 @@ require (
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.1
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/text v0.3.2
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)

View File

@ -32,14 +32,14 @@ func T(content string, language ...string) string {
return defaultManager.T(content, language...)
}
// TF is alias of TranslateFormat for convenience.
func TF(format string, values ...interface{}) string {
// Tf is alias of TranslateFormat for convenience.
func Tf(format string, values ...interface{}) string {
return defaultManager.TranslateFormat(format, values...)
}
// TFL is alias of TranslateFormatLang for convenience.
func TFL(format string, language string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(format, language, values...)
// Tfl is alias of TranslateFormatLang for convenience.
func Tfl(language string, format string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(language, format, values...)
}
// TranslateFormat translates, formats and returns the <format> with configured language
@ -52,7 +52,7 @@ func TranslateFormat(format string, values ...interface{}) string {
// and given <values>. The parameter <language> specifies custom translation language ignoring
// configured language. If <language> is given empty string, it uses the default configured
// language for the translation.
func TranslateFormatLang(format string, language string, values ...interface{}) string {
func TranslateFormatLang(language string, format string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(format, language, values...)
}

View File

@ -124,14 +124,14 @@ func (m *Manager) T(content string, language ...string) string {
return m.Translate(content, language...)
}
// TF is alias of TranslateFormat for convenience.
func (m *Manager) TF(format string, values ...interface{}) string {
// Tf is alias of TranslateFormat for convenience.
func (m *Manager) Tf(format string, values ...interface{}) string {
return m.TranslateFormat(format, values...)
}
// TFL is alias of TranslateFormatLang for convenience.
func (m *Manager) TFL(format string, language string, values ...interface{}) string {
return m.TranslateFormatLang(format, language, values...)
// Tfl is alias of TranslateFormatLang for convenience.
func (m *Manager) Tfl(language string, format string, values ...interface{}) string {
return m.TranslateFormatLang(language, format, values...)
}
// TranslateFormat translates, formats and returns the <format> with configured language
@ -144,7 +144,7 @@ func (m *Manager) TranslateFormat(format string, values ...interface{}) string {
// and given <values>. The parameter <language> specifies custom translation language ignoring
// configured language. If <language> is given empty string, it uses the default configured
// language for the translation.
func (m *Manager) TranslateFormatLang(format string, language string, values ...interface{}) string {
func (m *Manager) TranslateFormatLang(language string, format string, values ...interface{}) string {
return fmt.Sprintf(m.Translate(format, language), values...)
}

View File

@ -74,24 +74,24 @@ func Test_Basic(t *testing.T) {
}
func Test_TranslateFormat(t *testing.T) {
// TF
// Tf
gtest.C(t, func(t *gtest.T) {
i18n := gi18n.New(gi18n.Options{
Path: gdebug.TestDataPath("i18n"),
})
i18n.SetLanguage("none")
t.Assert(i18n.TF("{#hello}{#world} %d", 2020), "{#hello}{#world} 2020")
t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "{#hello}{#world} 2020")
i18n.SetLanguage("ja")
t.Assert(i18n.TF("{#hello}{#world} %d", 2020), "こんにちは世界 2020")
t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "こんにちは世界 2020")
})
// TFL
// Tfl
gtest.C(t, func(t *gtest.T) {
i18n := gi18n.New(gi18n.Options{
Path: gdebug.TestDataPath("i18n"),
})
t.Assert(i18n.TFL("{#hello}{#world} %d", "ja", 2020), "こんにちは世界 2020")
t.Assert(i18n.TFL("{#hello}{#world} %d", "zh-CN", 2020), "你好世界 2020")
t.Assert(i18n.Tfl("ja", "{#hello}{#world} %d", 2020), "こんにちは世界 2020")
t.Assert(i18n.Tfl("zh-CN", "{#hello}{#world} %d", 2020), "你好世界 2020")
})
}

View File

@ -69,12 +69,21 @@ func IsEmpty(value interface{}) bool {
default:
// Common interfaces checks.
if f, ok := value.(apiString); ok {
if f == nil {
return true
}
return f.String() == ""
}
if f, ok := value.(apiInterfaces); ok {
if f == nil {
return true
}
return len(f.Interfaces()) == 0
}
if f, ok := value.(apiMapStrAny); ok {
if f == nil {
return true
}
return len(f.MapStrAny()) == 0
}
// Finally using reflect.

View File

@ -33,9 +33,12 @@ func init() {
}
// SetEnabled enables/disables the internal logging manually.
// Note that this function is not current safe, be aware of the DATA RACE.
// Note that this function is not concurrent safe, be aware of the DATA RACE.
func SetEnabled(enabled bool) {
isGFDebug = enabled
// If they're the same, it does not write the <isGFDebug> but only reading operation.
if isGFDebug != enabled {
isGFDebug = enabled
}
}
// IsEnabled checks and returns whether current process is in GF development.

View File

@ -22,6 +22,21 @@ import (
//
// Note that it only retrieves the exported attributes with first letter up-case from struct.
func MapField(pointer interface{}, priority []string, recursive bool) map[string]*Field {
// If <pointer> points to an invalid address, for example a nil variable,
// it here creates an empty struct using reflect feature.
var (
tempValue reflect.Value
pointerValue = reflect.ValueOf(pointer)
)
for pointerValue.Kind() == reflect.Ptr {
tempValue = pointerValue.Elem()
if !tempValue.IsValid() {
pointer = reflect.New(pointerValue.Type().Elem()).Elem()
break
} else {
pointerValue = tempValue
}
}
var (
fields []*structs.Field
fieldMap = make(map[string]*Field)
@ -59,8 +74,10 @@ func MapField(pointer interface{}, priority []string, recursive bool) map[string
}
}
if recursive {
rv := reflect.ValueOf(field.Value())
kind := rv.Kind()
var (
rv = reflect.ValueOf(field.Value())
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()

View File

@ -27,6 +27,21 @@ func TagFields(pointer interface{}, priority []string, recursive bool) []*Field
// tag internally.
// The parameter <pointer> should be type of struct/*struct.
func doTagFields(pointer interface{}, priority []string, recursive bool, tagMap map[string]struct{}) []*Field {
// If <pointer> points to an invalid address, for example a nil variable,
// it here creates an empty struct using reflect feature.
var (
tempValue reflect.Value
pointerValue = reflect.ValueOf(pointer)
)
for pointerValue.Kind() == reflect.Ptr {
tempValue = pointerValue.Elem()
if !tempValue.IsValid() {
pointer = reflect.New(pointerValue.Type().Elem()).Elem()
break
} else {
pointerValue = tempValue
}
}
var fields []*structs.Field
if v, ok := pointer.(reflect.Value); ok {
fields = structs.Fields(v.Interface())

View File

@ -71,3 +71,20 @@ func Test_Basic(t *testing.T) {
t.Assert(structs.TagMapName(user2, []string{"params"}, true), g.Map{"password1": "Pass1", "password2": "Pass2"})
})
}
func Test_StructOfNilPointer(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Name string `params:"name"`
Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"`
}
var user *User
t.Assert(structs.TagMapName(user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"params", "my-tag1"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"my-tag1", "params"}, true), g.Map{"name": "Name", "pass1": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"my-tag2", "params"}, true), g.Map{"name": "Name", "pass2": "Pass"})
})
}

View File

@ -149,3 +149,14 @@ func (c *Client) Proxy(proxyURL string) *Client {
newClient.SetProxy(proxyURL)
return c
}
// RedirectLimit is a chaining function,
// which sets the redirect limit the number of jumps for the request.
func (c *Client) RedirectLimit(redirectLimit int) *Client {
newClient := c
if c.parent == nil {
newClient = c.Clone()
}
newClient.SetRedirectLimit(redirectLimit)
return c
}

View File

@ -123,7 +123,7 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien
param = BuildParams(data[0])
}
}
req := (*http.Request)(nil)
var req *http.Request
if strings.Contains(param, "@file:") {
// File uploading request.
buffer := new(bytes.Buffer)

View File

@ -8,12 +8,17 @@ package ghttp
import (
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/text/gstr"
"strings"
"github.com/gogf/gf/encoding/gurl"
"github.com/gogf/gf/util/gconv"
)
const (
fileUploadingKey = "@file:"
)
// BuildParams builds the request string for the http client. The <params> can be type of:
// string/[]byte/map/struct/*struct.
//
@ -38,13 +43,22 @@ func BuildParams(params interface{}, noUrlEncode ...bool) (encodedParamStr strin
if len(noUrlEncode) == 1 {
urlEncode = !noUrlEncode[0]
}
// If there's file uploading, it ignores the url encoding.
if urlEncode {
for k, v := range m {
if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) {
urlEncode = false
break
}
}
}
s := ""
for k, v := range m {
if len(encodedParamStr) > 0 {
encodedParamStr += "&"
}
s = gconv.String(v)
if urlEncode && len(s) > 6 && strings.Compare(s[0:6], "@file:") != 0 {
if urlEncode && len(s) > 6 && strings.Compare(s[0:6], fileUploadingKey) != 0 {
s = gurl.Encode(s)
}
encodedParamStr += k + "=" + s

View File

@ -120,7 +120,7 @@ func (m *Middleware) Next() {
// There should be a "Next" function to be called in the middleware in order to manage the workflow.
loop = false
}
}, func(exception interface{}) {
}, func(exception error) {
if e, ok := exception.(gerror.ApiStack); ok {
// It's already an error that has stack info.
m.request.error = e.(error)

View File

@ -25,6 +25,12 @@ import (
"strings"
)
const (
parseTypeRequest = 0
parseTypeQuery = 1
parseTypeForm = 2
)
var (
// xmlHeaderBytes is the most common XML format header.
xmlHeaderBytes = []byte("<?xml")
@ -37,11 +43,26 @@ var (
// The parameter <pointer> can be type of: *struct/**struct/*[]struct/*[]*struct.
//
// It supports single and multiple struct convertion:
// 1. Single struct, post content like: {"id":1, "name":"john"}
// 1. Single struct, post content like: {"id":1, "name":"john"} or ?id=1&name=john
// 2. Multiple struct, post content like: [{"id":1, "name":"john"}, {"id":, "name":"smith"}]
//
// TODO: Improve the performance by reducing duplicated reflect usage on the same variable across packages.
func (r *Request) Parse(pointer interface{}) error {
return r.doParse(pointer, parseTypeRequest)
}
// ParseQuery performs like function Parse, but only parses the query parameters.
func (r *Request) ParseQuery(pointer interface{}) error {
return r.doParse(pointer, parseTypeQuery)
}
// ParseForm performs like function Parse, but only parses the form parameters or the body content.
func (r *Request) ParseForm(pointer interface{}) error {
return r.doParse(pointer, parseTypeForm)
}
// doParse parses the request data to struct/structs according to request type.
func (r *Request) doParse(pointer interface{}, requestType int) error {
var (
reflectVal1 = reflect.ValueOf(pointer)
reflectKind1 = reflectVal1.Kind()
@ -58,18 +79,31 @@ func (r *Request) Parse(pointer interface{}) error {
)
switch reflectKind2 {
// Single struct, post content like:
// {"id":1, "name":"john"}
// 1. {"id":1, "name":"john"}
// 2. ?id=1&name=john
case reflect.Ptr, reflect.Struct:
// Conversion.
if err := r.GetStruct(pointer); err != nil {
return err
// Converting.
switch requestType {
case parseTypeQuery:
if err := r.GetQueryStruct(pointer); err != nil {
return err
}
case parseTypeForm:
if err := r.GetFormStruct(pointer); err != nil {
return err
}
default:
if err := r.GetStruct(pointer); err != nil {
return err
}
}
// Validation.
if err := gvalid.CheckStruct(pointer, nil); err != nil {
return err
}
// Multiple struct, post content like:
// Multiple struct, it only supports JSON type post content like:
// [{"id":1, "name":"john"}, {"id":, "name":"smith"}]
case reflect.Array, reflect.Slice:
// If struct slice conversion, it might post JSON/XML content,
@ -276,6 +310,10 @@ func (r *Request) parseBody() {
return
}
r.parsedBody = true
// There's no data posted.
if r.ContentLength == 0 {
return
}
if body := r.GetBody(); len(body) > 0 {
// Trim space/new line characters.
body = bytes.TrimSpace(body)
@ -306,6 +344,10 @@ func (r *Request) parseForm() {
return
}
r.parsedForm = true
// There's no data posted.
if r.ContentLength == 0 {
return
}
if contentType := r.Header.Get("Content-Type"); contentType != "" {
var err error
if gstr.Contains(contentType, "multipart/") {
@ -363,11 +405,14 @@ func (r *Request) parseForm() {
}
}
}
if r.formMap == nil {
}
// It parses the request body without checking the Content-Type.
if r.formMap == nil {
if r.Method != "GET" {
r.parseBody()
if len(r.bodyMap) > 0 {
r.formMap = r.bodyMap
}
}
if len(r.bodyMap) > 0 {
r.formMap = r.bodyMap
}
}
}

View File

@ -193,7 +193,7 @@ func (r *Request) GetFormStruct(pointer interface{}, mapping ...map[string]strin
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetFormToStruct is alias of GetFormStruct. See GetFormStruct.

View File

@ -204,7 +204,7 @@ func (r *Request) GetPostMapStrVar(kvMap ...map[string]interface{}) map[string]*
//
// Deprecated.
func (r *Request) GetPostStruct(pointer interface{}, mapping ...map[string]string) error {
return gconv.StructDeep(r.GetPostMap(), pointer, mapping...)
return gconv.Struct(r.GetPostMap(), pointer, mapping...)
}
// GetPostToStruct is alias of GetQueryStruct. See GetPostStruct.

View File

@ -34,7 +34,9 @@ func (r *Request) GetQuery(key string, def ...interface{}) interface{} {
return v
}
}
r.parseBody()
if r.Method == "GET" {
r.parseBody()
}
if len(r.bodyMap) > 0 {
if v, ok := r.bodyMap[key]; ok {
return v
@ -118,7 +120,9 @@ func (r *Request) GetQueryInterfaces(key string, def ...interface{}) []interface
// in order of priority: query > body.
func (r *Request) GetQueryMap(kvMap ...map[string]interface{}) map[string]interface{} {
r.parseQuery()
r.parseBody()
if r.Method == "GET" {
r.parseBody()
}
var m map[string]interface{}
if len(kvMap) > 0 && kvMap[0] != nil {
if len(r.queryMap) == 0 && len(r.bodyMap) == 0 {
@ -197,7 +201,7 @@ func (r *Request) GetQueryStruct(pointer interface{}, mapping ...map[string]stri
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetQueryToStruct is alias of GetQueryStruct. See GetQueryStruct.

View File

@ -271,7 +271,7 @@ func (r *Request) GetRequestStruct(pointer interface{}, mapping ...map[string]st
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetRequestToStruct is alias of GetRequestStruct. See GetRequestStruct.

View File

@ -267,11 +267,6 @@ func (s *Server) Start() error {
return errors.New("[ghttp] server is already running")
}
// If there's no route registered and no static service enabled,
// it then returns an error of invalid usage of server.
if len(s.routesMap) == 0 && !s.config.FileServerEnabled {
return errors.New(`[ghttp] there's no route set or static feature enabled, did you forget import the router?`)
}
// Logging path setting check.
if s.config.LogPath != "" {
if err := s.config.Logger.SetPath(s.config.LogPath); err != nil {
@ -316,6 +311,12 @@ func (s *Server) Start() error {
// Check the group routes again.
s.handlePreBindItems()
// If there's no route registered and no static service enabled,
// it then returns an error of invalid usage of server.
if len(s.routesMap) == 0 && !s.config.FileServerEnabled {
return errors.New(`[ghttp] there's no route set or static feature enabled, did you forget import the router?`)
}
// Start the HTTP server.
reloaded := false
fdMapStr := genv.Get(gADMIN_ACTION_RELOAD_ENVKEY)

View File

@ -92,7 +92,7 @@ type ServerConfig struct {
// size of the request body.
//
// It can be configured in configuration file using string like: 1m, 10m, 500kb etc.
// It's 1024 bytes in default.
// It's 10240 bytes in default.
MaxHeaderBytes int
// KeepAlive enables HTTP keep-alive.
@ -157,6 +157,9 @@ type ServerConfig struct {
// SessionIdName specifies the session id name.
SessionIdName string
// SessionCookieOutput specifies whether automatic outputting session id to cookie.
SessionCookieOutput bool
// SessionPath specifies the session storage directory path for storing session files.
// It only makes sense if the session storage is type of file storage.
SessionPath string
@ -231,50 +234,56 @@ type ServerConfig struct {
Graceful bool
}
// Config creates and returns a ServerConfig object with default configurations.
// Deprecated. Use NewConfig instead.
func Config() ServerConfig {
return NewConfig()
}
// NewConfig creates and returns a ServerConfig object with default configurations.
// Note that, do not define this default configuration to local package variable, as there're
// some pointer attributes that may be shared in different servers.
func Config() ServerConfig {
func NewConfig() ServerConfig {
return ServerConfig{
Address: "",
HTTPSAddr: "",
Handler: nil,
ReadTimeout: 60 * time.Second,
WriteTimeout: 0, // No timeout.
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 10240, // 10KB
KeepAlive: true,
IndexFiles: []string{"index.html", "index.htm"},
IndexFolder: false,
ServerAgent: "GF HTTP Server",
ServerRoot: "",
StaticPaths: make([]staticPathItem, 0),
FileServerEnabled: false,
CookieMaxAge: time.Hour * 24 * 365,
CookiePath: "/",
CookieDomain: "",
SessionMaxAge: time.Hour * 24,
SessionIdName: "gfsessionid",
SessionPath: gsession.DefaultStorageFilePath,
Logger: glog.New(),
LogStdout: true,
ErrorStack: true,
ErrorLogEnabled: true,
ErrorLogPattern: "error-{Ymd}.log",
AccessLogEnabled: false,
AccessLogPattern: "access-{Ymd}.log",
DumpRouterMap: true,
ClientMaxBodySize: 8 * 1024 * 1024, // 8MB
FormParsingMemory: 1024 * 1024, // 1MB
Rewrites: make(map[string]string),
Graceful: false,
Address: "",
HTTPSAddr: "",
Handler: nil,
ReadTimeout: 60 * time.Second,
WriteTimeout: 0, // No timeout.
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 10240, // 10KB
KeepAlive: true,
IndexFiles: []string{"index.html", "index.htm"},
IndexFolder: false,
ServerAgent: "GF HTTP Server",
ServerRoot: "",
StaticPaths: make([]staticPathItem, 0),
FileServerEnabled: false,
CookieMaxAge: time.Hour * 24 * 365,
CookiePath: "/",
CookieDomain: "",
SessionMaxAge: time.Hour * 24,
SessionIdName: "gfsessionid",
SessionPath: gsession.DefaultStorageFilePath,
SessionCookieOutput: true,
Logger: glog.New(),
LogStdout: true,
ErrorStack: true,
ErrorLogEnabled: true,
ErrorLogPattern: "error-{Ymd}.log",
AccessLogEnabled: false,
AccessLogPattern: "access-{Ymd}.log",
DumpRouterMap: true,
ClientMaxBodySize: 8 * 1024 * 1024, // 8MB
FormParsingMemory: 1024 * 1024, // 1MB
Rewrites: make(map[string]string),
Graceful: false,
}
}
// ConfigFromMap creates and returns a ServerConfig object with given map and
// default configuration object.
func ConfigFromMap(m map[string]interface{}) (ServerConfig, error) {
config := Config()
config := NewConfig()
if err := gconv.Struct(m, &config); err != nil {
return config, err
}

View File

@ -27,6 +27,11 @@ func (s *Server) SetSessionStorage(storage gsession.Storage) {
s.config.SessionStorage = storage
}
// SetSessionCookieOutput sets the SetSessionCookieOutput for server.
func (s *Server) SetSessionCookieOutput(enabled bool) {
s.config.SessionCookieOutput = enabled
}
// GetSessionMaxAge returns the SessionMaxAge of server.
func (s *Server) GetSessionMaxAge() time.Duration {
return s.config.SessionMaxAge

View File

@ -161,7 +161,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Automatically set the session id to cookie
// if it creates a new session id in this request.
if request.Session.IsDirty() && request.Session.Id() != request.GetSessionId() {
if s.config.SessionCookieOutput &&
request.Session.IsDirty() &&
request.Session.Id() != request.GetSessionId() {
request.Cookie.SetSessionId(request.Session.Id())
}
// Output the cookie content to client.

View File

@ -24,14 +24,19 @@ const (
// EnablePProf enables PProf feature for server.
func (s *Server) EnablePProf(pattern ...string) {
s.Domain(gDEFAULT_DOMAIN).EnablePProf(pattern...)
}
// EnablePProf enables PProf feature for server of specified domain.
func (d *Domain) EnablePProf(pattern ...string) {
p := gDEFAULT_PPROF_PATTERN
if len(pattern) > 0 && pattern[0] != "" {
p = pattern[0]
}
up := &utilPProf{}
_, _, uri, _ := s.parsePattern(p)
_, _, uri, _ := d.server.parsePattern(p)
uri = strings.TrimRight(uri, "/")
s.Group(uri, func(group *RouterGroup) {
d.Group(uri, func(group *RouterGroup) {
group.ALL("/*action", up.Index)
group.ALL("/cmdline", up.Cmdline)
group.ALL("/profile", up.Profile)

View File

@ -44,13 +44,15 @@ func (s *Server) getHandlersWithCache(r *Request) (parsedItems []*handlerParsedI
}
}
// Search and cache the router handlers.
value := s.serveCache.GetOrSetFunc(s.serveHandlerKey(method, r.URL.Path, r.GetHost()), func() interface{} {
parsedItems, hasHook, hasServe = s.searchHandlers(method, r.URL.Path, r.GetHost())
if parsedItems != nil {
return &handlerCacheItem{parsedItems, hasHook, hasServe}
}
return nil
}, gROUTE_CACHE_DURATION)
value, _ := s.serveCache.GetOrSetFunc(
s.serveHandlerKey(method, r.URL.Path, r.GetHost()),
func() (interface{}, error) {
parsedItems, hasHook, hasServe = s.searchHandlers(method, r.URL.Path, r.GetHost())
if parsedItems != nil {
return &handlerCacheItem{parsedItems, hasHook, hasServe}, nil
}
return nil, nil
}, gROUTE_CACHE_DURATION)
if value != nil {
item := value.(*handlerCacheItem)
return item.parsedItems, item.hasHook, item.hasServe

View File

@ -295,6 +295,8 @@ func Test_Client_Param_Containing_Special_Char(t *testing.T) {
})
}
// It posts data along with file uploading.
// It does not url-encodes the parameters.
func Test_Client_File_And_Param(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
@ -308,7 +310,7 @@ func Test_Client_File_And_Param(t *testing.T) {
_, err = file.Save(tmpPath)
gtest.Assert(err, nil)
r.Response.Write(
r.Get("key"),
r.Get("json"),
gfile.GetContents(gfile.Join(tmpPath, gfile.Basename(file.Filename))),
)
})
@ -320,12 +322,13 @@ func Test_Client_File_And_Param(t *testing.T) {
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
path := gdebug.TestDataPath("upload", "file1.txt")
data := g.Map{
"file": "@file:" + path,
"json": `{"uuid": "luijquiopm", "isRelative": false, "fileName": "test111.xls"}`,
}
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
filePath := gdebug.TestDataPath("upload", "file1.txt")
t.Assert(
c.PostContent("/", "key=1&file=@file:"+filePath),
"1"+gfile.GetContents(filePath),
)
t.Assert(c.PostContent("/", data), data["json"].(string)+gfile.GetContents(path))
})
}

View File

@ -27,13 +27,11 @@ func Test_Params_Parse(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse", func(r *ghttp.Request) {
if m := r.GetMap(); len(m) > 0 {
var user *User
if err := r.Parse(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Map["id"], user.Map["score"])
var user *User
if err := r.Parse(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Map["id"], user.Map["score"])
})
s.SetPort(p)
s.SetDumpRouterMap(false)
@ -48,6 +46,250 @@ func Test_Params_Parse(t *testing.T) {
})
}
func Test_Params_ParseQuery(t *testing.T) {
type User struct {
Id int
Name string
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse-query", func(r *ghttp.Request) {
var user *User
if err := r.ParseQuery(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Id, user.Name)
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
t.Assert(c.GetContent("/parse-query"), `0`)
t.Assert(c.GetContent("/parse-query?id=1&name=john"), `1john`)
t.Assert(c.PostContent("/parse-query"), `0`)
t.Assert(c.PostContent("/parse-query", g.Map{
"id": 1,
"name": "john",
}), `0`)
})
}
func Test_Params_ParseForm(t *testing.T) {
type User struct {
Id int
Name string
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse-form", func(r *ghttp.Request) {
var user *User
if err := r.ParseForm(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Id, user.Name)
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
t.Assert(c.GetContent("/parse-form"), `0`)
t.Assert(c.GetContent("/parse-form", g.Map{
"id": 1,
"name": "john",
}), 0)
t.Assert(c.PostContent("/parse-form"), `0`)
t.Assert(c.PostContent("/parse-form", g.Map{
"id": 1,
"name": "john",
}), `1john`)
})
}
func Test_Params_ComplexJsonStruct(t *testing.T) {
type ItemEnv struct {
Type string
Key string
Value string
Brief string
}
type ItemProbe struct {
Type string
Port int
Path string
Brief string
Period int
InitialDelay int
TimeoutSeconds int
}
type ItemKV struct {
Key string
Value string
}
type ItemPort struct {
Port int
Type string
Alias string
Brief string
}
type ItemMount struct {
Type string
DstPath string
Src string
SrcPath string
Brief string
}
type SaveRequest struct {
AppId uint
Name string
Type string
Cluster string
Replicas uint
ContainerName string
ContainerImage string
VersionTag string
Namespace string
Id uint
Status uint
Metrics string
InitImage string
CpuRequest uint
CpuLimit uint
MemRequest uint
MemLimit uint
MeshEnabled uint
ContainerPorts []ItemPort
Labels []ItemKV
NodeSelector []ItemKV
EnvReserve []ItemKV
EnvGlobal []ItemEnv
EnvContainer []ItemEnv
Mounts []ItemMount
LivenessProbe ItemProbe
ReadinessProbe ItemProbe
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse", func(r *ghttp.Request) {
if m := r.GetMap(); len(m) > 0 {
var data *SaveRequest
if err := r.Parse(&data); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(data)
}
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
content := `
{
"app_id": 5,
"cluster": "test",
"container_image": "nginx",
"container_name": "test",
"container_ports": [
{
"alias": "别名",
"brief": "描述",
"port": 80,
"type": "tcp"
}
],
"cpu_limit": 100,
"cpu_request": 10,
"create_at": "2020-10-10 12:00:00",
"creator": 1,
"env_container": [
{
"brief": "用户环境变量",
"key": "NAME",
"type": "string",
"value": "john"
}
],
"env_global": [
{
"brief": "数据数量",
"key": "NUMBER",
"type": "string",
"value": "1"
}
],
"env_reserve": [
{
"key": "NODE_IP",
"value": "status.hostIP"
}
],
"liveness_probe": {
"brief": "存活探针",
"initial_delay": 10,
"path": "",
"period": 5,
"port": 80,
"type": "tcpSocket"
},
"readiness_probe": {
"brief": "就绪探针",
"initial_delay": 10,
"path": "",
"period": 5,
"port": 80,
"type": "tcpSocket"
},
"id": 0,
"init_image": "",
"labels": [
{
"key": "app",
"value": "test"
}
],
"mem_limit": 1000,
"mem_request": 100,
"mesh_enabled": 0,
"metrics": "",
"mounts": [],
"name": "test",
"namespace": "test",
"node_selector": [
{
"key": "group",
"value": "app"
}
],
"replicas": 1,
"type": "test",
"update_at": "2020-10-10 12:00:00",
"version_tag": "test"
}
`
t.Assert(client.PostContent("/parse", content), `{"AppId":5,"Name":"test","Type":"test","Cluster":"test","Replicas":1,"ContainerName":"test","ContainerImage":"nginx","VersionTag":"test","Namespace":"test","Id":0,"Status":0,"Metrics":"","InitImage":"","CpuRequest":10,"CpuLimit":100,"MemRequest":100,"MemLimit":1000,"MeshEnabled":0,"ContainerPorts":[{"Port":80,"Type":"tcp","Alias":"别名","Brief":"描述"}],"Labels":[{"Key":"app","Value":"test"}],"NodeSelector":[{"Key":"group","Value":"app"}],"EnvReserve":[{"Key":"NODE_IP","Value":"status.hostIP"}],"EnvGlobal":[{"Type":"string","Key":"NUMBER","Value":"1","Brief":"数据数量"}],"EnvContainer":[{"Type":"string","Key":"NAME","Value":"john","Brief":"用户环境变量"}],"Mounts":[],"LivenessProbe":{"Type":"tcpSocket","Port":80,"Path":"","Brief":"存活探针","Period":5,"InitialDelay":10,"TimeoutSeconds":0},"ReadinessProbe":{"Type":"tcpSocket","Port":80,"Path":"","Brief":"就绪探针","Period":5,"InitialDelay":10,"TimeoutSeconds":0}}`)
})
}
func Test_Params_Parse_Attr_Pointer1(t *testing.T) {
type User struct {
Id *int

View File

@ -404,7 +404,7 @@ func Test_Params_Basic(t *testing.T) {
t.Assert(client.GetContent("/struct", `id=1&name=john&password1=123&password2=456`), `1john123456`)
t.Assert(client.PostContent("/struct", `id=1&name=john&password1=123&password2=456`), `1john123456`)
t.Assert(client.PostContent("/struct-with-nil", ``), ``)
t.Assert(client.PostContent("/struct-with-base", `id=1&name=john&password1=123&password2=456`), "1john1234561john123456")
t.Assert(client.PostContent("/struct-with-base", `id=1&name=john&password1=123&password2=456`), "1john1234561john")
})
}
@ -433,10 +433,10 @@ func Test_Params_SupportChars(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/form-value", func(r *ghttp.Request) {
r.Response.Write(r.GetQuery("test-value"))
r.Response.Write(r.GetForm("test-value"))
})
s.BindHandler("/form-array", func(r *ghttp.Request) {
r.Response.Write(r.GetQuery("test-array"))
r.Response.Write(r.GetForm("test-array"))
})
s.SetPort(p)
s.SetDumpRouterMap(false)
@ -445,12 +445,10 @@ func Test_Params_SupportChars(t *testing.T) {
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
prefix := fmt.Sprintf("http://127.0.0.1:%d", p)
client := ghttp.NewClient()
client.SetPrefix(prefix)
t.Assert(client.PostContent("/form-value", "test-value=100"), "100")
t.Assert(client.PostContent("/form-array", "test-array[]=1&test-array[]=2"), `["1","2"]`)
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
t.Assert(c.PostContent("/form-value", "test-value=100"), "100")
t.Assert(c.PostContent("/form-array", "test-array[]=1&test-array[]=2"), `["1","2"]`)
})
}

View File

@ -152,3 +152,47 @@ func Test_Session_StorageFile(t *testing.T) {
t.Assert(client.GetContent("/get?k=key1"), "")
})
}
func Test_Session_Custom_Id(t *testing.T) {
var (
sessionId = "1234567890"
key = "key"
value = "value"
p, _ = ports.PopRand()
s = g.Server(p)
)
s.BindHandler("/id", func(r *ghttp.Request) {
if err := r.Session.SetId(sessionId); err != nil {
r.Response.WriteExit(err.Error())
}
if err := r.Session.Set(key, value); err != nil {
r.Response.WriteExit(err.Error())
}
r.Response.WriteExit(r.Session.Id())
})
s.BindHandler("/value", func(r *ghttp.Request) {
r.Response.WriteExit(r.Session.Get(key))
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
r, err := client.Get("/id")
t.Assert(err, nil)
defer r.Close()
t.Assert(r.ReadAllString(), sessionId)
t.Assert(r.GetCookie(s.GetSessionIdName()), sessionId)
})
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
client.SetHeader(s.GetSessionIdName(), sessionId)
t.Assert(client.GetContent("/value"), value)
})
}

View File

@ -4,7 +4,8 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gcache provides high performance and concurrent-safe in-memory cache for process.
// Package gcache provides kinds of cache management for process.
// It default provides a concurrent-safe in-memory cache adapter for process.
package gcache
import (
@ -21,33 +22,27 @@ func Set(key interface{}, value interface{}, duration time.Duration) {
defaultCache.Set(key, value, duration)
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func Update(key interface{}, value interface{}) (oldValue interface{}, exist bool) {
return defaultCache.Update(key, value)
}
// SetIfNotExist sets cache with <key>-<value> pair if <key> does not exist in the cache,
// which is expired after <duration>. It does not expire if <duration> == 0.
func SetIfNotExist(key interface{}, value interface{}, duration time.Duration) bool {
func SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error) {
return defaultCache.SetIfNotExist(key, value, duration)
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func Sets(data map[interface{}]interface{}, duration time.Duration) {
defaultCache.Sets(data, duration)
func Sets(data map[interface{}]interface{}, duration time.Duration) error {
return defaultCache.Sets(data, duration)
}
// Get returns the value of <key>.
// It returns nil if it does not exist or its value is nil.
func Get(key interface{}) interface{} {
func Get(key interface{}) (interface{}, error) {
return defaultCache.Get(key)
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func GetVar(key interface{}) *gvar.Var {
func GetVar(key interface{}) (*gvar.Var, error) {
return defaultCache.GetVar(key)
}
@ -56,14 +51,14 @@ func GetVar(key interface{}) *gvar.Var {
// The key-value pair expires after <duration>.
//
// It does not expire if <duration> == 0.
func GetOrSet(key interface{}, value interface{}, duration time.Duration) interface{} {
func GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSet(key, value, duration)
}
// GetOrSetFunc returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>. It does not expire if <duration> == 0.
func GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration) interface{} {
func GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSetFunc(key, f, duration)
}
@ -72,18 +67,18 @@ func GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration)
// after <duration>. It does not expire if <duration> == 0.
//
// Note that the function <f> is executed within writing mutex lock.
func GetOrSetFuncLock(key interface{}, f func() interface{}, duration time.Duration) interface{} {
func GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSetFuncLock(key, f, duration)
}
// Contains returns true if <key> exists in the cache, or else returns false.
func Contains(key interface{}) bool {
func Contains(key interface{}) (bool, error) {
return defaultCache.Contains(key)
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func Remove(keys ...interface{}) (value interface{}) {
func Remove(keys ...interface{}) (value interface{}, err error) {
return defaultCache.Remove(keys...)
}
@ -94,38 +89,44 @@ func Removes(keys []interface{}) {
}
// Data returns a copy of all key-value pairs in the cache as map type.
func Data() map[interface{}]interface{} {
func Data() (map[interface{}]interface{}, error) {
return defaultCache.Data()
}
// Keys returns all keys in the cache as slice.
func Keys() []interface{} {
func Keys() ([]interface{}, error) {
return defaultCache.Keys()
}
// KeyStrings returns all keys in the cache as string slice.
func KeyStrings() []string {
func KeyStrings() ([]string, error) {
return defaultCache.KeyStrings()
}
// Values returns all values in the cache as slice.
func Values() []interface{} {
func Values() ([]interface{}, error) {
return defaultCache.Values()
}
// Size returns the size of the cache.
func Size() int {
func Size() (int, error) {
return defaultCache.Size()
}
// GetExpire retrieves and returns the expiration of <key>.
// It returns -1 if the <key> does not exist in the cache.
func GetExpire(key interface{}) time.Duration {
func GetExpire(key interface{}) (time.Duration, error) {
return defaultCache.GetExpire(key)
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error) {
return defaultCache.Update(key, value)
}
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
// It returns -1 if the <key> does not exist in the cache.
func UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration) {
func UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
return defaultCache.UpdateExpire(key, duration)
}

117
os/gcache/gcache_adapter.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gcache
import (
"time"
)
// Adapter is the adapter for cache features implements.
type Adapter interface {
// Set sets cache with <key>-<value> pair, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0.
Set(key interface{}, value interface{}, duration time.Duration) error
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the keys of <data> if <duration> < 0 or given <value> is nil.
Sets(data map[interface{}]interface{}, duration time.Duration) error
// SetIfNotExist sets cache with <key>-<value> pair which is expired after <duration>
// if <key> does not exist in the cache. It returns true the <key> dose not exist in the
// cache and it sets <value> successfully to the cache, or else it returns false.
//
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil.
SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error)
// Get retrieves and returns the associated value of given <key>.
// It returns nil if it does not exist or its value is nil.
Get(key interface{}) (interface{}, error)
// GetOrSet retrieves and returns the value of <key>, or sets <key>-<value> pair and
// returns <value> if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error)
// GetOrSetFunc retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error)
// GetOrSetFuncLock retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> should be executed within writing mutex lock for concurrent
// safety purpose.
GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error)
// Contains returns true if <key> exists in the cache, or else returns false.
Contains(key interface{}) (bool, error)
// GetExpire retrieves and returns the expiration of <key> in the cache.
//
// It returns 0 if the <key> does not expire.
// It returns -1 if the <key> does not exist in the cache.
GetExpire(key interface{}) (time.Duration, error)
// Remove deletes one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the last deleted item.
Remove(keys ...interface{}) (value interface{}, err error)
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned value <exist> is false if the <key> does not exist in the cache.
//
// It deletes the <key> if given <value> is nil.
// It does nothing if <key> does not exist in the cache.
Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error)
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
//
// It returns -1 and does nothing if the <key> does not exist in the cache.
// It deletes the <key> if <duration> < 0.
UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error)
// Size returns the number of items in the cache.
Size() (size int, err error)
// Data returns a copy of all key-value pairs in the cache as map type.
// Note that this function may leads lots of memory usage, you can implement this function
// if necessary.
Data() (map[interface{}]interface{}, error)
// Keys returns all keys in the cache as slice.
Keys() ([]interface{}, error)
// Values returns all values in the cache as slice.
Values() ([]interface{}, error)
// Clear clears all data of the cache.
// Note that this function is sensitive and should be carefully used.
Clear() error
// Close closes the cache if necessary.
Close() error
}

View File

@ -7,7 +7,6 @@
package gcache
import (
"github.com/gogf/gf/container/gvar"
"math"
"sync"
"time"
@ -17,11 +16,10 @@ import (
"github.com/gogf/gf/container/gtype"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/os/gtimer"
"github.com/gogf/gf/util/gconv"
)
// Internal cache object.
type memCache struct {
type adapterMemory struct {
// dataMu ensures the concurrent safety of underlying data map.
dataMu sync.RWMutex
@ -38,7 +36,7 @@ type memCache struct {
cap int
// data is the underlying cache data which is stored in a hash table.
data map[interface{}]memCacheItem
data map[interface{}]adapterMemoryItem
// expireTimes is the expiring key to its timestamp mapping,
// which is used for quick indexing and deleting.
@ -49,7 +47,7 @@ type memCache struct {
expireSets map[int64]*gset.Set
// lru is the LRU manager, which is enabled when attribute cap > 0.
lru *memCacheLru
lru *adapterMemoryLru
// lruGetList is the LRU history according with Get function.
lruGetList *glist.List
@ -62,13 +60,13 @@ type memCache struct {
}
// Internal cache item.
type memCacheItem struct {
type adapterMemoryItem struct {
v interface{} // Value.
e int64 // Expire timestamp in milliseconds.
}
// Internal event item.
type memCacheEvent struct {
type adapterMemoryEvent struct {
k interface{} // Key.
e int64 // Expire time in milliseconds.
}
@ -79,11 +77,11 @@ const (
gDEFAULT_MAX_EXPIRE = 9223372036854
)
// newMemCache creates and returns a new memory cache object.
func newMemCache(lruCap ...int) *memCache {
c := &memCache{
// newAdapterMemory creates and returns a new memory cache object.
func newAdapterMemory(lruCap ...int) *adapterMemory {
c := &adapterMemory{
lruGetList: glist.New(true),
data: make(map[interface{}]memCacheItem),
data: make(map[interface{}]adapterMemoryItem),
expireTimes: make(map[interface{}]int64),
expireSets: make(map[int64]*gset.Set),
eventList: glist.New(true),
@ -97,96 +95,330 @@ func newMemCache(lruCap ...int) *memCache {
}
// Set sets cache with <key>-<value> pair, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func (c *memCache) Set(key interface{}, value interface{}, duration time.Duration) {
// It deletes the <key> if <duration> < 0.
func (c *adapterMemory) Set(key interface{}, value interface{}, duration time.Duration) error {
expireTime := c.getInternalExpire(duration)
c.dataMu.Lock()
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: value,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&memCacheEvent{
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: expireTime,
})
return nil
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func (c *memCache) Update(key interface{}, value interface{}) (oldValue interface{}, exist bool) {
// The returned value <exist> is false if the <key> does not exist in the cache.
//
// It deletes the <key> if given <value> is nil.
// It does nothing if <key> does not exist in the cache.
func (c *adapterMemory) Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
if item, ok := c.data[key]; ok {
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: value,
e: item.e,
}
return item.v, true
return item.v, true, nil
}
return nil, false
return nil, false, nil
}
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
// It returns -1 if the <key> does not exist in the cache.
func (c *memCache) UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration) {
//
// It returns -1 and does nothing if the <key> does not exist in the cache.
// It deletes the <key> if <duration> < 0.
func (c *adapterMemory) UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
newExpireTime := c.getInternalExpire(duration)
c.dataMu.Lock()
defer c.dataMu.Unlock()
if item, ok := c.data[key]; ok {
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: item.v,
e: newExpireTime,
}
c.eventList.PushBack(&memCacheEvent{
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: newExpireTime,
})
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond, nil
}
return -1
return -1, nil
}
// GetExpire retrieves and returns the expiration of <key>.
// GetExpire retrieves and returns the expiration of <key> in the cache.
//
// It returns 0 if the <key> does not expire.
// It returns -1 if the <key> does not exist in the cache.
func (c *memCache) GetExpire(key interface{}) time.Duration {
func (c *adapterMemory) GetExpire(key interface{}) (time.Duration, error) {
c.dataMu.RLock()
defer c.dataMu.RUnlock()
if item, ok := c.data[key]; ok {
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond, nil
}
return -1
return -1, nil
}
// SetIfNotExist sets cache with <key>-<value> pair which is expired after <duration>
// if <key> does not exist in the cache. It returns true the <key> dose not exist in the
// cache and it sets <value> successfully to the cache, or else it returns false.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil.
func (c *adapterMemory) SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error) {
isContained, err := c.Contains(key)
if err != nil {
return false, err
}
if !isContained {
_, err := c.doSetWithLockCheck(key, value, duration)
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the keys of <data> if <duration> < 0 or given <value> is nil.
func (c *adapterMemory) Sets(data map[interface{}]interface{}, duration time.Duration) error {
expireTime := c.getInternalExpire(duration)
for k, v := range data {
c.dataMu.Lock()
c.data[k] = adapterMemoryItem{
v: v,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&adapterMemoryEvent{
k: k,
e: expireTime,
})
}
return nil
}
// Get retrieves and returns the associated value of given <key>.
// It returns nil if it does not exist or its value is nil.
func (c *adapterMemory) Get(key interface{}) (interface{}, error) {
c.dataMu.RLock()
item, ok := c.data[key]
c.dataMu.RUnlock()
if ok && !item.IsExpired() {
// Adding to LRU history if LRU feature is enabled.
if c.cap > 0 {
c.lruGetList.PushBack(key)
}
return item.v, nil
}
return nil, nil
}
// GetOrSet retrieves and returns the value of <key>, or sets <key>-<value> pair and
// returns <value> if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
func (c *adapterMemory) GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
return c.doSetWithLockCheck(key, value, duration)
} else {
return v, nil
}
}
// GetOrSetFunc retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
func (c *adapterMemory) GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
value, err := f()
if err != nil {
return nil, err
}
if value == nil {
return nil, nil
}
return c.doSetWithLockCheck(key, value, duration)
} else {
return v, nil
}
}
// GetOrSetFuncLock retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> should be executed within writing mutex lock for concurrent
// safety purpose.
func (c *adapterMemory) GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
return c.doSetWithLockCheck(key, f, duration)
} else {
return v, nil
}
}
// Contains returns true if <key> exists in the cache, or else returns false.
func (c *adapterMemory) Contains(key interface{}) (bool, error) {
v, err := c.Get(key)
if err != nil {
return false, err
}
return v != nil, nil
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func (c *adapterMemory) Remove(keys ...interface{}) (value interface{}, err error) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
for _, key := range keys {
item, ok := c.data[key]
if ok {
value = item.v
delete(c.data, key)
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: gtime.TimestampMilli() - 1000,
})
}
}
return value, nil
}
// Data returns a copy of all key-value pairs in the cache as map type.
func (c *adapterMemory) Data() (map[interface{}]interface{}, error) {
m := make(map[interface{}]interface{})
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
m[k] = v.v
}
}
c.dataMu.RUnlock()
return m, nil
}
// Keys returns all keys in the cache as slice.
func (c *adapterMemory) Keys() ([]interface{}, error) {
keys := make([]interface{}, 0)
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
keys = append(keys, k)
}
}
c.dataMu.RUnlock()
return keys, nil
}
// Values returns all values in the cache as slice.
func (c *adapterMemory) Values() ([]interface{}, error) {
values := make([]interface{}, 0)
c.dataMu.RLock()
for _, v := range c.data {
if !v.IsExpired() {
values = append(values, v.v)
}
}
c.dataMu.RUnlock()
return values, nil
}
// Size returns the size of the cache.
func (c *adapterMemory) Size() (size int, err error) {
c.dataMu.RLock()
size = len(c.data)
c.dataMu.RUnlock()
return size, nil
}
// Clear clears all data of the cache.
// Note that this function is sensitive and should be carefully used.
func (c *adapterMemory) Clear() error {
c.dataMu.Lock()
defer c.dataMu.Unlock()
c.data = make(map[interface{}]adapterMemoryItem)
return nil
}
// Close closes the cache.
func (c *adapterMemory) Close() error {
if c.cap > 0 {
c.lru.Close()
}
c.closed.Set(true)
return nil
}
// doSetWithLockCheck sets cache with <key>-<value> pair if <key> does not exist in the
// cache, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if the
// function result is nil.
//
// It doubly checks the <key> whether exists in the cache using mutex writing lock
// before setting it to the cache.
func (c *memCache) doSetWithLockCheck(key interface{}, value interface{}, duration time.Duration) interface{} {
func (c *adapterMemory) doSetWithLockCheck(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
expireTimestamp := c.getInternalExpire(duration)
c.dataMu.Lock()
defer c.dataMu.Unlock()
if v, ok := c.data[key]; ok && !v.IsExpired() {
return v.v
return v.v, nil
}
if f, ok := value.(func() interface{}); ok {
value = f()
if value == nil {
return nil
if f, ok := value.(func() (interface{}, error)); ok {
v, err := f()
if err != nil {
return nil, err
}
if v == nil {
return nil, nil
} else {
value = v
}
}
c.data[key] = memCacheItem{v: value, e: expireTimestamp}
c.eventList.PushBack(&memCacheEvent{k: key, e: expireTimestamp})
return value
c.data[key] = adapterMemoryItem{v: value, e: expireTimestamp}
c.eventList.PushBack(&adapterMemoryEvent{k: key, e: expireTimestamp})
return value, nil
}
// getInternalExpire converts and returns the expire time with given expired duration in milliseconds.
func (c *memCache) getInternalExpire(duration time.Duration) int64 {
func (c *adapterMemory) getInternalExpire(duration time.Duration) int64 {
if duration == 0 {
return gDEFAULT_MAX_EXPIRE
} else {
@ -195,12 +427,12 @@ func (c *memCache) getInternalExpire(duration time.Duration) int64 {
}
// makeExpireKey groups the <expire> in milliseconds to its according seconds.
func (c *memCache) makeExpireKey(expire int64) int64 {
func (c *adapterMemory) makeExpireKey(expire int64) int64 {
return int64(math.Ceil(float64(expire/1000)+1) * 1000)
}
// getExpireSet returns the expire set for given <expire> in seconds.
func (c *memCache) getExpireSet(expire int64) (expireSet *gset.Set) {
func (c *adapterMemory) getExpireSet(expire int64) (expireSet *gset.Set) {
c.expireSetMu.RLock()
expireSet, _ = c.expireSets[expire]
c.expireSetMu.RUnlock()
@ -209,7 +441,7 @@ func (c *memCache) getExpireSet(expire int64) (expireSet *gset.Set) {
// getOrNewExpireSet returns the expire set for given <expire> in seconds.
// It creates and returns a new set for <expire> if it does not exist.
func (c *memCache) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
func (c *adapterMemory) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
if expireSet = c.getExpireSet(expire); expireSet == nil {
expireSet = gset.New(true)
c.expireSetMu.Lock()
@ -223,202 +455,17 @@ func (c *memCache) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
return
}
// SetIfNotExist sets cache with <key>-<value> pair if <key> does not exist in the cache,
// which is expired after <duration>. It does not expire if <duration> == 0.
func (c *memCache) SetIfNotExist(key interface{}, value interface{}, duration time.Duration) bool {
if !c.Contains(key) {
c.doSetWithLockCheck(key, value, duration)
return true
}
return false
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func (c *memCache) Sets(data map[interface{}]interface{}, duration time.Duration) {
expireTime := c.getInternalExpire(duration)
for k, v := range data {
c.dataMu.Lock()
c.data[k] = memCacheItem{
v: v,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&memCacheEvent{
k: k,
e: expireTime,
})
}
}
// Get returns the value of <key>.
// It returns nil if it does not exist or its value is nil.
func (c *memCache) Get(key interface{}) interface{} {
c.dataMu.RLock()
item, ok := c.data[key]
c.dataMu.RUnlock()
if ok && !item.IsExpired() {
// Adding to LRU history if LRU feature is enabled.
if c.cap > 0 {
c.lruGetList.PushBack(key)
}
return item.v
}
return nil
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func (c *memCache) GetVar(key interface{}) *gvar.Var {
return gvar.New(c.Get(key))
}
// GetOrSet returns the value of <key>, or sets <key>-<value> pair and returns <value> if <key>
// does not exist in the cache. The key-value pair expires after <duration>. It does not expire
// if <duration> == 0.
func (c *memCache) GetOrSet(key interface{}, value interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
return c.doSetWithLockCheck(key, value, duration)
} else {
return v
}
}
// GetOrSetFunc returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
func (c *memCache) GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
value := f()
if value == nil {
return nil
}
return c.doSetWithLockCheck(key, value, duration)
} else {
return v
}
}
// GetOrSetFuncLock returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> is executed within writing mutex lock.
func (c *memCache) GetOrSetFuncLock(key interface{}, f func() interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
return c.doSetWithLockCheck(key, f, duration)
} else {
return v
}
}
// Contains returns true if <key> exists in the cache, or else returns false.
func (c *memCache) Contains(key interface{}) bool {
return c.Get(key) != nil
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func (c *memCache) Remove(keys ...interface{}) (value interface{}) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
for _, key := range keys {
item, ok := c.data[key]
if ok {
value = item.v
delete(c.data, key)
c.eventList.PushBack(&memCacheEvent{
k: key,
e: gtime.TimestampMilli() - 1000,
})
}
}
return
}
// Removes deletes <keys> in the cache.
// Deprecated, use Remove instead.
func (c *memCache) Removes(keys []interface{}) {
c.Remove(keys...)
}
// Data returns a copy of all key-value pairs in the cache as map type.
func (c *memCache) Data() map[interface{}]interface{} {
m := make(map[interface{}]interface{})
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
m[k] = v.v
}
}
c.dataMu.RUnlock()
return m
}
// Keys returns all keys in the cache as slice.
func (c *memCache) Keys() []interface{} {
keys := make([]interface{}, 0)
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
keys = append(keys, k)
}
}
c.dataMu.RUnlock()
return keys
}
// KeyStrings returns all keys in the cache as string slice.
func (c *memCache) KeyStrings() []string {
return gconv.Strings(c.Keys())
}
// Values returns all values in the cache as slice.
func (c *memCache) Values() []interface{} {
values := make([]interface{}, 0)
c.dataMu.RLock()
for _, v := range c.data {
if !v.IsExpired() {
values = append(values, v.v)
}
}
c.dataMu.RUnlock()
return values
}
// Size returns the size of the cache.
func (c *memCache) Size() (size int) {
c.dataMu.RLock()
size = len(c.data)
c.dataMu.RUnlock()
return
}
// Close closes the cache.
func (c *memCache) Close() {
if c.cap > 0 {
c.lru.Close()
}
c.closed.Set(true)
}
// syncEventAndClearExpired does the asynchronous task loop:
// 1. Asynchronously process the data in the event list,
// and synchronize the results to the <expireTimes> and <expireSets> properties.
// 2. Clean up the expired key-value pair data.
func (c *memCache) syncEventAndClearExpired() {
func (c *adapterMemory) syncEventAndClearExpired() {
if c.closed.Val() {
gtimer.Exit()
return
}
var (
event *memCacheEvent
event *adapterMemoryEvent
oldExpireTime int64
newExpireTime int64
)
@ -430,7 +477,7 @@ func (c *memCache) syncEventAndClearExpired() {
if v == nil {
break
}
event = v.(*memCacheEvent)
event = v.(*adapterMemoryEvent)
// Fetching the old expire set.
c.expireTimeMu.RLock()
oldExpireTime = c.expireTimes[event.k]
@ -487,7 +534,7 @@ func (c *memCache) syncEventAndClearExpired() {
// clearByKey deletes the key-value pair with given <key>.
// The parameter <force> specifies whether doing this deleting forcibly.
func (c *memCache) clearByKey(key interface{}, force ...bool) {
func (c *adapterMemory) clearByKey(key interface{}, force ...bool) {
c.dataMu.Lock()
// Doubly check before really deleting it from cache.
if item, ok := c.data[key]; (ok && item.IsExpired()) || (len(force) > 0 && force[0]) {

View File

@ -11,7 +11,7 @@ import (
)
// IsExpired checks whether <item> is expired.
func (item *memCacheItem) IsExpired() bool {
func (item *adapterMemoryItem) IsExpired() bool {
// Note that it should use greater than or equal judgement here
// imagining that the cache time is only 1 millisecond.
if item.e >= gtime.TimestampMilli() {

View File

@ -17,17 +17,17 @@ import (
// LRU cache object.
// It uses list.List from stdlib for its underlying doubly linked list.
type memCacheLru struct {
cache *memCache // Parent cache object.
data *gmap.Map // Key mapping to the item of the list.
list *glist.List // Key list.
rawList *glist.List // History for key adding.
closed *gtype.Bool // Closed or not.
type adapterMemoryLru struct {
cache *adapterMemory // Parent cache object.
data *gmap.Map // Key mapping to the item of the list.
list *glist.List // Key list.
rawList *glist.List // History for key adding.
closed *gtype.Bool // Closed or not.
}
// newMemCacheLru creates and returns a new LRU object.
func newMemCacheLru(cache *memCache) *memCacheLru {
lru := &memCacheLru{
func newMemCacheLru(cache *adapterMemory) *adapterMemoryLru {
lru := &adapterMemoryLru{
cache: cache,
data: gmap.New(true),
list: glist.New(true),
@ -39,12 +39,12 @@ func newMemCacheLru(cache *memCache) *memCacheLru {
}
// Close closes the LRU object.
func (lru *memCacheLru) Close() {
func (lru *adapterMemoryLru) Close() {
lru.closed.Set(true)
}
// Remove deletes the <key> FROM <lru>.
func (lru *memCacheLru) Remove(key interface{}) {
func (lru *adapterMemoryLru) Remove(key interface{}) {
if v := lru.data.Get(key); v != nil {
lru.data.Remove(key)
lru.list.Remove(v.(*glist.Element))
@ -52,17 +52,17 @@ func (lru *memCacheLru) Remove(key interface{}) {
}
// Size returns the size of <lru>.
func (lru *memCacheLru) Size() int {
func (lru *adapterMemoryLru) Size() int {
return lru.data.Size()
}
// Push pushes <key> to the tail of <lru>.
func (lru *memCacheLru) Push(key interface{}) {
func (lru *adapterMemoryLru) Push(key interface{}) {
lru.rawList.PushBack(key)
}
// Pop deletes and returns the key from tail of <lru>.
func (lru *memCacheLru) Pop() interface{} {
func (lru *adapterMemoryLru) Pop() interface{} {
if v := lru.list.PopBack(); v != nil {
lru.data.Remove(v)
return v
@ -71,7 +71,7 @@ func (lru *memCacheLru) Pop() interface{} {
}
// Print is used for test only.
//func (lru *memCacheLru) Print() {
//func (lru *adapterMemoryLru) Print() {
// for _, v := range lru.list.FrontAll() {
// fmt.Printf("%v ", v)
// }
@ -80,7 +80,7 @@ func (lru *memCacheLru) Pop() interface{} {
// SyncAndClear synchronizes the keys from <rawList> to <list> and <data>
// using Least Recently Used algorithm.
func (lru *memCacheLru) SyncAndClear() {
func (lru *adapterMemoryLru) SyncAndClear() {
if lru.closed.Val() {
gtimer.Exit()
return

View File

@ -7,31 +7,55 @@
package gcache
import (
"sync/atomic"
"time"
"unsafe"
"github.com/gogf/gf/container/gvar"
"github.com/gogf/gf/os/gtimer"
"github.com/gogf/gf/util/gconv"
"time"
)
// Cache struct.
type Cache struct {
*memCache
Adapter // Adapter for cache features.
}
// New creates and returns a new cache object.
// New creates and returns a new cache object using default memory adapter.
// Note that the LRU feature is only available using memory adapter.
func New(lruCap ...int) *Cache {
memAdapter := newAdapterMemory(lruCap...)
c := &Cache{
memCache: newMemCache(lruCap...),
Adapter: memAdapter,
}
gtimer.AddSingleton(time.Second, c.syncEventAndClearExpired)
// Here may be a "timer leak" if adapter is manually changed from memory adapter.
// Do not worry about this, as adapter is less changed and it dose nothing if it's not used.
gtimer.AddSingleton(time.Second, memAdapter.syncEventAndClearExpired)
return c
}
// Clear clears all data of the cache.
func (c *Cache) Clear() {
// atomic swap to ensure atomicity.
old := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.memCache)), unsafe.Pointer(newMemCache()))
// close the old cache object.
(*memCache)(old).Close()
// SetAdapter changes the adapter for this cache.
// Be very note that, this setting function is not concurrent-safe, which means you should not call
// this setting function concurrently in multiple goroutines.
func (c *Cache) SetAdapter(adapter Adapter) {
c.Adapter = adapter
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func (c *Cache) GetVar(key interface{}) (*gvar.Var, error) {
v, err := c.Get(key)
return gvar.New(v), err
}
// Removes deletes <keys> in the cache.
// Deprecated, use Remove instead.
func (c *Cache) Removes(keys []interface{}) error {
_, err := c.Remove(keys...)
return err
}
// KeyStrings returns all keys in the cache as string slice.
func (c *Cache) KeyStrings() ([]string, error) {
keys, err := c.Keys()
if err != nil {
return nil, err
}
return gconv.Strings(keys), nil
}

View File

@ -25,8 +25,10 @@ func TestCache_GCache_Set(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
gcache.Set(1, 11, 0)
defer gcache.Removes(g.Slice{1, 2, 3})
t.Assert(gcache.Get(1), 11)
t.Assert(gcache.Contains(1), true)
v, _ := gcache.Get(1)
t.Assert(v, 11)
b, _ := gcache.Contains(1)
t.Assert(b, true)
})
}
@ -34,44 +36,57 @@ func TestCache_Set(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
c := gcache.New()
defer c.Close()
c.Set(1, 11, 0)
t.Assert(c.Get(1), 11)
t.Assert(c.Contains(1), true)
t.Assert(c.Set(1, 11, 0), nil)
v, _ := c.Get(1)
t.Assert(v, 11)
b, _ := c.Contains(1)
t.Assert(b, true)
})
}
func TestCache_GetVar(t *testing.T) {
c := gcache.New()
defer c.Close()
gtest.C(t, func(t *gtest.T) {
c := gcache.New()
defer c.Close()
c.Set(1, 11, 0)
t.Assert(c.Get(1), 11)
t.Assert(c.Contains(1), true)
t.Assert(c.GetVar(1).Int(), 11)
t.Assert(c.GetVar(2).Int(), 0)
t.Assert(c.GetVar(2).IsNil(), true)
t.Assert(c.GetVar(2).IsEmpty(), true)
t.Assert(c.Set(1, 11, 0), nil)
v, _ := c.Get(1)
t.Assert(v, 11)
b, _ := c.Contains(1)
t.Assert(b, true)
})
gtest.C(t, func(t *gtest.T) {
v, _ := c.GetVar(1)
t.Assert(v.Int(), 11)
v, _ = c.GetVar(2)
t.Assert(v.Int(), 0)
t.Assert(v.IsNil(), true)
t.Assert(v.IsEmpty(), true)
})
}
func TestCache_Set_Expire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(2, 22, 100*time.Millisecond)
t.Assert(cache.Get(2), 22)
t.Assert(cache.Set(2, 22, 100*time.Millisecond), nil)
v, _ := cache.Get(2)
t.Assert(v, 22)
time.Sleep(200 * time.Millisecond)
t.Assert(cache.Get(2), nil)
v, _ = cache.Get(2)
t.Assert(v, nil)
time.Sleep(3 * time.Second)
t.Assert(cache.Size(), 0)
cache.Close()
n, _ := cache.Size()
t.Assert(n, 0)
t.Assert(cache.Close(), nil)
})
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 100*time.Millisecond)
t.Assert(cache.Get(1), 11)
t.Assert(cache.Set(1, 11, 100*time.Millisecond), nil)
v, _ := cache.Get(1)
t.Assert(v, 11)
time.Sleep(200 * time.Millisecond)
t.Assert(cache.Get(1), nil)
v, _ = cache.Get(1)
t.Assert(v, nil)
})
}
@ -80,20 +95,22 @@ func TestCache_Update_GetExpire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
key := guid.S()
gcache.Set(key, 11, 3*time.Second)
expire1 := gcache.GetExpire(key)
expire1, _ := gcache.GetExpire(key)
gcache.Update(key, 12)
expire2 := gcache.GetExpire(key)
t.Assert(gcache.GetVar(key), 12)
expire2, _ := gcache.GetExpire(key)
v, _ := gcache.GetVar(key)
t.Assert(v, 12)
t.Assert(math.Ceil(expire1.Seconds()), math.Ceil(expire2.Seconds()))
})
// gcache.Cache
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 3*time.Second)
expire1 := cache.GetExpire(1)
expire1, _ := cache.GetExpire(1)
cache.Update(1, 12)
expire2 := cache.GetExpire(1)
t.Assert(cache.GetVar(1), 12)
expire2, _ := cache.GetExpire(1)
v, _ := cache.GetVar(1)
t.Assert(v, 12)
t.Assert(math.Ceil(expire1.Seconds()), math.Ceil(expire2.Seconds()))
})
}
@ -104,34 +121,43 @@ func TestCache_UpdateExpire(t *testing.T) {
key := guid.S()
gcache.Set(key, 11, 3*time.Second)
defer gcache.Remove(key)
oldExpire := gcache.GetExpire(key)
oldExpire, _ := gcache.GetExpire(key)
newExpire := 10 * time.Second
gcache.UpdateExpire(key, newExpire)
t.AssertNE(gcache.GetExpire(key), oldExpire)
t.Assert(math.Ceil(gcache.GetExpire(key).Seconds()), 10)
e, _ := gcache.GetExpire(key)
t.AssertNE(e, oldExpire)
e, _ = gcache.GetExpire(key)
t.Assert(math.Ceil(e.Seconds()), 10)
})
// gcache.Cache
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 3*time.Second)
oldExpire := cache.GetExpire(1)
oldExpire, _ := cache.GetExpire(1)
newExpire := 10 * time.Second
cache.UpdateExpire(1, newExpire)
t.AssertNE(cache.GetExpire(1), oldExpire)
t.Assert(math.Ceil(cache.GetExpire(1).Seconds()), 10)
e, _ := cache.GetExpire(1)
t.AssertNE(e, oldExpire)
e, _ = cache.GetExpire(1)
t.Assert(math.Ceil(e.Seconds()), 10)
})
}
func TestCache_Keys_Values(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
c := gcache.New()
for i := 0; i < 10; i++ {
cache.Set(i, i*10, 0)
t.Assert(c.Set(i, i*10, 0), nil)
}
t.Assert(len(cache.Keys()), 10)
t.Assert(len(cache.Values()), 10)
t.AssertIN(0, cache.Keys())
t.AssertIN(90, cache.Values())
var (
keys, _ = c.Keys()
values, _ = c.Values()
)
t.Assert(len(keys), 10)
t.Assert(len(values), 10)
t.AssertIN(0, keys)
t.AssertIN(90, values)
})
}
@ -141,22 +167,30 @@ func TestCache_LRU(t *testing.T) {
for i := 0; i < 10; i++ {
cache.Set(i, i, 0)
}
t.Assert(cache.Size(), 10)
t.Assert(cache.Get(6), 6)
n, _ := cache.Size()
t.Assert(n, 10)
v, _ := cache.Get(6)
t.Assert(v, 6)
time.Sleep(4 * time.Second)
t.Assert(cache.Size(), 2)
t.Assert(cache.Get(6), 6)
t.Assert(cache.Get(1), nil)
cache.Close()
n, _ = cache.Size()
t.Assert(n, 2)
v, _ = cache.Get(6)
t.Assert(v, 6)
v, _ = cache.Get(1)
t.Assert(v, nil)
t.Assert(cache.Close(), nil)
})
}
func TestCache_LRU_expire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New(2)
cache.Set(1, nil, 1000)
t.Assert(cache.Size(), 1)
t.Assert(cache.Get(1), nil)
t.Assert(cache.Set(1, nil, 1000), nil)
n, _ := cache.Size()
t.Assert(n, 1)
v, _ := cache.Get(1)
t.Assert(v, nil)
})
}
@ -164,17 +198,22 @@ func TestCache_SetIfNotExist(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.SetIfNotExist(1, 11, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.SetIfNotExist(1, 22, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
cache.SetIfNotExist(2, 22, 0)
t.Assert(cache.Get(2), 22)
v, _ = cache.Get(2)
t.Assert(v, 22)
gcache.Removes(g.Slice{1, 2, 3})
gcache.SetIfNotExist(1, 11, 0)
t.Assert(gcache.Get(1), 11)
v, _ = gcache.Get(1)
t.Assert(v, 11)
gcache.SetIfNotExist(1, 22, 0)
t.Assert(gcache.Get(1), 11)
v, _ = gcache.Get(1)
t.Assert(v, 11)
})
}
@ -182,11 +221,13 @@ func TestCache_Sets(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
@ -194,63 +235,82 @@ func TestCache_GetOrSet(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSet(1, 11, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSet(1, 111, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSet(1, 11, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSet(1, 111, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
func TestCache_GetOrSetFunc(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSetFunc(1, func() interface{} {
return 11
cache.GetOrSetFunc(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(cache.Get(1), 11)
cache.GetOrSetFunc(1, func() interface{} {
return 111
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSetFunc(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSetFunc(1, func() interface{} {
return 11
gcache.GetOrSetFunc(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(gcache.Get(1), 11)
gcache.GetOrSetFunc(1, func() interface{} {
return 111
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSetFunc(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
func TestCache_GetOrSetFuncLock(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSetFuncLock(1, func() interface{} {
return 11
cache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(cache.Get(1), 11)
cache.GetOrSetFuncLock(1, func() interface{} {
return 111
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSetFuncLock(1, func() interface{} {
return 11
gcache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(gcache.Get(1), 11)
gcache.GetOrSetFuncLock(1, func() interface{} {
return 111
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
@ -259,7 +319,8 @@ func TestCache_Clear(t *testing.T) {
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
cache.Clear()
t.Assert(cache.Size(), 0)
n, _ := cache.Size()
t.Assert(n, 0)
})
}
@ -298,47 +359,57 @@ func TestCache_Basic(t *testing.T) {
{
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(cache.Contains(1), true)
t.Assert(cache.Get(1), 11)
data := cache.Data()
b, _ := cache.Contains(1)
t.Assert(b, true)
v, _ := cache.Get(1)
t.Assert(v, 11)
data, _ := cache.Data()
t.Assert(data[1], 11)
t.Assert(data[2], 22)
t.Assert(data[3], nil)
t.Assert(cache.Size(), 2)
keys := cache.Keys()
n, _ := cache.Size()
t.Assert(n, 2)
keys, _ := cache.Keys()
t.Assert(gset.NewFrom(g.Slice{1, 2}).Equal(gset.NewFrom(keys)), true)
keyStrs := cache.KeyStrings()
keyStrs, _ := cache.KeyStrings()
t.Assert(gset.NewFrom(g.Slice{"1", "2"}).Equal(gset.NewFrom(keyStrs)), true)
values := cache.Values()
values, _ := cache.Values()
t.Assert(gset.NewFrom(g.Slice{11, 22}).Equal(gset.NewFrom(values)), true)
removeData1 := cache.Remove(1)
removeData1, _ := cache.Remove(1)
t.Assert(removeData1, 11)
t.Assert(cache.Size(), 1)
n, _ = cache.Size()
t.Assert(n, 1)
cache.Removes(g.Slice{2})
t.Assert(cache.Size(), 0)
n, _ = cache.Size()
t.Assert(n, 0)
}
gcache.Remove(g.Slice{1, 2, 3}...)
{
gcache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(gcache.Contains(1), true)
t.Assert(gcache.Get(1), 11)
data := gcache.Data()
b, _ := gcache.Contains(1)
t.Assert(b, true)
v, _ := gcache.Get(1)
t.Assert(v, 11)
data, _ := gcache.Data()
t.Assert(data[1], 11)
t.Assert(data[2], 22)
t.Assert(data[3], nil)
t.Assert(gcache.Size(), 2)
keys := gcache.Keys()
n, _ := gcache.Size()
t.Assert(n, 2)
keys, _ := gcache.Keys()
t.Assert(gset.NewFrom(g.Slice{1, 2}).Equal(gset.NewFrom(keys)), true)
keyStrs := gcache.KeyStrings()
keyStrs, _ := gcache.KeyStrings()
t.Assert(gset.NewFrom(g.Slice{"1", "2"}).Equal(gset.NewFrom(keyStrs)), true)
values := gcache.Values()
values, _ := gcache.Values()
t.Assert(gset.NewFrom(g.Slice{11, 22}).Equal(gset.NewFrom(values)), true)
removeData1 := gcache.Remove(1)
removeData1, _ := gcache.Remove(1)
t.Assert(removeData1, 11)
t.Assert(gcache.Size(), 1)
n, _ = gcache.Size()
t.Assert(n, 1)
gcache.Removes(g.Slice{2})
t.Assert(gcache.Size(), 0)
n, _ = gcache.Size()
t.Assert(n, 0)
}
})
}

View File

@ -334,7 +334,10 @@ func (c *Config) getJson(file ...string) *gjson.Json {
content = ""
filePath = ""
)
// The configured content can be any kind of data type different from its file type.
isFromConfigContent := true
if content = GetContent(name); content == "" {
isFromConfigContent = false
filePath = c.filePath(name)
if filePath == "" {
return nil
@ -346,7 +349,17 @@ func (c *Config) getJson(file ...string) *gjson.Json {
}
}
// Note that the underlying configuration json object operations are concurrent safe.
if j, err := gjson.LoadContent(content, true); err == nil {
var (
j *gjson.Json
err error
)
dataType := gfile.ExtName(name)
if gjson.IsValidDataType(dataType) && !isFromConfigContent {
j, err = gjson.LoadContentType(dataType, content, true)
} else {
j, err = gjson.LoadContent(content, true)
}
if err == nil {
j.SetViolenceCheck(c.vc)
// Add monitor for this configuration file,
// any changes of this file will refresh its cache in Config object.

View File

@ -231,21 +231,16 @@ func Test_SetFileName(t *testing.T) {
func Test_Instance(t *testing.T) {
config := `
{
"array": [
1,
2,
3
],
"redis": {
"cache": "127.0.0.1:6379,1",
"disk": "127.0.0.1:6379,0"
},
"v1": 1,
"v2": "true",
"v3": "off",
"v4": "1.234"
}
array = [1.0, 2.0, 3.0]
v1 = 1.0
v2 = "true"
v3 = "off"
v4 = "1.234"
[redis]
cache = "127.0.0.1:6379,1"
disk = "127.0.0.1:6379,0"
`
gtest.C(t, func(t *gtest.T) {
path := gcfg.DEFAULT_CONFIG_FILE

Some files were not shown because too many files have changed in this diff Show More