mirror of
https://gitee.com/johng/gf
synced 2026-06-07 10:22:11 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@ -9,7 +9,6 @@ package driver
|
||||
import (
|
||||
"database/sql"
|
||||
"github.com/gogf/gf/database/gdb"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
)
|
||||
|
||||
@ -51,14 +50,13 @@ func (d *MyDriver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) {
|
||||
func (d *MyDriver) DoQuery(link gdb.Link, sql string, args ...interface{}) (rows *sql.Rows, err error) {
|
||||
tsMilli := gtime.TimestampMilli()
|
||||
rows, err = d.DriverMysql.DoQuery(link, sql, args...)
|
||||
if _, err := d.DriverMysql.InsertIgnore("monitor", g.Map{
|
||||
"sql": gdb.FormatSqlWithArgs(sql, args),
|
||||
"cost": gtime.TimestampMilli() - tsMilli,
|
||||
"time": gtime.Now(),
|
||||
"error": err.Error(),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
link.Exec(
|
||||
"INSERT INTO `monitor`(`sql`,`cost`,`time`,`error`) VALUES(?,?,?,?)",
|
||||
gdb.FormatSqlWithArgs(sql, args),
|
||||
gtime.TimestampMilli()-tsMilli,
|
||||
gtime.Now(),
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -67,13 +65,12 @@ func (d *MyDriver) DoQuery(link gdb.Link, sql string, args ...interface{}) (rows
|
||||
func (d *MyDriver) DoExec(link gdb.Link, sql string, args ...interface{}) (result sql.Result, err error) {
|
||||
tsMilli := gtime.TimestampMilli()
|
||||
result, err = d.DriverMysql.DoExec(link, sql, args...)
|
||||
if _, err := d.DriverMysql.InsertIgnore("monitor", g.Map{
|
||||
"sql": gdb.FormatSqlWithArgs(sql, args),
|
||||
"cost": gtime.TimestampMilli() - tsMilli,
|
||||
"time": gtime.Now(),
|
||||
"error": err.Error(),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
link.Exec(
|
||||
"INSERT INTO `monitor`(`sql`,`cost`,`time`,`error`) VALUES(?,?,?,?)",
|
||||
gdb.FormatSqlWithArgs(sql, args),
|
||||
gtime.TimestampMilli()-tsMilli,
|
||||
gtime.Now(),
|
||||
err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -512,16 +512,6 @@ func mapToStruct() {
|
||||
}
|
||||
}
|
||||
|
||||
// getQueriedSqls
|
||||
func getQueriedSqls() {
|
||||
for k, v := range db.GetQueriedSqls() {
|
||||
fmt.Println(k, ":")
|
||||
fmt.Println("Sql :", v.Sql)
|
||||
fmt.Println("Args :", v.Args)
|
||||
fmt.Println("Error:", v.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
db.PingMaster()
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
|
||||
# MySQL数据库配置
|
||||
# MySQL.
|
||||
[database]
|
||||
debug = true
|
||||
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?parseTime=true&loc=Local"
|
||||
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?parseTime=true"
|
||||
MaxOpen = 100
|
||||
|
||||
# Redis.
|
||||
[redis]
|
||||
default = "127.0.0.1:6379,0"
|
||||
cache = "127.0.0.1:6379,1"
|
||||
|
||||
#[database]
|
||||
# [[database.default]]
|
||||
# type = "mysql"
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/database/gdb"
|
||||
"time"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -18,10 +18,9 @@ func main() {
|
||||
|
||||
db.SetDebug(true)
|
||||
|
||||
type User struct {
|
||||
CreateTime time.Time `orm:"create_time"`
|
||||
}
|
||||
r, e := db.Table("user").Data(User{CreateTime: time.Now()}).Insert()
|
||||
r, e := db.Table("user").Data(g.Map{
|
||||
"create_at": "now()",
|
||||
}).Unscoped().Insert()
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/frame/g"
|
||||
)
|
||||
|
||||
func main() {
|
||||
db := g.DB()
|
||||
db.SetDebug(true)
|
||||
|
||||
db.Table("user").Data("num=num+1").Where("id", 8).Update()
|
||||
one, err := g.DB().Table("carlist c").
|
||||
LeftJoin("cardetail d", "c.postid=d.carid").
|
||||
Where("c.postid", "142039140032006").
|
||||
Fields("c.*,d.*").One()
|
||||
fmt.Println(err)
|
||||
g.Dump(one)
|
||||
}
|
||||
|
||||
@ -513,16 +513,6 @@ func mapToStruct() {
|
||||
}
|
||||
}
|
||||
|
||||
// getQueriedSqls
|
||||
func getQueriedSqls() {
|
||||
for k, v := range db.GetQueriedSqls() {
|
||||
fmt.Println(k, ":")
|
||||
fmt.Println("Sql :", v.Sql)
|
||||
fmt.Println("Args :", v.Args)
|
||||
fmt.Println("Error:", v.Error)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
db.PingMaster()
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -1,3 +1 @@
|
||||
|
||||
hello = "Hello"
|
||||
world = "World"
|
||||
OrderPaid = "You have successfully complete order #%d payment, paid amount: ¥%0.2f."
|
||||
@ -1,2 +1 @@
|
||||
hello = "你好"
|
||||
world = "世界"
|
||||
OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。"
|
||||
@ -15,10 +15,10 @@ func main() {
|
||||
r.Response.Writeln("end")
|
||||
})
|
||||
s.BindHookHandlerByMap(p, map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
glog.To(r.Response.Writer).Println("BeforeServe")
|
||||
},
|
||||
ghttp.HOOK_AFTER_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookAfterServe: func(r *ghttp.Request) {
|
||||
glog.To(r.Response.Writer).Println("AfterServe")
|
||||
},
|
||||
})
|
||||
|
||||
@ -3,15 +3,12 @@ package main
|
||||
import (
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/net/ghttp"
|
||||
"github.com/gogf/gf/os/glog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.SetIndexFolder(true)
|
||||
s.BindHandler("/", func(r *ghttp.Request) {
|
||||
glog.Println(r.Header)
|
||||
r.Response.Write("hello world")
|
||||
r.Response.Write("Hello World")
|
||||
})
|
||||
s.SetPort(8999)
|
||||
s.Run()
|
||||
|
||||
@ -12,7 +12,7 @@ func Order(r *ghttp.Request) {
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.Group("/api.v1", func(group *ghttp.RouterGroup) {
|
||||
group.Hook("/*any", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
group.Hook("/*any", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.CORSDefault()
|
||||
})
|
||||
g.GET("/order", Order)
|
||||
|
||||
@ -11,10 +11,10 @@ func main() {
|
||||
p := "/:name/info/{uid}"
|
||||
s := g.Server()
|
||||
s.BindHookHandlerByMap(p, map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) { glog.Println(ghttp.HOOK_BEFORE_SERVE) },
|
||||
ghttp.HOOK_AFTER_SERVE: func(r *ghttp.Request) { glog.Println(ghttp.HOOK_AFTER_SERVE) },
|
||||
ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) { glog.Println(ghttp.HOOK_BEFORE_OUTPUT) },
|
||||
ghttp.HOOK_AFTER_OUTPUT: func(r *ghttp.Request) { glog.Println(ghttp.HOOK_AFTER_OUTPUT) },
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) { glog.Println(ghttp.HookBeforeServe) },
|
||||
ghttp.HookAfterServe: func(r *ghttp.Request) { glog.Println(ghttp.HookAfterServe) },
|
||||
ghttp.HookBeforeOutput: func(r *ghttp.Request) { glog.Println(ghttp.HookBeforeOutput) },
|
||||
ghttp.HookAfterOutput: func(r *ghttp.Request) { glog.Println(ghttp.HookAfterOutput) },
|
||||
})
|
||||
s.BindHandler(p, func(r *ghttp.Request) {
|
||||
r.Response.Write("用户:", r.Get("name"), ", uid:", r.Get("uid"))
|
||||
|
||||
@ -11,7 +11,7 @@ func main() {
|
||||
// 多事件回调示例,事件1
|
||||
pattern1 := "/:name/info"
|
||||
s.BindHookHandlerByMap(pattern1, map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
r.SetParam("uid", 1000)
|
||||
},
|
||||
})
|
||||
@ -22,7 +22,7 @@ func main() {
|
||||
// 多事件回调示例,事件2
|
||||
pattern2 := "/{object}/list/{page}.java"
|
||||
s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_OUTPUT: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeOutput: func(r *ghttp.Request) {
|
||||
r.Response.SetBuffer([]byte(
|
||||
fmt.Sprintf("通过事件修改输出内容, object:%s, page:%s", r.Get("object"), r.GetRouterString("page"))),
|
||||
)
|
||||
|
||||
@ -7,10 +7,10 @@ import (
|
||||
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.BindHookHandler("/*any", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
s.BindHookHandler("/*any", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Writeln("/*any")
|
||||
})
|
||||
s.BindHookHandler("/v1/*", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
s.BindHookHandler("/v1/*", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Writeln("/v1/*")
|
||||
r.ExitHook()
|
||||
})
|
||||
|
||||
@ -11,7 +11,7 @@ func main() {
|
||||
r.Response.Writeln(r.Get("name"))
|
||||
})
|
||||
s.BindHookHandlerByMap("/", map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
r.SetParam("name", "john")
|
||||
},
|
||||
})
|
||||
|
||||
@ -12,17 +12,17 @@ func main() {
|
||||
})
|
||||
|
||||
s.BindHookHandlerByMap("/priority/:name", map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
r.Response.Writeln("/priority/:name")
|
||||
},
|
||||
})
|
||||
s.BindHookHandlerByMap("/priority/*any", map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
r.Response.Writeln("/priority/*any")
|
||||
},
|
||||
})
|
||||
s.BindHookHandlerByMap("/priority/show", map[string]ghttp.HandlerFunc{
|
||||
ghttp.HOOK_BEFORE_SERVE: func(r *ghttp.Request) {
|
||||
ghttp.HookBeforeServe: func(r *ghttp.Request) {
|
||||
r.Response.Writeln("/priority/show")
|
||||
},
|
||||
})
|
||||
|
||||
@ -27,10 +27,10 @@ func main() {
|
||||
})
|
||||
})
|
||||
group.Group("/hook", func(group *ghttp.RouterGroup) {
|
||||
group.Hook("/*", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
group.Hook("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Write("hook any")
|
||||
})
|
||||
group.Hook("/:name", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
group.Hook("/:name", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Write("hook name")
|
||||
})
|
||||
})
|
||||
|
||||
@ -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("哈喽世界!")
|
||||
})
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.SetConfigWithMap(g.Map{"Graceful": true})
|
||||
s.EnableAdmin()
|
||||
s.SetPort(8199)
|
||||
s.Run()
|
||||
|
||||
@ -18,7 +18,7 @@ func main() {
|
||||
s := g.Server()
|
||||
s.SetIndexFolder(true)
|
||||
s.SetServerRoot("root")
|
||||
s.BindHookHandler("/*", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
s.BindHookHandler("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
fmt.Println(r.URL.Path, r.IsFileRequest())
|
||||
})
|
||||
s.BindHandler("/template", func(r *ghttp.Request) {
|
||||
|
||||
@ -27,7 +27,7 @@ func main() {
|
||||
s := g.Server()
|
||||
obj := new(Object)
|
||||
s.Group("/api").Bind([]ghttp.GroupItem{
|
||||
{"ALL", "*", HookHandler, ghttp.HOOK_BEFORE_SERVE},
|
||||
{"ALL", "*", HookHandler, ghttp.HookBeforeServe},
|
||||
{"ALL", "/handler", Handler},
|
||||
{"ALL", "/obj", obj},
|
||||
{"GET", "/obj/show", obj, "Show"},
|
||||
|
||||
@ -54,10 +54,10 @@ func main() {
|
||||
})
|
||||
})
|
||||
group.Group("/hook", func(group *ghttp.RouterGroup) {
|
||||
group.Hook("/*", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
group.Hook("/*", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Write("hook any")
|
||||
})
|
||||
group.Hook("/:name", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
group.Hook("/:name", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
r.Response.Write("hook name")
|
||||
})
|
||||
})
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
|
||||
func main() {
|
||||
s := g.Server()
|
||||
s.BindHookHandler("/*any", ghttp.HOOK_BEFORE_SERVE, func(r *ghttp.Request) {
|
||||
s.BindHookHandler("/*any", ghttp.HookBeforeServe, func(r *ghttp.Request) {
|
||||
fmt.Println(r.Router)
|
||||
fmt.Println(r.Get("customer_id"))
|
||||
})
|
||||
|
||||
@ -13,7 +13,7 @@ func main() {
|
||||
key := "key"
|
||||
|
||||
// 第一次锁带时间
|
||||
gmlock.Lock(key, 1000)
|
||||
gmlock.Lock(key)
|
||||
glog.Println("lock1")
|
||||
// 这个时候上一次的计时解锁已失效
|
||||
gmlock.Unlock(key)
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
{{define "container"}}
|
||||
<h1>CONTAINER</h1>
|
||||
<h1>{{.container}}</h1>
|
||||
{{end}}
|
||||
@ -1,3 +1,3 @@
|
||||
{{define "footer"}}
|
||||
<h1>FOOTER</h1>
|
||||
<h1>{{.footer}}</h1>
|
||||
{{end}}
|
||||
@ -1,3 +1,3 @@
|
||||
{{define "header"}}
|
||||
<h1>HEADER</h1>
|
||||
<h1>{{.header}}</h1>
|
||||
{{end}}
|
||||
@ -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>
|
||||
@ -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",
|
||||
})
|
||||
})
|
||||
|
||||
@ -11,7 +11,7 @@ func main() {
|
||||
fmt.Println(1)
|
||||
gutil.Throw("error")
|
||||
fmt.Println(2)
|
||||
}, func(err interface{}) {
|
||||
}, func(err error) {
|
||||
fmt.Println(err)
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ go:
|
||||
- "1.12.x"
|
||||
- "1.13.x"
|
||||
- "1.14.x"
|
||||
- "1.15.x"
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
||||
99
DONATOR.MD
99
DONATOR.MD
@ -1,99 +0,0 @@
|
||||
# Donators
|
||||
|
||||
We currently accept donation by [Alipay](https://goframe.org/images/donate.png) / [Gitee](https://gitee.com/johng/gf),
|
||||
please note your github/gitee account in your payment bill.
|
||||
|
||||
| Name | Channel | Amount | Comment
|
||||
|---|---|--- | ---
|
||||
|[hailaz](https://gitee.com/hailaz)|gitee|¥20.00 |
|
||||
|[ireadx](https://github.com/ireadx)|alipay|¥301.00 |
|
||||
|[mg91](https://gitee.com/mg91)|gitee|¥10.00 |
|
||||
|[pibigstar](https://github.com/pibigstar)|alipay|¥10.00 |
|
||||
|[tiangenglan](https://gitee.com/tiangenglan)|gitee|¥30.00 |
|
||||
|[wxkj](https://gitee.com/wxkj)|wechat|¥10.00 |
|
||||
|[zhuhuan12](https://gitee.com/zhuhuan12)|gitee|¥50.00 |
|
||||
|[zfan_codes](https://gitee.com/zfan_codes)|gitee|¥10.00 |
|
||||
|[arden](https://github.com/arden)|alipay|¥10.00 |
|
||||
|[macnie](https://www.macnie.com)|wechat|¥110.00 |
|
||||
|lah|wechat|¥100.00 |
|
||||
|x*z|wechat|¥20.00 |
|
||||
|潘兄|wechat|¥100.00 |
|
||||
|Fly的狐狸|wechat|¥100.00 |
|
||||
|全|alipay|¥100.00 |
|
||||
|东东|wechat|¥100.00 |
|
||||
|严宇轩|alipay|¥99.99 |
|
||||
|土豆相公|alipay|¥66.60 |
|
||||
|Hades|alipay|¥66.66 |
|
||||
|蔡蔡|wechat|¥666.00 | gf越来越强
|
||||
|上海金保证网络科技|bank|¥2000.00 |
|
||||
|[foxhack](https://github.com/foxhack)|wechat|¥20.00 |
|
||||
|*栈|wechat|¥5.00 |
|
||||
|*络|wechat|¥10.00|
|
||||
|M*e|wechat|¥20.00|
|
||||
|*G|wechat|¥10.00|
|
||||
|E*_|wechat|¥10.00|
|
||||
|A*y|wechat|¥1.00| 感谢大佬goframe
|
||||
|K*e|wechat|¥168.00| 感谢老大
|
||||
|*雨|wechat|¥100.00|
|
||||
|*洁|wechat|¥10.00|赞助你肥宅快乐水
|
||||
|R*s|wechat|¥18.88| 谢谢GF!辛苦了!
|
||||
|粟*e|wechat|¥50.00|
|
||||
|[李超](https://github.com/effortlee)|wechat|¥124.00|
|
||||
|soidea666|wechat+qq|¥800.00|
|
||||
|[王哈哈](https://gitee.com/develop1024)|wechat|¥6.66| 希望gf越来越好
|
||||
|夕景|alipay+qq|¥9.96+3.57|
|
||||
|struggler|alipay|¥18.80|
|
||||
|*铁|wechat|¥0.01|
|
||||
|C*e|wechat|¥66.66| GF越来越好,棒👍!
|
||||
|---P คิดถึง|wechat|¥6.66|
|
||||
|[王飞](https://gitee.com/wang_2018)|gitee|¥20.00| 感谢您的开源项目!
|
||||
|[Zeroing-ZY](https://gitee.com/yunjieg)|gitee|¥20.00| 感谢您的开源项目!
|
||||
|[katydid酱](https://gitee.com/katydid2005)|gitee|¥50.00| 感谢您的开源项目!框架给予了很大的帮助!谢谢大佬!
|
||||
|[李海峰](https://gitee.com/dlhf)|gitee|¥10.00| 希望GF越来越好,框架很牛逼!
|
||||
|陆昱天|alipay|¥100.00|
|
||||
|[Dockercore](https://github.com/dockercore)|wechat|¥200.00| 非常喜欢!简洁好用!文档超级全!
|
||||
|🚶|wechat|¥6.88| 喝杯冰阔落
|
||||
|a*l|wechat|¥10.00| gf
|
||||
|[wxkj](https://gitee.com/wxkj)|wechat|¥10.00|
|
||||
|[米司特包](https://github.com/misitebao)|wechat|¥9.99|
|
||||
|重庆宝尔威科技|wechat|¥6.66|
|
||||
|琦玉-QPT|wechat|¥6.66|
|
||||
|sailsea|wechat|¥11.00|
|
||||
|[seny0929](https://gitee.com/seny0929)|wechat|¥99.90|
|
||||
|*华|wechat|¥6.66| 感谢郭强的热心
|
||||
|[Playhi](https://github.com/Playhi)|alipay|¥10.00|
|
||||
|北京京纬互动科技|alipay|¥200.00|
|
||||
|[米司特包](https://github.com/misitebao)|wechat|¥99.99|
|
||||
|金毛|alipay|¥100.00|
|
||||
|1*1x|wechat|¥100.00|
|
||||
|[ywanbing](https://github.com/ywanbing)|wechat|¥66.66|
|
||||
|[侯哥](http://www.macnie.com)|wechat|¥10.00|
|
||||
|如果🍋|alipay|¥100.00| 错过的奶茶^_^
|
||||
|蔡蔡|wechat|¥666.00| gf真强大,让项目省心
|
||||
|jack|wechat|¥100.00|
|
||||
|sbilly|wechat|¥100.00| 祝好!
|
||||
|[米司特包](https://github.com/misitebao)|wechat|¥166.66| 大佬加油!
|
||||
|*秦|wechat|¥20.00| 给群主献上一杯咖啡...
|
||||
|[zhuhuan12](https://gitee.com/zhuhuan12)|wechat|¥50.00|
|
||||
|faddei|qq|¥9.99|
|
||||
|*天|wechat|¥10.00|
|
||||
|*.|wechat|¥20.00| 学生一个微薄之力,冲
|
||||
|李勇|wechat|¥50.00|
|
||||
|[米司特包](https://github.com/misitebao)|wechat|¥66.66|
|
||||
|千年|wechat|¥1.08|
|
||||
|[szzxing](https://github.com/szzxing)|wechat|¥50.00|
|
||||
|伟客互联|wechat|¥66.66|
|
||||
|[sbilly](https://github.com/sbilly)|wechat|¥99.00|
|
||||
|**阳|alipay|¥100.01|
|
||||
|**亮|alipay|¥10.00|
|
||||
|[mingzaily](https://github.com/mingzaily)|alipay|¥30.00|
|
||||
|[seny0929](https://gitee.com/seny0929)|alipay|¥9.90|
|
||||
|Fly的狐狸|alipay|¥100.00|
|
||||
|六七 ·|wechat|¥88.88| gf越来越好
|
||||
|Tom|wechat|¥500.00| 一点心意 希望GF越来越好
|
||||
|Chao|wechat|¥166.00|
|
||||
|
||||
|
||||
|
||||
<img src="https://goframe.org/images/donate.jpeg"/>
|
||||
|
||||
73
README.MD
73
README.MD
@ -9,8 +9,8 @@
|
||||
|
||||
English | [简体中文](README_ZH.MD)
|
||||
|
||||
`GF(GoFrame)` is a modular, powerful, high-performance and production-ready application development framework
|
||||
of golang. Providing a series of core components and dozens of practical modules, such as:
|
||||
`GF(GoFrame)` is a modular, powerful, high-performance and enterprise-class application development framework
|
||||
of Golang. Providing a series of core components and dozens of practical modules, such as:
|
||||
cache, logging, containers, timer, resource, validator, database orm, etc.
|
||||
Supporting web server integrated with router, cookie, session, middleware, logger, configure,
|
||||
template, https, hooks, rewrites and many more features.
|
||||
@ -47,56 +47,9 @@ golang version >= 1.11
|
||||
|
||||
# Performance
|
||||
|
||||
Here's the most popular Golang frameworks and libraries performance testing result in `WEB Server`. Performance testing cases source codes are hosted at: https://github.com/gogf/gf-performance
|
||||
The `Web` component performance of `GoFrame`, please refer to third-party project: https://github.com/the-benchmarker/web-frameworks
|
||||
|
||||
## Environment
|
||||
|
||||
OS : Ubuntu 18.04 amd64
|
||||
CPU : AMD A8-6600K x 4
|
||||
MEM : 32GB
|
||||
GO : v1.13.4
|
||||
|
||||
## Testing Tool
|
||||
|
||||
`ab`: Apache HTTP server benchmarking tool.
|
||||
|
||||
Command:
|
||||
```
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/hello
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/query?id=10000
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/json
|
||||
```
|
||||
The concurrency starts from `100` to `10000`.
|
||||
|
||||
> Run `5` times for each case of each project and pick up the best testing result.
|
||||
|
||||
## 1. Hello World
|
||||
<table>
|
||||
<tr>
|
||||
<th>Throughputs</th>
|
||||
<th>Mean Latency</th>
|
||||
<th>P99 Latency</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/throughputs1.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/meanlatency1.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/p99latency1.jpeg"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 2. Json Response
|
||||
<table>
|
||||
<tr>
|
||||
<th>Throughputs</th>
|
||||
<th>Mean Latency</th>
|
||||
<th>P99 Latency</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/throughputs3.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/meanlatency3.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/p99latency3.jpeg"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
# Documentation
|
||||
|
||||
@ -115,9 +68,19 @@ The concurrency starts from `100` to `10000`.
|
||||
|
||||
`GF` is licensed under the [MIT License](LICENSE), 100% free and open-source, forever.
|
||||
|
||||
# Known Users
|
||||
# Part Of Users
|
||||
|
||||
Logos are not authorized to be shown due to trademark copyrights.
|
||||
- [Tencent](https://www.tencent.com/)
|
||||
- [ZTE](https://www.zte.com.cn/china/)
|
||||
- [Ant Financial Services](https://www.antfin.com/)
|
||||
- [MedLinker](https://www.medlinker.com/)
|
||||
- [KuCoin](https://www.kucoin.io/)
|
||||
- [LeYouJia](https://www.leyoujia.com/)
|
||||
- [IGG](https://igg.com)
|
||||
- [XiMaLaYa](https://www.ximalaya.com)
|
||||
- [ZYBang](https://www.zybang.com/)
|
||||
|
||||
> We list part of the users here, if your company or products are using `GoFrame`, please let us know [here](https://github.com/gogf/gf/issues/168).
|
||||
|
||||
|
||||
# Contributors
|
||||
@ -127,7 +90,7 @@ This project exists thanks to all the people who contribute. [[Contributors](htt
|
||||
|
||||
# Donators
|
||||
|
||||
We currently accept donation by Alipay/WechatPay, please note your github/gitee account in your payment bill. If you like `GF`, why not [buy developer a cup of coffee](DONATOR.MD)?
|
||||
If you love `GF`, why not [buy developer a cup of coffee](https://itician.org/pages/viewpage.action?pageId=1115633)?
|
||||
|
||||
# Sponsors
|
||||
We appreciate any kind of sponsorship for `GF` development. If you've got some interesting, please contact WeChat `389961817` / Email `john@goframe.org`.
|
||||
@ -135,8 +98,8 @@ We appreciate any kind of sponsorship for `GF` development. If you've got some i
|
||||
|
||||
|
||||
# Thanks
|
||||
<a href="https://www.jetbrains.com/?from=GoFrame"><img src="https://goframe.org/images/jetbrains.png" width="100" alt="JetBrains"/></a>
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=GoFrame"><img src="https://itician.org/download/thumbnails/1114119/jetbrains.png?version=1&modificationDate=1608649325806&api=v2" height="120" alt="JetBrains"/></a>
|
||||
<a href="https://www.atlassian.com/?from=GoFrame"><img src="https://itician.org/download/thumbnails/1114119/u%3D605052180%2C3099422872%26fm%3D11%26gp%3D0.jpg?version=1&modificationDate=1608649343601&api=v2" height="120" alt="Atlassian"/></a>
|
||||
|
||||
|
||||
|
||||
|
||||
73
README_ZH.MD
73
README_ZH.MD
@ -8,7 +8,7 @@
|
||||
|
||||
[English](README.MD) | 简体中文
|
||||
|
||||
`GF(Go Frame)`是一款模块化、高性能、生产级的Go基础开发框架。
|
||||
`GF(Go Frame)`是一款模块化、高性能、企业级的Go基础开发框架。
|
||||
实现了比较完善的基础设施建设以及开发工具链,提供了常用的基础开发模块,
|
||||
如:缓存、日志、队列、数组、集合、容器、定时器、命令行、内存锁、对象池、
|
||||
配置管理、资源管理、数据校验、数据编码、定时任务、数据库ORM、TCP/UDP组件、进程管理/通信等等。
|
||||
@ -65,58 +65,7 @@ golang版本 >= 1.11
|
||||
|
||||
# 性能
|
||||
|
||||
以下是目前最流行的`WEB Server` Golang框架/类库性能测试结果。
|
||||
性能测试用例源代码仓库: https://github.com/gogf/gf-performance
|
||||
|
||||
## 环境:
|
||||
|
||||
OS : Ubuntu 18.04 amd64
|
||||
CPU : AMD A8-6600K x 4
|
||||
MEM : 32GB
|
||||
GO : v1.13.4
|
||||
|
||||
## 工具
|
||||
|
||||
`ab`: Apache HTTP server benchmarking tool.
|
||||
|
||||
测试命令:
|
||||
```
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/hello
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/query?id=10000
|
||||
ab -t 10 -c 100 http://127.0.0.1:3000/json
|
||||
```
|
||||
并发客户端数量从 `100` 递增到 `10000`。
|
||||
|
||||
> 每个项目的每个用例均运行`5`次,取最优的结果展示。
|
||||
|
||||
## 1. Hello World
|
||||
<table>
|
||||
<tr>
|
||||
<th>Throughputs</th>
|
||||
<th>Mean Latency</th>
|
||||
<th>P99 Latency</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/throughputs1.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/meanlatency1.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/p99latency1.jpeg"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 2. Json Response
|
||||
<table>
|
||||
<tr>
|
||||
<th>Throughputs</th>
|
||||
<th>Mean Latency</th>
|
||||
<th>P99 Latency</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/throughputs3.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/meanlatency3.jpeg"></td>
|
||||
<td width="30%"><img src="http://gfcdn.johng.cn/images/performance/p99latency3.jpeg"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
`Web`组件的性能测试,请参考第三方性能测试评估:https://github.com/the-benchmarker/web-frameworks
|
||||
|
||||
# 文档
|
||||
|
||||
@ -137,7 +86,17 @@ ab -t 10 -c 100 http://127.0.0.1:3000/json
|
||||
|
||||
# 用户
|
||||
|
||||
由于商标版权缘故,未经厂商商务部授权允许无法用于宣传展示。
|
||||
- [腾讯科技](https://www.tencent.com/)
|
||||
- [中兴科技](https://www.zte.com.cn/china/)
|
||||
- [蚂蚁金服](https://www.antfin.com/)
|
||||
- [医联科技](https://www.medlinker.com/)
|
||||
- [库币科技](https://www.kucoin.io/)
|
||||
- [乐有家](https://www.leyoujia.com/)
|
||||
- [IGG](https://igg.com)
|
||||
- [喜马拉雅](https://www.ximalaya.com)
|
||||
- [作业帮](https://www.zybang.com/)
|
||||
|
||||
> 在这里只列举了部分知名的用户,如果您的企业或者产品正在使用`GoFrame`,欢迎到 [这里](https://github.com/gogf/gf/issues/168) 留言。
|
||||
|
||||
# 贡献
|
||||
|
||||
@ -147,7 +106,7 @@ ab -t 10 -c 100 http://127.0.0.1:3000/json
|
||||
|
||||
# 捐赠
|
||||
|
||||
如果您喜欢`GF`,要不[给开发者来杯咖啡吧](DONATOR.MD)!
|
||||
如果您喜欢`GF`,要不给开发者 [来杯咖啡](https://itician.org/pages/viewpage.action?pageId=1115633) 吧!
|
||||
请在捐赠时备注您的`github`/`gitee`账号名称。
|
||||
|
||||
# 赞助
|
||||
@ -155,5 +114,5 @@ ab -t 10 -c 100 http://127.0.0.1:3000/json
|
||||
赞助支持`GF`框架的快速研发,如果您感兴趣,请联系 微信 `389961817` / 邮件 `john@goframe.org`。
|
||||
|
||||
# 感谢
|
||||
<a href="https://www.jetbrains.com/?from=GoFrame"><img src="https://goframe.org/images/jetbrains.png" width="100" alt="JetBrains"/></a>
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=GoFrame"><img src="https://itician.org/download/thumbnails/1114119/jetbrains.png?version=1&modificationDate=1608649325806&api=v2" height="120" alt="JetBrains"/></a>
|
||||
<a href="https://www.atlassian.com/?from=GoFrame"><img src="https://itician.org/download/thumbnails/1114119/u%3D605052180%2C3099422872%26fm%3D11%26gp%3D0.jpg?version=1&modificationDate=1608649343601&api=v2" height="120" alt="Atlassian"/></a>
|
||||
|
||||
566
RELEASE.1.MD
566
RELEASE.1.MD
@ -1,566 +0,0 @@
|
||||
# `v1.8.0` (2019-07-15)
|
||||
|
||||
## 新功能改进
|
||||
1. 框架目前 `69` 个开发模块(不包括内部模块),原生代码 `65302` 行(不包含第三包依赖包),单元测试覆盖率达到`77%`;
|
||||
1. 新增`gerror`错误处理模块:https://goframe.org/errors/gerror/index
|
||||
1. 改进`gcharset`字符编码转换模块,支持更多的字符集:https://goframe.org/encoding/gcharset/index
|
||||
1. 新增`gmutex`模块,基于`channel`实现的高级互斥锁模块,支持更丰富的互斥锁特性:https://goframe.org/os/gmutex/index
|
||||
1. 改进`glog`日志模块:
|
||||
- 新增日志异步输出特性:https://goframe.org/os/glog/async
|
||||
- 新增`Flags`额外功能特性:https://goframe.org/os/glog/flags
|
||||
- 新增`Json`数据格式输出:https://goframe.org/os/glog/json
|
||||
- 新增自定义`Writer`接口特性:https://goframe.org/os/glog/writer
|
||||
- **修改`Backtrace`名称为`Stack`,并改进调用堆栈输出格式;**
|
||||
- 新增`Expose`方法暴露内部默认`Logger`对象;
|
||||
1. 改进`gdb`数据库ORM模块:
|
||||
- **改进错误处理,当数据库操作没有查询到数据时,`error`返回`sql.ErrNoRows`**:https://goframe.org/database/gdb/error
|
||||
- 改进`Update`/`Delete`方法支持`Order BY`及`LIMIT`特性;
|
||||
- 数据库链式操作及方法操作中,预处理变量参数支持`slice`参数:https://goframe.org/database/gdb/chaining/model
|
||||
- **修改`Priority`权重配置名称为`Weight`;**
|
||||
- 新增`Debug`配置,可配置开启/关闭调试特性:https://goframe.org/database/gdb/config
|
||||
- 新增`Offset`方法,该方法为可选链式操作方法,`pgsql`数据库可直接通过`Limit`方法第二个参数自动识别为`Offset`语法;
|
||||
- 改进数据库动态切换特性,支持不同数据库类型的当前操作数据库切换;
|
||||
- 改进简化配置文件结构:https://goframe.org/database/gdb/config
|
||||
1. 改进`gconv`数据转换模块:
|
||||
- 对结构体对象转换时支持更多的标签:`gconv/c/json`;
|
||||
- 支持`*struct/[]struct/[]*struct`自动初始化创建对象/数组:https://goframe.org/util/gconv/struct
|
||||
- 新增`Strusts/StrctsDeep`方法,用于结构体数组的递归转换;
|
||||
- 新增`StructDeep`方法,用于对结构体对象的递归转换;
|
||||
- 新增`MapDeep`方法,用于对结构体属性的递归转换;
|
||||
1. 改进`ghttp`模块:
|
||||
- 改进`ghttp`模块的分组路由功能,完善逻辑处理细节,程序更加稳健;
|
||||
- 改进`ghttp.Request.Get*ToStruct`方法,支持`params/param/p`标签,支持结构体递归转换,并且支持`**struct`参数的对象自动初始化;
|
||||
- 改进`ghttp.CORSDefault`的跨域设置参数,`AllowOrigin`参数调整为`*`;
|
||||
1. 改进`gvalid`数据校验模块:
|
||||
- 增加对校验标签`gvalid/valid/v`的支持;
|
||||
- 改进`CheckStruct`支持对结构体对象的递归校验:https://goframe.org/util/gvalid/checkstruct
|
||||
1. 改进`gtcp`TCP通信模块:
|
||||
- 改进通信包协议设计,更加轻量级高效:https://goframe.org/net/gtcp/conn/pkg
|
||||
- 改进`TCP Server`增加对`TLS`的支持:https://goframe.org/net/gtcp/tls
|
||||
- 增加`Server.Cloce`服务端关闭方法;
|
||||
1. 改进`gproc`模块的通信数据结构,并使用`gtcp`的轻量级包协议重构消息发送逻辑;
|
||||
1. 改进`gqueue`模块增加数据同步缓冲机制,解决大数据量下的内存占用及延迟问题;
|
||||
1. 改进`gmlock`模块,使用`gmutex`模块替换内部的互斥锁,增加更多的操作方法;
|
||||
1. 改进`gaes`加密模块,增加`CBC`模式的加密/解密方法:
|
||||
1. 改进`garray.Range/SubSlice`方法,改进设计,提高性能;
|
||||
1. 改进`gjson`/`gparser`模块实现`MarshalJSON`接口以实现自定义的`JSON`数据格式转换;
|
||||
1. **改进`crypto`分类下模块的方法返回值,增加`error`错误变量返回,以保证更严谨的接口设计风格;**
|
||||
1. **改进`gbase64`模块,输入输出类型发生改变,并增加多个相关方法;**
|
||||
1. 改进`gflock`修改方法名`UnLock`为`Unlock`,新增`IsRLocked`方法;
|
||||
1. 新增`gfile.CopyFile/CopyDir`方法,用于文件及目录的复制;
|
||||
1. 改进`gjson/gparser/gvar/gcfg`模块增加更多的类型转换方法;
|
||||
1. 改进`gcache`模块,过期时间参数支持`time.Duration`类型;
|
||||
1. 新增`internal/structs`包,强大且便捷的结构体解析,并改进框架中所有涉及到结构体反射处理的模块;
|
||||
1. 改进`gbinary`增加封装方法对`BigEndian`的支持;
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`garray.Search`返回值问题;
|
||||
1. 修复`garray.Contains`, `garray.New*ArrayFromCopy`方法逻辑问题;
|
||||
1. 修复`gjson.Remove`删除`slice`参数问题;
|
||||
1. 修复`gtree.AVLTree.Remove`方法返回值问题;
|
||||
1. 修复`gqueue.Size`不准确的大小问题;
|
||||
1. 修复`queue.Close`问题;
|
||||
1. 修复`gcache.GetOrSetLockFunc`当回调函数返回`nil`结果时的死锁问题;
|
||||
1. 修复`gfsnotify.Add`方法默认递归监控添加失效问题;
|
||||
1. 修复`gdb.Model.Scan`在某些参数类型下的失效问题;
|
||||
|
||||
## 注意事项
|
||||
|
||||
请注意以上粗体文字部分,如有使用,在您升级时可能会出现不兼容性。
|
||||
|
||||
|
||||
|
||||
# `v1.7.0` (2019-06-10)
|
||||
## 新功能/改进
|
||||
1. 重构改进`glog`模块:
|
||||
- 去掉日志模块所有的锁机制,改为无锁设计,执行性能更加高效
|
||||
- 增加日志内容的异步输出特性:https://goframe.org/os/glog/async
|
||||
- 增加日志输出内容的`Json`格式支持:https://goframe.org/os/glog/json
|
||||
- 增加`Flags`额外特性支持,包括文件行号打印、自定义时间格式、异步输出等特性控制:https://goframe.org/os/glog/flags
|
||||
- 增加`Writer`接口支持,便于开发者进行自定义的日志功能扩展,或者与第三方服务/模块对接集成:https://goframe.org/os/glog/writer
|
||||
- 修改`SetStdPrint`方法名为`SetStdoutPrint`
|
||||
- 修改链式方法`StdPrint`方法名为`Stdout`
|
||||
- 标记淘汰`*fln`日志输出方法,`*f`方法支持自动的换行输出
|
||||
- 新增更多的链式方法支持:https://goframe.org/os/glog/chain
|
||||
1. 重构改进`gmap`模块:
|
||||
- 增加更多数据格式支持:`HashMap`/`ListMap`/`TreeMap`
|
||||
- 简化类型名称,如`gmap.StringInterfaceMap`简化为`gmap.StrAnyMap`
|
||||
- 改进`Map/Keys/Values`方法以提高性能
|
||||
- 修改`BatchSet`/`BatchRemove`方法名为`Sets`/`Removes`
|
||||
- 新增更多功能方法支持:https://goframe.org/container/gmap/index
|
||||
1. 改进`gtime`时间模块:
|
||||
- 增加并完善更多的类`PHP`时间格式支持
|
||||
- 新增更多功能方法,如`FormatTo`/`LayoutTo`等等
|
||||
- 详见开发文档:https://goframe.org/os/gtime/index
|
||||
1. 改进`gdb`数据库模块:
|
||||
- 增加对继承结构体的数据转换支持:https://goframe.org/database/gdb/senior
|
||||
- 新增`GetLastSql`方法,用以在调试模式下获取最近一条执行的SQL语句
|
||||
- 其他的细节处理改进
|
||||
1. 改进`gtcp`通信模块:
|
||||
- 完善处理细节,提高通信性能;
|
||||
- 增加`TLS`服务端/客户端通信支持:https://goframe.org/net/gtcp/tls
|
||||
- 增加简单协议支持,便于开发者封包/解包,并解决粘包/半包问题:https://goframe.org/net/gtcp/conn/pkg
|
||||
- TCP服务端增加`Close`方法
|
||||
- 更多细节查看开发文档:https://goframe.org/net/gtcp/index
|
||||
1. 改进`gconv`类型转换模块
|
||||
- 修改`gconv.TimeDuration`转换方法名称为`gconv.Duration`
|
||||
- 新增`gconv.StructDeep`及`gconv.MapDeep`方法,支持递归转换
|
||||
- 详见开发文档:https://goframe.org/util/gconv/struct
|
||||
1. 改进`ghttp`模块:
|
||||
- 日志输出增加`http/https`字段:https://goframe.org/net/ghttp/logs
|
||||
- 新增`ghttp.Server.SetKeepAlive`设置方法,用以开启/关闭`KeepAlive`特性
|
||||
- 增加`ghttp.Request.GetUrl`方法,用以获取当前完整的URL请求地址
|
||||
- `ghttp.Client`客户端支持开发者自定义`Transport`属性,`ghttp.Client.Post`方法支持`浏览器模式`:https://goframe.org/net/ghttp/client
|
||||
1. 新增`gtree`树形数据结构容器支持:https://goframe.org/container/gtree/index
|
||||
1. 改进`gudp`通信模块,具体请参考开发文档:https://goframe.org/net/gudp/index
|
||||
1. 改进`gcfg`配置管理模块,所有`Get*`方法增加默认值支持:https://goframe.org/os/gcfg/index
|
||||
1. `gredis`模块新增`DoVar`/`ReceiveVar`方法以便于开发者对执行结果进行灵活的数据格式转换:https://goframe.org/database/gredis/index
|
||||
1. `gcache`模块`BatchSet`/`BatchRemove`方法名修改为`Sets`/`Removes`
|
||||
1. 改进`gjson`/`gparser`模块,增加更多方法:https://goframe.org/encoding/gjson/index
|
||||
1. 改进`gfile.MainPkgPath`方法,以支持不同平台的开发环境;
|
||||
1. 改进`grpool`协程池模块,提高执行性能:https://goframe.org/os/grpool/index
|
||||
1. 改进`TryCatch`方法,当开发者不传递`Catch`参数时,默认抑制并忽略错误的处理
|
||||
1. 改进`gmlock`模块,增加`TryLockFunc`/`TryRLockFunc`方法,并且为`gmlock.Mutex`高级互斥锁对象增加`TryLockFunc`/`TryRLockFunc`方法
|
||||
1. 去除`gvar.VarRead`接口类型支持
|
||||
|
||||
## Bug Fix
|
||||
1. 解决`gdb`模块与其他第三方`ORM`模块同时使用的冲突;
|
||||
1. 修复`gcron.AddOnce`方法的细节逻辑问题;
|
||||
1. 修复内部`empty`模块的`IsEmpty`方法对结构体属性的空校验错误;
|
||||
1. 修复`gview`模板引擎的并发安全问题;
|
||||
1. 修复`ghttp.Server`的SESSION初始化过期时间问题;
|
||||
|
||||
# `v1.6.0` (2019-04-09)
|
||||
|
||||
## 新功能/改进
|
||||
1. `gcron`定时任务模块增加运行日志记录功能:https://goframe.org/os/gcron/index
|
||||
1. `gredis`增加全局分组配置功能,并增加更多的配置选项`maxIdle/maxActive/idleTimeout/maxConnLifetime`:https://goframe.org/database/gredis/index
|
||||
1. `gcfg`模块增加更多的默认配置文件检索路径,并且增加全局分组配置特性,增加`Instance`单例方法:https://goframe.org/os/gcfg/index
|
||||
1. `gview`模块增加更多的默认配置文件检索路径,并且增加`Instance`单例方法:https://goframe.org/os/gview/index
|
||||
1. `ghttp`模块新功能及改进:
|
||||
- 新增`CORS`HTTP(S)跨域请求特性: https://goframe.org/net/ghttp/cors
|
||||
- 增加`TLSConfig`配置功能;
|
||||
- 去掉路由注册方法的`error`返回值,当产生注册错误时直接终端打印错误/输出到日志文件;
|
||||
- 增加在`HTTP Code 302`跳转时的`Set-Cookie`支持;
|
||||
- 增加对`SESSION ID`的安全性检查;
|
||||
- 增加对基于`HTTPS`的`WebSocket`支持(`WSS`):https://goframe.org/net/ghttp/websocket/index
|
||||
- `Request`对象增加`Error`方法,用于输出自定义错误信息到`WebServer`错误日志中;
|
||||
- 其他一些改进;
|
||||
1. `gdb`模块新功能及改进:
|
||||
- 新增`Instance`单例管理方法;
|
||||
- 新增`Structs/Scan`链式操作方法,`gdb.DB/TX`新增`GetStructs/GetScan`方法,用于结果集`struct`/`slice`映射转换:https://goframe.org/database/gdb/chaining
|
||||
- 新增`Safe`链式操作方法(默认非并发安全),用于链式安全控制:https://goframe.org/database/gdb/chaining
|
||||
- `Where`链式操作方法改进:
|
||||
- 方法支持任意的`string/map/slice/struct/*struct`类型;
|
||||
- 逻辑调整,当链式操作中存在多个`Where`方法调用时,自动转换为`And`条件;
|
||||
- 支持`slice`条件参数,常用在`SELECT IN`查询中,例如:`Where("uid IN(?)", g.Slice{1,2,3})`;
|
||||
- 支持在`map`类型条件参数的`key`中传递条件,例如:`Where(g.Map{"uid>?", uid})`;
|
||||
1. `gconv`及`gvalid`模块改进并去掉对私有`struct`方法属性的转换/校验;
|
||||
1. `gconv.Map`转换方法新增对`json tag`: `-`, `omitempty`的支持: https://goframe.org/util/gconv/map
|
||||
1. `gstr`模块新增 `ReplaceI/ReplaceIByArray/ReplaceIByMap`大小写非敏感替换方法;
|
||||
1. `gutil`模块增加`IsEmpty`方法,用于判断给定变量是否为空(整型0, 布尔false, slice/map长度为0, 其他为nil的情况,判断为空),并增加快捷方法`g.IsEmpty`;
|
||||
1. `gutil`模块增加`Export`方法,用于导出返回格式化打印的变量内容字符串,并增加快捷方法`g.Export`;
|
||||
1. `gspath`增加缓存及非缓存检索检索方法`Search`/`SearchWithCache`;
|
||||
1. `gjson`模块增加默认的`UseNumber`功能支持;
|
||||
1. `gmap`增加`SetIfNotExistFunc/SetIfNotExistFuncLock`方法;
|
||||
1. 迁移`greuseport`模块到新的仓库:https://github.com/gogf/greuseport
|
||||
1. 大量的单元测试完善;
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`gqueue`模块的资源竞争问题;
|
||||
1. 修复`gconv.GTime`转换失败问题;
|
||||
1. 修复`gconv.String`在转换`int`参数时字节溢出问题;
|
||||
1. 修复`ghttp.Request`的`HTTP Basic Auth`校验问题;
|
||||
1. 修复`gxml`针对于非`UTF-8`编码内容转换的并发安全问题;
|
||||
1. 修复`gtime`部分`Format`(`G`&`j`)格式失效问题;
|
||||
1. 修复`gudp.Conn`对象的`RemoteAddr`获取客户端连接地址方法问题;
|
||||
1. 修复`gmap/gcache`模块的`GetOrSetFuncLock`方法,增加对回调方法返回值的`nil`判断,只有非nil返回值才会被保存;
|
||||
|
||||
|
||||
|
||||
# `v1.5.8` (2019-02-28)
|
||||
|
||||
## 新特性
|
||||
1. 主库从`gitee`迁移到了`github`( https://github.com/gogf/gf ),`gitee`作为镜像站,用于国内的代码贡献及ISSUE提交,迁移说明详见:https://goframe.org/upgradeto150
|
||||
1. 对常用的`container`数组模块: `garray`做了大量改进/完善工作,新增大量常用方法,并完善单元测试用例及方法注释,详见API文档:https://godoc.org/github.com/gogf/gf/container/garray
|
||||
1. 对常用的`container`集合模块: `gset`做了大量改进/完善工作,新增大量常用方法,并完善单元测试用例及方法注释,详见API文档:https://godoc.org/github.com/gogf/gf/container/gset
|
||||
1. 对常用的`container`MAP模块: `gmap`做了大量改进/完善工作,新增大量常用方法,并完善单元测试用例及方法注释,详见API文档:https://godoc.org/github.com/gogf/gf/container/gmap
|
||||
1. 对常用的字符串模块: `gstr`做了大量改进/完善工作,新增大量常用方法,并完善单元测试用例及方法注释,详见API文档:https://godoc.org/github.com/gogf/gf/text/gstr
|
||||
1. 改进`gform`中对`struct`/`*struct`参数的支持,`*Insert/*Save/*Replace/*Update/Where/Data`方法的参数调整为`interface{}`类型,并支持任意类型的: `string/map/slice/struct/*struct`参数传递,具体请参考:https://goframe.org/database/orm/chaining
|
||||
1. 新增/完善若干模块的单元测试用例, 包括:`gvalid`/`gregex`/`garray`/`gset`/`gmap`/`gstr`/`gconv`/`ghttp`/`gdb`;
|
||||
1. 由于`gkafka`模块比较重,且不是框架核心模块,因此将该模块迁移到新的仓库中独立管理,并去掉相关依赖包:https://github.com/gogf/gkafka
|
||||
1. 新增`greuseport`模块,用以实现TCP的`REUSEPORT`特性:https://godoc.org/github.com/gogf/gf/net/greuseport
|
||||
|
||||
## 新功能/改进
|
||||
1. 去掉模板引擎内置变量中自动初始化`session`对象带来的内存占用问题;
|
||||
1. `ghttp.Client`改进,增加若干方法,详见:https://goframe.org/net/ghttp/client
|
||||
1. `ghttp`分组路由增加`COMMON`方法,用以注册常用的`HTTP METHOD`(`GET/PUT/POST/DELETE`)路由;
|
||||
1. 更新框架依赖的`golang.org/x/sys`模块;
|
||||
1. 改进`gform`的批量操作(`Batch*`操作)返回结果对象,可以通过该结果对象获得批量操作准确的受影响记录行数;
|
||||
1. 将`gstr`/`gregex`模块从`util`分类迁移到了`text`分类目录下;
|
||||
1. 将`gtest`模块从`util`分类迁移到了`test`分类目录下;
|
||||
1. 完善`glog`方法注释;
|
||||
|
||||
## Bug Fix
|
||||
1. 修复带点的邮件格式,用`gvalid.Check`的"`email`"规则不能匹配成功;
|
||||
1. 修复`gvalid.Check`在`regex`规则下的检查失败问题;
|
||||
1. 修复`gcron`模块定时规则中天和周不允许`?`符号的问题;
|
||||
1. 修复`ghttp.Server`在部分异常情况下仍然返回`200`状态码的问题;
|
||||
1. 修复`gfpool`模块中由于原子操作问题造成的高并发"内存泄露"问题;
|
||||
1. 修复分组路由注册对象/控制时,方法`Index`的路由仅能通过`/xxx/index`访问的问题;
|
||||
1. 修复模板引擎使用中,当不存在`config.toml`(即使没使用)配置文件时的报错问题;
|
||||
1. 其他一些修复;
|
||||
|
||||
|
||||
|
||||
# `v1.4.6` (2019-01-24)
|
||||
|
||||
## 新特性
|
||||
1. 新增并发安全的高性能任务定时器模块`gtimer`, 类似于Java的`Timer`,但是比较于Java的`Timer`更加强大,内部实现采用灵活高效的`分层时间轮`设计,被设计为可管理维护百万级别以上数量的定时任务。`gtimer`为`GF`框架的核心模块之一,单元测试覆盖率达到`93.6%`:[https://goframe.org/os/gtimer/index](https://goframe.org/os/gtimer/index)
|
||||
1. 采用任务定时器`gtimer`重构`gcron`定时任务模块,去掉第三方`github.com/robfig/cron`包的使用。`gcron`增加单例模式的定时任务:[https://goframe.org/os/gcron/index#](https://goframe.org/os/gcron/index#);
|
||||
1. `gconv`类型转换模块支持对`struct`结构体中的**指针属性**转换:[https://goframe.org/util/gconv/struct](https://goframe.org/util/gconv/struct);
|
||||
1. `gform`增加对数据库类型的自动识别特性,这一特性在需要将查询结果`json`编码返回时非常有用: [https://goframe.org/database/orm/index](https://goframe.org/database/orm/index)
|
||||
1. `Travis CI`增加对`386`架构的自动化测试支持(目前已支持`386`和`amd64`);
|
||||
|
||||
## 新功能
|
||||
1. `ghttp`模块新增`Exit`、`ExitAll`、`ExitHook`方法,用于HTTP请求处理流程控制: [https://goframe.org/net/ghttp/service/object](https://goframe.org/net/ghttp/service/object);
|
||||
1. `grand`模块增加`Meet/MeetProb`方法,用于给定概率的随机满足判断,增加别名方法`N/Str/Digits/Letters`;
|
||||
1. `gvalid`数据/表单校验模块增加`16X`及`19X`手机号的校验支持;
|
||||
|
||||
## 功能改进
|
||||
1. `gform`设置默认的数据库连接池`CONN_MAX_LIFE`参数值为`30`秒;
|
||||
1. 改进`glist`模块,提高约`20%`左右性能,并增加若干链表操作方法;
|
||||
1. 改进`gqueue`模块,提高约`50`左右性能,并增加模块对`select`语法的支持(使用`Queue.C`): [https://goframe.org/container/gqueue/index](https://goframe.org/container/gqueue/index);
|
||||
1. 改进`gmlock`内存锁模块,并完善单元测试用例:[https://goframe.org/os/gmlock/index](https://goframe.org/os/gmlock/index);
|
||||
1. 改进并发安全容器所有的模块,调整并发安全控制非必需参数`safe...bool`为`unsafe...bool`;
|
||||
1. 改进`gpool`对象复用模块,支持并发安全;
|
||||
1. 更新`gkafka`模块的第三方依赖包;
|
||||
1. 完善`ghttp`模块的单元测试用例;
|
||||
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`gmd5`模块操作文件时的文件指针未关闭问题;
|
||||
1. 修复`gcache`缓存项过期删除失效问题;
|
||||
1. 其他修复;
|
||||
|
||||
# `v1.3.8` (2018-12-26)
|
||||
|
||||
## 新特性
|
||||
1. 对`gform`完成重构,以提高扩展性,并修复部分细节问题、完善单元测试用例([https://goframe.org/database/orm/index](https://goframe.org/database/orm/index));
|
||||
1. `WebServer`路由注册新增分组路由特性([https://goframe.org/net/ghttp/group](https://goframe.org/net/ghttp/group));
|
||||
1. `WebServer`新增`Rewrite`路由重写特性([https://goframe.org/net/ghttp/static](https://goframe.org/net/ghttp/static));
|
||||
1. 增加框架运行时对开发环境的自动识别;
|
||||
1. 增加了`Travis CI`自动化构建/测试;
|
||||
|
||||
## 新功能
|
||||
1. 改进`WebServer`静态文件服务功能,增加`SetStaticPath`/`AddStaticPath`方法([https://goframe.org/net/ghttp/static](https://goframe.org/net/ghttp/static));
|
||||
1. `gform`新增`Filter`链式操作方法,用于过滤参数中的非表字段键值对([https://goframe.org/database/orm/linkop](https://goframe.org/database/orm/linkop));
|
||||
1. `gcache`新增`Data`方法,用以获取所有的缓存数据项;
|
||||
1. `gredis`增加`GetConn`方法获取原生redis连接对象;
|
||||
|
||||
## 功能改进
|
||||
1. 改进`gform`的`Where`方法,支持`slice`类型的参数,并更方便地支持`in`操作查询([https://goframe.org/database/orm/linkop](https://goframe.org/database/orm/linkop));
|
||||
1. 改进`gproc`进程间通信数据结构,将`pid`字段从`16bit`扩展为`24bit`;
|
||||
1. 改进`gconv`/`gmap`/`garray`,增加若干操作方法;
|
||||
1. 改进`gview`模板引擎中的`date`内置函数,当给定的时间戳为空时打印当前的系统时间;
|
||||
1. 改进`gview`模板引擎中,当打印的变量不存在时,显示为空(标准库默认显示为`<no value>`);
|
||||
1. 改进`WebServer`,去掉`HANGUP`的信号监听,避免程序通过`nohup`运行时产生异常退出问题;
|
||||
1. 改进`gcache`性能,并完善基准测试;
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`gcache`在非LRU特性开启时的缓存关闭资源竞争问题,并修复`doSetWithLockCheck`内部方法的返回值问题;
|
||||
1. 修复`grand.intn`内部方法在`x86`架构下的随机数位溢出问题;
|
||||
1. 修复`gbinary`中`Int`方法针对`[]byte`参数长度自动匹配造成的字节长度溢出问题;
|
||||
1. 修复`gjson`由于官方标准库`json`不支持`map[interface{}]*`类型造成的Go变量编码问题;
|
||||
1. 修复`garray`中部分方法的数据竞争问题,修复二分查找排序问题;
|
||||
1. 修复`ghttp.Request.GetVar`方法获取参数问题;
|
||||
1. 修复`gform`的数据库连接池不起作用的问题;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `v1.2.11` (2018-11-26)
|
||||
## 新特性
|
||||
1. `ORM`新增对`SQLServer`及`Oracle`的支持([https://goframe.org/database/orm/database](https://goframe.org/database/orm/database));
|
||||
1. 完成`gvalid`模块校验结果的顺序特性([https://goframe.org/util/gvalid/checkmap](https://goframe.org/util/gvalid/checkmap));
|
||||
1. 改进`ghttp.Request.Exit`,使得调用该方法时立即退出业务执行,开发者无需调用`Exit`方法时再使用`return`返回([https://goframe.org/net/ghttp/service/object](https://goframe.org/net/ghttp/service/object));
|
||||
1. 模板引擎新增若干内置函数:`text/html/htmldecode/url/urldecode/date/compare/substr/strlimit/hidestr/highlight/toupper/tolower/nl2br` ([https://goframe.org/os/gview/funcs](https://goframe.org/os/gview/funcs));
|
||||
1. 模板引擎新增内置变量`Config` ([https://goframe.org/os/gview/vars](https://goframe.org/os/gview/vars));
|
||||
1. 改进`gconv.Struct`转换默认规则,支持不区分大小写的键名与属性名称匹配;
|
||||
1. `gform`配置文件支持`linkinfo`自定义数据库连接字段([https://goframe.org/database/orm/config](https://goframe.org/database/orm/config));
|
||||
1. `gfsnotify`模块增加对特定回调的取消注册功能([https://goframe.org/os/gfsnotify/index](https://goframe.org/os/gfsnotify/index));
|
||||
|
||||
|
||||
|
||||
## 新功能
|
||||
1. 改进`ghttp.Request`,增加`SetParam/GetParam`请求流程自定义变量设置/获取方法,用于在请求流程中的回调函数共享变量([https://goframe.org/net/ghttp/request](https://goframe.org/net/ghttp/request));
|
||||
1. 改进`ghttp.Response`,增加`ServeFileDownload`方法,用于WebServer引导客户端下载文件([https://goframe.org/net/ghttp/response](https://goframe.org/net/ghttp/response));
|
||||
1. `gvar`模块新增`gvar.VarRead`只读接口,用于控制对外只暴露数据读取功能;
|
||||
1. 增加`g.Throw`抛异常方法,`g.TryCatch`异常捕获方法封装;
|
||||
1. 改进`gcron`模块,增加自定义的Cron管理对象,增加`New/Start/Stop`方法;
|
||||
|
||||
|
||||
## 功能改进
|
||||
1. WebServer添加`RouterCacheExpire`配置参数,用于设置路由检索缓存过期时间;
|
||||
1. WebServer允许同一`HOOK`事件被多次绑定注册,先注册的回调函数优先级更高([https://goframe.org/net/ghttp/service/hook](https://goframe.org/net/ghttp/service/hook));
|
||||
1. 当前工作目录为系统临时目录时,`gcfg`/`gview`/`ghttp`模块默认不添加工作目录到搜索路径;
|
||||
1. 改进`WebSocket`默认支持跨域请求([https://goframe.org/net/ghttp/websocket](https://goframe.org/net/ghttp/websocket));
|
||||
1. 改进`gtime.Format`支持中文;
|
||||
1. 改进`gfsnotify`,支持编辑器对文件非执行标准编辑时(RENAME+CHMOD)的热更新问题;
|
||||
1. 改进`gtype.Set`方法,增加Set原子操作返回旧的变量值;
|
||||
1. `gfile.ScanDir`增加支持`pattern`多个文件模式匹配,使用'`,`'符号分隔多个匹配模式;
|
||||
1. `gcfg`模块增加获取配置变量为`*gvar.Var`;
|
||||
1. `gstr`模块增加对中文截取方法;
|
||||
1. 改进`gtime.StrToTime`对常用时间格式匹配模式,新增`gtime.ParseTimeFromContent`方法;
|
||||
1. 修改配置管理、模板引擎、调试模式的环境变量名称为大写下划线标准格式;
|
||||
1. 改进`grand`模块随机数生成设计,底层使用`crypto/rand`+缓冲区实现高速的随机数生成([https://goframe.org/util/grand/index](https://goframe.org/util/grand/index));
|
||||
|
||||
## 问题修复
|
||||
1. 修复`gspath`模块在`windows`下搜索失效问题;
|
||||
1. 修复`gspath`模块Search时带有indexFiles的检索问题;
|
||||
1. bug fix INZS1([https://github.com/gogf/gf/issues/INZS1](https://github.com/gogf/gf/issues/INZS1));
|
||||
1. 修复`gproc.ShellRun`在windows下的执行问题;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `v1.0.898 stable` (2018-10-24)
|
||||
|
||||
## 新特性
|
||||
1. `gf-orm`增加`sqlite`数据库类型支持([http://gf.johng.cn/database/orm/database](http://gf.johng.cn/database/orm/database));
|
||||
1. 增加`gkafka`模块,对kafka的客户端程序封装,支持分组消费及指定起始位置等特性,并提供简便易用的API接口([http://gf.johng.cn/database/gkafka/index](http://gf.johng.cn/database/gkafka/index));
|
||||
1. 增加go语言最新版本的`go modules`特性支持;
|
||||
1. 增加`gcron`定时任务模块([http://gf.johng.cn/os/gcron/index](http://gf.johng.cn/os/gcron/index));
|
||||
1. `Web Server`增加路由注册项获取/打印特性,所有的路由注册/回调注册一览无余;
|
||||
1. 模板引擎增加全局变量管理,并增加多个常用的内置函数及内置变量([http://gf.johng.cn/os/gview/funcs](http://gf.johng.cn/os/gview/funcs));
|
||||
1. `gredis`改进为单例操作方式(基于基层连接池特性),每次操作`redis`服务器时开发者无需显示调用`Close`方法执行关闭([http://gf.johng.cn/database/gredis/index](http://gf.johng.cn/database/gredis/index));
|
||||
1. `gf-orm`增加数据库操作自动`Close`特性(基于底层链接池特性),开发者无需再`defer db.Close()`,并增加`g.DB`数据库对象单例别名([http://gf.johng.cn/database/orm/linkop](http://gf.johng.cn/database/orm/linkop));
|
||||
1. 增加`gvar`通用动态变量模块([http://gf.johng.cn/container/gvar/index](http://gf.johng.cn/container/gvar/index));
|
||||
1. 数据结构容器增加`并发安全特性开启/关闭功能`,当关闭后和普通的数据结构无异,且在非并发安全模式下性能会得到提高;
|
||||
1. 新增`gmlock`内存锁模块([http://gf.johng.cn/os/gmlock/index](http://gf.johng.cn/os/gmlock/index));
|
||||
1. 增加`gaes`算法模块([http://gf.johng.cn/crypto/gaes/index](http://gf.johng.cn/crypto/gaes/index));
|
||||
1. `gproc`模块增加执行`shell`命令方法([http://gf.johng.cn/os/gproc/index](http://gf.johng.cn/os/gproc/index));
|
||||
1. 新增`gfcache`模块,用于带自动缓存更新的文件内容操作(文档待完善);
|
||||
|
||||
## 新功能
|
||||
1. `glog`增加链式操作方法,增加日志级别管理控制、分类管理、调试管理功能;
|
||||
1. `g.View`增加分组名称设置,支持通过`g.*`对象管理器获取多个命名的单例模板引擎对象;
|
||||
1. `glog`增加对文件名称格式的自定义设置,支持`gtime日期格式`;
|
||||
1. `gconv`增加`Ints/Uints/Floats/Interfaces`转换方法;
|
||||
1. `gjson`增加`Append`方法;
|
||||
1. `gparser`增加`NewUnsafe/Append`方法;
|
||||
1. `gcache`增加`GetOrSet/GetOrSetFunc/GetOrSetFuncLock`方法;
|
||||
1. `gset`增加`LockFunc/RLockFunc`方法;
|
||||
1. `ghttp.Response`方法完善,增加`ParseTpl/ParseTplContent/TplContent`方法,`Template`修改为`Tpl`方法;
|
||||
1. `ghttp.Request`增加获取用户真实IP判断;
|
||||
1. `Session`增加`Contains`方法;
|
||||
1. 完善`ghtml`模块,增加多个方法;
|
||||
1. `gcache`新增`Contains/SetIfNotExist`方法;
|
||||
1. `gvalid`增加`Error`对象,用以管理校验错误信息;
|
||||
1. `gvalid`模块增加`struct tag`的校验规则、自定义错误提示信息绑定的支持特性([http://gf.johng.cn/util/gvalid/index](http://gf.johng.cn/util/gvalid/index));
|
||||
1. `ghttp`增加输入参数与`struct`的`绑定机制`,并增加对应`params`标签支持([http://gf.johng.cn/net/ghttp/service/handler](http://gf.johng.cn/net/ghttp/service/handler));
|
||||
1. `ghttp.Request`增加服务端`BasicAuth`功能(文档待完善);
|
||||
1. `gvalid`增加字段校验别名,用于自定义返回结果字段,并更新WebServer中相关使用的模块;
|
||||
1. `gf-orm`链式操作增加`ForPage`方法,调整`Chunks`方法;
|
||||
1. `ghttp`对象路由注册增加`Init&Shut`自动回调方法,增加重复路由注册检测功能;
|
||||
1. `gfsnotify`增加默认递归`Add/Remove`特性;
|
||||
1. `ghttp.Response`增加`ServiceFile`方法;
|
||||
1. 其他一些新功能;
|
||||
|
||||
## 功能改进
|
||||
1. 改进`ghttp.Server`配置管理;
|
||||
1. 改进`gcache`底层对象继承关系,改进部分设计细节,提高性能;
|
||||
1. 改进`gfpool`文件指针池,修复部分错误,提升性能,并增加基准测试代码;
|
||||
1. 改进`gmap`系列并发安全map数据结构,增加多个易用性的方法;
|
||||
1. 改进`gconv.Struct`对象转换功能([http://gf.johng.cn/util/gconv/index](http://gf.johng.cn/util/gconv/index));
|
||||
1. 改进`grand`随机数生成规则,提供了极高的随机数生成性能,并保证每一次调用随机方法时生成的都是不同的随机数值([http://gf.johng.cn/util/grand/index](http://gf.johng.cn/util/grand/index));
|
||||
1. 改进`gfile`文件内容操作方法,增加若干常用的文件内容读取方法;
|
||||
1. 改进`gtime`模块,并增加时区转换方法;
|
||||
1. 改进`COOKIE`,去掉锁机制;
|
||||
1. 改进`SESSION`获取方法,新增多个类型获取方法;
|
||||
1. 改进`g.DB/g.Config`单例缓存键名;
|
||||
1. 改进`gtcp/gudp`超时错误判断机制;
|
||||
1. 改进`gtype`底层统一修改为原子操作;
|
||||
1. 改进`gvalid`对`struct`的`string`属性的默认值非必需校验;
|
||||
1. 改进`gvalid`在关联规则下的非必需校验;
|
||||
1. 改进`gf-orm`在调试模式下日志自动输出功能;
|
||||
1. `ghttp.Server/gspath`模块静态文件检索改进;
|
||||
1. 优化`ghttp.ServerConfig`配置,增加`struct/method``名称到uri`的转换规则,通过`SetNameToUri`方法进行灵活配置([http://gf.johng.cn/net/ghttp/service/object](http://gf.johng.cn/net/ghttp/service/object));
|
||||
1. 改进`*any/:name`路由匹配规则,支持不带名字的`*/:`路由规则;
|
||||
1. 修改默认配置文件名称 `config.yml` -> `config.toml`([http://gf.johng.cn/os/gcfg/index](http://gf.johng.cn/os/gcfg/index));
|
||||
1. 调整服务注册的`BindControllerMethod`及`BindObjectMethod`逻辑为绑定路由到指定的方法执行;
|
||||
1. 改进`garray`二分查找方法,增加安全操作处理;
|
||||
1. 改进`gdb.Result/Recorde` `ToXml`方法,增加可选的`rootTag`参数;
|
||||
1. 其他一些改进;
|
||||
|
||||
## 问题修复
|
||||
1. 修复`ghttp.Server`在`windows`下的重启失效问题;
|
||||
1. 修复`ghttp.Server`服务注册与回调注册路由重复判断问题;
|
||||
1. 修复`garray`排序数组`Add`变参时的死锁问题;
|
||||
1. 修复`gfsnotify`默认递归监控整个`gspath.Add`添加的目录的问题;
|
||||
1. 修复`ghttp.BindParams`对`@file`文件上传标识符的转义问题;
|
||||
1. 修复`ghttp.Server`日志路径丢失问题;
|
||||
1. 修复`多WebServer`下的状态检测问题;
|
||||
1. 修复`gvalid`模块`min/max`校验问题;
|
||||
1. 修复控制器和执行对象服务注册时绑定'/'路由的问题;
|
||||
1. 修复`gvalid.CheckStruct`自定义错误提示失效问题;
|
||||
1. `ghttp.Server`修复`hook`与`serve`方法的路由影响,并新增跳转方法;
|
||||
1. 其他一些修复;
|
||||
|
||||
## 其他改动
|
||||
1. 去掉`gfile.IsExecutable`方法;
|
||||
1. 目录调整,将`加密/解密`相关的包从`encoding`目录迁移到`crypto`目录下;
|
||||
1. 增加`gfsnotify/gfcache`调试信息;
|
||||
1. `gf-orm`允许写入的键值为`nil`时往数据库中写入`null`;
|
||||
1. 统一使用`gview.Params`类型作为模板变量类型;
|
||||
1. `gconv.MapToStruct`方法名称修改为`gconv.Struct`;
|
||||
1. `ghttp.Server`完善重启及停止的终端提示信息;
|
||||
1. 完善`gring`模块,增加`约瑟夫问题`代码作为`gring`示例程序;
|
||||
1. 其他一些改动;
|
||||
|
||||
|
||||
|
||||
# `v0.99.682 beta` (2018-08-07)
|
||||
## 新特性
|
||||
1、新增gdes包,用于DES加密/加密算法处理;
|
||||
2、新增gkafka包,kafka的golang客户端;
|
||||
3、新增gpool对象复用池,比较于标准库的sync.Pool更加灵活强大,可自定义对象的缓存时间、创建方法、销毁方法(http://gf.johng.cn/686654);
|
||||
4、完成网络通信gtcp/gudp包的重构,并进行了大量的改进工作,新增了详尽的开发文档及示例代码(http://gf.johng.cn/494382);
|
||||
5、增加gring并发安全环,标准库container/ring包的并发安全版本,并做了易用性的封装(http://gf.johng.cn/686655);
|
||||
6、gtime包新增了自定义日期格式话的支持,格式化语法类似PHP的date语法(http://gf.johng.cn/494387);
|
||||
7、gdb增加调试模式特性,使用SetDebug方法实现,在调试模式下可以获取详细的SQL执行记录,增加了详细的开发文档及示例代码(http://gf.johng.cn/702801);
|
||||
8、gdb增加查询缓存特性,使用Cache方法实现,增加了详细的开发文档及示例代码(http://gf.johng.cn/702801);
|
||||
9、ghttp.Server路由功能增加字段匹配规则特性,支持如:/order/list/{page}.html 动态路由规则特性(http://gf.johng.cn/702766);
|
||||
10、gpage分页包增加分页URL规则生成模板特性,内部可使用{.page}变量指定页码位置(http://gf.johng.cn/716438);
|
||||
11、增加gmap.Map对象,这是gmap.InterfaceInterfaceMap的别名;
|
||||
|
||||
## 新功能
|
||||
1、gdb增加MaxIdleConnCount/MaxOpenConnCount/MaxConnLifetime三项配置,并增加SetMaxConnLifetime方法;
|
||||
2、ghttp.Client增加HTTP账号密码设置功能(SetBasicAuth);
|
||||
3、glog新增对系统换行符号的自适配调整(\n|\r\n);
|
||||
4、增加glog控制台调试模式打印开关(SetDebug);
|
||||
5、gcfg增加SetFileName方法设置默认读取的配置文件名称;
|
||||
6、gcfg/gjson/gparser包新增Int8/16/32/64,Uint8/16/32/64方法;
|
||||
7、增加gzip方法的封装(Zip/Unzip);
|
||||
8、gview增加模板变量分隔符设置方法SetDelimiters;
|
||||
9、ghttp.Response增加Writef、Writefln方法;
|
||||
|
||||
## 功能改进
|
||||
1、改进gfilepool文件指针池设计;改进gfile文本内容写入,增加指针池使用
|
||||
2、gdb包增加调试模式特性,并支持在调试模式下获得已执行的SQL列表结果
|
||||
3、改进gproc进程间通信机制,增加进程消息分组特性,并限定队列大小
|
||||
4、gdb结果方法处理增加ToXml/ToJson方法
|
||||
5、gregx包名修改为gregex
|
||||
6、改进gtime.StrToTime方法,新增对常见标准时间日期的自动转换,以及对时区的自动识别支持,并调整gconv,gvalid对该包的引用
|
||||
7、增加对字符集转换的封装,gxml包中使用新增的字符集转换包来做处理
|
||||
8、ghttp.Server.EnableAdmin页面Restart接口支持GET参数newExeFilePath支持
|
||||
9、ghttp.Server平滑重启机制增加可自定义重启可执行文件路径,特别是针对windows系统特别有用(因为windows下不支持可执行文件覆盖更新)
|
||||
10、改进ghttp.Server静态文件检索设计,增加开发环境时的main包源码目录查找机制;改进gcfg/gview的main包源码目录查找机制
|
||||
11、优化gcache设计,LRU特性非默认开启;优化gtype/gcache基准测试脚本;新增gregx基准测试脚本,改进设计,提升性能
|
||||
12、gfile包增加GoRootOfBuild方法,用于获取编译时的GOROOT数值;并改进glog包中backtrace的GOROOT路径过滤处理;
|
||||
13、改进grpool代码质量,并改进对池化goroutine数量的限制设计
|
||||
14、改进gdb.Map/List及g.Map/List的类型定义,改用别名特性以便支持原生类型输入(map/slice),并修复gdb.Model.Update方法参数处理问题
|
||||
15、调整ghttp包示例代码目录结构,增加ghttp.Client自定义Header方法,ghttp.Cookie增加Map方法用于获得客户端提交的所有cookie值,构造成map返回
|
||||
16、删除gcharset中的getcharset方法
|
||||
17、去掉gmap中常用的基本数据类型转换获取方法
|
||||
18、改进gconv.String方法,当无法使用基本类型进行字符串转换时,使用json.Marshal进行转换
|
||||
19、gvalid.CheckObject方法名称修改为gvalid.CheckStruct
|
||||
|
||||
|
||||
## 问题修复
|
||||
1、修正gstr.IsNumeric错误
|
||||
2、修复当xml中encoding字符集为非UTF-8字符集时报错的问题
|
||||
3、修正gconv包float32->float64精度问题
|
||||
4、修复gpage包分页计数问题
|
||||
5、修复gdb批量数据Save错误
|
||||
6、去掉gpool中math.MAXINT64常量的使用,以修复int64到int类型的转换错误,兼容32位系统
|
||||
7、修正ghttp包没有使用Server仍然初始化相关异步goroutine的问题
|
||||
|
||||
|
||||
|
||||
|
||||
# `v0.98.503 beta` (2018-05-21)
|
||||
## 新特性
|
||||
1、平滑重启特性( http://gf.johng.cn/625833 );
|
||||
2、gflock文件锁模块( http://gf.johng.cn/626062 );
|
||||
3、gproc进程管理及通信模块( http://gf.johng.cn/626063 );
|
||||
4、gpage分页管理模块,强大的动态分页及静态分页功能,并为开发者自定义分页样式提供了极高的灵活度( http://gf.johng.cn/597431 );
|
||||
5、ghttp.Server增加多端口监听特性,并支持HTTP/HTTPS( http://gf.johng.cn/494366 , http://gf.johng.cn/598802 );
|
||||
6、增加gspath目录检索包管理工具,支持对多目录下的文件检索特性;
|
||||
7、ghttp包控制器及执行对象注册增加更灵活的动态路由特性,路由规则增加{method}变量支持;
|
||||
|
||||
## 新功能
|
||||
1、gutil包增加MapToStruct方法,支持将map数据类型映射为struct对象;
|
||||
2、gconv
|
||||
1)、gconv包增加按照类型名称字符串进行类型转换;
|
||||
2)、gconv包新增Time/TimeDuration类型转换方法;
|
||||
3、ghttp
|
||||
1)、增加Web Server目录安全访问控制机制;
|
||||
2)、ghttp.Server增加自定义状态码回调函数注册处理;
|
||||
4、gdb
|
||||
1)、gdb包增加gdb.GetStruct/gdb.Model.Struct方法,获取查询结果记录自动转换为指定对象;
|
||||
2)、gdb增加Value/Record/Result类型,增加对Value类型的系列类型转换方法;
|
||||
3)、gdb包增加db.GetCount,tx.GetCount,model.Count数量查询方法;
|
||||
|
||||
## 功能改进
|
||||
1、改进gredis客户端功能封装;
|
||||
2、改进grand包随机数生成性能;
|
||||
3、grand/gdb/gredis包增加benchmark性能测试脚本;
|
||||
4、改进gjson/gparser包的ToStruct方法实现;
|
||||
5、gdb :改进gdb.New获取ORM操作对象性能;
|
||||
6、gcfg :改进配置文件检索功能;
|
||||
7、gview:模板引擎增加多目录检索功能;
|
||||
8、gfile:增加源码main包目录获取方法MainPkgPath;
|
||||
9、ghttp
|
||||
1)、ghttp.Request增加请求进入和完成时间记录,并增加到默认日志内容中;
|
||||
2)、ghttp.Server事件回调之间支持通过ghttp.Request.Param自定义参数进行流程传参;
|
||||
10、gdb
|
||||
1)、改进gdb.Result与gdb.List, gdb.Record与gdb.Map之间的类型转换,便于业务层数据编码处理(如json/xml);
|
||||
2)、改进gdb.Tx.GetValue返回值类型;
|
||||
3)、gdb.Model.Data参数支持更加灵活的map参数;
|
||||
|
||||
## 问题修复
|
||||
1、ghttp
|
||||
1)、修复ghttp包路由缓存问题;
|
||||
2)、修复服务注册时的控制器及执行对象方法丢失问题;
|
||||
2、gconv
|
||||
1)、修正gconv.Float64方法位大小设置问题;
|
||||
2)、修复gconv.Int64(float64(xxx))问题;
|
||||
2、gdb
|
||||
1)、修复gdb.GetAll针对返回数据列表的for..range...的返回结果slice相同指针问题;
|
||||
2)、修复gdb.Delete方法错误;
|
||||
3)、修复gdb.Model.And/Or方法;
|
||||
4)、修复gdb.Model.Where方法参数处理问题;
|
||||
3、garray:修复garray包Remove方法锁机制问题;
|
||||
4、gtype :修复gtype.Float32/gtype.Float64对象类型的方法逻辑错误;
|
||||
5、gfsnotify:修复在windows下文件参数中不同文件分隔符引起的热更新机制失效问题;
|
||||
6、修复gvalid包验证问题:如果值为nil,并且不需要require*验证时,其他验证失效。并增加单元测试项,测试通过。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# `v0.97.399 beta` (2018-04-23)
|
||||
1、 增加gfsnotify文件监控模块;
|
||||
2、 配置管理模块增加配置文件自动检测更新机制;
|
||||
3、 模板引擎增加对模板文件的自动检测更新机制;
|
||||
4、 改进gconv包基本类型转换功能,提高转换性能;
|
||||
5、 增加gpage分页管理包,支持动态分页、静态分页以及自定义分页样式特性;
|
||||
6、 ghttp.Request增加Exit方法,用以标记服务退出,当在服务执行前调用后,服务将不再执行;
|
||||
7、 ghttp.Response去掉WriteString方法,统一使用Write方法返回数据流,是使用灵活的参数形式;
|
||||
8、 模板引擎增加模板变量暴露接口LockFunc/RLockFunc,以便支持开发者灵活处理模板变量;
|
||||
9、 ghttp.Server增加access & error log功能,并支持开发者自定义日志处理回调函数注册;
|
||||
10、增加gredis包,支持对redis的客户端操作封装,并将gredis.Redis对象加入到gins单例管理器中进行统一配置管理维护;
|
||||
11、gins单例管理器增加对单例对象配置文件的自动检测更新机制,当配置文件在外部发生变更时,自动刷新单例管理器中的单例对象;
|
||||
12、gdb数据库ORM包增加And/Or条件链式方法,并改进Where/Data方法参数灵活性;
|
||||
13、对于新增加的模块,同时也增加了对应的开发文档,并梳理完善了现有的其他模块开发文档;
|
||||
14、修复ISSUE:
|
||||
#IISWI github.com/gogf/gf/issues/IISWI,
|
||||
#IISMY github.com/gogf/gf/issues/IISMY,
|
||||
反馈并跟踪完成第三方依赖mxj包的ISSUE修复(github.com/clbanning/mxj/issues/48);
|
||||
|
||||
|
||||
|
||||
824
RELEASE.2.MD
824
RELEASE.2.MD
@ -1,824 +0,0 @@
|
||||
# `v1.13.1` (2020-06-10)
|
||||
|
||||
# GoFrame
|
||||
|
||||
`GF(Go Frame)`是一款模块化、高性能、生产级的Go基础开发框架。实现了比较完善的基础设施建设以及开发工具链,提供了常用的基础开发模块,如:缓存、日志、队列、数组、集合、容器、定时器、命令行、内存锁、对象池、配置管理、资源管理、数据校验、数据编码、定时任务、数据库ORM、TCP/UDP组件、进程管理/通信等等。并提供了Web服务开发的系列核心组件,如:Router、Cookie、Session、Middleware、服务注册、模板引擎等等,支持热重启、热更新、域名绑定、TLS/HTTPS、Rewrite等特性。
|
||||
|
||||
## 特点
|
||||
|
||||
* 模块化、松耦合设计;
|
||||
* 模块丰富、开箱即用;
|
||||
* 简便易用、易于维护;
|
||||
* 高代码质量、高单元测试覆盖率;
|
||||
* 社区活跃,大牛谦逊低调脾气好;
|
||||
* 详尽的开发文档及示例;
|
||||
* 完善的本地中文化支持;
|
||||
* 设计为团队及企业使用;
|
||||
|
||||
## 发展
|
||||
|
||||
`GoFrame`开始得比较早,`2011`年始于北京一个智能物联网平台项目,那时还没有这么多物联网的现行标准,`Go`的标准库以及生态也未如此丰富。`2017`年的时候`GF`才开始发布测试版,`2018`年`1024`程序员节日的时候才发布`v1.0`正式版,为`Go`生态发展添砖加瓦。开源以来快速迭代、发展成长,广受开发者和企业的青睐,也有许多的开发者加入了贡献行列。`GF`原本是为开发团队设计的,因此她的开发效率和可维护性做得非常好,有着很高的代码质量以及丰富的单元测试和示例,并且`GF`是目前中文化文档做的最好的`Golang`开发框架。
|
||||
|
||||
# Change Log
|
||||
|
||||
1. 应多数开发者的要求,框架要求的最低`Golang`运行版本降级为了`v1.11`。
|
||||
1. 新增`GoFrame`视频教程地址:
|
||||
- bilibili:https://www.bilibili.com/video/av94410029
|
||||
- 西瓜视频: https://www.ixigua.com/pseries/6809291194665796100/
|
||||
1. 将不常用的`guuid`模块迁移到 github.com/gogf/guuid 作为社区模块维护,保持`gf`主仓库的轻量级。
|
||||
1. 新增`guid`模块,用于高效轻量级的唯一字符串生成:https://goframe.org/util/guid/index
|
||||
|
||||
|
||||
## `tool chain`
|
||||
|
||||
1. 工具链更新:https://goframe.org/toolchain/cli
|
||||
1. 新增`gf env`命令,更优雅地查看当前`Golang`环境变量信息。
|
||||
1. 新增`gf mod path`命令,用于将当前`go modules`包拷贝到`GOPATH`中,以便使用原始的`GOPATH`方式开发项目。
|
||||
1. 对现有`cli`命令进行了一些改进,提高使用体验;预编译二进制版本在部分平台下提供了`upx`压缩,使得下载的文件更小。
|
||||
|
||||
## `container`
|
||||
1. `garray`
|
||||
- https://goframe.org/container/garray/index
|
||||
- 简化数组使用方式,支持类似于`var garray.Array`的变量定义使用方式;
|
||||
- 增加`Walk`方法,用于自定义的数组元素处理方法;
|
||||
- 增加`ContainsI`方法,用于大小写忽略匹配的数组元素项存在性查找;
|
||||
- 完善单元测试,代码覆盖率`94%`;
|
||||
- 代码改进,提高性能;
|
||||
- 修复一些问题;
|
||||
1. `gchan`
|
||||
- 由于该封装包实际意义不是很大,因此从主框架中删除;
|
||||
1. `glist`
|
||||
- https://goframe.org/container/glist/index
|
||||
- 简化链表使用方式,支持类似于`var glist.List`的变量定义使用方式;
|
||||
- 完善单元测试,代码覆盖率`99%`;
|
||||
1. `gmap`
|
||||
- https://goframe.org/container/gmap/index
|
||||
- 简化`Map`使用方式,支持类似于`var gmap.Map`的变量定义使用方式;
|
||||
- 完善单元测试,代码覆盖率`81%`;
|
||||
- 代码改进,提高性能;
|
||||
1. `gset`
|
||||
- https://goframe.org/container/gset/index
|
||||
- 简化集合使用方式,支持类似于`var gset.Set`的变量定义使用方式;
|
||||
- 增加`Walk`方法,用于自定义的集合元素处理方法;
|
||||
- 完善单元测试,代码覆盖率`90%`;
|
||||
- 代码改进,提高性能;
|
||||
1. `gtree`
|
||||
- https://goframe.org/container/gtree/index
|
||||
- 简化树型使用方式,支持类似于`var gtree.BTree`的变量定义使用方式;
|
||||
- 完善单元测试,代码覆盖率`90%`;
|
||||
1. `gvar`
|
||||
- https://goframe.org/container/gvar/index
|
||||
- 完善单元测试,代码覆盖率`69%`;
|
||||
- 代码组织结构调整,提高维护性;
|
||||
- 代码改进,提高性能;
|
||||
|
||||
## `database`
|
||||
|
||||
1. `gdb`
|
||||
- 增加`Transaction(f func(tx *TX) error) (err error)`接口方法,用于通过闭包实现事务封装处理:https://goframe.org/database/gdb/transaction
|
||||
- 去掉不常用的`From`接口方法,改进`Table`及`Model`方法的参数为不定参数,并支持通过不定参数传递表别名:https://goframe.org/database/gdb/chaining/select
|
||||
- 增加`DryRun`特性,支持空跑时只执行查询不执行写入/更新/删除操作:https://goframe.org/database/gdb/senior
|
||||
- 增加`create_at`, `update_at`写入时间、更新时间字段自动填充特性:https://goframe.org/database/gdb/chaining/auto-time
|
||||
- 增加`delete_at`软删除特性:https://goframe.org/database/gdb/chaining/auto-time
|
||||
- 增加`Having`链式操作方法,用于`having`条件查询:https://goframe.org/database/gdb/chaining/select
|
||||
- `Result`结果对象增加`Chunk`方法,用于自定义的数据分批处理:https://goframe.org/database/gdb/result
|
||||
- 改进`Schema`数据库运行时切换特性;
|
||||
- 改进对`pgsql`, `mssql`, `sqlite`, `oracle`数据库字段类型的支持;
|
||||
- 进一步完善单元测试;
|
||||
- 代码组织结构调整,提高维护性;
|
||||
- 代码改进,提高性能;
|
||||
1. `gredis`
|
||||
- 增加`MaxActive`连接池参数默认配置为`100`,限制默认的连接数量;
|
||||
- 改进`Conn`连接对象的`Do`方法,支持对`map/slice/struct`类型进行自动的`json.Marshal`处理,注意获取数据时使用`DoVar`方法获取:https://goframe.org/database/gredis/usage
|
||||
- 完善单元测试,代码覆盖率`72%`;
|
||||
|
||||
## `net`
|
||||
1. `ghttp`
|
||||
- 增加`Prefix`及`Retry`客户端链式操作方法;
|
||||
- 增加客户端原始请求打印特性:https://goframe.org/net/ghttp/client/demo/dump
|
||||
- 增加`ClientMaxBodySize`的服务端配置,用于限制客户端提交的`Body`大小,默认为`8MB`;在涉及到上传的Server中需要增加该配置的大小,在配置文件中指定对应的大小即可,如`ClientMaxBodySize="100MB"`:https://goframe.org/net/ghttp/config
|
||||
- 改进`SessionId`生成的随机性,提高`Session`安全性:https://goframe.org/os/gsession/index
|
||||
- 改进`ghttp.Server`实现了标准库的`http.Handler`接口,便于与其他第三方的服务如`Prometheus`进行代码集成;
|
||||
- 其他大量的代码细节改进工作,提高性能及持久维护性;
|
||||
- 完善单元测试,代码覆盖率`61%`;
|
||||
|
||||
1. `gipv4`
|
||||
- 增加`GetIpArray`方法,用于获取当前主机的所有IPv4地址;
|
||||
- 增加`GetMacArray`及`GetMac`方法,用于获取当前主机的`MAC`地址信息;
|
||||
- 修改`IntranetIP`方法名称为`GetIntranetIp`,修改`IntranetIPArray`方法名称为`GetIntranetIpArray`;
|
||||
|
||||
## `encoding`
|
||||
1. `gjson`
|
||||
- 新增`GetMaps`获取`JSON`内部节点变量方法;
|
||||
- 改进`NewWithTag`方法对`map/struct`的处理;
|
||||
- 完善单元测试,代码覆盖率`77%`;
|
||||
1. `gyaml`
|
||||
- 升级依赖的第三方`yaml`解析包,解决了`map[interface{}]interface{}`转换问题;
|
||||
|
||||
## `error`
|
||||
1. `gerror`
|
||||
- 新增`NewfSkip`方法,用于创建`skip`指定堆栈的错误对象;
|
||||
- 放开框架所有的堆栈链路打印,展示错误时真实的链路调用详情;
|
||||
|
||||
## `os`
|
||||
1. `gcache`
|
||||
- 增加`GetVar`方法,用于获得可以便捷转换为其他数据类型的"泛型"变量;
|
||||
- 标记`Removes`方法废弃,改进`Remove`方法参数为不定参数,统一使用`Remove`方法删除单个/多个键值对;
|
||||
- 完善单元测试,代码覆盖率`96%`;
|
||||
|
||||
1. `genv`
|
||||
- 增加`GetVar`方法,用于获得可以便捷转换为其他数据类型的"泛型"变量;
|
||||
|
||||
1. `gfile`
|
||||
- 改进`CopyDir/CopyFile`复制目录/文件方法;
|
||||
- 新增`ScanDirFunc`方法,用于支持自定义处理回调的目录检索;
|
||||
- 完善单元测试,代码覆盖率`64%`;
|
||||
|
||||
1. `glog`
|
||||
- 增加支持`Context`上下文变量的日志打印特性:https://goframe.org/os/glog/context
|
||||
|
||||
1. `gres`
|
||||
- 改进打包特性,增强生成二进制文件及Go文件的压缩比,比旧版本增加`20%`压缩率,使得编译生成的二进制文件体积更小;
|
||||
- 代码结构改进,提高执行效率及可持久维护性;
|
||||
|
||||
1. `gsession`
|
||||
- 改进`SessionId`默认生成方法,采用`guid.S`方法生成;
|
||||
- 增加`SetId`及`SetIdFunc`方法,用于自定义`SessionId`及自定义的`SessionId`生成方法;
|
||||
|
||||
## `frame`
|
||||
1. `g`
|
||||
- 新增`g.Table`方法,用于快速创建数据库模型操作对象;
|
||||
|
||||
## `i18n`
|
||||
1. `gi18n`
|
||||
- 新增`GetContent`方法,用于获取指定`i18n`关键字为转译内容;
|
||||
- 改进代码细节,提高性能和持久可维护性;
|
||||
- 完善单元测试,代码覆盖率`74%`;
|
||||
|
||||
## `test`
|
||||
1. `gtest`
|
||||
- 增加`AssertNQ`断言方法,用于强类型的不相等判断;
|
||||
|
||||
## `text`
|
||||
1. `gstr`
|
||||
- 增加`SubStrRune`方法,用于支持`unicode`的字符串截取;
|
||||
- 增加`StrLimitRune`方法,用于支持`unicode`的字符串截断隐藏;
|
||||
- 增加`LenRune`方法,用于替换`RuneLen`方法,统一方法命名风格;
|
||||
- 增加`PosRune/PosIRune/PosRRune/PosRIRune`方法,用于支持`unicode`的字符串左右位置查找;
|
||||
- 增加`CompareVersionGo`方法,用于`Golang`风格的版本号大小比较;
|
||||
- 完善单元测试,代码覆盖率`75%`;
|
||||
|
||||
## `util`
|
||||
1. `gconv`
|
||||
- 改进`Convert`转换方法,支持常见`map`类型的转换;
|
||||
- 改进类型转换过程中异常错误的捕获,通过`error`返回;
|
||||
- 其他一些细节改进;
|
||||
- 完善单元测试,代码覆盖率`63%`;
|
||||
|
||||
1. `grand`
|
||||
- 增加`B`方法,用于获得随机的二进制数据;
|
||||
- 改进代码底层实现,部分接口性能提高`50%`;
|
||||
- 完善单元测试,代码覆盖率`74%`;
|
||||
|
||||
1. `guid`
|
||||
- 新增`guid`模块,用于高效轻量级的唯一字符串生成:https://goframe.org/util/guid/index
|
||||
|
||||
1. `gutil`
|
||||
- 增加`MapContains`方法,用于判断map中是否包含指定键名;
|
||||
- 增加`MapDelete`方法,用于删除map中指定的键名,可以为多个键名;
|
||||
- 增加`MapMerge`方法,用于合并两个map;
|
||||
- 增加`MapMergeCopy`方法,用于拷贝多个map;
|
||||
- 增加`MapContainsPossibleKey`方法,用于查找指定键名,忽略大小写及字符`'-'/'_'/'.'/' '`;
|
||||
|
||||
1. `gvalid`
|
||||
- 所有默认的错误提示改为了英文;
|
||||
- 错误提示的配置改为了通过`i18n`来配置实现,以便支持国际化:https://goframe.org/util/gvalid/message
|
||||
- 身份证号规则名称从`id-number`改为了`resident-id `;
|
||||
- 银行卡号规则名称从`luhn`改为了`bank-card`;
|
||||
- 完善单元测试,代码覆盖率`96%`;
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`gcompress`的多文件`zip`压缩问题;
|
||||
1. 修复`ghttp.Client`获取返回的过期`Cookie`的问题;
|
||||
1. 修复`gres.File`对于`http.File`接口的实现细节;
|
||||
1. 修复`garray.Pop*`方法的边界问题;
|
||||
1. 修复`gres`中`Readdir`方法参数为`0`时报错的问题;
|
||||
1. 其他一些修复:https://github.com/gogf/gf/issues?q=is%3Aissue+label%3Abug
|
||||
|
||||
|
||||
|
||||
|
||||
# `v1.12.1` (2020-03-31)
|
||||
|
||||
大家好啊!久等啦!
|
||||
|
||||
由于自从上次版本的发布以来,越来越多小伙伴加入了`GF`的大家庭,并提供了许多不错的建议和反馈,这次版本对其中大部分反馈进行了处理,包括大部分的改进建议和部分新特性,因此这次的版本发布时隔了两个多月。`GF`非常注重代码质量以及可持续维护性,这次版本也进一步对框架大部分模块的示例、注释和单元测试用例进行了完善,目前单元测试用例数量约为`1991`例,代码覆盖率为`71%`,覆盖了所有模块的绝大部分主要功能。
|
||||
|
||||
`GF`框架提供了比较常用、高质量的基础开发模块,轻量级、模块化、高性能,推荐大家阅读框架源码了解更多细节。本次发布有个别的不兼容升级,往往批量替换即可,以下`change log`比较完善,建议升级前仔细阅读。
|
||||
|
||||
本次发布即意味下一版本开发计划的开启,欢迎更多小伙伴参与开源贡献:https://github.com/gogf/gf/projects/8
|
||||
|
||||
感谢大家支持!Enjoy your `GF`!
|
||||
|
||||
|
||||
# GoFrame
|
||||
|
||||
`GF(Go Frame)`是一款模块化、高性能、生产级的Go基础开发框架。实现了比较完善的基础设施建设以及开发工具链,提供了常用的基础开发模块,如:缓存、日志、队列、数组、集合、容器、定时器、命令行、内存锁、对象池、配置管理、资源管理、数据校验、数据编码、定时任务、数据库ORM、TCP/UDP组件、进程管理/通信等等。并提供了Web服务开发的系列核心组件,如:Router、Cookie、Session、Middleware、服务注册、模板引擎等等,支持热重启、热更新、域名绑定、TLS/HTTPS、Rewrite等特性。
|
||||
|
||||
## 特点
|
||||
* 模块化、松耦合设计;
|
||||
* 模块丰富,开箱即用;
|
||||
* 简便易用,易于维护;
|
||||
* 社区活跃,大牛谦逊低调脾气好;
|
||||
* 高代码质量、高单元测试覆盖率;
|
||||
* 详尽的开发文档及示例;
|
||||
* 完善的本地中文化支持;
|
||||
* 更适合企业及团队使用;
|
||||
|
||||
## 地址
|
||||
- 官网:https://goframe.org
|
||||
- 主库:https://github.com/gogf/gf
|
||||
- 码云:https://gitee.com/johng/gf
|
||||
|
||||
# Change Log
|
||||
|
||||
从`GF v1.12`版本开始,框架要求的最低`Golang`运行版本为`v1.13`,由于`Golang`新版本都是向后兼容的,因此推荐大家更新使用`Golang`新版本:https://golang.google.cn/dl/
|
||||
|
||||
> 本次版本也新增了`Swagger`的工具及插件支持,另行独立发布。
|
||||
|
||||
## `tool chain`
|
||||
1. `gen model`命令新增对`pgsql/mssql/sqlite/oracle`的模型生成支持。
|
||||
1. `gen model`命令生成模型新增公开包变量`Columns`用于获得表的字段名称。
|
||||
|
||||
## `net`
|
||||
1. `ghttp`
|
||||
- 注意:从该版本开始,`Server`默认关闭了平滑重启特性。开发者可以通过相应的配置选项打开。
|
||||
- 改进`Client.Get`方法,增加可选的请求参数。
|
||||
- 新增`Client`链式操作方法:`Header`, `HeaderRaw`, `Cookie`, `ContentType`, `ContentJson`, `ContentXml`, `Timeout`, `BasicAuth`, `Ctx`:https://goframe.org/net/ghttp/client/chain
|
||||
- 新增`Request.GetCtx/GetCtxVar/SetCtxVar/Context`上下文变量管理方法,用于请求内部的上下文变量特性:
|
||||
- 自定义变量:https://goframe.org/net/ghttp/request/custom
|
||||
- 上下文变量:https://goframe.org/net/ghttp/request/context
|
||||
- 新增`Request.GetUploadFile/GetUploadFiles`方法,以及`UploadFile`类型,极大简化文件上传处理逻辑:https://goframe.org/net/ghttp/client/demo/upload
|
||||
- 新增`Request.GetPage`方法,用于便捷地获得分页对象:
|
||||
- 基本介绍:https://goframe.org/util/gpage/index
|
||||
- 动态分页:https://goframe.org/util/gpage/dynamic
|
||||
- 静态分页:https://goframe.org/util/gpage/static
|
||||
- Ajax分页:https://goframe.org/util/gpage/ajax
|
||||
- URL模板:https://goframe.org/util/gpage/template
|
||||
- 自定义分页:https://goframe.org/util/gpage/custom
|
||||
- 改进`Response.Redirect*`方法,增加自定义的跳转HTTP状态码参数。
|
||||
- 改进`CORS`特性,完善跨域功能处理,并完全遵守`W3C`关于`OPTIONS`请求方法的规范约定:https://goframe.org/net/ghttp/cors
|
||||
- 模板视图对象增加`Request`内置变量,用于模板获得请求参数。由于`GF`框架的模板引擎采用两级缓存设计,减少`IO`开销的同时提升了执行效率,即使增加了大量的内置变量以及内置方法,经过大规模的性能测试,性能比其他`WebServer`库相同逻辑下高出`50% - 200%`的效率。
|
||||
- 新增`Server`实验性的`Plugin`特性。
|
||||
- 改进`Server`底层路由存储、检索逻辑,优化代码,提升效率。
|
||||
- 改进分组路由注册的源码位置记录功能,当路由注册冲突时能够精准定位及提示重复路由源码位置。
|
||||
- 改进`Server`日志处理。
|
||||
- 完善单元测试。
|
||||
|
||||
|
||||
## `database`
|
||||
1. `gdb`
|
||||
- 代码重构改进,增加`Driver`驱动接口设计,方便开发者自定义驱动实现。新增`Register`包方法,用于开发者注册自定义的数据库类型驱动:https://goframe.org/database/gdb/driver
|
||||
- 新增`GetArray`接口及实现,用于获取指定字段列的数据,构造成数组返回:https://goframe.org/database/gdb/chaining/select
|
||||
- 新增`InsertIgnore`接口及实现,用于写入时忽略写入冲突,仅对`mysql`数据库类型有效:https://goframe.org/database/gdb/chaining/insert-save
|
||||
- 新增`Schema`接口及实现,用于动态切换并获取指定名称的数据库对象:https://goframe.org/database/gdb/chaining/schema
|
||||
- 新增`FieldsStr/FieldsExStr`模型方法,用于获取表字段,并构造成字符串返回:hhttps://goframe.org/database/gdb/chaining/fields-retrieve
|
||||
- 新增`LockUpdate/LockShared`模型链式操作方法,用于实现悲观锁操作:https://goframe.org/database/gdb/chaining/lock
|
||||
- 改进`Where/Data`方法对更新参数输入方式的支持,提高灵活性:
|
||||
- https://goframe.org/database/gdb/chaining/select
|
||||
- https://goframe.org/database/gdb/chaining/update-delete
|
||||
- 查询结果对象`Result`新增`Array`方法,用于获得指定字段的数值,构造成数组返回:https://goframe.org/database/gdb/result
|
||||
- 改进`OmitEmpty`方法对`Data`输入参数的过滤,当给定的`Data`参数为空时间对象时,将会被过滤。
|
||||
- 增加默认的数据库连接池参数:`MaxIdleConns=10`。
|
||||
- 其他一些改进。
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gredis`
|
||||
- 增加/修改默认的数据库连接池参数:`MaxIdle=10`, `IdleTimeout=10s`, `MaxConnLifetime=30s`, `Wait=true`。
|
||||
- 完善单元测试。
|
||||
|
||||
## `container`
|
||||
1. 所有容器对象新增`UnmarshalValue(interface{}) error`接口方法实现,用于`gconv`转换时根据任意类型变量创建/设置对象内容,`GF`框架的所有容器对象均实现了该接口。
|
||||
|
||||
1. `garray`
|
||||
- 新增`RemoveValue`方法,用于根据数值检索并删除元素项。
|
||||
- 新增`FilterNil`方法,用于遍历并删除数组中的`nil`元素项。
|
||||
- 新增`FilterEmpty`方法,用于遍历并删除数组中的空值元素项,空值包括:`0, nil, false, "", len(slice/map/chan) == 0`。
|
||||
- 改进`Set/Fill/InsertBefore/InsertAfter`方法严谨性,将原有的链式操作返回值对象修改为`error`返回值。
|
||||
- 改进`Get/Remove/PopRand/PopLeft/PopRight/Rand`方法严谨性,增加`found`的`bool`返回值,标识当前方法是否有数据返回,当空数组或者操作越界时`found`返回值为`false`。
|
||||
- 改变`Rands`方法原有逻辑,保证按照给定大小返回随机数组。
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gpool`
|
||||
- 调整缓存池过期时间参数类型,旧版本为`int`类型表示毫秒,新版本为`time.Duration`类型使得时间控制更灵活。
|
||||
- 内部代码优化,性能改进。
|
||||
- 完善单元测试。
|
||||
- 完善注释。
|
||||
|
||||
## `os`
|
||||
1. `glog`
|
||||
- 增加`Rotation`日志滚动切分特性,新增按照文件大小或过期时间进行日志切分,并支持切分文件数量限制、对日志文件进行自动压缩、可自定义压缩级别(`1-9`)、支持对切分文件过期时间清理:https://goframe.org/os/glog/rotate
|
||||
- 新增`LevelPrefixes`特性,支持对日志级别的前缀名称进行自定义:https://goframe.org/os/glog/level
|
||||
- 新增`SetLevelStr`方法,并增加按照字符串进行日志级别配置的特性:
|
||||
- https://goframe.org/os/glog/level
|
||||
- https://goframe.org/os/glog/config
|
||||
- 新增`SetDefaultLogger`包方法,用于设置全局默认的`Logger`对象。
|
||||
|
||||
1. `gres`
|
||||
1. 改进资源内容编码设计,采用新的压缩算法,减少资源文件大小,例如原本`15MB`的网站静态资源文件(`css/js/html`等),资源文件打包后约为`4MB`左右:https://goframe.org/os/gres/index
|
||||
1. 注意:该改进与旧版本无法兼容,需要重新打包原有的资源文件。
|
||||
1. 完善单元测试。
|
||||
|
||||
1. `gcfg`
|
||||
- 去掉配置对象属性的原子并发安全控制,改为普通变量类型。由于配置管理是最常用模块之一,因此确保高效的设计及方法实现。
|
||||
- 单例对象做较大调整:为方便多文件场景下的配置文件调用,简便使用并提高开发效率,因此当给定的单例名称对应的`toml`配置文件在配置目录中存在时,将自动设置该单例对象的默认配置文件为该文件。例如:`g.Cfg("redis")`获取到的单例对象将会默认去检索并设置默认的配置文件为`redis.toml`,当该文件不存在时,则使用默认的配置文件(`config.toml`):https://goframe.org/net/ghttp/config
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gview`
|
||||
- 新增`concat`内置字符串拼接方法:https://goframe.org/os/gview/function/buildin
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gfile`
|
||||
- 改进`SelfPath`获取当前执行文件路径方法,提高执行效率。
|
||||
- 改进`Join`文件路径连接方法,防止多余的路径连接符号。
|
||||
- 改建`GetContents`文件内容获取方法执行效率,降低内存占用。
|
||||
- 新增`StrToSize`方法,用于将大小字符串转换为字节数字,大小字符串例如`10m`, `5KB`, `1.25Gib`等。
|
||||
- 新增`ReadLines/ReadByteLines`方法,用于按行读取指定文件内容,并给定读取回调函数。
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gtime`
|
||||
- 改进`gtime.Time`对象实现,统一字符串打印时间格式为`Y-m-d H:i:s`,如:`2020-01-01 12:00:00`。
|
||||
|
||||
1. `gcmd`
|
||||
- 命令行解析方法增加`strict`参数,用于设置当前解析是否严格解析,严格解析下如果给定了非法的选项,将会停止解析并返回错误。默认情况下为非严格解析。
|
||||
|
||||
1. `genv`
|
||||
- 改进`Remove`删除环境变量键值对方法,增加对多个键值对环境变量的删除。
|
||||
|
||||
1. `gfpool`
|
||||
- 改进代码实现,提高效率。
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gfsnotify`
|
||||
- 改进包初始化方法,当系统原因引起默认`Watcher`对象创建失败时,直接`panic`。
|
||||
|
||||
1. `gproc`
|
||||
- 改进`SearchBinaryPath`方法。
|
||||
- 改进`Process.Kill`方法在`windows`及`*niux`平台下不同表现的处理。
|
||||
|
||||
## `encoding`
|
||||
1. `gjson`
|
||||
- 代码改进、完善注释、新增大量代码示例。
|
||||
- 文档更新:
|
||||
- 基本介绍:https://goframe.org/encoding/gjson/index
|
||||
- 对象创建:https://goframe.org/encoding/gjson/object
|
||||
- 层级访问:https://goframe.org/encoding/gjson/pattern
|
||||
- Struct转换:https://goframe.org/encoding/gjson/conversion-struct
|
||||
- 动态创建修改:https://goframe.org/encoding/gjson/dataset
|
||||
- 数据格式转换:https://goframe.org/encoding/gjson/conversion-format
|
||||
|
||||
## `frame`
|
||||
1. `g`
|
||||
- 新增`IsNil`方法,用于判断当前给定的变量是否为`nil`,该方法有可能会使用到反射来实现判断。
|
||||
- 新增`IsEmpty`方法,用于判断当前给定的变量是否为`空`,该方法优先使用断言判断但有可能会使用到反射来实现判断。空值包括:`0, nil, false, "", len(slice/map/chan) == 0`。
|
||||
- 标记废弃`SetServerGraceful`方法,转而统一交给`Server`的配置来管理。
|
||||
1. `gins`
|
||||
- 改进代码结构,方便维护。
|
||||
- 改进、完善单元测试。
|
||||
1. `gmvc`
|
||||
- 新增`M`类型,为`*gdb.Model`的别名简称,用于工具链自动生成模型中的`M`属性。
|
||||
|
||||
## `text`
|
||||
1. `gstr`
|
||||
- 新增`HasPrefix/HasSuffix`方法。
|
||||
- 新增`OctStr`方法,用于将八进制字符串转换为其对应的`unicode`字符串,例如转换为中文。常用于`gRPC`底层通信编码中。
|
||||
- 完善单元测试。
|
||||
|
||||
## `debug`
|
||||
1. `gdebug`
|
||||
- 改进代码结构,方便维护。
|
||||
- 新增`TestDataPath`方法,用于当前包单元测试中获得当前包中`testdata`目录的绝对路径。
|
||||
|
||||
## `util`
|
||||
1. `gconv`
|
||||
- 改进`String`字符串转换方法,增加对`time.Time/*time.Time/gtime.Time`类型的内置支持。
|
||||
- 改进`Map/Struct`转换方法,增加对一些特殊场景的细节处理。经过大规模的使用,大量的反馈改进,不断完善了细节。
|
||||
- 改进`Struct`转换方法,增加对`UnmarshalValue(interface{}) error`接口的支持。
|
||||
- 完善单元测试。
|
||||
|
||||
1. `grand`
|
||||
- 注意:不兼容调整,原有的`Str`方法更名为`S`方法,用以获取指定长度的随机字符串、数字,并增加`symbol`参数,指定是否可以随机返回特殊可见字符。
|
||||
- 新增`Str`方法,用于从指定的字符串字符中随机获取指定长度的字符串。该方法同时支持`unicode`字符串,例如:中文:https://goframe.org/util/grand/index
|
||||
- 新增`Symbols`方法,用于随机返回指定场孤独的特殊可见字符:https://goframe.org/util/grand/index
|
||||
- 完善单元测试。
|
||||
|
||||
1. `gvalid`
|
||||
- 长度校验规则增加对`unicode`字符串的支持,例如:中文。
|
||||
|
||||
# Bug Fix
|
||||
1. 修复`Server`的视图对象配置失效问题。
|
||||
1. 修复`Server`中间件在中间件`panic`情况下,忽略`Middleware.Next`方法控制,导致鉴权中间件失效的问题。
|
||||
1. 修复`gudp.Server`在请求包大小超过`64bytes`时的断包问题,并调整默认缓冲区大小为`1024byte`,开发者可自定义缓冲区大小。
|
||||
1. 修复`gfile.MTimeMillisecond`方法返回错误的文件修改毫秒时间戳。
|
||||
1. 修复`gconv.Int64`对负数转换的支持。
|
||||
1. 其他一些修复。
|
||||
1. 详见:https://github.com/gogf/gf/issues?q=label%3Abug
|
||||
|
||||
|
||||
|
||||
# `v1.11.2` (2020-01-14)
|
||||
|
||||
`GF(Go Frame)` https://goframe.org 是一款模块化、高性能、生产级的Go基础开发框架。实现了比较完善的基础设施建设,包括常用的核心开发组件, 如:缓存、日志、文件、时间、队列、数组、集合、字符串、定时器、命令行、文件锁、内存锁、对象池、连接池、资源管理、数据校验、数据编码、文件监控、 定时任务、数据库ORM、TCP/UDP组件、进程管理/通信、并发安全容器等等。 并提供了Web服务开发的系列核心组件,如:Router、Cookie、Session、Middleware、服务注册、配置管理、模板引擎等等, 支持热重启、热更新、多域名、多端口、多服务、HTTPS、Rewrite等特性。
|
||||
|
||||
`GF`有着丰富的基础模块、完善的工具链、详尽的开发文档。开源近两年以来,`GF`得到越来越多小伙伴的肯定和支持,从寂寂无名到现在被广泛应用于微服务、物联网、区块链、电商系统、银行系统等企业级的生产项目中,经历了百万级、千万级项目的考验,2019年度被码云`gitee`评选为`GVP`最有价值开源项目。`GF`正在快速地成长中,目前保持着1-2个月迭代版本的发布规律,社区活跃,欢迎加入`GF`大家庭。
|
||||
|
||||
最后,祝大家2020新年快乐,鼠年大吉!
|
||||
|
||||
|
||||
|
||||
## 新特性
|
||||
|
||||
1. 新年新气象,官网文档大量更新:https://goframe.org/index
|
||||
1. `GF`工具链更新:https://goframe.org/toolchain/cli
|
||||
- 新增`gf run`热编译运行命令;
|
||||
- 新增`gf docker` Docker镜像编译命令;
|
||||
- 新增`gf gen model` 强大的模型自动生成命令;
|
||||
- `gf build`命令增加对配置文件配置支持;
|
||||
- 大量命令行工具改进工作;
|
||||
- 新增自动代理设置特性;
|
||||
1. 数据库`ORM`新特性:
|
||||
- 增加`prefix`数据表前缀支持:https://goframe.org/database/gdb/config
|
||||
- 新增`Schema`数据库对象并改进数据库切换特性:https://goframe.org/database/gdb/chaining/schema
|
||||
- 新增`WherePri`方法,用于自动识别主键的条件方法:https://goframe.org/database/gdb/chaining/select
|
||||
- 文档及示例大量更新,覆盖95%以上的功能特性;
|
||||
|
||||
|
||||
|
||||
## 功能改进
|
||||
|
||||
### `container`
|
||||
1. `garray`
|
||||
- 新增`New*ArrayRange`方法,用于初始化创建指定数值范围的数组。
|
||||
- 新增`Iterator*`方法,用于数组项元素回调遍历。
|
||||
- 完善单元测试。
|
||||
1. `gvar`
|
||||
- 改进`MapStrStr`、`MapStrStrDeep`方法实现。
|
||||
|
||||
### `net`
|
||||
1. `ghttp`
|
||||
- 改进HTTP客户端,增加对提交参数的自动`Content-Type`识别功能。
|
||||
- `Request`对象增加`Parse`方法,用于快捷的对象转换即参数校验。
|
||||
- `Request.GetPost*`方法全部标记为`deprecated`,统一客户端参数提交方式为`QueryString`, `Form`, `Body`。
|
||||
- 去掉`Response`模板解析时的`Get`/`Post`内置变量,新增`Query`, `Form`, `Request`内置变量:https://goframe.org/net/ghttp/response/template
|
||||
- 改进`Response.WriteJson*`及`Response.WriteXml*`方法,增加对`string`, `[]byte`类型参数的支持。
|
||||
- `Server`新增`GetRouterArray`方法,用于向应用层暴露并获取`Server`的路由列表。
|
||||
- `Server`新增`Use`方法,该方法为`BindMiddlewareDefault`的别名,用以全局中间件的注册。
|
||||
- `Server`新增`RouteOverWrite`配置项,用于控制是否在注册路由冲突时自动覆盖,默认关闭并提示。
|
||||
- `Server`新增`Graceful`配置项,用于在单服务场景下控制平滑重启特性的开启/关闭,默认开启。
|
||||
- 完善单元测试。
|
||||
1. `gtcp`
|
||||
- 改进简单协议下的数据包发送接收功能。
|
||||
- 将连接池默认的缓存过期时间`30`秒修改为`10`秒。
|
||||
- 完善单元测试。
|
||||
|
||||
### `database`
|
||||
1. `gdb`
|
||||
- 新增`As`数据表别名方法。
|
||||
- 改进数据表、字段的安全字符自动识别添加功能。
|
||||
- 新增`DB`数据库对象切换方法。
|
||||
- 新增`TX`链式操作事务支持方法。
|
||||
- 完善单元测试。
|
||||
### `os`
|
||||
1. `gcfg`
|
||||
- 新增`GetMapStrStr`方法。
|
||||
1. `gcmd`
|
||||
- 增加参数解析的`strict`严格参数,默认严格解析,不存在指定参数/选项名称时则报错返回。
|
||||
1. `genv`
|
||||
- 改进`Remove`方法支持多个环境变量的删除。
|
||||
1. `gfile`
|
||||
- 改进`TempDir`临时目录获取方法,在`*nix`系统下默认为`/tmp`目录。
|
||||
- 新增`ReadLines`, `ReadByteLines`方法,用以按行回调读取文件内容。
|
||||
- 新增`Copy*`方法,用以文件/目录的拷贝,支持递归。
|
||||
- 新增`Replace*`方法,用以目录下的文件内容替换,支持递归。
|
||||
- 改进`Scan*`方法,用以检索并返回指定目录下的所有文件/目录,支持文件模式指定,支持递归。
|
||||
- 完善单元测试。
|
||||
1. `gproc`
|
||||
- 改进命令行运行方法。
|
||||
- 改进`Shell`命令文件检索逻辑。
|
||||
- 改进实验性的进程间通信设计。
|
||||
1. `gtime`
|
||||
- 将包方法以及`Time`对象的时间戳方法`Second`, `Millisecond`, `Microsecond`, `Nanosecond`标记为废除,
|
||||
并新增`Timestamp`, `TimestampMilli`, `TimestampMicro`, `TimestampNano`替换。
|
||||
- 需要注意的是以上修改可能和老版本存在兼容性问题。
|
||||
|
||||
1. `gview`
|
||||
- 解析功能、缓存设计改进。
|
||||
- 新增`encode`, `decode`HTML编码/解码模板函数。
|
||||
- 新增`concat`字符串拼接模板函数。
|
||||
- 新增`dump`模板函数,功能类似于`g.Dump`方法。
|
||||
- 新增`AutoEncode`配置项,用于自动转码输出的`HTML`内容,常用于防止`XSS`,默认关闭。需要注意的是该特性并不会影响`include`内置函数: https://goframe.org/os/gview/xss
|
||||
- 单元测试完善。
|
||||
|
||||
### `crypto`
|
||||
1. `gmd5`
|
||||
- 增加`MustEncrypt`, `MustEncryptBytes`, `MustEncryptString`, `MustEncryptFile`方法。
|
||||
1. `gsha1`
|
||||
- 增加`MustEncryptFile`方法
|
||||
|
||||
### `encoding`
|
||||
1. `gbase64`
|
||||
- 新增`MustEncodeFile`, `MustEncodeFileToString`, `MustDecode`, `MustDecodeToString`方法。
|
||||
1. `gjson`/`gparser`
|
||||
- 新增`GetMapStrStr`方法。
|
||||
- 新增`Must*`方法,用于指定数据格式的转换失败时产生`panic`错误,而不会返回`error`参数。
|
||||
|
||||
### `util`
|
||||
1. `gconv`
|
||||
- 改进`Convert`方法增加对`[]int32`, `[]int64`, `[]uint`, `[]uint32`, `[]uint64`, `[]float32`, `[]float64`数据类型的转换支持。
|
||||
- 改进`String`字符串转换方法对指针参数的支持。
|
||||
- 改进`Map*` Map转换方法的代码结构及性能。
|
||||
- 新增`Floats`, `Float32s`, `Float64s`对`[]float32`, `[]float64`类型转换方法。
|
||||
- 新增`Ints`, `Int32s`, `Int64s`对`[]int`, `[]int32`, `[]int64`类型转换方法。
|
||||
- 新增`Uints`, `Uint32s`, `Uint64s`对`[]uint`, `[]uint32`, `[]uint64`类型转换方法。
|
||||
- 完善单元测试。
|
||||
|
||||
|
||||
### `frame`
|
||||
1. `gins`
|
||||
- 所有的单例对象在获取失败时产生`panic`错误。
|
||||
|
||||
## Bug Fix
|
||||
1. 增加对常见错误路由格式例如`/user//index`的兼容支持。
|
||||
1. 修复`gtcp`/`gudp`在数据接收时的间隔时间单位问题。
|
||||
1. 修复`gfile`/`gspath`/`gfsnotify`包对文件的存在性判断不严谨问题。
|
||||
1. 修复`gproc.Kill`方法在`windows`系统下的运行阻塞问题。
|
||||
1. 修复`gstr.TrimLeftStr`/`gstr.TrimRightStr`在被替换字符串长度小于替换字符串长度时的数组溢出问题。
|
||||
|
||||
|
||||
|
||||
# `v1.10.0` (2019-12-05)
|
||||
|
||||
各位`gfer`久等了,较上一次发布时间过去已有两个多月了,这段时间`GF`也在不断地迭代改进,细节比较多,拟了个大概,以下是`release log`。
|
||||
|
||||
另外,`GoFrame`也参加了2019最受欢迎中国开源软件评选投票,明天就结束了,欢迎为`GF`投票啊:https://www.oschina.net/project/top_cn_2019 网页可以投一票,微信也可以投一票。
|
||||
|
||||
## 新特性
|
||||
|
||||
1. `Web Server`新特性:
|
||||
- 改进中间件及分组路由实现:https://goframe.org/net/ghttp/router/middleware
|
||||
- 增加文件配置管理特性:https://goframe.org/net/ghttp/config
|
||||
- 改进参数获取:https://goframe.org/net/ghttp/request
|
||||
- 改进文件上传:https://goframe.org/net/ghttp/client/demo/upload
|
||||
1. `Session`增加内置的多种`Storage`实现:
|
||||
- 基本介绍:https://goframe.org/os/gsession/index
|
||||
- 文件存储:https://goframe.org/os/gsession/file
|
||||
- 内存存储:https://goframe.org/os/gsession/memory
|
||||
- `Redis`存储:https://goframe.org/os/gsession/redis
|
||||
1. 增加日志组件单例对象,并优化配置管理:
|
||||
- https://goframe.org/frame/g/index
|
||||
- https://goframe.org/os/glog/config
|
||||
1. 常用的`container`容器增加`JSON`数据格式的`Marshal`/`UnMarshal`接口实现:
|
||||
- https://goframe.org/container/gmap/index
|
||||
- https://goframe.org/container/garray/index
|
||||
- https://goframe.org/container/gset/index
|
||||
- https://goframe.org/container/gvar/index
|
||||
- https://goframe.org/container/gtype/index
|
||||
- https://goframe.org/container/glist/index
|
||||
- https://goframe.org/container/gvar/index
|
||||
1. 新增`guuid`模块,用于通用的`UUID`生成:https://goframe.org/util/guuid/index
|
||||
|
||||
## 功能改进
|
||||
|
||||
### `net`
|
||||
1. `ghttp`
|
||||
- 改进请求流程处理性能;
|
||||
- `Server`增加对`Logger`日志对象的配置;
|
||||
- `Server`开放了`GetRouterMap`方法,用于获得当前服务的路由列表信息,使得开发者可以更方便地实现自定义权限管理;
|
||||
- `Server`配置管理优化;
|
||||
- `Client`客户端对象进行了大量的改进工作;
|
||||
- `Client`客户端对象增加多文件上传功能;
|
||||
- `Request`对象增加`GetError`方法,用于获取当前处理错误;
|
||||
- `Request`对象增加独立的视图对象及视图变量绑定功能,使得每个请求可以独立视图管理,也可以通过中间件切换请求对象的视图对象。默认情况下该功能关闭,视图解析时使用的是`Server`对象的视图对象;
|
||||
- 改建`Response`对象的`CORS`功能;
|
||||
- 增加`Response.WriteTplDefault`方法,用于解析并返回默认的模板内容;
|
||||
- 增加更多的单元测试用例;
|
||||
- 其他改进;
|
||||
1. `gipv4`/`gipv6`
|
||||
- 一些改进工作;
|
||||
1. `gtcp`/`gudp`
|
||||
- 一些改进工作;
|
||||
|
||||
### `database`
|
||||
1. `gdb`
|
||||
- 大量细节改进工作;
|
||||
- 去掉查询数据为空时的`sql.ErrNoRows`错误返回,保留`Struct`/`Structs`/`Scan`方法在操作数据为空的该错误返回;
|
||||
- 调试模式开启时,输出的SQL语句改进为完整的带参数的SQL,仅作参考;
|
||||
- `Where`方法增加对`gmap`数据类型支持,包括顺序性的`ListMap`/`TreeMap`等等;
|
||||
- 查询缓存方法`Cache`的缓存时间参数类型修改为`time.Duration`;
|
||||
- 修改`Record`/`Result`的数据类型转换方法名称,原有的转换方法标记为`deprecated`;
|
||||
- `Record`/`Result`查询结果类型增加`IsEmpty`方法,用于判断结果集是否为空;
|
||||
- `Record`类型增加`GMap`方法,用于将查询记录转换为`gmap`类型;
|
||||
- 增加`Option`/`OptionOmitEmpty`方法,用于输入参数过滤,包括`Data`参数及`Where`参数:https://goframe.org/database/gdb/empty
|
||||
- 增加字段排除方法`FieldsEx`:https://goframe.org/database/gdb/senior
|
||||
- 增加日志功能特性:https://goframe.org/database/gdb/senior
|
||||
- 改进数据库配置管理:https://goframe.org/database/gdb/config
|
||||
- 增加大量单元测试;
|
||||
1. `gredis`
|
||||
- 返回数据类型转换改进:https://github.com/gogf/gf/issues/415
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
|
||||
### `os`
|
||||
1. `gcache`
|
||||
- 需要注意了:缓存的有效时间参数从`interface{}`类型调整为了`time.Duration`类型,因此不再兼容之前的`int`参数类型,以保证更好的性能;
|
||||
1. `gfcache`
|
||||
- 由于`gcache`组件的缓存时间参数类型的变更,因此该组件的时间参数也变更为了`time.Duration`类型;
|
||||
1. `gcfg`
|
||||
- 增加`Available`方法,用以判断配置是否有效;
|
||||
1. `gfile`
|
||||
- 增加`Chdir`方法,用于工作目录切换;
|
||||
1. `gtime`
|
||||
- 增加`JSON`数据格式的`Marshal`/`UnMarshal`接口实现;
|
||||
|
||||
### `container`
|
||||
1. `gmap`
|
||||
- 增加`MapStrAny`方法,用于常见`map`类型的转换;
|
||||
- 增加`MapCopy`方法,用于底层`map`数据复制;
|
||||
- 增加`FilterEmpty`方法,用于`map`空值过滤;
|
||||
- 增加`Pop`/`Pops`方法,用于随机返回`map`中的数据项(并删除);
|
||||
- 增加`Replace`方法,用于给定的`map`数据覆盖底层`map`数据项;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
1. `garray`
|
||||
- 增加`Interfaces`转换方法,返回`[]interface{}`类型;
|
||||
- 对排序数组增加`SetComparator`方法用户自定义修改比较器;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
1. `glist`
|
||||
- 增加`NewFrom`方法,基于给定的`[]interface{}`变量创建链表;
|
||||
- 增加`Join`方法,用于将链表项使用给定字符串连接为字符串返回;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
1. `gset`
|
||||
- 增加`AddIfNotExistFunc`/`AddIfNotExistFuncLock`方法;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
1. `gtree`
|
||||
- 增加`Replace`方法,用于更新现有树的数据项;
|
||||
- 其他改进;
|
||||
1. `gtype`
|
||||
- 一些细节改进工作,不一一列出;
|
||||
- 完善基准测试、单元测试;
|
||||
1. `gvar`
|
||||
- 增加`Ints`/`Uints`类型转换方法;
|
||||
- 其他改进;
|
||||
|
||||
### `crypto`
|
||||
1. `gmd5`
|
||||
- 小细节改进;
|
||||
1. `gsha1`
|
||||
- 小细节改进;
|
||||
|
||||
### `text`
|
||||
1. `gstr`
|
||||
- 改进`SplitAndTrim`方法,将`SplitAndTrimSpace`标记为`deprecated`;
|
||||
- 增加`TrimStr`方法;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
|
||||
### `debug`
|
||||
|
||||
1. `gdebug`
|
||||
- 增加`CallerFileLineShort`/`FuncPath`/`FuncName`方法;
|
||||
- 其他改进;
|
||||
|
||||
### `encoding`
|
||||
|
||||
1. `gbase64`
|
||||
- 增加`EncodeToString`/`EncodeFile`/`EncodeFileToString`/`DecodeToString`方法;
|
||||
- 完善单元测试;
|
||||
1. `gjson`
|
||||
- 完善单元测试;
|
||||
|
||||
### `frame`
|
||||
|
||||
1. `g`/`gins`
|
||||
- https://goframe.org/frame/g/index
|
||||
- 增加`CreateVar`方法;
|
||||
- 完善单元测试;
|
||||
- 其他改进;
|
||||
|
||||
### `util`
|
||||
|
||||
1. `gconv`
|
||||
- 改进优化部分类型转换方法性能;
|
||||
- 增加`Uints`/`SliceUint`类型转换方法;
|
||||
- 增加`UnsafeStrToBytes`/`UnsafeBytesToStr`高性能的类型转换方法;
|
||||
- 增加对`MapStrAny`接口方法的支持,用于常见`map`类型的转换;
|
||||
- 其他改进;
|
||||
1. `gvalid`
|
||||
- 改进对中国身份证号的识别校验功能;
|
||||
- 增加`luhn`银行卡号的校验功能;
|
||||
1. `grand`
|
||||
- 一些性能改进工作;
|
||||
|
||||
|
||||
## Bug Fix
|
||||
1. 解决`WebSocket`关闭时的`hijacked`报错问题:https://github.com/gogf/gf/issues/381
|
||||
1. 解决静态文件服务时大文件的内存占用问题;
|
||||
1. 修复前置`Nginx`后默认情况下的`Cookie`域名设置问题;
|
||||
1. 修复`gconv.Struct`在属性为`[]struct`并且输入属性参数为空时的转换失败问题:https://github.com/gogf/gf/issues/405
|
||||
1. 其他一些修复;
|
||||
|
||||
|
||||
|
||||
# `v1.9.3` (2019-09-24)
|
||||
|
||||
该版本实际为`v2.0`的大版本发布,为避免`go module`机制严格要求`v2`版本以上需要修改`import`并加上`v2`后缀,因此使用了`v1.9`版本进行发布。
|
||||
|
||||
## 新特性
|
||||
|
||||
1. 新增`gf`命令行开发辅助工具:https://goframe.org/toolchain/cli
|
||||
1. 新增`gres`资源管理器模块:https://goframe.org/os/gres/index
|
||||
1. 重构`Session`功能,新增`gsession`模块,`WebServer`默认使用文件存储`Session`:https://goframe.org/net/ghttp/session
|
||||
1. `WebServer`新增中间件特性,并保留原有的HOOK设计,两者都可实现请求拦截、预处理等等特性:https://goframe.org/net/ghttp/router/middleware
|
||||
1. 新增`gi18n`国际化管理模块:https://goframe.org/i18n/gi18n/index
|
||||
1. 新增`gini`模块:https://goframe.org/encoding/gini/index
|
||||
1. `WebServer`新增更便捷的层级路由注册方式:https://goframe.org/net/ghttp/group/level
|
||||
1. `gcmd`命令行参数解析模块重构,增加`Parser`解析对象:https://goframe.org/os/gcmd/index
|
||||
1. 新增`gdebug`模块,用于堆栈信息获取/打印:https://goframe.org/debug/gdebug/index
|
||||
|
||||
|
||||
## 重大调整
|
||||
1. 去掉`1.x`版本中已经被标记为`deprecated`的方法;
|
||||
1. 调整`container`分类的容器模块,将默认并发安全参数调整为默认非并发安全;
|
||||
1. 目录调整:
|
||||
- 去掉`third`目录,统一使用`go module`管理包依赖;
|
||||
- 将原有`g`目录中的模块移出到框架主目录,原有的`g`模块移动到`frame/g`目录;
|
||||
- 将原有`geg`示例代码目录名称修改为`.example`;
|
||||
|
||||
|
||||
|
||||
## 功能改进
|
||||
|
||||
1. `ghttp`
|
||||
- 改进`Request`参数解析方式:https://goframe.org/net/ghttp/request
|
||||
- 改进跨域请求功能,新增`Origin`设置及校验功能:https://goframe.org/net/ghttp/cors
|
||||
- `Cookie`及`Session`的`TTL`配置数据类型修改为`time.Duration`;
|
||||
- 新增允许同时通过`Header/Cookie`传递`SessionId`;
|
||||
- 新增`ConfigFromMap/SetConfigWithMap`方法,支持通过`map`参数设置WebServer;
|
||||
- 改进默认的`CORS`配置,增加对常见`Header`参数的默认支持;
|
||||
- 新增`IsExitError`方法,用于开发者自定义处理`recover`错误处理时,过滤框架本身自定义的非异常错误;
|
||||
- 新增`SetSessionStorage`配置方法,用于开发者自定义`Session`存储;
|
||||
- `ghttp.Request`新增更多的参数获取方法;
|
||||
1. `gdb`
|
||||
- 增加对SQL中部分字段的自动转义(`Quote`)功能;
|
||||
- 增加对方法操作以及链式操作中的`slice`参数的支持;
|
||||
- 增加`SetLogger`方法用于开发者自定义数据库的日志打印;
|
||||
- 增加`Master/Slave`方法,开发者可自主选择数据库操作执行的主从节点;
|
||||
- 增加对`mssql/pgsql/oracle`的单元测试;
|
||||
- 在`debug`模式支持完整带参数整合的SQL语句调试打印;
|
||||
- 增加了更多的功能方法;
|
||||
1. `glog`
|
||||
- 新增`Default`方法用于获取默认的`Logger`对象;
|
||||
- 新增`StackWithFilter`方法用于自定义堆栈打印过滤;
|
||||
- 增加了更多的功能方法;
|
||||
1. `gfile`
|
||||
- 部分方法名称调整:`Get/PutBinContents`修改为`Get/PutBytes`;
|
||||
- 增加`ScanDirFile`方法,用于仅检索文件目录,支持递归检索;
|
||||
- 增加了更多的功能方法;
|
||||
1. `gview`
|
||||
- 新增`SetI18n`方法用于设置视图对象自定义的`gi18n`国际化对象;
|
||||
- 新增对`gres`资源管理器的内置支持;
|
||||
1. `gcompress`
|
||||
- 增加`zip`算法的文件/目录的压缩/解压方法;
|
||||
- 文件/目录压缩参数支持多路径;
|
||||
1. `gconv`
|
||||
- 改进对`[]byte`数据类型参数的支持;
|
||||
- 新增`Unsafe`转换方法,开发者可在特定场景下使用,提高转换效率;
|
||||
- 新增`MapDeep/StructDeep/StructsDeep`方法,支持递归`struct`转换;
|
||||
1. `gjson/gparser`
|
||||
- 改进类型自动识别功能;
|
||||
- 新增`LoadJson/LoadXml/LoadToml/LoadYaml/LoadIni`方法用于自定义的数据类型内容加载;
|
||||
- 增加了更多的功能方法;
|
||||
1. `gerror`
|
||||
- 改进错误堆栈获取逻辑;
|
||||
- 增加了更多的功能方法;
|
||||
1. `gmap/garray/gset/glist/gvar`
|
||||
- 改进并发安全基准测试脚本;
|
||||
- 修改`garray.StringArray`为`garray.StrArray`;
|
||||
- 增加了更多的功能方法;
|
||||
1. `gdes`
|
||||
- 规范化修改方法名称;
|
||||
1. `gstr`
|
||||
- 增加`Camel/Snake`相关命名转换方法;
|
||||
- 增加了更多的功能方法;
|
||||
1. `genv`
|
||||
- 增加了更多的功能方法;
|
||||
|
||||
|
||||
## Bug Fix
|
||||
1. 修复`gvalid`校验`struct`时的`tag`自定义错误失效的问题;
|
||||
1. 修复`gcfg`配置管理模块在特定情况下的内容类型自动识别失败问题;
|
||||
1. 修复`gqueue`在用户主动关闭队列时的并发安全问题;
|
||||
1. 修复`session`在开发者设置的`TTL`过大时的整型变量溢出问题;
|
||||
@ -1,135 +0,0 @@
|
||||
> This markdown is deprecated and will be removed in future. All TODO features are staged in the issue: https://github.com/gogf/gf/issues
|
||||
|
||||
# ON THE WAY
|
||||
1. 增加图形验证码支持,至少支持数字和英文字母;
|
||||
1. Cookie&Session数据池化处理;
|
||||
1. ghttp.Client增加proxy特性;
|
||||
1. gtime增加对时区转换的封装,并简化失去转换时对类似+80500时区的支持;
|
||||
1. orm增加sqlite对Save方法的支持(去掉触发器语句);
|
||||
1. ghttp.Server增加Ip访问控制功能(DenyIps&AllowIps);
|
||||
1. ghttp增加返回数据压缩机制;
|
||||
1. ghttp.Server增加proxy功能特性,本地proxy和远程proxy,本地即将路由规则映射;远程即反向代理;
|
||||
1. gjson对大json数据的解析效率问题;
|
||||
1. ghttp增加route name特性,并同时支持backend和template(提供内置函数)引用,可以通过RedirectRoute方法给定route name和路由参数跳转到指定的路由地址上;
|
||||
1. gvalid校验支持当第一个规则失败后便不再校验后续的规则,最好做成链式操作;
|
||||
1. ghttp.Request增加对输入参数的自动HtmlEncode机制;
|
||||
1. 常量命名风格根据golint进行修改;
|
||||
1. 开放rwmutex包,并将gjson的互斥锁使用自定义的mutex替换;
|
||||
1. 文档完善:
|
||||
- gconv struct tag、
|
||||
- 控制器及执行对象注册的Init&Shut方法、
|
||||
- ghttp.Response&ServeFile、gfcache、gproc shell执行、
|
||||
- ghttp Server&Client basic auth、
|
||||
- glog分类&日志等级&链式操作、gdb debug自动输出调试信息、gmlock内存锁、
|
||||
1. 服务注册域名增加对泛域名的支持;
|
||||
1. 项目参考:
|
||||
- https://github.com/namreg/godown
|
||||
- https://github.com/Masterminds/sprig
|
||||
1. gform参考 https://gohouse.github.io/gorose/dist/index.html 进行改进
|
||||
1. gtcp提供简便的包发送/接收方法(SendPkg/RecvPkg)以解决常见的TCP通信粘包问题,并完善文档(参考:https://www.cnblogs.com/kex1n/p/6502002.html);
|
||||
1. 路由增加不区分大小写得匹配方式;
|
||||
1. 改进WebServer获取POST参数处理逻辑,当提交非form数据时,例如json数据,针对某些方法可以直接解析;
|
||||
1. WebServer增加可选择的路由覆盖配置,默认情况下不覆盖;
|
||||
1. 增加jumplist的数据结构容器;
|
||||
1. DelayQueue/PriorityQueue;
|
||||
1. 权限管理模块;
|
||||
1. 从ghttp中剥离SESSION功能构成单独的模块gsession;
|
||||
1. 改进gproc进程间通信处理逻辑,提高稳定性,以应对进程间大批量的数据发送/接收;
|
||||
1. 添加Save/Replace/BatchSave/BatchReplace方法对sqlite数据库的支持;
|
||||
1. 添加sqlite数据库的单元测试用例;
|
||||
1. gredis增加cluster支持;
|
||||
1. gset.Add/Remove/Contains方法增加批量操作支持;
|
||||
1. gmlock增加手动清理机制:当内存锁不再使用时,由调用端决定是否清理内存锁;
|
||||
1. gtimer增加DelayAdd*方法返回Entry对象,以便DelayAdd*的定时任务也能进行状态控制;gcron同理需要改进;
|
||||
1. grpool增加支持阻塞添加任务接口;
|
||||
1. gdb.Model在链式安全的对象创建中增加sync.Pool的使用;
|
||||
1. 增加g.Table快捷方法以方便操作数据表,但是得考虑后续模型操作设计,特别是脚手架的模型管理;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# DONE
|
||||
1. Cookie设置中文失效问题;
|
||||
1. gvalid增加支持对[]rune的长度校验(一个中文占3个字节);
|
||||
1. grpool性能压测结果变慢的问题;
|
||||
1. ghttp的热重启的本地进程端口监听,在不使用该特性时默认关闭掉;
|
||||
1. gtcp增加对TLS加密通信的支持;
|
||||
1. 改进gdb对pgsql/mssql/oracle的支持,使用方法覆盖的方式改进操作,而不是完全依靠正则替换的方式;
|
||||
1. gdb的Cache缓存功能增加可自定义缓存接口,以便支持外部缓存功能,缓存接口可以通过io.ReadWriter接口实现;
|
||||
1. 改进ghttp分组路由中对hook的支持方式,以便格式与BindHookHandler统一;
|
||||
1. 使用gconv将slice映射到struct属性上,例如redis hscan的结果集;
|
||||
1. gconv完善针对不同类型的判断,例如:尽量减少sprintf("%v", xxx)来执行string类型的转换;
|
||||
2. ghttp.Server请求执行中增加服务退出的方法,不再执行后续操作;
|
||||
3. ghttp.Response对象完善并改进数据返回方法(Write/WriteString);
|
||||
4. ghttp.Server请求执行中增加服务退出的方法,不再执行后续操作;
|
||||
5. 增加fsnotify包支持;
|
||||
6. 改进gcfg和gview的文件自动更新机制;
|
||||
7. 将模板变量进行暴露,以便应用端可以进行灵活控制;
|
||||
8. 跟踪第三方mxj包的issue问题:https://github.com/clbanning/mxj/issues/48;
|
||||
9. gdb Where方法参数的改进,研究是否可以将string参数类型修改为interface{};
|
||||
10. gpage分页控制功能;
|
||||
11. https支持;
|
||||
12. ghttp.Server日志中增加请求时间和返回时间,以便计算执行时间差;
|
||||
13. 由于去掉了gdb的单例模式,并且将gins的部分对象封装迁移到了g包中,需要同时梳理文档,完善修改;
|
||||
14. 在代码中增加https与http同时开启使用的示例代码,这块大家问得比较多;
|
||||
15. ghttp.Server多个事件之间通过ghttp.Request.Param自定义参数传参;
|
||||
16. 研究是否增加配置文件目录检索功能,特别是如何友好改进开发环境的配置文件默认目录问题;
|
||||
17. 增加ghttp.Server不同状态码的自定义处理方法;
|
||||
18. ghttp.Server平滑重启方案;
|
||||
19. 完善gconv类型转换功能,增加time.Time/time.Duration类型转换,并增加benchmark测试脚本
|
||||
20. 当二进制参数为nil时,gjson.LoadContent并将gjson.Json对象ToMap时会报错;
|
||||
21. 改进控制器及执行对象注册,更友好地支持动态路由注册,例如:注册规则为 /channel/:name,现有的控制器及执行对象注册很难友好支持这种动态形式;
|
||||
22. 当前gpage分页包的输出标签不支持li,大多数CSS框架都是li+a标签模式,需要提供可更加灵活的定制化功能实现;
|
||||
23. 平滑重启机制改进,以便于开发阶段调试;
|
||||
24. 对grpool进行优化改进,包括属性原子操作封装采用gtype实现,修正设计BUG:https://github.com/gogf/gf/issues/6;
|
||||
25. gredis增加redis密码支持;
|
||||
26. 改进ghttp.Server平滑重启机制,当新进程接管服务后,再使用进程间通信方式通知父进程销毁;
|
||||
27. gproc进程间通信增加分组特性,不同的进程间可以通过进程ID以及分组名称发送/获取进程消息;
|
||||
28. ORM增加获取被执行的sql语句的方法;
|
||||
29. gdb增加查询缓存特性;
|
||||
30. gpage分页增加对自定义后缀的支持,如:2.html, 2.php等等;
|
||||
31. gvalid包增加struct tag的校验规则、自定义错误提示信息绑定的支持特性;
|
||||
32. 增加文件缓存包,可根据fsnotify机制进行缓存更新;
|
||||
33. *any/:name路由匹配路由改进支持不带名字的*/:路由规则;
|
||||
34. ghttp静态文件服务改进(特别是403返回状态的修改);
|
||||
35. map转struct增加对tag的支持;
|
||||
36. gcache检查在i386下的int64->int转换问题;
|
||||
37. ghttp获取参数支持直接转struct功能;
|
||||
38. gfsnotify增加对于目录的监控;
|
||||
39. 检查windows下的平滑重启失效问题;
|
||||
40. ghttp.Server的Cookie及Session锁机制优化(去掉map锁机制);
|
||||
41. 解决glog串日志情况;
|
||||
42. glog增加对日志文件名称的生成规则设定,支持时间格式规则;
|
||||
43. ghttp日志增加客户端IP信息;
|
||||
44. 完善gform配置管理说明,g.DB/Database和gdb.New的区别;
|
||||
1. 完善配置管理章节,说明默认的配置文件更改方式;
|
||||
1. 服务注册时判断方法定义满足规范时才执行绑定,否则提示WARN信息;
|
||||
1. `gfsnotify`增加添加监听文件时的监听ID返回,以便调用端删除监听时只删除自己添加的监听,而不影响其他对该同一文件的监听回调;
|
||||
1. `gfsnotify`针对添加目录监听时无法使用多个`Watcher`,考虑改进,并考虑动态扩容全局`Watcher`方案;
|
||||
1. 由于系统对inotify实例数量(`fs.inotify.max_user_instances`)以及队列大小(`fs.inotify.max_user_watches`)有限制,需要改进`gfsnotify`;
|
||||
1. WebServer事件回调允许对同一个路由规则绑定多个事件回调;
|
||||
1. gcfg/gview/ghttp等模块加上对临时文件目录的自动添加监听判断(基本是开发环境下,特别是windows环境),去掉临时文件的监听,避免临时文件过大引起的运行缓慢占用内存问题;
|
||||
1. 改进gfpool在文件指针变化时的更新;
|
||||
1. ghttp hook回调使用方式在注册路由比较多的时候,优先级可能使得开发者混乱,考虑方式便于管理;
|
||||
1. gform对于MySQL字段类型为datetime类型的时区问题分析;
|
||||
1. 改进证书打开失败时的WebServer错误提示,前置HOOK校验后关闭后续的HOOK逻辑执行;
|
||||
1. 目前WebServer的HOOK是按照优先级执行的,需要增加覆盖特性;
|
||||
1. 更新跨域请求CORS相关功能文档;
|
||||
1. ghttp.Response增加输出内容后自动退出当前请求机制,不需要用户手动return,参考beego如何实现;
|
||||
1. gcfg包目前允许添加重复的目录路径,需要在SetPath/AddPath时判断重复性,不能添加重复的路径;
|
||||
1. gdb执行数据写入时,如果参数为struct/[]struct,自动映射与表字段对应关系,不再使用gconv标签标识;
|
||||
1. gdb的Data方法支持struct参数传入;
|
||||
1. gfcache依旧使用gcache作为缓存控制对象,不要使用gmap;
|
||||
1. 增加对ghttp路由注册的{.struct}/{.method}单元测试;
|
||||
1. gconv针对struct的转换增加json tag支持,gconv.Map默认也支持json tag, 完善开发文档;
|
||||
1. 增加SO_REUSEPORT的支持;
|
||||
1. gkafka这个包比较重,未来从框架中剥离出来;
|
||||
1. str_ireplace: http://php.net/manual/en/function.str-ireplace.php
|
||||
1. strpos/stripos/strrpos/strripos: http://php.net/manual/en/function.stripos.php
|
||||
1. gfile对于文件的读写强行使用了gfpool,在某些场景下不合适,需要考虑剥离开,并为开发者提供单独的指针池文件操作特性;
|
||||
1. ghttp.Client自动Close机制;
|
||||
1. ghttp路由功能增加分组路由特性;
|
||||
1. 增加可选择性的orm tag特性,用以数据表记录与struct对象转换的键名属性映射;
|
||||
1. gview中的template标签失效问题;
|
||||
@ -453,7 +453,7 @@ func (a *Array) Clone() (newArray *Array) {
|
||||
array := make([]interface{}, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewArrayFrom(array, !a.mu.IsSafe())
|
||||
return NewArrayFrom(array, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -469,7 +469,7 @@ func (a *IntArray) Clone() (newArray *IntArray) {
|
||||
array := make([]int, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewIntArrayFrom(array, !a.mu.IsSafe())
|
||||
return NewIntArrayFrom(array, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -457,7 +457,7 @@ func (a *StrArray) Clone() (newArray *StrArray) {
|
||||
array := make([]string, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewStrArrayFrom(array, !a.mu.IsSafe())
|
||||
return NewStrArrayFrom(array, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -446,7 +446,7 @@ func (a *SortedArray) binSearch(value interface{}, lock bool) (index int, result
|
||||
mid := 0
|
||||
cmp := -2
|
||||
for min <= max {
|
||||
mid = (min + max) / 2
|
||||
mid = min + int((max-min)/2)
|
||||
cmp = a.getComparator()(value, a.array[mid])
|
||||
switch {
|
||||
case cmp < 0:
|
||||
@ -499,7 +499,7 @@ func (a *SortedArray) Clone() (newArray *SortedArray) {
|
||||
array := make([]interface{}, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewSortedArrayFrom(array, a.comparator, !a.mu.IsSafe())
|
||||
return NewSortedArrayFrom(array, a.comparator, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -443,7 +443,7 @@ func (a *SortedIntArray) binSearch(value int, lock bool) (index int, result int)
|
||||
mid := 0
|
||||
cmp := -2
|
||||
for min <= max {
|
||||
mid = (min + max) / 2
|
||||
mid = min + int((max-min)/2)
|
||||
cmp = a.getComparator()(value, a.array[mid])
|
||||
switch {
|
||||
case cmp < 0:
|
||||
@ -496,7 +496,7 @@ func (a *SortedIntArray) Clone() (newArray *SortedIntArray) {
|
||||
array := make([]int, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewSortedIntArrayFrom(array, !a.mu.IsSafe())
|
||||
return NewSortedIntArrayFrom(array, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -445,7 +445,7 @@ func (a *SortedStrArray) binSearch(value string, lock bool) (index int, result i
|
||||
mid := 0
|
||||
cmp := -2
|
||||
for min <= max {
|
||||
mid = (min + max) / 2
|
||||
mid = min + int((max-min)/2)
|
||||
cmp = a.getComparator()(value, a.array[mid])
|
||||
switch {
|
||||
case cmp < 0:
|
||||
@ -498,7 +498,7 @@ func (a *SortedStrArray) Clone() (newArray *SortedStrArray) {
|
||||
array := make([]string, len(a.array))
|
||||
copy(array, a.array)
|
||||
a.mu.RUnlock()
|
||||
return NewSortedStrArrayFrom(array, !a.mu.IsSafe())
|
||||
return NewSortedStrArrayFrom(array, a.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Clear deletes all items of current array.
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -56,7 +56,7 @@ func (m *IntAnyMap) Iterator(f func(k int, v interface{}) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *IntAnyMap) Clone() *IntAnyMap {
|
||||
return NewIntAnyMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewIntAnyMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -54,7 +54,7 @@ func (m *IntIntMap) Iterator(f func(k int, v int) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *IntIntMap) Clone() *IntIntMap {
|
||||
return NewIntIntMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewIntIntMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -54,7 +54,7 @@ func (m *IntStrMap) Iterator(f func(k int, v string) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *IntStrMap) Clone() *IntStrMap {
|
||||
return NewIntStrMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewIntStrMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -56,7 +56,7 @@ func (m *StrAnyMap) Iterator(f func(k string, v interface{}) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *StrAnyMap) Clone() *StrAnyMap {
|
||||
return NewStrAnyMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewStrAnyMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -54,7 +54,7 @@ func (m *StrIntMap) Iterator(f func(k string, v int) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *StrIntMap) Clone() *StrIntMap {
|
||||
return NewStrIntMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewStrIntMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -55,7 +55,7 @@ func (m *StrStrMap) Iterator(f func(k string, v string) bool) {
|
||||
|
||||
// Clone returns a new hash map with copy of current map data.
|
||||
func (m *StrStrMap) Clone() *StrStrMap {
|
||||
return NewStrStrMapFrom(m.MapCopy(), !m.mu.IsSafe())
|
||||
return NewStrStrMapFrom(m.MapCopy(), m.mu.IsSafe())
|
||||
}
|
||||
|
||||
// Map returns the underlying data map.
|
||||
@ -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()
|
||||
|
||||
@ -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()))
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -19,18 +19,10 @@ import (
|
||||
|
||||
// Pool is an Object-Reusable Pool.
|
||||
type Pool struct {
|
||||
// Available/idle items list.
|
||||
list *glist.List
|
||||
|
||||
// Whether the pool is closed.
|
||||
closed *gtype.Bool
|
||||
|
||||
// Time To Live for pool items.
|
||||
TTL time.Duration
|
||||
|
||||
// Callback function to create pool item.
|
||||
NewFunc func() (interface{}, error)
|
||||
|
||||
list *glist.List // Available/idle items list.
|
||||
closed *gtype.Bool // Whether the pool is closed.
|
||||
TTL time.Duration // Time To Live for pool items.
|
||||
NewFunc func() (interface{}, error) // Callback function to create pool item.
|
||||
// ExpireFunc is the for expired items destruction.
|
||||
// This function needs to be defined when the pool items
|
||||
// need to perform additional destruction operations.
|
||||
@ -40,8 +32,8 @@ type Pool struct {
|
||||
|
||||
// Pool item.
|
||||
type poolItem struct {
|
||||
expire int64 // Expire timestamp in milliseconds.
|
||||
value interface{} // Item value.
|
||||
value interface{} // Item value.
|
||||
expireAt int64 // Expire timestamp in milliseconds.
|
||||
}
|
||||
|
||||
// Creation function for object.
|
||||
@ -80,11 +72,11 @@ func (p *Pool) Put(value interface{}) error {
|
||||
value: value,
|
||||
}
|
||||
if p.TTL == 0 {
|
||||
item.expire = 0
|
||||
item.expireAt = 0
|
||||
} else {
|
||||
// As for Golang version < 1.13, there's no method Milliseconds for time.Duration.
|
||||
// So we need calculate the milliseconds using its nanoseconds value.
|
||||
item.expire = gtime.TimestampMilli() + p.TTL.Nanoseconds()/1000000
|
||||
item.expireAt = gtime.TimestampMilli() + p.TTL.Nanoseconds()/1000000
|
||||
}
|
||||
p.list.PushBack(item)
|
||||
return nil
|
||||
@ -112,8 +104,11 @@ func (p *Pool) Get() (interface{}, error) {
|
||||
for !p.closed.Val() {
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
f := r.(*poolItem)
|
||||
if f.expire == 0 || f.expire > gtime.TimestampMilli() {
|
||||
if f.expireAt == 0 || f.expireAt > gtime.TimestampMilli() {
|
||||
return f.value, nil
|
||||
} else if p.ExpireFunc != nil {
|
||||
// TODO: move expire function calling asynchronously from `Get` operation.
|
||||
p.ExpireFunc(f.value)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
@ -169,9 +164,9 @@ func (p *Pool) checkExpireItems() {
|
||||
}
|
||||
if r := p.list.PopFront(); r != nil {
|
||||
item := r.(*poolItem)
|
||||
latestExpire = item.expire
|
||||
latestExpire = item.expireAt
|
||||
// TODO improve the auto-expiration mechanism of the pool.
|
||||
if item.expire > timestampMilli {
|
||||
if item.expireAt > timestampMilli {
|
||||
p.list.PushFront(item)
|
||||
break
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ func NewAVLTreeFrom(comparator func(v1, v2 interface{}) int, data map[interface{
|
||||
|
||||
// Clone returns a new tree with a copy of current tree.
|
||||
func (tree *AVLTree) Clone() *AVLTree {
|
||||
newTree := NewAVLTree(tree.comparator, !tree.mu.IsSafe())
|
||||
newTree := NewAVLTree(tree.comparator, tree.mu.IsSafe())
|
||||
newTree.Sets(tree.Map())
|
||||
return newTree
|
||||
}
|
||||
@ -441,9 +441,9 @@ func (tree *AVLTree) MapStrAny() map[string]interface{} {
|
||||
func (tree *AVLTree) Flip(comparator ...func(v1, v2 interface{}) int) {
|
||||
t := (*AVLTree)(nil)
|
||||
if len(comparator) > 0 {
|
||||
t = NewAVLTree(comparator[0], !tree.mu.IsSafe())
|
||||
t = NewAVLTree(comparator[0], tree.mu.IsSafe())
|
||||
} else {
|
||||
t = NewAVLTree(tree.comparator, !tree.mu.IsSafe())
|
||||
t = NewAVLTree(tree.comparator, tree.mu.IsSafe())
|
||||
}
|
||||
tree.IteratorAsc(func(key, value interface{}) bool {
|
||||
t.put(value, key, nil, &t.root)
|
||||
|
||||
@ -68,7 +68,7 @@ func NewBTreeFrom(m int, comparator func(v1, v2 interface{}) int, data map[inter
|
||||
|
||||
// Clone returns a new tree with a copy of current tree.
|
||||
func (tree *BTree) Clone() *BTree {
|
||||
newTree := NewBTree(tree.m, tree.comparator, !tree.mu.IsSafe())
|
||||
newTree := NewBTree(tree.m, tree.comparator, tree.mu.IsSafe())
|
||||
newTree.Sets(tree.Map())
|
||||
return newTree
|
||||
}
|
||||
@ -620,7 +620,7 @@ func (tree *BTree) middle() int {
|
||||
func (tree *BTree) search(node *BTreeNode, key interface{}) (index int, found bool) {
|
||||
low, mid, high := 0, 0, len(node.Entries)-1
|
||||
for low <= high {
|
||||
mid = (high + low) / 2
|
||||
mid = low + int((high-low)/2)
|
||||
compare := tree.getComparator()(key, node.Entries[mid].Key)
|
||||
switch {
|
||||
case compare > 0:
|
||||
|
||||
@ -83,7 +83,7 @@ func (tree *RedBlackTree) SetComparator(comparator func(a, b interface{}) int) {
|
||||
|
||||
// Clone returns a new tree with a copy of current tree.
|
||||
func (tree *RedBlackTree) Clone() *RedBlackTree {
|
||||
newTree := NewRedBlackTree(tree.comparator, !tree.mu.IsSafe())
|
||||
newTree := NewRedBlackTree(tree.comparator, tree.mu.IsSafe())
|
||||
newTree.Sets(tree.Map())
|
||||
return newTree
|
||||
}
|
||||
@ -656,9 +656,9 @@ func (tree *RedBlackTree) Search(key interface{}) (value interface{}, found bool
|
||||
func (tree *RedBlackTree) Flip(comparator ...func(v1, v2 interface{}) int) {
|
||||
t := (*RedBlackTree)(nil)
|
||||
if len(comparator) > 0 {
|
||||
t = NewRedBlackTree(comparator[0], !tree.mu.IsSafe())
|
||||
t = NewRedBlackTree(comparator[0], tree.mu.IsSafe())
|
||||
} else {
|
||||
t = NewRedBlackTree(tree.comparator, !tree.mu.IsSafe())
|
||||
t = NewRedBlackTree(tree.comparator, tree.mu.IsSafe())
|
||||
}
|
||||
tree.IteratorAsc(func(key, value interface{}) bool {
|
||||
t.doSet(value, key)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018-2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -11,8 +11,6 @@ import (
|
||||
"github.com/gogf/gf/internal/json"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/internal/empty"
|
||||
|
||||
"github.com/gogf/gf/container/gtype"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
@ -88,16 +86,6 @@ func (v *Var) Interface() interface{} {
|
||||
return v.Val()
|
||||
}
|
||||
|
||||
// IsNil checks whether <v> is nil.
|
||||
func (v *Var) IsNil() bool {
|
||||
return v.Val() == nil
|
||||
}
|
||||
|
||||
// IsEmpty checks whether <v> is empty.
|
||||
func (v *Var) IsEmpty() bool {
|
||||
return empty.IsEmpty(v.Val())
|
||||
}
|
||||
|
||||
// Bytes converts and returns <v> as []byte.
|
||||
func (v *Var) Bytes() []byte {
|
||||
return gconv.Bytes(v.Val())
|
||||
|
||||
98
container/gvar/gvar_is.go
Normal file
98
container/gvar/gvar_is.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright GoFrame 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 gvar
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/internal/empty"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// IsNil checks whether <v> is nil.
|
||||
func (v *Var) IsNil() bool {
|
||||
return v.Val() == nil
|
||||
}
|
||||
|
||||
// IsEmpty checks whether <v> is empty.
|
||||
func (v *Var) IsEmpty() bool {
|
||||
return empty.IsEmpty(v.Val())
|
||||
}
|
||||
|
||||
// IsInt checks whether <v> is type of int.
|
||||
func (v *Var) IsInt() bool {
|
||||
switch v.Val().(type) {
|
||||
case int, *int, int8, *int8, int16, *int16, int32, *int32, int64, *int64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUint checks whether <v> is type of uint.
|
||||
func (v *Var) IsUint() bool {
|
||||
switch v.Val().(type) {
|
||||
case uint, *uint, uint8, *uint8, uint16, *uint16, uint32, *uint32, uint64, *uint64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsFloat checks whether <v> is type of float.
|
||||
func (v *Var) IsFloat() bool {
|
||||
switch v.Val().(type) {
|
||||
case float32, *float32, float64, *float64:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSlice checks whether <v> is type of slice.
|
||||
func (v *Var) IsSlice() bool {
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(v.Val())
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsMap checks whether <v> is type of map.
|
||||
func (v *Var) IsMap() bool {
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(v.Val())
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Map:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsStruct checks whether <v> is type of struct.
|
||||
func (v *Var) IsStruct() bool {
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(v.Val())
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
switch reflectKind {
|
||||
case reflect.Struct:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@ -20,6 +20,7 @@ func (v *Var) Struct(pointer interface{}, mapping ...map[string]string) error {
|
||||
// Struct maps value of <v> to <pointer> recursively.
|
||||
// The parameter <pointer> should be a pointer to a struct instance.
|
||||
// The parameter <mapping> is used to specify the key-to-attribute mapping rules.
|
||||
// Deprecated, use Struct instead.
|
||||
func (v *Var) StructDeep(pointer interface{}, mapping ...map[string]string) error {
|
||||
return gconv.StructDeep(v.Val(), pointer, mapping...)
|
||||
}
|
||||
@ -30,6 +31,7 @@ func (v *Var) Structs(pointer interface{}, mapping ...map[string]string) error {
|
||||
}
|
||||
|
||||
// StructsDeep converts and returns <v> as given struct slice recursively.
|
||||
// Deprecated, use Struct instead.
|
||||
func (v *Var) StructsDeep(pointer interface{}, mapping ...map[string]string) error {
|
||||
return gconv.StructsDeep(v.Val(), pointer, mapping...)
|
||||
}
|
||||
|
||||
170
container/gvar/gvar_z_unit_is_test.go
Normal file
170
container/gvar/gvar_z_unit_is_test.go
Normal file
@ -0,0 +1,170 @@
|
||||
// Copyright GoFrame 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 gvar_test
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/frame/g"
|
||||
"github.com/gogf/gf/test/gtest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVar_IsNil(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsNil(), false)
|
||||
t.Assert(g.NewVar(nil).IsNil(), true)
|
||||
t.Assert(g.NewVar(g.Map{}).IsNil(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsNil(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsNil(), false)
|
||||
t.Assert(g.NewVar(0.1).IsNil(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsNil(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsNil(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsEmpty(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsEmpty(), true)
|
||||
t.Assert(g.NewVar(nil).IsEmpty(), true)
|
||||
t.Assert(g.NewVar(g.Map{}).IsEmpty(), true)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsEmpty(), true)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsEmpty(), false)
|
||||
t.Assert(g.NewVar(0.1).IsEmpty(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsEmpty(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsEmpty(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsInt(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsInt(), true)
|
||||
t.Assert(g.NewVar(nil).IsInt(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsInt(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsInt(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsInt(), true)
|
||||
t.Assert(g.NewVar(-1).IsInt(), true)
|
||||
t.Assert(g.NewVar(0.1).IsInt(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsInt(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsInt(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(int8(1)).IsInt(), true)
|
||||
t.Assert(g.NewVar(uint8(1)).IsInt(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsUint(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsUint(), false)
|
||||
t.Assert(g.NewVar(nil).IsUint(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsUint(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsUint(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsUint(), false)
|
||||
t.Assert(g.NewVar(-1).IsUint(), false)
|
||||
t.Assert(g.NewVar(0.1).IsUint(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsUint(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsUint(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(int8(1)).IsUint(), false)
|
||||
t.Assert(g.NewVar(uint8(1)).IsUint(), true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsFloat(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsFloat(), false)
|
||||
t.Assert(g.NewVar(nil).IsFloat(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsFloat(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsFloat(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsFloat(), false)
|
||||
t.Assert(g.NewVar(-1).IsFloat(), false)
|
||||
t.Assert(g.NewVar(0.1).IsFloat(), true)
|
||||
t.Assert(g.NewVar(float64(1)).IsFloat(), true)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsFloat(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsFloat(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(int8(1)).IsFloat(), false)
|
||||
t.Assert(g.NewVar(uint8(1)).IsFloat(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsSlice(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsSlice(), false)
|
||||
t.Assert(g.NewVar(nil).IsSlice(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsSlice(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsSlice(), true)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsSlice(), false)
|
||||
t.Assert(g.NewVar(-1).IsSlice(), false)
|
||||
t.Assert(g.NewVar(0.1).IsSlice(), false)
|
||||
t.Assert(g.NewVar(float64(1)).IsSlice(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsSlice(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsSlice(), true)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(int8(1)).IsSlice(), false)
|
||||
t.Assert(g.NewVar(uint8(1)).IsSlice(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsMap(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsMap(), false)
|
||||
t.Assert(g.NewVar(nil).IsMap(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsMap(), true)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsMap(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsMap(), false)
|
||||
t.Assert(g.NewVar(-1).IsMap(), false)
|
||||
t.Assert(g.NewVar(0.1).IsMap(), false)
|
||||
t.Assert(g.NewVar(float64(1)).IsMap(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsMap(), true)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsMap(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(int8(1)).IsMap(), false)
|
||||
t.Assert(g.NewVar(uint8(1)).IsMap(), false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestVar_IsStruct(t *testing.T) {
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(0).IsStruct(), false)
|
||||
t.Assert(g.NewVar(nil).IsStruct(), false)
|
||||
t.Assert(g.NewVar(g.Map{}).IsStruct(), false)
|
||||
t.Assert(g.NewVar(g.Slice{}).IsStruct(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
t.Assert(g.NewVar(1).IsStruct(), false)
|
||||
t.Assert(g.NewVar(-1).IsStruct(), false)
|
||||
t.Assert(g.NewVar(0.1).IsStruct(), false)
|
||||
t.Assert(g.NewVar(float64(1)).IsStruct(), false)
|
||||
t.Assert(g.NewVar(g.Map{"k": "v"}).IsStruct(), false)
|
||||
t.Assert(g.NewVar(g.Slice{0}).IsStruct(), false)
|
||||
})
|
||||
gtest.C(t, func(t *gtest.T) {
|
||||
a := &struct {
|
||||
}{}
|
||||
t.Assert(g.NewVar(a).IsStruct(), true)
|
||||
t.Assert(g.NewVar(*a).IsStruct(), true)
|
||||
t.Assert(g.NewVar(&a).IsStruct(), true)
|
||||
})
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,12 +8,15 @@
|
||||
package gdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/os/gcmd"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/os/glog"
|
||||
|
||||
@ -40,6 +43,12 @@ type DB interface {
|
||||
// Note that it is not recommended using the this function manually.
|
||||
Open(config *ConfigNode) (*sql.DB, error)
|
||||
|
||||
// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
|
||||
// of current DB object and with given context in it.
|
||||
// Note that this returned DB object can be used only once, so do not assign it to
|
||||
// a global or package variable for long using.
|
||||
Ctx(ctx context.Context) DB
|
||||
|
||||
// ===========================================================================
|
||||
// Query APIs.
|
||||
// ===========================================================================
|
||||
@ -126,6 +135,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)
|
||||
@ -134,6 +144,7 @@ type DB interface {
|
||||
// Utility methods.
|
||||
// ===========================================================================
|
||||
|
||||
GetCtx() context.Context
|
||||
GetChars() (charLeft string, charRight string)
|
||||
GetMaster(schema ...string) (*sql.DB, error)
|
||||
GetSlave(schema ...string) (*sql.DB, error)
|
||||
@ -142,6 +153,7 @@ type DB interface {
|
||||
QuotePrefixTableName(table string) string
|
||||
Tables(schema ...string) (tables []string, err error)
|
||||
TableFields(table string, schema ...string) (map[string]*TableField, error)
|
||||
HasTable(name string) (bool, error)
|
||||
|
||||
// HandleSqlBeforeCommit is a hook function, which deals with the sql string before
|
||||
// it's committed to underlying driver. The parameter <link> specifies the current
|
||||
@ -150,27 +162,24 @@ type DB interface {
|
||||
HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{})
|
||||
|
||||
// ===========================================================================
|
||||
// Internal methods.
|
||||
// Internal methods, for internal usage purpose, you do not need consider it.
|
||||
// ===========================================================================
|
||||
|
||||
filterFields(schema, table string, data map[string]interface{}) map[string]interface{}
|
||||
convertValue(fieldValue []byte, fieldType string) interface{}
|
||||
rowsToResult(rows *sql.Rows) (Result, error)
|
||||
mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error)
|
||||
convertFieldValueToLocalValue(fieldValue interface{}, fieldType string) interface{}
|
||||
convertRowsToResult(rows *sql.Rows) (Result, error)
|
||||
}
|
||||
|
||||
// Core is the base struct for database management.
|
||||
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.
|
||||
schema *gtype.String // Custom schema for this object.
|
||||
dryrun *gtype.Bool // Dry run.
|
||||
prefix string // Table prefix.
|
||||
logger *glog.Logger // Logger.
|
||||
maxIdleConnCount int // Max idle connection count.
|
||||
maxOpenConnCount int // Max open connection count.
|
||||
maxConnLifetime time.Duration // Max TTL for a connection.
|
||||
DB DB // DB interface object.
|
||||
group string // Configuration group name.
|
||||
debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime.
|
||||
cache *gcache.Cache // Cache manager, SQL result cache only.
|
||||
schema *gtype.String // Custom schema for this object.
|
||||
logger *glog.Logger // Logger.
|
||||
config *ConfigNode // Current config node.
|
||||
ctx context.Context // Context for chaining operation only.
|
||||
}
|
||||
|
||||
// Driver is the interface for integrating sql drivers into package gdb.
|
||||
@ -187,6 +196,7 @@ type Sql struct {
|
||||
Error error // Execution result.
|
||||
Start int64 // Start execution timestamp in milliseconds.
|
||||
End int64 // End execution timestamp in milliseconds.
|
||||
Group string // Group is the group name of the configuration that the sql is executed from.
|
||||
}
|
||||
|
||||
// TableField is the struct for table field.
|
||||
@ -208,32 +218,30 @@ type Link interface {
|
||||
Prepare(sql string) (*sql.Stmt, error)
|
||||
}
|
||||
|
||||
// Counter is the type for update count.
|
||||
type Counter struct {
|
||||
Field string
|
||||
Value float64
|
||||
}
|
||||
|
||||
type (
|
||||
// Value is the field value type.
|
||||
Value = *gvar.Var
|
||||
|
||||
// Record is the row record of the table.
|
||||
Record map[string]Value
|
||||
|
||||
// Result is the row record array.
|
||||
Result []Record
|
||||
|
||||
// Map is alias of map[string]interface{},
|
||||
// which is the most common usage map type.
|
||||
Map = map[string]interface{}
|
||||
|
||||
// List is type of map array.
|
||||
List = []Map
|
||||
Raw string // Raw is a raw sql that will not be treated as argument but as a direct sql part.
|
||||
Value = *gvar.Var // Value is the field value type.
|
||||
Record map[string]Value // Record is the row record of the table.
|
||||
Result []Record // Result is the row record array.
|
||||
Map = map[string]interface{} // Map is alias of map[string]interface{}, which is the most common usage map type.
|
||||
List = []Map // List is type of map array.
|
||||
)
|
||||
|
||||
const (
|
||||
gINSERT_OPTION_DEFAULT = 0
|
||||
gINSERT_OPTION_REPLACE = 1
|
||||
gINSERT_OPTION_SAVE = 2
|
||||
gINSERT_OPTION_IGNORE = 3
|
||||
gDEFAULT_BATCH_NUM = 10 // Per count for batch insert/replace/save
|
||||
gDEFAULT_CONN_MAX_IDLE_COUNT = 10 // Max idle connection count in pool.
|
||||
gDEFAULT_CONN_MAX_LIFE_TIME = 30 // Max life time for per connection in pool in seconds.
|
||||
insertOptionDefault = 0
|
||||
insertOptionReplace = 1
|
||||
insertOptionSave = 2
|
||||
insertOptionIgnore = 3
|
||||
defaultBatchNumber = 10 // Per count for batch insert/replace/save.
|
||||
defaultMaxIdleConnCount = 10 // Max idle connection count in pool.
|
||||
defaultMaxOpenConnCount = 100 // Max open connection count in pool.
|
||||
defaultMaxConnLifeTime = 30 * time.Second // Max life time for per connection in pool in seconds.
|
||||
)
|
||||
|
||||
var (
|
||||
@ -258,9 +266,21 @@ var (
|
||||
|
||||
// regularFieldNameRegPattern is the regular expression pattern for a string
|
||||
// which is a regular field name of table.
|
||||
regularFieldNameRegPattern = `^[\w\.\-]+$`
|
||||
regularFieldNameRegPattern = `^[\w\.\-\_]+$`
|
||||
|
||||
// internalCache is the memory cache for internal usage.
|
||||
internalCache = gcache.New()
|
||||
|
||||
// allDryRun sets dry-run feature for all database connections.
|
||||
// It is commonly used for command options for convenience.
|
||||
allDryRun = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
// allDryRun is initialized from environment or command options.
|
||||
allDryRun = gcmd.GetWithEnv("gf.gdb.dryrun", false).Bool()
|
||||
}
|
||||
|
||||
// Register registers custom database driver to gdb.
|
||||
func Register(name string, driver Driver) error {
|
||||
driverMap[name] = driver
|
||||
@ -269,30 +289,27 @@ 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]
|
||||
// which is DefaultGroupName in default.
|
||||
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()
|
||||
|
||||
if len(configs.config) < 1 {
|
||||
return nil, errors.New("empty database configuration")
|
||||
return nil, gerror.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,
|
||||
debug: gtype.NewBool(),
|
||||
cache: gcache.New(),
|
||||
schema: gtype.NewString(),
|
||||
dryrun: gtype.NewBool(),
|
||||
logger: glog.New(),
|
||||
prefix: node.Prefix,
|
||||
maxIdleConnCount: gDEFAULT_CONN_MAX_IDLE_COUNT,
|
||||
maxConnLifetime: gDEFAULT_CONN_MAX_LIFE_TIME, // Default max connection life time if user does not configure.
|
||||
group: groupName,
|
||||
debug: gtype.NewBool(),
|
||||
cache: gcache.New(),
|
||||
schema: gtype.NewString(),
|
||||
logger: glog.New(),
|
||||
config: node,
|
||||
}
|
||||
if v, ok := driverMap[node.Type]; ok {
|
||||
c.DB, err = v.New(c, node)
|
||||
@ -301,19 +318,19 @@ func New(name ...string) (db DB, err error) {
|
||||
}
|
||||
return c.DB, nil
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf(`unsupported database type "%s"`, node.Type))
|
||||
return nil, gerror.New(fmt.Sprintf(`unsupported database type "%s"`, node.Type))
|
||||
}
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf(`database configuration node "%s" is not found`, group))
|
||||
return nil, gerror.New(fmt.Sprintf(`database configuration node "%s" is not found`, groupName))
|
||||
}
|
||||
}
|
||||
|
||||
// Instance returns an instance for DB operations.
|
||||
// The parameter <name> specifies the configuration group name,
|
||||
// which is DEFAULT_GROUP_NAME in default.
|
||||
// which is DefaultGroupName in default.
|
||||
func Instance(name ...string) (db DB, err error) {
|
||||
group := configs.group
|
||||
if len(name) > 0 && name[0] != "" {
|
||||
@ -347,7 +364,7 @@ func getConfigNodeByGroup(group string, master bool) (*ConfigNode, error) {
|
||||
}
|
||||
}
|
||||
if len(masterList) < 1 {
|
||||
return nil, errors.New("at least one master node configuration's need to make sense")
|
||||
return nil, gerror.New("at least one master node configuration's need to make sense")
|
||||
}
|
||||
if len(slaveList) < 1 {
|
||||
slaveList = masterList
|
||||
@ -358,7 +375,7 @@ func getConfigNodeByGroup(group string, master bool) (*ConfigNode, error) {
|
||||
return getConfigNodeByWeight(slaveList), nil
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New(fmt.Sprintf("empty database configuration for item name '%s'", group))
|
||||
return nil, gerror.New(fmt.Sprintf("empty database configuration for item name '%s'", group))
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,30 +442,34 @@ 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, _ := internalCache.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)
|
||||
} else if node.MaxIdleConnCount > 0 {
|
||||
sqlDb.SetMaxIdleConns(node.MaxIdleConnCount)
|
||||
if c.config.MaxIdleConnCount > 0 {
|
||||
sqlDb.SetMaxIdleConns(c.config.MaxIdleConnCount)
|
||||
} else {
|
||||
sqlDb.SetMaxIdleConns(defaultMaxIdleConnCount)
|
||||
}
|
||||
|
||||
if c.maxOpenConnCount > 0 {
|
||||
sqlDb.SetMaxOpenConns(c.maxOpenConnCount)
|
||||
} else if node.MaxOpenConnCount > 0 {
|
||||
sqlDb.SetMaxOpenConns(node.MaxOpenConnCount)
|
||||
if c.config.MaxOpenConnCount > 0 {
|
||||
sqlDb.SetMaxOpenConns(c.config.MaxOpenConnCount)
|
||||
} else {
|
||||
sqlDb.SetMaxOpenConns(defaultMaxOpenConnCount)
|
||||
}
|
||||
|
||||
if c.maxConnLifetime > 0 {
|
||||
sqlDb.SetConnMaxLifetime(c.maxConnLifetime * time.Second)
|
||||
} else if node.MaxConnLifetime > 0 {
|
||||
sqlDb.SetConnMaxLifetime(node.MaxConnLifetime * time.Second)
|
||||
if c.config.MaxConnLifetime > 0 {
|
||||
// Automatically checks whether MaxConnLifetime is configured using string like: "30s", "60s", etc.
|
||||
// Or else it is configured just using number, which means value in seconds.
|
||||
if c.config.MaxConnLifetime > time.Second {
|
||||
sqlDb.SetConnMaxLifetime(c.config.MaxConnLifetime)
|
||||
} else {
|
||||
sqlDb.SetConnMaxLifetime(c.config.MaxConnLifetime * time.Second)
|
||||
}
|
||||
} else {
|
||||
sqlDb.SetConnMaxLifetime(defaultMaxConnLifeTime)
|
||||
}
|
||||
return sqlDb
|
||||
return sqlDb, nil
|
||||
}, 0)
|
||||
if v != nil && sqlDb == nil {
|
||||
sqlDb = v.(*sql.DB)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,19 +8,51 @@
|
||||
package gdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/internal/utils"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/internal/utils"
|
||||
|
||||
"github.com/gogf/gf/container/gvar"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
)
|
||||
|
||||
// Ctx is a chaining function, which creates and returns a new DB that is a shallow copy
|
||||
// of current DB object and with given context in it.
|
||||
// Note that this returned DB object can be used only once, so do not assign it to
|
||||
// a global or package variable for long using.
|
||||
func (c *Core) Ctx(ctx context.Context) DB {
|
||||
if ctx == nil {
|
||||
return c.DB
|
||||
}
|
||||
var (
|
||||
err error
|
||||
newCore = &Core{}
|
||||
configNode = c.DB.GetConfig()
|
||||
)
|
||||
*newCore = *c
|
||||
newCore.ctx = ctx
|
||||
newCore.DB, err = driverMap[configNode.Type].New(newCore, configNode)
|
||||
// Seldom error, just log it.
|
||||
if err != nil {
|
||||
c.DB.GetLogger().Ctx(ctx).Error(err)
|
||||
}
|
||||
return newCore.DB
|
||||
}
|
||||
|
||||
// GetCtx returns the context for current DB.
|
||||
// Note that it might be nil.
|
||||
func (c *Core) GetCtx() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
// Master creates and returns a connection from master node if master-slave configured.
|
||||
// It returns the default connection if master-slave not configured.
|
||||
func (c *Core) Master() (*sql.DB, error) {
|
||||
@ -59,6 +91,7 @@ func (c *Core) DoQuery(link Link, sql string, args ...interface{}) (rows *sql.Ro
|
||||
Error: err,
|
||||
Start: mTime1,
|
||||
End: mTime2,
|
||||
Group: c.DB.GetGroup(),
|
||||
}
|
||||
c.writeSqlToLogger(s)
|
||||
} else {
|
||||
@ -102,6 +135,7 @@ func (c *Core) DoExec(link Link, sql string, args ...interface{}) (result sql.Re
|
||||
Error: err,
|
||||
Start: mTime1,
|
||||
End: mTime2,
|
||||
Group: c.DB.GetGroup(),
|
||||
}
|
||||
c.writeSqlToLogger(s)
|
||||
} else {
|
||||
@ -160,7 +194,7 @@ func (c *Core) DoGetAll(link Link, sql string, args ...interface{}) (result Resu
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return c.DB.rowsToResult(rows)
|
||||
return c.DB.convertRowsToResult(rows)
|
||||
}
|
||||
|
||||
// GetOne queries and returns one record from database.
|
||||
@ -307,6 +341,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
|
||||
@ -331,7 +370,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.
|
||||
@ -344,7 +386,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.
|
||||
@ -360,7 +405,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.
|
||||
@ -375,11 +423,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"})
|
||||
@ -407,13 +458,19 @@ 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))
|
||||
return result, gerror.New(fmt.Sprint("unsupported data type:", reflectKind))
|
||||
}
|
||||
if len(dataMap) == 0 {
|
||||
return nil, errors.New("data cannot be empty")
|
||||
return nil, gerror.New("data cannot be empty")
|
||||
}
|
||||
var (
|
||||
charL, charR = c.DB.GetChars()
|
||||
@ -422,14 +479,18 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
|
||||
)
|
||||
for k, v := range dataMap {
|
||||
fields = append(fields, charL+k+charR)
|
||||
values = append(values, "?")
|
||||
params = append(params, v)
|
||||
if s, ok := v.(Raw); ok {
|
||||
values = append(values, gconv.String(s))
|
||||
} else {
|
||||
values = append(values, "?")
|
||||
params = append(params, v)
|
||||
}
|
||||
}
|
||||
if option == gINSERT_OPTION_SAVE {
|
||||
if option == insertOptionSave {
|
||||
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 {
|
||||
@ -462,45 +523,58 @@ 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 v := list.(type) {
|
||||
switch value := list.(type) {
|
||||
case Result:
|
||||
listMap = v.List()
|
||||
listMap = value.List()
|
||||
case Record:
|
||||
listMap = List{v.Map()}
|
||||
listMap = List{value.Map()}
|
||||
case List:
|
||||
listMap = v
|
||||
listMap = value
|
||||
case Map:
|
||||
listMap = List{v}
|
||||
listMap = List{value}
|
||||
default:
|
||||
var (
|
||||
rv = reflect.ValueOf(list)
|
||||
@ -515,16 +589,29 @@ 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{ConvertDataForTableRecord(value)}
|
||||
case reflect.Struct:
|
||||
if v, ok := value.(apiInterfaces); ok {
|
||||
var (
|
||||
array = v.Interfaces()
|
||||
list = make(List, len(array))
|
||||
)
|
||||
for i := 0; i < len(array); i++ {
|
||||
list[i] = ConvertDataForTableRecord(array[i])
|
||||
}
|
||||
listMap = list
|
||||
} else {
|
||||
listMap = List{ConvertDataForTableRecord(value)}
|
||||
}
|
||||
case reflect.Map, reflect.Struct:
|
||||
listMap = List{DataToMapDeep(v)}
|
||||
default:
|
||||
return result, errors.New(fmt.Sprint("unsupported list type:", kind))
|
||||
return result, gerror.New(fmt.Sprint("unsupported list type:", kind))
|
||||
}
|
||||
}
|
||||
if len(listMap) < 1 {
|
||||
return result, errors.New("data list cannot be empty")
|
||||
return result, gerror.New("data list cannot be empty")
|
||||
}
|
||||
if link == nil {
|
||||
if link, err = c.DB.Master(); err != nil {
|
||||
@ -532,25 +619,22 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
|
||||
}
|
||||
}
|
||||
// Handle the field names and place holders.
|
||||
holders := []string(nil)
|
||||
for k, _ := range listMap[0] {
|
||||
keys = append(keys, k)
|
||||
holders = append(holders, "?")
|
||||
}
|
||||
// Prepare the batch result pointer.
|
||||
var (
|
||||
charL, charR = c.DB.GetChars()
|
||||
batchResult = new(SqlResult)
|
||||
keysStr = charL + strings.Join(keys, charR+","+charL) + charR
|
||||
valueHolderStr = "(" + strings.Join(holders, ",") + ")"
|
||||
operation = GetInsertOperationByOption(option)
|
||||
updateStr = ""
|
||||
charL, charR = c.DB.GetChars()
|
||||
batchResult = new(SqlResult)
|
||||
keysStr = charL + strings.Join(keys, charR+","+charL) + charR
|
||||
operation = GetInsertOperationByOption(option)
|
||||
updateStr = ""
|
||||
)
|
||||
if option == gINSERT_OPTION_SAVE {
|
||||
if option == insertOptionSave {
|
||||
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 {
|
||||
@ -564,27 +648,34 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
|
||||
}
|
||||
updateStr = fmt.Sprintf("ON DUPLICATE KEY UPDATE %s", updateStr)
|
||||
}
|
||||
batchNum := gDEFAULT_BATCH_NUM
|
||||
batchNum := defaultBatchNumber
|
||||
if len(batch) > 0 && batch[0] > 0 {
|
||||
batchNum = batch[0]
|
||||
}
|
||||
listMapLen := len(listMap)
|
||||
var (
|
||||
listMapLen = len(listMap)
|
||||
valueHolder = make([]string, 0)
|
||||
)
|
||||
for i := 0; i < listMapLen; i++ {
|
||||
values = values[:0]
|
||||
// Note that the map type is unordered,
|
||||
// so it should use slice+key to retrieve the value.
|
||||
for _, k := range keys {
|
||||
params = append(params, listMap[i][k])
|
||||
if s, ok := listMap[i][k].(Raw); ok {
|
||||
values = append(values, gconv.String(s))
|
||||
} else {
|
||||
values = append(values, "?")
|
||||
params = append(params, listMap[i][k])
|
||||
}
|
||||
}
|
||||
values = append(values, valueHolderStr)
|
||||
valueHolder = append(valueHolder, "("+gstr.Join(values, ",")+")")
|
||||
if len(values) == batchNum || (i == listMapLen-1 && len(values) > 0) {
|
||||
r, err := c.DB.DoExec(
|
||||
link,
|
||||
fmt.Sprintf(
|
||||
"%s INTO %s(%s) VALUES%s %s",
|
||||
operation,
|
||||
table,
|
||||
keysStr,
|
||||
strings.Join(values, ","),
|
||||
operation, table, keysStr,
|
||||
gstr.Join(valueHolder, ","),
|
||||
updateStr,
|
||||
),
|
||||
params...,
|
||||
@ -599,7 +690,7 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
|
||||
batchResult.affected += n
|
||||
}
|
||||
params = params[:0]
|
||||
values = values[:0]
|
||||
valueHolder = valueHolder[:0]
|
||||
}
|
||||
}
|
||||
return batchResult, nil
|
||||
@ -620,15 +711,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 (
|
||||
@ -647,18 +734,38 @@ 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)+"=?")
|
||||
params = append(params, v)
|
||||
switch value := v.(type) {
|
||||
case *Counter:
|
||||
if value.Value != 0 {
|
||||
column := c.DB.QuoteWord(value.Field)
|
||||
fields = append(fields, fmt.Sprintf("%s=%s+?", column, column))
|
||||
params = append(params, value.Value)
|
||||
}
|
||||
case Counter:
|
||||
if value.Value != 0 {
|
||||
column := c.DB.QuoteWord(value.Field)
|
||||
fields = append(fields, fmt.Sprintf("%s=%s+?", column, column))
|
||||
params = append(params, value.Value)
|
||||
}
|
||||
default:
|
||||
if s, ok := v.(Raw); ok {
|
||||
fields = append(fields, c.DB.QuoteWord(k)+"="+gconv.String(s))
|
||||
} else {
|
||||
fields = append(fields, c.DB.QuoteWord(k)+"=?")
|
||||
params = append(params, v)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
updates = strings.Join(fields, ",")
|
||||
default:
|
||||
updates = gconv.String(data)
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return nil, errors.New("data cannot be empty")
|
||||
return nil, gerror.New("data cannot be empty")
|
||||
}
|
||||
if len(params) > 0 {
|
||||
args = append(params, args...)
|
||||
@ -688,15 +795,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 {
|
||||
@ -707,8 +810,8 @@ func (c *Core) DoDelete(link Link, table string, condition string, args ...inter
|
||||
return c.DB.DoExec(link, fmt.Sprintf("DELETE FROM %s%s", table, condition), args...)
|
||||
}
|
||||
|
||||
// rowsToResult converts underlying data record type sql.Rows to Result type.
|
||||
func (c *Core) rowsToResult(rows *sql.Rows) (Result, error) {
|
||||
// convertRowsToResult converts underlying data record type sql.Rows to Result type.
|
||||
func (c *Core) convertRowsToResult(rows *sql.Rows) (Result, error) {
|
||||
if !rows.Next() {
|
||||
return nil, nil
|
||||
}
|
||||
@ -724,7 +827,7 @@ func (c *Core) rowsToResult(rows *sql.Rows) (Result, error) {
|
||||
columnNames[k] = v.Name()
|
||||
}
|
||||
var (
|
||||
values = make([]sql.RawBytes, len(columnNames))
|
||||
values = make([]interface{}, len(columnNames))
|
||||
records = make(Result, 0)
|
||||
scanArgs = make([]interface{}, len(values))
|
||||
)
|
||||
@ -735,19 +838,12 @@ func (c *Core) rowsToResult(rows *sql.Rows) (Result, error) {
|
||||
if err := rows.Scan(scanArgs...); err != nil {
|
||||
return records, err
|
||||
}
|
||||
// Creates a new row object.
|
||||
row := make(Record)
|
||||
// Note that the internal looping variable <value> is type of []byte,
|
||||
// which points to the same memory address. So it should do a copy.
|
||||
for i, value := range values {
|
||||
if value == nil {
|
||||
row[columnNames[i]] = gvar.New(nil)
|
||||
} else {
|
||||
// As sql.RawBytes is type of slice,
|
||||
// it should do a copy of it.
|
||||
v := make([]byte, len(value))
|
||||
copy(v, value)
|
||||
row[columnNames[i]] = gvar.New(c.DB.convertValue(v, columnTypes[i]))
|
||||
row[columnNames[i]] = gvar.New(c.DB.convertFieldValueToLocalValue(value, columnTypes[i]))
|
||||
}
|
||||
}
|
||||
records = append(records, row)
|
||||
@ -768,13 +864,46 @@ func (c *Core) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
|
||||
// writeSqlToLogger outputs the sql object to logger.
|
||||
// It is enabled when configuration "debug" is true.
|
||||
// It is enabled only if configuration "debug" is true.
|
||||
func (c *Core) writeSqlToLogger(v *Sql) {
|
||||
s := fmt.Sprintf("[%3d ms] %s", v.End-v.Start, v.Format)
|
||||
s := fmt.Sprintf("[%3d ms] [%s] %s", v.End-v.Start, v.Group, v.Format)
|
||||
if v.Error != nil {
|
||||
s += "\nError: " + v.Error.Error()
|
||||
c.logger.Error(s)
|
||||
c.logger.Ctx(c.DB.GetCtx()).Error(s)
|
||||
} else {
|
||||
c.logger.Debug(s)
|
||||
c.logger.Ctx(c.DB.GetCtx()).Debug(s)
|
||||
}
|
||||
}
|
||||
|
||||
// HasTable determine whether the table name exists in the database.
|
||||
func (c *Core) HasTable(name string) (bool, error) {
|
||||
tableList, err := c.DB.Tables()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, table := range tableList {
|
||||
if table == name {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -16,7 +16,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_GROUP_NAME = "default" // Default group name.
|
||||
DEFAULT_GROUP_NAME = "default" // Deprecated, use DefaultGroupName instead.
|
||||
DefaultGroupName = "default" // Default group name.
|
||||
)
|
||||
|
||||
// Config is the configuration management object.
|
||||
@ -27,22 +28,26 @@ type ConfigGroup []ConfigNode
|
||||
|
||||
// ConfigNode is configuration for one node.
|
||||
type ConfigNode struct {
|
||||
Host string // Host of server, ip or domain like: 127.0.0.1, localhost
|
||||
Port string // Port, it's commonly 3306.
|
||||
User string // Authentication username.
|
||||
Pass string // Authentication password.
|
||||
Name string // Default used database name.
|
||||
Type string // Database type: mysql, sqlite, mssql, pgsql, oracle.
|
||||
Role string // (Optional, "master" in default) Node role, used for master-slave mode: master, slave.
|
||||
Debug bool // (Optional) Debug mode enables debug information logging and output.
|
||||
Prefix string // (Optional) Table prefix.
|
||||
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.
|
||||
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.
|
||||
MaxConnLifetime time.Duration `json:"maxlifetime"` // (Optional) Max connection TTL configuration for underlying connection pool.
|
||||
Host string // Host of server, ip or domain like: 127.0.0.1, localhost
|
||||
Port string // Port, it's commonly 3306.
|
||||
User string // Authentication username.
|
||||
Pass string // Authentication password.
|
||||
Name string // Default used database name.
|
||||
Type string // Database type: mysql, sqlite, mssql, pgsql, oracle.
|
||||
Role string // (Optional, "master" in default) Node role, used for master-slave mode: master, slave.
|
||||
Debug bool // (Optional) Debug mode enables debug information logging and output.
|
||||
Prefix string // (Optional) Table prefix.
|
||||
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.
|
||||
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.
|
||||
MaxConnLifetime time.Duration `json:"maxlifetime"` // (Optional) Max connection TTL configuration for underlying connection pool.
|
||||
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.
|
||||
TimeMaintainDisabled bool // (Optional) Disable the automatic time maintaining feature.
|
||||
}
|
||||
|
||||
// configs is internal used configuration object.
|
||||
@ -54,7 +59,7 @@ var configs struct {
|
||||
|
||||
func init() {
|
||||
configs.config = make(Config)
|
||||
configs.group = DEFAULT_GROUP_NAME
|
||||
configs.group = DefaultGroupName
|
||||
}
|
||||
|
||||
// SetConfig sets the global configuration for package.
|
||||
@ -84,12 +89,12 @@ func AddConfigNode(group string, node ConfigNode) {
|
||||
|
||||
// AddDefaultConfigNode adds one node configuration to configuration of default group.
|
||||
func AddDefaultConfigNode(node ConfigNode) {
|
||||
AddConfigNode(DEFAULT_GROUP_NAME, node)
|
||||
AddConfigNode(DefaultGroupName, node)
|
||||
}
|
||||
|
||||
// AddDefaultConfigGroup adds multiple node configurations to configuration of default group.
|
||||
func AddDefaultConfigGroup(nodes ConfigGroup) {
|
||||
SetConfigGroup(DEFAULT_GROUP_NAME, nodes)
|
||||
SetConfigGroup(DefaultGroupName, nodes)
|
||||
}
|
||||
|
||||
// GetConfig retrieves and returns the configuration of given group.
|
||||
@ -115,6 +120,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
|
||||
@ -127,18 +140,18 @@ func (c *Core) GetLogger() *glog.Logger {
|
||||
|
||||
// SetMaxIdleConnCount sets the max idle connection count for underlying connection pool.
|
||||
func (c *Core) SetMaxIdleConnCount(n int) {
|
||||
c.maxIdleConnCount = n
|
||||
c.config.MaxIdleConnCount = n
|
||||
}
|
||||
|
||||
// SetMaxOpenConnCount sets the max open connection count for underlying connection pool.
|
||||
func (c *Core) SetMaxOpenConnCount(n int) {
|
||||
c.maxOpenConnCount = n
|
||||
c.config.MaxOpenConnCount = n
|
||||
}
|
||||
|
||||
// SetMaxConnLifetime sets the connection TTL for underlying connection pool.
|
||||
// If parameter <d> <= 0, it means the connection never expires.
|
||||
func (c *Core) SetMaxConnLifetime(d time.Duration) {
|
||||
c.maxConnLifetime = d
|
||||
c.config.MaxConnLifetime = d
|
||||
}
|
||||
|
||||
// String returns the node as string.
|
||||
@ -154,6 +167,11 @@ func (node *ConfigNode) String() string {
|
||||
)
|
||||
}
|
||||
|
||||
// GetConfig returns the current used node configuration.
|
||||
func (c *Core) GetConfig() *ConfigNode {
|
||||
return c.config
|
||||
}
|
||||
|
||||
// SetDebug enables/disables the debug mode.
|
||||
func (c *Core) SetDebug(debug bool) {
|
||||
c.debug.Set(debug)
|
||||
@ -169,24 +187,27 @@ func (c *Core) GetCache() *gcache.Cache {
|
||||
return c.cache
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix string configured.
|
||||
func (c *Core) GetPrefix() string {
|
||||
return c.prefix
|
||||
}
|
||||
|
||||
// GetGroup returns the group string configured.
|
||||
func (c *Core) GetGroup() string {
|
||||
return c.group
|
||||
}
|
||||
|
||||
// SetDryRun enables/disables the DryRun feature.
|
||||
func (c *Core) SetDryRun(dryrun bool) {
|
||||
c.dryrun.Set(dryrun)
|
||||
// Deprecated, use GetConfig instead.
|
||||
func (c *Core) SetDryRun(enabled bool) {
|
||||
c.config.DryRun = enabled
|
||||
}
|
||||
|
||||
// GetDryRun returns the DryRun value.
|
||||
// Deprecated, use GetConfig instead.
|
||||
func (c *Core) GetDryRun() bool {
|
||||
return c.dryrun.Val()
|
||||
return c.config.DryRun
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix string configured.
|
||||
// Deprecated, use GetConfig instead.
|
||||
func (c *Core) GetPrefix() string {
|
||||
return c.config.Prefix
|
||||
}
|
||||
|
||||
// SetSchema changes the schema for this database connection object.
|
||||
|
||||
197
database/gdb/gdb_core_structure.go
Normal file
197
database/gdb/gdb_core_structure.go
Normal file
@ -0,0 +1,197 @@
|
||||
// Copyright GoFrame 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/errors/gerror"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
|
||||
"github.com/gogf/gf/encoding/gbinary"
|
||||
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
)
|
||||
|
||||
// convertFieldValueToLocalValue automatically checks and converts field value from database type
|
||||
// to golang variable type.
|
||||
func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType string) interface{} {
|
||||
// If there's no type retrieved, it returns the <fieldValue> directly
|
||||
// to use its original data type, as <fieldValue> is type of interface{}.
|
||||
if fieldType == "" {
|
||||
return fieldValue
|
||||
}
|
||||
t, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
|
||||
t = strings.ToLower(t)
|
||||
switch t {
|
||||
case
|
||||
"binary",
|
||||
"varbinary",
|
||||
"blob",
|
||||
"tinyblob",
|
||||
"mediumblob",
|
||||
"longblob":
|
||||
return gconv.Bytes(fieldValue)
|
||||
|
||||
case
|
||||
"int",
|
||||
"tinyint",
|
||||
"small_int",
|
||||
"smallint",
|
||||
"medium_int",
|
||||
"mediumint",
|
||||
"serial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
gconv.Uint(gconv.String(fieldValue))
|
||||
}
|
||||
return gconv.Int(gconv.String(fieldValue))
|
||||
|
||||
case
|
||||
"big_int",
|
||||
"bigint",
|
||||
"bigserial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
gconv.Uint64(gconv.String(fieldValue))
|
||||
}
|
||||
return gconv.Int64(gconv.String(fieldValue))
|
||||
|
||||
case "real":
|
||||
return gconv.Float32(gconv.String(fieldValue))
|
||||
|
||||
case
|
||||
"float",
|
||||
"double",
|
||||
"decimal",
|
||||
"money",
|
||||
"numeric",
|
||||
"smallmoney":
|
||||
return gconv.Float64(gconv.String(fieldValue))
|
||||
|
||||
case "bit":
|
||||
s := gconv.String(fieldValue)
|
||||
// mssql is true|false string.
|
||||
if strings.EqualFold(s, "true") {
|
||||
return 1
|
||||
}
|
||||
if strings.EqualFold(s, "false") {
|
||||
return 0
|
||||
}
|
||||
return gbinary.BeDecodeToInt64(gconv.Bytes(fieldValue))
|
||||
|
||||
case "bool":
|
||||
return gconv.Bool(fieldValue)
|
||||
|
||||
case "date":
|
||||
if t, ok := fieldValue.(time.Time); ok {
|
||||
return gtime.NewFromTime(t).Format("Y-m-d")
|
||||
}
|
||||
t, _ := gtime.StrToTime(gconv.String(fieldValue))
|
||||
return t.Format("Y-m-d")
|
||||
|
||||
case
|
||||
"datetime",
|
||||
"timestamp":
|
||||
if t, ok := fieldValue.(time.Time); ok {
|
||||
return gtime.NewFromTime(t)
|
||||
}
|
||||
t, _ := gtime.StrToTime(gconv.String(fieldValue))
|
||||
return t.String()
|
||||
|
||||
default:
|
||||
// Auto detect field type, using key match.
|
||||
switch {
|
||||
case strings.Contains(t, "text") || strings.Contains(t, "char") || strings.Contains(t, "character"):
|
||||
return gconv.String(fieldValue)
|
||||
|
||||
case strings.Contains(t, "float") || strings.Contains(t, "double") || strings.Contains(t, "numeric"):
|
||||
return gconv.Float64(gconv.String(fieldValue))
|
||||
|
||||
case strings.Contains(t, "bool"):
|
||||
return gconv.Bool(gconv.String(fieldValue))
|
||||
|
||||
case strings.Contains(t, "binary") || strings.Contains(t, "blob"):
|
||||
return fieldValue
|
||||
|
||||
case strings.Contains(t, "int"):
|
||||
return gconv.Int(gconv.String(fieldValue))
|
||||
|
||||
case strings.Contains(t, "time"):
|
||||
s := gconv.String(fieldValue)
|
||||
t, err := gtime.StrToTime(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return t.String()
|
||||
|
||||
case strings.Contains(t, "date"):
|
||||
s := gconv.String(fieldValue)
|
||||
t, err := gtime.StrToTime(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return t.Format("Y-m-d")
|
||||
|
||||
default:
|
||||
return gconv.String(fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mappingAndFilterData automatically mappings the map key to table field and removes
|
||||
// all key-value pairs that are not the field of given table.
|
||||
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 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
|
||||
//}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -13,8 +13,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@ -64,10 +64,10 @@ func (d *DriverMssql) GetChars() (charLeft string, charRight string) {
|
||||
// HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver.
|
||||
func (d *DriverMssql) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) {
|
||||
var index int
|
||||
// Convert place holder char '?' to string "@vx".
|
||||
// Convert place holder char '?' to string "@px".
|
||||
str, _ := gregex.ReplaceStringFunc("\\?", sql, func(s string) string {
|
||||
index++
|
||||
return fmt.Sprintf("@v%d", index)
|
||||
return fmt.Sprintf("@p%d", index)
|
||||
})
|
||||
str, _ = gregex.ReplaceString("\"", "", str)
|
||||
return d.parseSql(str), args
|
||||
@ -192,53 +192,64 @@ func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[st
|
||||
charL, charR := d.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return nil, errors.New("function TableFields supports only single table operations")
|
||||
return nil, gerror.New("function TableFields supports only single table operations")
|
||||
}
|
||||
checkSchema := d.DB.GetSchema()
|
||||
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, _ := internalCache.GetOrSetFunc(
|
||||
fmt.Sprintf(`mssql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()),
|
||||
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 +257,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)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,8 +8,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
@ -31,6 +31,7 @@ func (d *DriverMysql) New(core *Core, node *ConfigNode) (DB, error) {
|
||||
}
|
||||
|
||||
// Open creates and returns a underlying sql.DB object for mysql.
|
||||
// Note that it converts time.Time argument to local timezone in default.
|
||||
func (d *DriverMysql) Open(config *ConfigNode) (*sql.DB, error) {
|
||||
var source string
|
||||
if config.LinkInfo != "" {
|
||||
@ -41,7 +42,7 @@ func (d *DriverMysql) Open(config *ConfigNode) (*sql.DB, error) {
|
||||
}
|
||||
} else {
|
||||
source = fmt.Sprintf(
|
||||
"%s:%s@tcp(%s:%s)/%s?charset=%s&multiStatements=true&parseTime=true&loc=Local",
|
||||
"%s:%s@tcp(%s:%s)/%s?charset=%s&multiStatements=true&parseTime=true",
|
||||
config.User, config.Pass, config.Host, config.Port, config.Name, config.Charset,
|
||||
)
|
||||
}
|
||||
@ -96,27 +97,29 @@ func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[st
|
||||
charL, charR := d.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return nil, errors.New("function TableFields supports only single table operations")
|
||||
return nil, gerror.New("function TableFields supports only single table operations")
|
||||
}
|
||||
checkSchema := d.schema.Val()
|
||||
if len(schema) > 0 && schema[0] != "" {
|
||||
checkSchema = schema[0]
|
||||
}
|
||||
v := d.cache.GetOrSetFunc(
|
||||
fmt.Sprintf(`mysql_table_fields_%s_%s`, table, checkSchema),
|
||||
func() interface{} {
|
||||
var result Result
|
||||
var link *sql.DB
|
||||
v, _ := internalCache.GetOrSetFunc(
|
||||
fmt.Sprintf(`mysql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()),
|
||||
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)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -13,10 +13,11 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -72,50 +73,60 @@ func (d *DriverOracle) HandleSqlBeforeCommit(link Link, sql string, args []inter
|
||||
return fmt.Sprintf(":v%d", index)
|
||||
})
|
||||
str, _ = gregex.ReplaceString("\"", "", str)
|
||||
// Change time string argument wrapping with TO_DATE function.
|
||||
for i, v := range args {
|
||||
if reflect.TypeOf(v).Kind() == reflect.String {
|
||||
valueStr := gconv.String(v)
|
||||
if gregex.IsMatchString(`^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$`, valueStr) {
|
||||
args[i] = fmt.Sprintf(`TO_DATE('%s','yyyy-MM-dd HH:MI:SS')`, valueStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d.parseSql(str), args
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@ -151,24 +162,30 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
|
||||
charL, charR := d.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return nil, errors.New("function TableFields supports only single table operations")
|
||||
return nil, gerror.New("function TableFields supports only single table operations")
|
||||
}
|
||||
checkSchema := d.DB.GetSchema()
|
||||
if len(schema) > 0 && schema[0] != "" {
|
||||
checkSchema = schema[0]
|
||||
}
|
||||
v := d.DB.GetCache().GetOrSetFunc(
|
||||
fmt.Sprintf(`oracle_table_fields_%s_%s`, table, checkSchema),
|
||||
func() interface{} {
|
||||
v, _ := internalCache.GetOrSetFunc(
|
||||
fmt.Sprintf(`oracle_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()),
|
||||
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 +195,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 +205,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, _ := internalCache.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,15 +250,15 @@ 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))
|
||||
return result, gerror.New(fmt.Sprint("unsupported data type:", kind))
|
||||
}
|
||||
|
||||
indexs := make([]string, 0)
|
||||
indexMap := make(map[string]string)
|
||||
indexExists := false
|
||||
if option != gINSERT_OPTION_DEFAULT {
|
||||
if option != insertOptionDefault {
|
||||
index, err := d.getTableUniqueIndex(table)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -267,7 +286,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio
|
||||
k = strings.ToUpper(k)
|
||||
|
||||
// 操作类型为REPLACE/SAVE时且存在唯一索引才使用merge,否则使用insert
|
||||
if (option == gINSERT_OPTION_REPLACE || option == gINSERT_OPTION_SAVE) && indexExists {
|
||||
if (option == insertOptionReplace || option == insertOptionSave) && indexExists {
|
||||
fields = append(fields, tableAlias1+"."+charL+k+charR)
|
||||
values = append(values, tableAlias2+"."+charL+k+charR)
|
||||
params = append(params, v)
|
||||
@ -293,18 +312,18 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio
|
||||
}
|
||||
}
|
||||
|
||||
if indexExists && option != gINSERT_OPTION_DEFAULT {
|
||||
if indexExists && option != insertOptionDefault {
|
||||
switch option {
|
||||
case gINSERT_OPTION_REPLACE:
|
||||
case insertOptionReplace:
|
||||
fallthrough
|
||||
case gINSERT_OPTION_SAVE:
|
||||
case insertOptionSave:
|
||||
tmp := fmt.Sprintf(
|
||||
"MERGE INTO %s %s USING(SELECT %s FROM DUAL) %s ON(%s) WHEN MATCHED THEN UPDATE SET %s WHEN NOT MATCHED THEN INSERT (%s) VALUES(%s)",
|
||||
table, tableAlias1, strings.Join(subSqlStr, ","), tableAlias2,
|
||||
strings.Join(onStr, "AND"), strings.Join(updateStr, ","), strings.Join(fields, ","), strings.Join(values, ","),
|
||||
)
|
||||
return d.DB.DoExec(link, tmp, params...)
|
||||
case gINSERT_OPTION_IGNORE:
|
||||
case insertOptionIgnore:
|
||||
return d.DB.DoExec(link,
|
||||
fmt.Sprintf(
|
||||
"INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(%s(%s)) */ INTO %s(%s) VALUES(%s)",
|
||||
@ -351,19 +370,19 @@ 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))
|
||||
return result, gerror.New(fmt.Sprint("unsupported list type:", kind))
|
||||
}
|
||||
}
|
||||
// 判断长度
|
||||
if len(listMap) < 1 {
|
||||
return result, errors.New("empty data list")
|
||||
return result, gerror.New("empty data list")
|
||||
}
|
||||
if link == nil {
|
||||
if link, err = d.DB.Master(); err != nil {
|
||||
@ -382,7 +401,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{},
|
||||
valueHolderStr := strings.Join(holders, ",")
|
||||
|
||||
// 当操作类型非insert时调用单笔的insert功能
|
||||
if option != gINSERT_OPTION_DEFAULT {
|
||||
if option != insertOptionDefault {
|
||||
for _, v := range listMap {
|
||||
r, err := d.DB.DoInsert(link, table, v, option, 1)
|
||||
if err != nil {
|
||||
@ -400,7 +419,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{},
|
||||
}
|
||||
|
||||
// 构造批量写入数据格式(注意map的遍历是无序的)
|
||||
batchNum := gDEFAULT_BATCH_NUM
|
||||
batchNum := defaultBatchNumber
|
||||
if len(batch) > 0 {
|
||||
batchNum = batch[0]
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -13,8 +13,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"strings"
|
||||
@ -62,10 +62,10 @@ func (d *DriverPgsql) GetChars() (charLeft string, charRight string) {
|
||||
// HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver.
|
||||
func (d *DriverPgsql) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) {
|
||||
var index int
|
||||
// Convert place holder char '?' to string "$vx".
|
||||
// Convert place holder char '?' to string "$x".
|
||||
sql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string {
|
||||
index++
|
||||
return fmt.Sprintf("$v%d", index)
|
||||
return fmt.Sprintf("$%d", index)
|
||||
})
|
||||
sql, _ = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, sql)
|
||||
return sql, args
|
||||
@ -100,28 +100,35 @@ func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[st
|
||||
charL, charR := d.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return nil, errors.New("function TableFields supports only single table operations")
|
||||
return nil, gerror.New("function TableFields supports only single table operations")
|
||||
}
|
||||
table, _ = gregex.ReplaceString("\"", "", table)
|
||||
checkSchema := d.DB.GetSchema()
|
||||
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, _ := internalCache.GetOrSetFunc(
|
||||
fmt.Sprintf(`pgsql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()),
|
||||
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 +139,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)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -12,8 +12,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/internal/intlog"
|
||||
"github.com/gogf/gf/os/gfile"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
@ -36,15 +36,14 @@ func (d *DriverSqlite) New(core *Core, node *ConfigNode) (DB, error) {
|
||||
// Open creates and returns a underlying sql.DB object for sqlite.
|
||||
func (d *DriverSqlite) Open(config *ConfigNode) (*sql.DB, error) {
|
||||
var source string
|
||||
var err error
|
||||
if config.LinkInfo != "" {
|
||||
source = config.LinkInfo
|
||||
} else {
|
||||
source = config.Name
|
||||
}
|
||||
source, err = gfile.Search(source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// It searches the source file to locate its absolute path..
|
||||
if absolutePath, _ := gfile.Search(source); absolutePath != "" {
|
||||
source = absolutePath
|
||||
}
|
||||
intlog.Printf("Open: %s", source)
|
||||
if db, err := sql.Open("sqlite3", source); err == nil {
|
||||
@ -92,23 +91,26 @@ func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[s
|
||||
charL, charR := d.GetChars()
|
||||
table = gstr.Trim(table, charL+charR)
|
||||
if gstr.Contains(table, " ") {
|
||||
return nil, errors.New("function TableFields supports only single table operations")
|
||||
return nil, gerror.New("function TableFields supports only single table operations")
|
||||
}
|
||||
checkSchema := d.DB.GetSchema()
|
||||
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, _ := internalCache.GetOrSetFunc(
|
||||
fmt.Sprintf(`sqlite_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()),
|
||||
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 +120,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)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,9 +8,10 @@ package gdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"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"
|
||||
@ -55,21 +56,41 @@ const (
|
||||
var (
|
||||
// quoteWordReg is the regular expression object for a word check.
|
||||
quoteWordReg = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
|
||||
|
||||
// Priority tags for struct converting for orm field mapping.
|
||||
structTagPriority = append([]string{ORM_TAG_FOR_STRUCT}, gconv.StructTagPriority...)
|
||||
)
|
||||
|
||||
// ListItemValues is alias for gutil.ListItemValues.
|
||||
// ListItemValues retrieves and returns the elements of all item struct/map with key <key>.
|
||||
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
|
||||
// or else it returns an empty slice.
|
||||
//
|
||||
// The parameter <list> supports types like:
|
||||
// []map[string]interface{}
|
||||
// []map[string]sub-map
|
||||
// []struct
|
||||
// []struct:sub-struct
|
||||
// Note that the sub-map/sub-struct makes sense only if the optional parameter <subKey> is given.
|
||||
// See gutil.ListItemValues.
|
||||
func ListItemValues(list interface{}, key interface{}, subKey ...interface{}) (values []interface{}) {
|
||||
return gutil.ListItemValues(list, key, subKey...)
|
||||
}
|
||||
|
||||
// ListItemValuesUnique retrieves and returns the unique elements of all struct/map with key <key>.
|
||||
// Note that the parameter <list> should be type of slice which contains elements of map or struct,
|
||||
// or else it returns an empty slice.
|
||||
// See gutil.ListItemValuesUnique.
|
||||
func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) []interface{} {
|
||||
return gutil.ListItemValuesUnique(list, key, subKey...)
|
||||
}
|
||||
|
||||
// GetInsertOperationByOption returns proper insert option with given parameter <option>.
|
||||
func GetInsertOperationByOption(option int) string {
|
||||
var operator string
|
||||
switch option {
|
||||
case gINSERT_OPTION_REPLACE:
|
||||
case insertOptionReplace:
|
||||
operator = "REPLACE"
|
||||
case gINSERT_OPTION_IGNORE:
|
||||
case insertOptionIgnore:
|
||||
operator = "INSERT IGNORE"
|
||||
default:
|
||||
operator = "INSERT"
|
||||
@ -77,34 +98,144 @@ func GetInsertOperationByOption(option int) string {
|
||||
return operator
|
||||
}
|
||||
|
||||
// DataToMapDeep converts struct object to map type recursively.
|
||||
func DataToMapDeep(obj interface{}) map[string]interface{} {
|
||||
data := gconv.Map(obj, ORM_TAG_FOR_STRUCT)
|
||||
for key, value := range data {
|
||||
rv := reflect.ValueOf(value)
|
||||
kind := rv.Kind()
|
||||
if kind == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
kind = rv.Kind()
|
||||
// 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 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:
|
||||
// The underlying driver supports time.Time/*time.Time types.
|
||||
if _, ok := value.(time.Time); ok {
|
||||
switch v.(type) {
|
||||
case time.Time, *time.Time, gtime.Time, *gtime.Time:
|
||||
continue
|
||||
}
|
||||
if _, ok := value.(*time.Time); ok {
|
||||
case Counter, *Counter:
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Use string conversion in default.
|
||||
if s, ok := value.(apiString); ok {
|
||||
data[key] = s.String()
|
||||
continue
|
||||
}
|
||||
delete(data, key)
|
||||
for k, v := range DataToMapDeep(value) {
|
||||
}
|
||||
}
|
||||
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
|
||||
rvKind reflect.Kind
|
||||
rtField reflect.StructField
|
||||
)
|
||||
if v, ok := value.(reflect.Value); ok {
|
||||
rvValue = v
|
||||
} else {
|
||||
rvValue = reflect.ValueOf(value)
|
||||
}
|
||||
rvKind = rvValue.Kind()
|
||||
if rvKind == reflect.Ptr {
|
||||
rvValue = rvValue.Elem()
|
||||
rvKind = rvValue.Kind()
|
||||
}
|
||||
// If given <value> is not a struct, it uses gconv.Map for converting.
|
||||
if rvKind != reflect.Struct {
|
||||
return gconv.Map(value, structTagPriority...)
|
||||
}
|
||||
// Struct handling.
|
||||
var (
|
||||
fieldTag reflect.StructTag
|
||||
rvType = rvValue.Type()
|
||||
name = ""
|
||||
data = make(map[string]interface{})
|
||||
)
|
||||
for i := 0; i < rvValue.NumField(); i++ {
|
||||
rtField = rvType.Field(i)
|
||||
rvField = rvValue.Field(i)
|
||||
fieldName := rtField.Name
|
||||
if !utils.IsLetterUpper(fieldName[0]) {
|
||||
continue
|
||||
}
|
||||
// Struct attribute inherit
|
||||
if rtField.Anonymous {
|
||||
for k, v := range DataToMapDeep(rvField) {
|
||||
data[k] = v
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Other attributes.
|
||||
name = ""
|
||||
fieldTag = rtField.Tag
|
||||
for _, tag := range structTagPriority {
|
||||
if s := fieldTag.Get(tag); s != "" {
|
||||
name = s
|
||||
break
|
||||
}
|
||||
}
|
||||
if name == "" {
|
||||
name = fieldName
|
||||
} else {
|
||||
// The "orm" tag supports json tag feature: -, omitempty
|
||||
// The "orm" tag would be like: "id,priority", so it should use splitting handling.
|
||||
name = gstr.Trim(name)
|
||||
if name == "-" {
|
||||
continue
|
||||
}
|
||||
array := gstr.SplitAndTrim(name, ",")
|
||||
if len(array) > 1 {
|
||||
switch array[1] {
|
||||
case "omitempty":
|
||||
if empty.IsEmpty(rvField.Interface()) {
|
||||
continue
|
||||
} else {
|
||||
name = array[0]
|
||||
}
|
||||
default:
|
||||
name = array[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The underlying driver supports time.Time/*time.Time types.
|
||||
fieldValue := rvField.Interface()
|
||||
switch fieldValue.(type) {
|
||||
case time.Time, *time.Time, gtime.Time, *gtime.Time:
|
||||
data[name] = fieldValue
|
||||
default:
|
||||
// Use string conversion in default.
|
||||
if s, ok := fieldValue.(apiString); ok {
|
||||
data[name] = s.String()
|
||||
} else {
|
||||
data[name] = fieldValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
@ -183,32 +314,40 @@ func doQuoteString(s, charLeft, charRight string) string {
|
||||
|
||||
// GetWhereConditionOfStruct returns the where condition sql and arguments by given struct pointer.
|
||||
// This function automatically retrieves primary or unique field and its attribute value as condition.
|
||||
func GetWhereConditionOfStruct(pointer interface{}) (where string, args []interface{}) {
|
||||
func GetWhereConditionOfStruct(pointer interface{}) (where string, args []interface{}, err error) {
|
||||
tagField, err := structs.TagFields(pointer, []string{ORM_TAG_FOR_STRUCT})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
array := ([]string)(nil)
|
||||
for _, field := range structs.TagFields(pointer, []string{ORM_TAG_FOR_STRUCT}, true) {
|
||||
array = strings.Split(field.Tag, ",")
|
||||
for _, field := range tagField {
|
||||
array = strings.Split(field.TagValue, ",")
|
||||
if len(array) > 1 && gstr.InArray([]string{ORM_TAG_FOR_UNIQUE, ORM_TAG_FOR_PRIMARY}, array[1]) {
|
||||
return array[0], []interface{}{field.Value()}
|
||||
return array[0], []interface{}{field.Value()}, nil
|
||||
}
|
||||
if len(where) > 0 {
|
||||
where += " "
|
||||
where += " AND "
|
||||
}
|
||||
where += field.Tag + "=?"
|
||||
where += field.TagValue + "=?"
|
||||
args = append(args, field.Value())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetPrimaryKey retrieves and returns primary key field name from given struct.
|
||||
func GetPrimaryKey(pointer interface{}) string {
|
||||
func GetPrimaryKey(pointer interface{}) (string, error) {
|
||||
tagField, err := structs.TagFields(pointer, []string{ORM_TAG_FOR_STRUCT})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
array := ([]string)(nil)
|
||||
for _, field := range structs.TagFields(pointer, []string{ORM_TAG_FOR_STRUCT}, true) {
|
||||
array = strings.Split(field.Tag, ",")
|
||||
for _, field := range tagField {
|
||||
array = strings.Split(field.TagValue, ",")
|
||||
if len(array) > 1 && array[1] == ORM_TAG_FOR_PRIMARY {
|
||||
return array[0]
|
||||
return array[0], nil
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// GetPrimaryKeyCondition returns a new where condition by primary field name.
|
||||
@ -231,8 +370,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()
|
||||
@ -255,14 +396,14 @@ 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)
|
||||
}
|
||||
|
||||
// formatWhere formats where statement and its arguments.
|
||||
// TODO []interface{} type support for parameter <where> does not completed yet.
|
||||
func formatWhere(db DB, where interface{}, args []interface{}, omitEmpty bool) (newWhere string, newArgs []interface{}) {
|
||||
var (
|
||||
buffer = bytes.NewBuffer(nil)
|
||||
@ -344,20 +485,26 @@ func formatWhere(db DB, where interface{}, args []interface{}, omitEmpty bool) (
|
||||
}
|
||||
|
||||
// formatWhereInterfaces formats <where> as []interface{}.
|
||||
// TODO supporting for parameter <where> with []interface{} type is not completed yet.
|
||||
func formatWhereInterfaces(db DB, where []interface{}, buffer *bytes.Buffer, newArgs []interface{}) []interface{} {
|
||||
if len(where) == 0 {
|
||||
return newArgs
|
||||
}
|
||||
if len(where)%2 != 0 {
|
||||
buffer.WriteString(gstr.Join(gconv.Strings(where), ""))
|
||||
return newArgs
|
||||
}
|
||||
var str string
|
||||
var array []interface{}
|
||||
var holderCount int
|
||||
for i := 0; i < len(where); {
|
||||
if holderCount > 0 {
|
||||
array = gconv.Interfaces(where[i])
|
||||
newArgs = append(newArgs, array...)
|
||||
holderCount -= len(array)
|
||||
for i := 0; i < len(where); i += 2 {
|
||||
str = gconv.String(where[i])
|
||||
if buffer.Len() > 0 {
|
||||
buffer.WriteString(" AND " + db.QuoteWord(str) + "=?")
|
||||
} else {
|
||||
str = gconv.String(where[i])
|
||||
holderCount = gstr.Count(str, "?")
|
||||
buffer.WriteString(str)
|
||||
buffer.WriteString(db.QuoteWord(str) + "=?")
|
||||
}
|
||||
if s, ok := where[i+1].(Raw); ok {
|
||||
buffer.WriteString(gconv.String(s))
|
||||
} else {
|
||||
newArgs = append(newArgs, where[i+1])
|
||||
}
|
||||
}
|
||||
return newArgs
|
||||
@ -426,14 +573,18 @@ func formatWhereKeyValue(db DB, buffer *bytes.Buffer, newArgs []interface{}, key
|
||||
} else {
|
||||
buffer.WriteString(quotedKey)
|
||||
}
|
||||
newArgs = append(newArgs, value)
|
||||
if s, ok := value.(Raw); ok {
|
||||
buffer.WriteString(gconv.String(s))
|
||||
} else {
|
||||
newArgs = append(newArgs, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newArgs
|
||||
}
|
||||
|
||||
// handleArguments is a nice function which handles the query and its arguments before committing to
|
||||
// underlying driver.
|
||||
// handleArguments is an important function, which handles the sql and all its arguments
|
||||
// before committing them to underlying driver.
|
||||
func handleArguments(sql string, args []interface{}) (newSql string, newArgs []interface{}) {
|
||||
newSql = sql
|
||||
// insertHolderCount is used to calculate the inserting position for the '?' holder.
|
||||
@ -442,14 +593,14 @@ func handleArguments(sql string, args []interface{}) (newSql string, newArgs []i
|
||||
if len(args) > 0 {
|
||||
for index, arg := range args {
|
||||
var (
|
||||
rv = reflect.ValueOf(arg)
|
||||
kind = rv.Kind()
|
||||
reflectValue = reflect.ValueOf(arg)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
if kind == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
kind = rv.Kind()
|
||||
for reflectKind == reflect.Ptr {
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
}
|
||||
switch kind {
|
||||
switch reflectKind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
// It does not split the type of []byte.
|
||||
// Eg: table.Where("name = ?", []byte("john"))
|
||||
@ -458,7 +609,7 @@ func handleArguments(sql string, args []interface{}) (newSql string, newArgs []i
|
||||
continue
|
||||
}
|
||||
|
||||
if rv.Len() == 0 {
|
||||
if reflectValue.Len() == 0 {
|
||||
// Empty slice argument, it converts the sql to a false sql.
|
||||
// Eg:
|
||||
// Query("select * from xxx where id in(?)", g.Slice{}) -> select * from xxx where 0=1
|
||||
@ -472,15 +623,15 @@ func handleArguments(sql string, args []interface{}) (newSql string, newArgs []i
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
newArgs = append(newArgs, rv.Index(i).Interface())
|
||||
for i := 0; i < reflectValue.Len(); i++ {
|
||||
newArgs = append(newArgs, reflectValue.Index(i).Interface())
|
||||
}
|
||||
}
|
||||
|
||||
// If the '?' holder count equals the length of the slice,
|
||||
// it does not implement the arguments splitting logic.
|
||||
// Eg: db.Query("SELECT ?+?", g.Slice{1, 2})
|
||||
if len(args) == 1 && gstr.Count(newSql, "?") == rv.Len() {
|
||||
if len(args) == 1 && gstr.Count(newSql, "?") == reflectValue.Len() {
|
||||
break
|
||||
}
|
||||
// counter is used to finding the inserting position for the '?' holder.
|
||||
@ -495,36 +646,37 @@ func handleArguments(sql string, args []interface{}) (newSql string, newArgs []i
|
||||
counter++
|
||||
if counter == index+insertHolderCount+1 {
|
||||
replaced = true
|
||||
insertHolderCount += rv.Len() - 1
|
||||
return "?" + strings.Repeat(",?", rv.Len()-1)
|
||||
insertHolderCount += reflectValue.Len() - 1
|
||||
return "?" + strings.Repeat(",?", reflectValue.Len()-1)
|
||||
}
|
||||
return s
|
||||
})
|
||||
|
||||
// Special struct handling.
|
||||
case reflect.Struct:
|
||||
// The underlying driver supports time.Time/*time.Time types.
|
||||
if _, ok := arg.(time.Time); ok {
|
||||
newArgs = append(newArgs, arg)
|
||||
continue
|
||||
}
|
||||
if _, ok := arg.(*time.Time); ok {
|
||||
newArgs = append(newArgs, arg)
|
||||
continue
|
||||
}
|
||||
switch v := arg.(type) {
|
||||
// The underlying driver supports time.Time/*time.Time types.
|
||||
case time.Time, *time.Time:
|
||||
newArgs = append(newArgs, arg)
|
||||
continue
|
||||
|
||||
// Special handling for gtime.Time.
|
||||
// Special handling for gtime.Time/*gtime.Time.
|
||||
//
|
||||
// DO NOT use its underlying gtime.Time.Time as its argument,
|
||||
// because the std time.Time will be converted to certain timezone
|
||||
// according to underlying driver. And the underlying driver also
|
||||
// converts the time.Time to string automatically as the following does.
|
||||
case gtime.Time:
|
||||
newArgs = append(newArgs, v.String())
|
||||
continue
|
||||
|
||||
case *gtime.Time:
|
||||
newArgs = append(newArgs, v.String())
|
||||
continue
|
||||
|
||||
default:
|
||||
// It converts the struct to string in default
|
||||
// if it implements the String interface.
|
||||
// if it has implemented the String interface.
|
||||
if v, ok := arg.(apiString); ok {
|
||||
newArgs = append(newArgs, v.String())
|
||||
continue
|
||||
@ -543,7 +695,7 @@ func handleArguments(sql string, args []interface{}) (newSql string, newArgs []i
|
||||
// formatError customizes and returns the SQL error.
|
||||
func formatError(err error, sql string, args ...interface{}) error {
|
||||
if err != nil && err != ErrNoRows {
|
||||
return errors.New(fmt.Sprintf("%s, %s\n", err.Error(), FormatSqlWithArgs(sql, args)))
|
||||
return gerror.New(fmt.Sprintf("%s, %s\n", err.Error(), FormatSqlWithArgs(sql, args)))
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -553,7 +705,7 @@ func formatError(err error, sql string, args ...interface{}) error {
|
||||
func FormatSqlWithArgs(sql string, args []interface{}) string {
|
||||
index := -1
|
||||
newQuery, _ := gregex.ReplaceStringFunc(
|
||||
`(\?|:v\d+|\$v\d+|@v\d+)`, sql, func(s string) string {
|
||||
`(\?|:v\d+|\$\d+|@p\d+)`, sql, func(s string) string {
|
||||
index++
|
||||
if len(args) > index {
|
||||
if args[index] == nil {
|
||||
@ -575,7 +727,7 @@ func FormatSqlWithArgs(sql string, args []interface{}) string {
|
||||
return `'` + gstr.QuoteMeta(gconv.String(args[index]), `'`) + `'`
|
||||
case reflect.Struct:
|
||||
if t, ok := args[index].(time.Time); ok {
|
||||
return `'` + gtime.NewFromTime(t).String() + `'`
|
||||
return `'` + t.Format(`2006-01-02 15:04:05`) + `'`
|
||||
}
|
||||
return `'` + gstr.QuoteMeta(gconv.String(args[index]), `'`) + `'`
|
||||
}
|
||||
@ -586,13 +738,17 @@ func FormatSqlWithArgs(sql string, args []interface{}) string {
|
||||
return newQuery
|
||||
}
|
||||
|
||||
// mapToStruct maps the <data> to given struct.
|
||||
// convertMapToStruct maps the <data> to given struct.
|
||||
// Note that the given parameter <pointer> should be a pointer to s struct.
|
||||
func mapToStruct(data map[string]interface{}, pointer interface{}) error {
|
||||
func convertMapToStruct(data map[string]interface{}, pointer interface{}) error {
|
||||
tagNameMap, err := structs.TagMapName(pointer, []string{ORM_TAG_FOR_STRUCT})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// It retrieves and returns the mapping between orm tag and the struct attribute name.
|
||||
mapping := make(map[string]string)
|
||||
for tag, attr := range structs.TagMapName(pointer, []string{ORM_TAG_FOR_STRUCT}, true) {
|
||||
for tag, attr := range tagNameMap {
|
||||
mapping[strings.Split(tag, ",")[0]] = attr
|
||||
}
|
||||
return gconv.StructDeep(data, pointer, mapping)
|
||||
return gconv.Struct(data, pointer, mapping)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -7,6 +7,7 @@
|
||||
package gdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"time"
|
||||
@ -52,13 +53,14 @@ type whereHolder struct {
|
||||
}
|
||||
|
||||
const (
|
||||
gLINK_TYPE_MASTER = 1
|
||||
gLINK_TYPE_SLAVE = 2
|
||||
gWHERE_HOLDER_WHERE = 1
|
||||
gWHERE_HOLDER_AND = 2
|
||||
gWHERE_HOLDER_OR = 3
|
||||
OPTION_OMITEMPTY = 1 << iota
|
||||
OPTION_ALLOWEMPTY
|
||||
OPTION_OMITEMPTY = 1
|
||||
OPTION_ALLOWEMPTY = 2
|
||||
|
||||
linkTypeMaster = 1
|
||||
linkTypeSlave = 2
|
||||
whereHolderWhere = 1
|
||||
whereHolderAnd = 2
|
||||
whereHolderOr = 3
|
||||
)
|
||||
|
||||
// Table creates and returns a new ORM model from given schema.
|
||||
@ -112,6 +114,16 @@ func (tx *TX) Model(table ...string) *Model {
|
||||
return tx.Table(table...)
|
||||
}
|
||||
|
||||
// Ctx sets the context for current operation.
|
||||
func (m *Model) Ctx(ctx context.Context) *Model {
|
||||
if ctx == nil {
|
||||
return m
|
||||
}
|
||||
model := m.getModel()
|
||||
model.db = model.db.Ctx(ctx)
|
||||
return model
|
||||
}
|
||||
|
||||
// As sets an alias name for current table.
|
||||
func (m *Model) As(as string) *Model {
|
||||
if m.tables != "" {
|
||||
@ -141,6 +153,7 @@ func (m *Model) DB(db DB) *Model {
|
||||
// TX sets/changes the transaction for current operation.
|
||||
func (m *Model) TX(tx *TX) *Model {
|
||||
model := m.getModel()
|
||||
model.db = tx.db
|
||||
model.tx = tx
|
||||
return model
|
||||
}
|
||||
@ -177,7 +190,7 @@ func (m *Model) Clone() *Model {
|
||||
// Master marks the following operation on master node.
|
||||
func (m *Model) Master() *Model {
|
||||
model := m.getModel()
|
||||
model.linkType = gLINK_TYPE_MASTER
|
||||
model.linkType = linkTypeMaster
|
||||
return model
|
||||
}
|
||||
|
||||
@ -185,7 +198,7 @@ func (m *Model) Master() *Model {
|
||||
// Note that it makes sense only if there's any slave node configured.
|
||||
func (m *Model) Slave() *Model {
|
||||
model := m.getModel()
|
||||
model.linkType = gLINK_TYPE_SLAVE
|
||||
model.linkType = linkTypeSlave
|
||||
return model
|
||||
}
|
||||
|
||||
@ -199,3 +212,10 @@ func (m *Model) Safe(safe ...bool) *Model {
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Args sets custom arguments for model operation.
|
||||
func (m *Model) Args(args ...interface{}) *Model {
|
||||
model := m.getModel()
|
||||
model.extraArgs = append(model.extraArgs, args)
|
||||
return model
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -27,7 +27,7 @@ func (m *Model) Where(where interface{}, args ...interface{}) *Model {
|
||||
model.whereHolder = make([]*whereHolder, 0)
|
||||
}
|
||||
model.whereHolder = append(model.whereHolder, &whereHolder{
|
||||
operator: gWHERE_HOLDER_WHERE,
|
||||
operator: whereHolderWhere,
|
||||
where: where,
|
||||
args: args,
|
||||
})
|
||||
@ -65,7 +65,7 @@ func (m *Model) And(where interface{}, args ...interface{}) *Model {
|
||||
model.whereHolder = make([]*whereHolder, 0)
|
||||
}
|
||||
model.whereHolder = append(model.whereHolder, &whereHolder{
|
||||
operator: gWHERE_HOLDER_AND,
|
||||
operator: whereHolderAnd,
|
||||
where: where,
|
||||
args: args,
|
||||
})
|
||||
@ -79,7 +79,7 @@ func (m *Model) Or(where interface{}, args ...interface{}) *Model {
|
||||
model.whereHolder = make([]*whereHolder, 0)
|
||||
}
|
||||
model.whereHolder = append(model.whereHolder, &whereHolder{
|
||||
operator: gWHERE_HOLDER_OR,
|
||||
operator: whereHolderOr,
|
||||
where: where,
|
||||
args: args,
|
||||
})
|
||||
@ -140,7 +140,7 @@ func (m *Model) Offset(offset int) *Model {
|
||||
|
||||
// Page sets the paging number for the model.
|
||||
// The parameter <page> is started from 1 for paging.
|
||||
// Note that, it differs that the Limit function start from 0 for "LIMIT" statement.
|
||||
// Note that, it differs that the Limit function starts from 0 for "LIMIT" statement.
|
||||
func (m *Model) Page(page, limit int) *Model {
|
||||
model := m.getModel()
|
||||
if page <= 0 {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -9,16 +9,11 @@ package gdb
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
)
|
||||
|
||||
// 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,8 +27,8 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
|
||||
}
|
||||
}()
|
||||
var (
|
||||
fieldNameDelete = m.getSoftFieldNameDelete()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
|
||||
fieldNameDelete = m.getSoftFieldNameDeleted()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false, false)
|
||||
)
|
||||
// Soft deleting.
|
||||
if !m.unscoped && fieldNameDelete != "" {
|
||||
@ -45,5 +40,9 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
|
||||
append([]interface{}{gtime.Now().String()}, conditionArgs...),
|
||||
)
|
||||
}
|
||||
return m.db.DoDelete(m.getLink(true), m.tables, conditionWhere+conditionExtra, conditionArgs...)
|
||||
conditionStr := conditionWhere + conditionExtra
|
||||
if !gstr.ContainsI(conditionStr, " WHERE ") {
|
||||
return nil, gerror.New("there should be WHERE condition statement for DELETE operation")
|
||||
}
|
||||
return m.db.DoDelete(m.getLink(true), m.tables, conditionStr, conditionArgs...)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -10,6 +10,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/gogf/gf/container/gset"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
)
|
||||
|
||||
// Filter marks filtering the fields which does not exist in the fields of the operated table.
|
||||
@ -24,49 +26,70 @@ 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
|
||||
// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
|
||||
func (m *Model) Fields(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
length := len(fieldNamesOrMapStruct)
|
||||
if length == 0 {
|
||||
return m
|
||||
}
|
||||
switch {
|
||||
// String slice.
|
||||
case length >= 2:
|
||||
model := m.getModel()
|
||||
model.fields = gstr.Join(m.mappingAndFilterToTableFields(gconv.Strings(fieldNamesOrMapStruct)), ",")
|
||||
return model
|
||||
// It need type asserting.
|
||||
case length == 1:
|
||||
model := m.getModel()
|
||||
switch r := fieldNamesOrMapStruct[0].(type) {
|
||||
case string:
|
||||
model.fields = gstr.Join(m.mappingAndFilterToTableFields([]string{r}), ",")
|
||||
case []string:
|
||||
model.fields = gstr.Join(m.mappingAndFilterToTableFields(r), ",")
|
||||
default:
|
||||
model.fields = gstr.Join(m.mappingAndFilterToTableFields(gutil.Keys(r)), ",")
|
||||
}
|
||||
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")
|
||||
}
|
||||
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))
|
||||
// The parameter <fieldNamesOrMapStruct> can be type of string/map/*map/struct/*struct.
|
||||
func (m *Model) FieldsEx(fieldNamesOrMapStruct ...interface{}) *Model {
|
||||
length := len(fieldNamesOrMapStruct)
|
||||
if length == 0 {
|
||||
return m
|
||||
}
|
||||
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
|
||||
switch {
|
||||
case length >= 2:
|
||||
model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(gconv.Strings(fieldNamesOrMapStruct)), ",")
|
||||
return model
|
||||
case length == 1:
|
||||
switch r := fieldNamesOrMapStruct[0].(type) {
|
||||
case string:
|
||||
model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields([]string{r}), ",")
|
||||
case []string:
|
||||
model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(r), ",")
|
||||
default:
|
||||
model.fieldsEx = gstr.Join(m.mappingAndFilterToTableFields(gutil.Keys(r)), ",")
|
||||
}
|
||||
if len(model.fields) > 0 {
|
||||
model.fields += ","
|
||||
}
|
||||
model.fields += k
|
||||
return model
|
||||
}
|
||||
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 +116,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]
|
||||
@ -127,3 +156,24 @@ func (m *Model) FieldsExStr(fields string, prefix ...string) string {
|
||||
newFields = m.db.QuoteString(newFields)
|
||||
return newFields
|
||||
}
|
||||
|
||||
// HasField determine whether the field exists in the table.
|
||||
func (m *Model) HasField(field string) (bool, error) {
|
||||
tableFields, err := m.db.TableFields(m.tables)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(tableFields) == 0 {
|
||||
return false, fmt.Errorf(`empty table fields for table "%s"`, m.tables)
|
||||
}
|
||||
fieldsArray := make([]string, len(tableFields))
|
||||
for k, v := range tableFields {
|
||||
fieldsArray[v.Index] = k
|
||||
}
|
||||
for _, f := range fieldsArray {
|
||||
if f == field {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,7 +8,7 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
@ -25,6 +25,8 @@ func (m *Model) Batch(batch int) *Model {
|
||||
|
||||
// Data sets the operation data for the model.
|
||||
// The parameter <data> can be type of string/map/gmap/slice/struct/*struct, etc.
|
||||
// Note that, it uses shallow value copying for `data` if `data` is type of map/slice
|
||||
// to avoid changing it inside function.
|
||||
// Eg:
|
||||
// Data("uid=10000")
|
||||
// Data("uid", 10000)
|
||||
@ -34,8 +36,7 @@ func (m *Model) Batch(batch int) *Model {
|
||||
func (m *Model) Data(data ...interface{}) *Model {
|
||||
model := m.getModel()
|
||||
if len(data) > 1 {
|
||||
s := gconv.String(data[0])
|
||||
if gstr.Contains(s, "?") {
|
||||
if s := gconv.String(data[0]); gstr.Contains(s, "?") {
|
||||
model.data = s
|
||||
model.extraArgs = data[1:]
|
||||
} else {
|
||||
@ -52,12 +53,18 @@ func (m *Model) Data(data ...interface{}) *Model {
|
||||
case Record:
|
||||
model.data = params.Map()
|
||||
case List:
|
||||
model.data = params
|
||||
list := make(List, len(params))
|
||||
for k, v := range params {
|
||||
list[k] = gutil.MapCopy(v)
|
||||
}
|
||||
model.data = list
|
||||
case Map:
|
||||
model.data = params
|
||||
model.data = gutil.MapCopy(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,23 +73,23 @@ 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].(apiMapStrAny); ok {
|
||||
model.data = v.MapStrAny()
|
||||
} else if v, ok := data[0].(apiInterfaces); ok {
|
||||
array := v.Interfaces()
|
||||
list := make(List, len(array))
|
||||
if v, ok := data[0].(apiInterfaces); ok {
|
||||
var (
|
||||
array = v.Interfaces()
|
||||
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]
|
||||
@ -99,7 +106,7 @@ func (m *Model) Insert(data ...interface{}) (result sql.Result, err error) {
|
||||
if len(data) > 0 {
|
||||
return m.Data(data...).Insert()
|
||||
}
|
||||
return m.doInsertWithOption(gINSERT_OPTION_DEFAULT, data...)
|
||||
return m.doInsertWithOption(insertOptionDefault, data...)
|
||||
}
|
||||
|
||||
// InsertIgnore does "INSERT IGNORE INTO ..." statement for the model.
|
||||
@ -107,9 +114,9 @@ func (m *Model) Insert(data ...interface{}) (result sql.Result, err error) {
|
||||
// see Model.Data.
|
||||
func (m *Model) InsertIgnore(data ...interface{}) (result sql.Result, err error) {
|
||||
if len(data) > 0 {
|
||||
return m.Data(data...).Insert()
|
||||
return m.Data(data...).InsertIgnore()
|
||||
}
|
||||
return m.doInsertWithOption(gINSERT_OPTION_IGNORE, data...)
|
||||
return m.doInsertWithOption(insertOptionIgnore, data...)
|
||||
}
|
||||
|
||||
// Replace does "REPLACE INTO ..." statement for the model.
|
||||
@ -119,7 +126,7 @@ func (m *Model) Replace(data ...interface{}) (result sql.Result, err error) {
|
||||
if len(data) > 0 {
|
||||
return m.Data(data...).Replace()
|
||||
}
|
||||
return m.doInsertWithOption(gINSERT_OPTION_REPLACE, data...)
|
||||
return m.doInsertWithOption(insertOptionReplace, data...)
|
||||
}
|
||||
|
||||
// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the model.
|
||||
@ -132,7 +139,7 @@ func (m *Model) Save(data ...interface{}) (result sql.Result, err error) {
|
||||
if len(data) > 0 {
|
||||
return m.Data(data...).Save()
|
||||
}
|
||||
return m.doInsertWithOption(gINSERT_OPTION_SAVE, data...)
|
||||
return m.doInsertWithOption(insertOptionSave, data...)
|
||||
}
|
||||
|
||||
// doInsertWithOption inserts data with option parameter.
|
||||
@ -143,17 +150,17 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
|
||||
}
|
||||
}()
|
||||
if m.data == nil {
|
||||
return nil, errors.New("inserting into table with empty data")
|
||||
return nil, gerror.New("inserting into table with empty data")
|
||||
}
|
||||
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 {
|
||||
batch := gDEFAULT_BATCH_NUM
|
||||
batch := defaultBatchNumber
|
||||
if m.batch > 0 {
|
||||
batch = m.batch
|
||||
}
|
||||
@ -170,10 +177,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,12 +201,16 @@ 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,
|
||||
)
|
||||
}
|
||||
return nil, errors.New("inserting into table with invalid data type")
|
||||
return nil, gerror.New("inserting into table with invalid data type")
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -6,29 +6,30 @@
|
||||
|
||||
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,
|
||||
// and also with its alias name, like:
|
||||
// Table("user").LeftJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").LeftJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").LeftJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) LeftJoin(table ...string) *Model {
|
||||
model := m.getModel()
|
||||
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],
|
||||
)
|
||||
} else if len(table) == 2 {
|
||||
model.tables += fmt.Sprintf(
|
||||
" LEFT JOIN %s ON (%s)",
|
||||
m.db.QuotePrefixTableName(table[0]), table[1],
|
||||
)
|
||||
} else {
|
||||
panic("invalid join table parameter")
|
||||
}
|
||||
return model
|
||||
return m.doJoin("LEFT", table...)
|
||||
}
|
||||
|
||||
// RightJoin does "RIGHT JOIN ... ON ..." statement on the model.
|
||||
@ -36,22 +37,9 @@ func (m *Model) LeftJoin(table ...string) *Model {
|
||||
// and also with its alias name, like:
|
||||
// Table("user").RightJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").RightJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").RightJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) RightJoin(table ...string) *Model {
|
||||
model := m.getModel()
|
||||
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],
|
||||
)
|
||||
} else if len(table) == 2 {
|
||||
model.tables += fmt.Sprintf(
|
||||
" RIGHT JOIN %s ON (%s)",
|
||||
m.db.QuotePrefixTableName(table[0]), table[1],
|
||||
)
|
||||
} else {
|
||||
panic("invalid join table parameter")
|
||||
}
|
||||
return model
|
||||
return m.doJoin("RIGHT", table...)
|
||||
}
|
||||
|
||||
// InnerJoin does "INNER JOIN ... ON ..." statement on the model.
|
||||
@ -59,20 +47,48 @@ func (m *Model) RightJoin(table ...string) *Model {
|
||||
// and also with its alias name, like:
|
||||
// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
func (m *Model) InnerJoin(table ...string) *Model {
|
||||
model := m.getModel()
|
||||
return m.doJoin("INNER", table...)
|
||||
}
|
||||
|
||||
// doJoin does "LEFT/RIGHT/INNER JOIN ... ON ..." statement on the model.
|
||||
// The parameter <table> can be joined table and its joined condition,
|
||||
// and also with its alias name, like:
|
||||
// Table("user").InnerJoin("user_detail", "user_detail.uid=user.uid")
|
||||
// Table("user", "u").InnerJoin("user_detail", "ud", "ud.uid=u.uid")
|
||||
// Table("user", "u").InnerJoin("SELECT xxx FROM xxx AS a", "a.uid=u.uid")
|
||||
// Related issues:
|
||||
// https://github.com/gogf/gf/issues/1024
|
||||
func (m *Model) doJoin(operator string, table ...string) *Model {
|
||||
var (
|
||||
model = m.getModel()
|
||||
joinStr = ""
|
||||
)
|
||||
if len(table) > 0 {
|
||||
if isSubQuery(table[0]) {
|
||||
joinStr = gstr.Trim(table[0])
|
||||
if joinStr[0] != '(' {
|
||||
joinStr = "(" + joinStr + ")"
|
||||
}
|
||||
} 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],
|
||||
" %s JOIN %s AS %s ON (%s)",
|
||||
operator, 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],
|
||||
" %s JOIN %s ON (%s)",
|
||||
operator, joinStr, table[1],
|
||||
)
|
||||
} else if len(table) == 1 {
|
||||
model.tables += fmt.Sprintf(
|
||||
" %s JOIN %s", operator, joinStr,
|
||||
)
|
||||
} else {
|
||||
panic("invalid join table parameter")
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -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"
|
||||
)
|
||||
@ -43,7 +46,7 @@ func (m *Model) doGetAll(limit1 bool, where ...interface{}) (Result, error) {
|
||||
}
|
||||
var (
|
||||
softDeletingCondition = m.getConditionForSoftDeleting()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(limit1)
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(limit1, false)
|
||||
)
|
||||
if !m.unscoped && softDeletingCondition != "" {
|
||||
if conditionWhere == "" {
|
||||
@ -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,10 +70,60 @@ 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.
|
||||
if !gstr.Contains(m.fields, ".") && !gstr.Contains(m.fields, " ") {
|
||||
return m.db.QuoteString(m.fields)
|
||||
}
|
||||
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 += m.db.QuoteWord(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
|
||||
if page == 0 {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
model := m
|
||||
@ -290,7 +344,7 @@ func (m *Model) Count(where ...interface{}) (int, error) {
|
||||
}
|
||||
var (
|
||||
softDeletingCondition = m.getConditionForSoftDeleting()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false, true)
|
||||
)
|
||||
if !m.unscoped && softDeletingCondition != "" {
|
||||
if conditionWhere == "" {
|
||||
@ -381,23 +435,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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -15,58 +15,94 @@ 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{"deleted_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 {
|
||||
// It checks whether this feature disabled.
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return ""
|
||||
}
|
||||
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, []string{config.CreatedAt})
|
||||
}
|
||||
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) {
|
||||
// It checks whether this feature disabled.
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return ""
|
||||
}
|
||||
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, []string{config.UpdatedAt})
|
||||
}
|
||||
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) {
|
||||
// It checks whether this feature disabled.
|
||||
if m.db.GetConfig().TimeMaintainDisabled {
|
||||
return ""
|
||||
}
|
||||
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, []string{config.DeletedAt})
|
||||
}
|
||||
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 +122,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 +139,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 +158,7 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string {
|
||||
} else {
|
||||
table = array2[0]
|
||||
}
|
||||
field = m.getSoftFieldNameDelete(table)
|
||||
field = m.getSoftFieldNameDeleted(table)
|
||||
if field == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,8 +8,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
@ -38,14 +38,14 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
|
||||
}
|
||||
}()
|
||||
if m.data == nil {
|
||||
return nil, errors.New("updating table with empty data")
|
||||
return nil, gerror.New("updating table with empty data")
|
||||
}
|
||||
var (
|
||||
updateData = m.data
|
||||
fieldNameCreate = m.getSoftFieldNameCreate()
|
||||
fieldNameUpdate = m.getSoftFieldNameUpdate()
|
||||
fieldNameDelete = m.getSoftFieldNameDelete()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
|
||||
fieldNameCreate = m.getSoftFieldNameCreated()
|
||||
fieldNameUpdate = m.getSoftFieldNameUpdated()
|
||||
fieldNameDelete = m.getSoftFieldNameDeleted()
|
||||
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false, false)
|
||||
)
|
||||
// Automatically update the record updating time.
|
||||
if !m.unscoped && fieldNameUpdate != "" {
|
||||
@ -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,11 +73,19 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
|
||||
updateData = updates
|
||||
}
|
||||
}
|
||||
newData, err := m.filterDataForInsertOrUpdate(updateData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conditionStr := conditionWhere + conditionExtra
|
||||
if !gstr.ContainsI(conditionStr, " WHERE ") {
|
||||
return nil, gerror.New("there should be WHERE condition statement for UPDATE operation")
|
||||
}
|
||||
return m.db.DoUpdate(
|
||||
m.getLink(true),
|
||||
m.tables,
|
||||
m.filterDataForInsertOrUpdate(updateData),
|
||||
conditionWhere+conditionExtra,
|
||||
newData,
|
||||
conditionStr,
|
||||
m.mergeArguments(conditionArgs)...,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -11,8 +11,10 @@ import (
|
||||
"github.com/gogf/gf/container/gset"
|
||||
"github.com/gogf/gf/internal/empty"
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -26,29 +28,66 @@ func (m *Model) getModel() *Model {
|
||||
}
|
||||
}
|
||||
|
||||
// mappingAndFilterToTableFields mappings and changes given field name to really table field name.
|
||||
func (m *Model) mappingAndFilterToTableFields(fields []string) []string {
|
||||
fieldsMap, err := m.db.TableFields(m.tables)
|
||||
if err != nil || len(fieldsMap) == 0 {
|
||||
return fields
|
||||
}
|
||||
var (
|
||||
inputFieldsArray = gstr.SplitAndTrim(gstr.Join(fields, ","), ",")
|
||||
outputFieldsArray = make([]string, 0, len(inputFieldsArray))
|
||||
)
|
||||
fieldsKeyMap := make(map[string]interface{}, len(fieldsMap))
|
||||
for k, _ := range fieldsMap {
|
||||
fieldsKeyMap[k] = nil
|
||||
}
|
||||
for _, field := range inputFieldsArray {
|
||||
if _, ok := fieldsKeyMap[field]; !ok {
|
||||
if !gregex.IsMatchString(regularFieldNameRegPattern, field) {
|
||||
outputFieldsArray = append(outputFieldsArray, field)
|
||||
continue
|
||||
} else {
|
||||
if foundKey, _ := gutil.MapPossibleItemByKey(fieldsKeyMap, field); foundKey != "" {
|
||||
outputFieldsArray = append(outputFieldsArray, foundKey)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
outputFieldsArray = append(outputFieldsArray, field)
|
||||
}
|
||||
}
|
||||
return outputFieldsArray
|
||||
}
|
||||
|
||||
// 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 +142,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.
|
||||
@ -115,19 +154,19 @@ func (m *Model) getLink(master bool) Link {
|
||||
linkType := m.linkType
|
||||
if linkType == 0 {
|
||||
if master {
|
||||
linkType = gLINK_TYPE_MASTER
|
||||
linkType = linkTypeMaster
|
||||
} else {
|
||||
linkType = gLINK_TYPE_SLAVE
|
||||
linkType = linkTypeSlave
|
||||
}
|
||||
}
|
||||
switch linkType {
|
||||
case gLINK_TYPE_MASTER:
|
||||
case linkTypeMaster:
|
||||
link, err := m.db.GetMaster(m.schema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return link
|
||||
case gLINK_TYPE_SLAVE:
|
||||
case linkTypeSlave:
|
||||
link, err := m.db.GetSlave(m.schema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@ -158,11 +197,11 @@ func (m *Model) getPrimaryKey() string {
|
||||
// Note that this function does not change any attribute value of the <m>.
|
||||
//
|
||||
// The parameter <limit1> specifies whether limits querying only one record if m.limit is not set.
|
||||
func (m *Model) formatCondition(limit1 bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) {
|
||||
func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) {
|
||||
if len(m.whereHolder) > 0 {
|
||||
for _, v := range m.whereHolder {
|
||||
switch v.operator {
|
||||
case gWHERE_HOLDER_WHERE:
|
||||
case whereHolderWhere:
|
||||
if conditionWhere == "" {
|
||||
newWhere, newArgs := formatWhere(
|
||||
m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0,
|
||||
@ -175,7 +214,7 @@ func (m *Model) formatCondition(limit1 bool) (conditionWhere string, conditionEx
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case gWHERE_HOLDER_AND:
|
||||
case whereHolderAnd:
|
||||
newWhere, newArgs := formatWhere(
|
||||
m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0,
|
||||
)
|
||||
@ -190,7 +229,7 @@ func (m *Model) formatCondition(limit1 bool) (conditionWhere string, conditionEx
|
||||
conditionArgs = append(conditionArgs, newArgs...)
|
||||
}
|
||||
|
||||
case gWHERE_HOLDER_OR:
|
||||
case whereHolderOr:
|
||||
newWhere, newArgs := formatWhere(
|
||||
m.db, v.where, v.args, m.option&OPTION_OMITEMPTY > 0,
|
||||
)
|
||||
@ -213,9 +252,6 @@ func (m *Model) formatCondition(limit1 bool) (conditionWhere string, conditionEx
|
||||
if m.groupBy != "" {
|
||||
conditionExtra += " GROUP BY " + m.groupBy
|
||||
}
|
||||
if m.orderBy != "" {
|
||||
conditionExtra += " ORDER BY " + m.orderBy
|
||||
}
|
||||
if len(m.having) > 0 {
|
||||
havingStr, havingArgs := formatWhere(
|
||||
m.db, m.having[0], gconv.Interfaces(m.having[1]), m.option&OPTION_OMITEMPTY > 0,
|
||||
@ -225,18 +261,25 @@ func (m *Model) formatCondition(limit1 bool) (conditionWhere string, conditionEx
|
||||
conditionArgs = append(conditionArgs, havingArgs...)
|
||||
}
|
||||
}
|
||||
if m.limit != 0 {
|
||||
if m.start >= 0 {
|
||||
conditionExtra += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit)
|
||||
} else {
|
||||
conditionExtra += fmt.Sprintf(" LIMIT %d", m.limit)
|
||||
if m.orderBy != "" {
|
||||
conditionExtra += " ORDER BY " + m.orderBy
|
||||
}
|
||||
if !isCountStatement {
|
||||
if m.limit != 0 {
|
||||
if m.start >= 0 {
|
||||
conditionExtra += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit)
|
||||
} else {
|
||||
conditionExtra += fmt.Sprintf(" LIMIT %d", m.limit)
|
||||
}
|
||||
} else if limit1 {
|
||||
conditionExtra += " LIMIT 1"
|
||||
}
|
||||
|
||||
if m.offset >= 0 {
|
||||
conditionExtra += fmt.Sprintf(" OFFSET %d", m.offset)
|
||||
}
|
||||
} else if limit1 {
|
||||
conditionExtra += " LIMIT 1"
|
||||
}
|
||||
if m.offset >= 0 {
|
||||
conditionExtra += fmt.Sprintf(" OFFSET %d", m.offset)
|
||||
}
|
||||
|
||||
if m.lockInfo != "" {
|
||||
conditionExtra += " " + m.lockInfo
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,147 +0,0 @@
|
||||
// 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 (
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
|
||||
"github.com/gogf/gf/os/gtime"
|
||||
|
||||
"github.com/gogf/gf/encoding/gbinary"
|
||||
|
||||
"github.com/gogf/gf/text/gregex"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
)
|
||||
|
||||
// convertValue automatically checks and converts field value from database type
|
||||
// to golang variable type.
|
||||
func (c *Core) convertValue(fieldValue []byte, fieldType string) interface{} {
|
||||
t, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType)
|
||||
t = strings.ToLower(t)
|
||||
switch t {
|
||||
case
|
||||
"binary",
|
||||
"varbinary",
|
||||
"blob",
|
||||
"tinyblob",
|
||||
"mediumblob",
|
||||
"longblob":
|
||||
return fieldValue
|
||||
|
||||
case
|
||||
"int",
|
||||
"tinyint",
|
||||
"small_int",
|
||||
"smallint",
|
||||
"medium_int",
|
||||
"mediumint",
|
||||
"serial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
gconv.Uint(string(fieldValue))
|
||||
}
|
||||
return gconv.Int(string(fieldValue))
|
||||
|
||||
case
|
||||
"big_int",
|
||||
"bigint",
|
||||
"bigserial":
|
||||
if gstr.ContainsI(fieldType, "unsigned") {
|
||||
gconv.Uint64(string(fieldValue))
|
||||
}
|
||||
return gconv.Int64(string(fieldValue))
|
||||
|
||||
case "real":
|
||||
return gconv.Float32(string(fieldValue))
|
||||
|
||||
case
|
||||
"float",
|
||||
"double",
|
||||
"decimal",
|
||||
"money",
|
||||
"numeric",
|
||||
"smallmoney":
|
||||
return gconv.Float64(string(fieldValue))
|
||||
|
||||
case "bit":
|
||||
s := string(fieldValue)
|
||||
// mssql is true|false string.
|
||||
if strings.EqualFold(s, "true") {
|
||||
return 1
|
||||
}
|
||||
if strings.EqualFold(s, "false") {
|
||||
return 0
|
||||
}
|
||||
return gbinary.BeDecodeToInt64(fieldValue)
|
||||
|
||||
case "bool":
|
||||
return gconv.Bool(fieldValue)
|
||||
|
||||
case "date":
|
||||
t, _ := gtime.StrToTime(string(fieldValue))
|
||||
return t.Format("Y-m-d")
|
||||
|
||||
case
|
||||
"datetime",
|
||||
"timestamp":
|
||||
t, _ := gtime.StrToTime(string(fieldValue))
|
||||
return t.String()
|
||||
|
||||
default:
|
||||
// Auto detect field type, using key match.
|
||||
switch {
|
||||
case strings.Contains(t, "text") || strings.Contains(t, "char") || strings.Contains(t, "character"):
|
||||
return string(fieldValue)
|
||||
|
||||
case strings.Contains(t, "float") || strings.Contains(t, "double") || strings.Contains(t, "numeric"):
|
||||
return gconv.Float64(string(fieldValue))
|
||||
|
||||
case strings.Contains(t, "bool"):
|
||||
return gconv.Bool(string(fieldValue))
|
||||
|
||||
case strings.Contains(t, "binary") || strings.Contains(t, "blob"):
|
||||
return fieldValue
|
||||
|
||||
case strings.Contains(t, "int"):
|
||||
return gconv.Int(string(fieldValue))
|
||||
|
||||
case strings.Contains(t, "time"):
|
||||
s := string(fieldValue)
|
||||
t, err := gtime.StrToTime(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return t.String()
|
||||
|
||||
case strings.Contains(t, "date"):
|
||||
s := string(fieldValue)
|
||||
t, err := gtime.StrToTime(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
return t.Format("Y-m-d")
|
||||
|
||||
default:
|
||||
return string(fieldValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2017 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -59,7 +59,7 @@ func (tx *TX) GetAll(sql string, args ...interface{}) (Result, error) {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return tx.db.rowsToResult(rows)
|
||||
return tx.db.convertRowsToResult(rows)
|
||||
}
|
||||
|
||||
// GetOne queries and returns one record from database.
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,9 +8,9 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"github.com/gogf/gf/container/gmap"
|
||||
"github.com/gogf/gf/encoding/gparser"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"reflect"
|
||||
)
|
||||
@ -52,24 +52,24 @@ func (r Record) Struct(pointer interface{}) error {
|
||||
}
|
||||
// Special handling for parameter type: reflect.Value
|
||||
if _, ok := pointer.(reflect.Value); ok {
|
||||
return mapToStruct(r.Map(), pointer)
|
||||
return convertMapToStruct(r.Map(), pointer)
|
||||
}
|
||||
var (
|
||||
reflectValue = reflect.ValueOf(pointer)
|
||||
reflectKind = reflectValue.Kind()
|
||||
)
|
||||
if reflectKind != reflect.Ptr {
|
||||
return errors.New("parameter should be type of *struct/**struct")
|
||||
return gerror.New("parameter should be type of *struct/**struct")
|
||||
}
|
||||
reflectValue = reflectValue.Elem()
|
||||
reflectKind = reflectValue.Kind()
|
||||
if reflectKind == reflect.Invalid {
|
||||
return errors.New("parameter is an invalid pointer, maybe nil")
|
||||
return gerror.New("parameter is an invalid pointer, maybe nil")
|
||||
}
|
||||
if reflectKind != reflect.Ptr && reflectKind != reflect.Struct {
|
||||
return errors.New("parameter should be type of *struct/**struct")
|
||||
return gerror.New("parameter should be type of *struct/**struct")
|
||||
}
|
||||
return mapToStruct(r.Map(), pointer)
|
||||
return convertMapToStruct(r.Map(), pointer)
|
||||
}
|
||||
|
||||
// IsEmpty checks and returns whether <r> is empty.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -224,19 +224,16 @@ func (r Result) Structs(pointer interface{}) (err error) {
|
||||
itemKind = itemType.Kind()
|
||||
)
|
||||
for i := 0; i < length; i++ {
|
||||
var elem reflect.Value
|
||||
if itemKind == reflect.Ptr {
|
||||
e := reflect.New(itemType.Elem()).Elem()
|
||||
if err = r[i].Struct(e); err != nil {
|
||||
return fmt.Errorf(`slice element conversion failed: %s`, err.Error())
|
||||
}
|
||||
array.Index(i).Set(e.Addr())
|
||||
elem = reflect.New(itemType.Elem())
|
||||
} else {
|
||||
e := reflect.New(itemType).Elem()
|
||||
if err = r[i].Struct(e); err != nil {
|
||||
return fmt.Errorf(`slice element conversion failed: %s`, err.Error())
|
||||
}
|
||||
array.Index(i).Set(e)
|
||||
elem = reflect.New(itemType).Elem()
|
||||
}
|
||||
if err = r[i].Struct(elem); err != nil {
|
||||
return fmt.Errorf(`slice element conversion failed: %s`, err.Error())
|
||||
}
|
||||
array.Index(i).Set(elem)
|
||||
}
|
||||
reflect.ValueOf(pointer).Elem().Set(array)
|
||||
return nil
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2020 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -8,8 +8,8 @@ package gdb
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gogf/gf/errors/gerror"
|
||||
"github.com/gogf/gf/text/gstr"
|
||||
"github.com/gogf/gf/util/gconv"
|
||||
"github.com/gogf/gf/util/gutil"
|
||||
@ -32,23 +32,27 @@ import (
|
||||
// ScanList(&users, "User")
|
||||
// ScanList(&users, "UserDetail", "User", "uid:Uid")
|
||||
// ScanList(&users, "UserScores", "User", "uid:Uid")
|
||||
//
|
||||
// The parameters "User"/"UserDetail"/"UserScores" in the example codes specify the target attribute struct
|
||||
// that current result will be bound to.
|
||||
//
|
||||
// The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational
|
||||
// struct attribute name. It automatically calculates the HasOne/HasMany relationship with given <relation>
|
||||
// parameter.
|
||||
// struct attribute name - not the attribute name of the bound to target. In the example codes, it's attribute
|
||||
// name "Uid" of "User" of entity "Entity". It automatically calculates the HasOne/HasMany relationship with
|
||||
// given <relation> parameter.
|
||||
//
|
||||
// See the example or unit testing cases for clear understanding for this function.
|
||||
func (r Result) ScanList(listPointer interface{}, attributeName string, relation ...string) (err error) {
|
||||
// Necessary checks for parameters.
|
||||
if attributeName == "" {
|
||||
return errors.New(`attributeName should not be empty`)
|
||||
return gerror.New(`attributeName should not be empty`)
|
||||
}
|
||||
if len(relation) > 0 {
|
||||
if len(relation) < 2 {
|
||||
return errors.New(`relation name and key should are both necessary`)
|
||||
return gerror.New(`relation name and key should are both necessary`)
|
||||
}
|
||||
if relation[0] == "" || relation[1] == "" {
|
||||
return errors.New(`relation name and key should not be empty`)
|
||||
return gerror.New(`relation name and key should not be empty`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,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:
|
||||
@ -192,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)
|
||||
@ -203,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
|
||||
}
|
||||
}
|
||||
@ -222,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
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2018 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -57,7 +57,7 @@ func init() {
|
||||
nodePrefix.Prefix = PREFIX1
|
||||
gdb.AddConfigNode("test", configNode)
|
||||
gdb.AddConfigNode("prefix", nodePrefix)
|
||||
gdb.AddConfigNode(gdb.DEFAULT_GROUP_NAME, configNode)
|
||||
gdb.AddConfigNode(gdb.DefaultGroupName, configNode)
|
||||
// Default db.
|
||||
if r, err := gdb.New(); err != nil {
|
||||
gtest.Error(err)
|
||||
@ -114,7 +114,7 @@ func createTableWithDb(db gdb.DB, table ...string) (name string) {
|
||||
case "sqlite":
|
||||
if _, err := db.Exec(fmt.Sprintf(`
|
||||
CREATE TABLE %s (
|
||||
id bigint NOT NULL,
|
||||
id bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
passport varchar(45),
|
||||
password char(32) NOT NULL,
|
||||
nickname varchar(45) NOT NULL,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
|
||||
// Copyright GoFrame 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,
|
||||
@ -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()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user