Merge remote-tracking branch 'upstream/master'

This commit is contained in:
lindan.he
2020-12-28 11:06:38 +08:00
380 changed files with 12589 additions and 6190 deletions

View File

@ -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
}

View File

@ -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()

View File

@ -1,10 +1,15 @@
# MySQL数据库配置
# MySQL.
[database]
debug = true
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?parseTime=true&loc=Local"
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"

View File

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

View File

@ -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)
}

View File

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

View File

@ -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)
}

View File

@ -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()

View File

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

View File

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

View File

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

View File

@ -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")
},
})

View File

@ -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()

View File

@ -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)

View File

@ -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"))

View File

@ -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"))),
)

View File

@ -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()
})

View File

@ -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")
},
})

View File

@ -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")
},
})

View File

@ -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")
})
})

View File

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

View File

@ -6,6 +6,7 @@ import (
func main() {
s := g.Server()
s.SetConfigWithMap(g.Map{"Graceful": true})
s.EnableAdmin()
s.SetPort(8199)
s.Run()

View File

@ -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) {

View File

@ -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"},

View File

@ -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")
})
})

View File

@ -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"))
})

View File

@ -13,7 +13,7 @@ func main() {
key := "key"
// 第一次锁带时间
gmlock.Lock(key, 1000)
gmlock.Lock(key)
glog.Println("lock1")
// 这个时候上一次的计时解锁已失效
gmlock.Unlock(key)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ go:
- "1.12.x"
- "1.13.x"
- "1.14.x"
- "1.15.x"
branches:
only:

View File

@ -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"/>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

@ -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`视频教程地址:
- bilibilihttps://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`过大时的整型变量溢出问题;

View File

@ -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实现修正设计BUGhttps://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标签失效问题

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

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

View File

@ -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
}

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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
View 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
}

View File

@ -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...)
}

View 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)
})
}

View 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,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)

View 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,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
}

View 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,
@ -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.

View 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
//}

View 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,

View 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)

View 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,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)

View 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,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]
}

View 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"
"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)

View 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,
@ -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)

View File

@ -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)
}

View 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,
@ -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
}

View 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,

View 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 {

View 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,
@ -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...)
}

View 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,
@ -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
}

View 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,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")
}

View 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,
@ -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
}

View 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,

View 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,

View 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

View 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,
@ -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 ""
}

View 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,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)...,
)
}

View 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,
@ -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
}

View 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,

View 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,

View 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
}

View 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,
@ -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()
}

View 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,
@ -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.

View 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,

View 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

View 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,

View 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
}
}

View 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,

View 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,

View 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,

View 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,
@ -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