diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f6ba75c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.go] +indent_style = tab +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f6df75f --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +Thumbs.db +.DS_Store +*.swp +/.idea/ +/vendor/ +glide.lock +*.sqlite +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.sqlite +*.log diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4d7d1d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Gorose Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..efafbc5 --- /dev/null +++ b/README.md @@ -0,0 +1,355 @@ +# GoRose-Pro for Commercial(专业版) + +[![GoDoc](https://godoc.org/github.com/tobycroft/gorose-pro?status.svg)](https://godoc.org/github.com/tobycroft/gorose-pro) +[![Go Report Card](https://goreportcard.com/badge/github.com/tobycroft/gorose-pro)](https://goreportcard.com/report/github.com/tobycroft/gorose-pro) +[![GitHub release](https://img.shields.io/github/release/tobycroft/gorose.svg)](https://github.com/tobycroft/gorose-pro/releases/latest) +[![Gitter](https://badges.gitter.im/tobycroft/gorose.svg)](https://gitter.im/gorose/wechat) +![GitHub](https://img.shields.io/github/license/tobycroft/gorose?color=blue) +![GitHub All Releases](https://img.shields.io/github/downloads/tobycroft/gorose/total?color=blue) + +gorose-orm + +``` + _______ ______ .______ ______ _______. _______ + / _____| / __ \ | _ \ / __ \ / || ____| +| | __ | | | | | |_) | | | | | | (----`| |__ +| | |_ | | | | | | / | | | | \ \ | __| +| |__| | | `--' | | |\ \----.| `--' | .----) | | |____ + \______| \______/ | _| `._____| \______/ |_______/ |_______| +``` + +## 文档 + +如下的开发实例我已经在自己的项目和多个商业项目中跑过了,代码上没有问题,在书写或者思想上如果和你有冲突你可以用你自己的模式来,这里只是给刚玩的朋友准备的 + +[文档开发实例1](./doc/intro.md) +[文档开发实例2](./doc/intro.md) +[文档开发实例3](./doc/intro.md) + +## 简介 + +gorose for Tuuz版是我从飞哥接手过来的项目,知道人家更新了好几版然后可能有点没兴趣了,但是我是做项目的 很多时候如果原作者不更新,我项目中有很多麻烦事都没办法解决,所以很无奈只能继续扛旗向前走了 + +因为原版框架已经很优秀了,所以这里只会做一些更新,在架构上不会做出大调整(如果大家满意这个Pro版,请Star) + +## 安装 + +- go.mod 中添加 + +```bash +require github.com/tobycroft/gorose-pro v1.2.5 +``` + +- go get + +```bash +go get -u github.com/tobycroft/gorose-pro +``` + +## 支持驱动 + +- mysql : https://github.com/go-sql-driver/mysql +- sqlite3 : https://github.com/mattn/go-sqlite3 +- postgres : https://github.com/lib/pq +- oracle : https://github.com/mattn/go-oci8 +- mssql : https://github.com/denisenkom/go-mssqldb +- clickhouse : https://github.com/kshvakov/clickhouse + +## api预览(详情请参阅文档,或如下演示) + +```go +db.Table("table_name").Fields().Where().GroupBy().Having().OrderBy().Limit().Select() +db.Table(&ModelStruct).Data().Replace() +db.Table(&ModelStruct).Data().Insert() +db.Table(....).Data().Where().Update() +db.Table(....).Where().Delete() +``` + +## Thinkphp模式用法示例 + +```go +package main + +import ( + "github.com/tobycroft/gorose-pro" +) + +func dsn() string { + dbname := "GobotQ2" + dbuser := "GobotQ" + dbpass := "123456" + dbhost := "10.0.0.170" + conntype := "tcp" + dbport := "3306" + charset := "utf8mb4" + return dbuser + ":" + dbpass + "@" + conntype + "(" + dbhost + ":" + dbport + ")/" + dbname + "?charset=" + charset + "&parseTime=true" +} + +func DbConfig() *gorose.Config { + var conf gorose.Config + conf.Driver = "mysql" + conf.SetMaxIdleConns = 90 + conf.SetMaxOpenConns = 300 + conf.Prefix = "" + conf.Dsn = dsn() + return &conf +} + +func init() { + var err error + Database, err = gorose.Open(DbConfig()) + if err != nil { + log.Panic(err) + } +} + +func DB() gorose.IOrm { + return database.Database.NewOrm() +} + +//这里是Model层,Model采用单例模式 + +//增 +func Api_insert(qq, token, ip interface{}) bool { + db := tuuz.Db().Table(table) + data := map[string]interface{}{ + "qq": qq, + "token": token, + "ip": ip, + } + db.Data(data) + _, err := db.Insert() + //_, err := db.Replace()也可以使用replace方法,看你个人 + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +//删 +func Api_delete_byToken(qq, token interface{}) bool { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + "token": token, + } + db.Where(where) + _, err := db.Delete() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +//修改 +func Api_update_password(qq, password interface{}) bool { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + data := map[string]interface{}{ + "password": password, + } + db.Data(data) + _, err := db.Update() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +//查询单条 +func Api_find(qq interface{}) gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + ret, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} + +//查询多条 +func Api_select(qq interface{}) []gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + ret, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} + +``` + +## 使用建议 + +如果你的数据返回处理比较复杂,并且是“Long Term”项目,这里建议用原版Gorose方法处理,因为我大多数是外包项目, Thinkphp类似的操作方法可以大大降低编码复杂性,性能上并不会差太多,请放心 + +单例模式极好理解,你可以使用我的方式来解耦,或者也可以使用你自己喜欢的方式 + +## 配置和链接初始化 + +简单配置DSN + +```go +var conf gorose.Config +conf.Driver = "mysql" +conf.SetMaxIdleConns = 90 +conf.SetMaxOpenConns = 300 +conf.Prefix = "" +conf.Dsn = dsn() +return &conf +``` + +更多配置, 可以配置集群,甚至可以同时配置不同数据库在一个集群中, 数据库会随机选择集群的数据库来完成对应的读写操作, 其中master是写库, slave是读库, 需要自己做好主从复制, 这里只负责读写 + +```go +var config1 = gorose.Config{Dsn: 上面的dsn} +var config2 = gorose.Config{Dsn: 上面的dsn} +var config3 = gorose.Config{Dsn: 上面的dsn} +var config4 = gorose.Config{Dsn: 上面的dsn} +var configCluster = &gorose.ConfigCluster{ +Master: []gorose.Config{config3, config4}, +Slave: []gorose.Config{config1, config2}, +Driver: "sqlite3", +} +``` + +初始化使用 + +```go +var engin *gorose.Engin +engin, err := Open(config) +//engin, err := Open(configCluster) + +if err != nil { +panic(err.Error()) +} +``` + +这里跳过原生操作,如果你喜欢这么操作,你也不会来用这个框架,这个框架就是简单方便, +自动化没那么多JJYY的规矩,只要你会写Thinkphp或者Laravel,你就可以按照自己的编程习惯来开发, +这一点上我和原作者想法是相同的 + +## 对象关系映射, orm的使用 + +- + 1. 基本链式使用,你可以在测试中这么使用,在做项目时我强烈建议你使用单例模式来调用, + 2. 我对单例模式的支持度时非常深的,这也是Pro版和原版最大的区别 + 3. 如果你并不介意repetitive代码,喜欢传统MVC模式开发,请无视如上 + +```go +var u Users +db := engin.NewOrm() +err := db.Table(&u).Fields("name").AddFields("uid","age").Distinct().Where("uid", ">", 0).OrWhere("age",18). +Group("age").Having("age>1").OrderBy("uid desc").Limit(10).Offset(1).Select() +``` + +也可以使用`xxx.Limit().Page()`,这个是固定用法,`Page()`必须在`Limit()`后边 + +- + 2. 如果不想定义struct, 又想绑定指定类型的map结果, 则可以定义map类型, 如 + +```go +type user gorose.Map +// 或者 以下的type定义, 都是可以正常解析的 +type user2 map[string]interface{} +type users3 []user +type users4 []map[string]string +type users5 []gorose.Map +type users6 []gorose.Data +``` + +- 2.1 开始使用map绑定 + +```go +db.Table(&user).Select() +db.Table(&users4).Limit(5).Select() +``` + +> 注意: 如果使用的不是slice数据结构, 则只能获取到一条数据 + +--- +这里使用的 gorose.Data , 实际上就是 `map[string]interface{}` 类型. +而 `gorose.Map`, 实际上是 `t.MapStringT` 类型, 这里出现了一个 `t` 包, +是一个golang基本数据类型的相互转换包, 请看详细介绍 http://github.com/gohouse/t + +- + 3. laravel的`First()`,`Get()`, 用来返回结果集 + 4. TP使用select和find来取回结果或者结果集,因为Select方法已经被占用,所以请按照TP的Model来理解即可 + 5. 你也可以使用直接模式来操作,就是直接填写表名, 返回两个参数, 一个是 `[]gorose.Map`结果集, 第二个是`error`,堪称简单粗暴 + 用法就是把上边的 `Select()` 方法换成 Get,First 即可, 只不过, `Select()` 只返回一个参数 + 6. 请不要使用直接表名模式的时候还使用Select方法,取不到数据哦~ + +- + 7. orm的增删改查 + +```go +db.Table(&user2).Limit(10.Select() +db.Table(&user2).Where("uid", 1).Data(gorose.Data{"name", "gorose"}).Update() +db.Table(&user2).Data(gorose.Data{"name", "gorose33"}).Insert() +db.Table(&user2).Data([]gorose.Data{{"name", "gorose33"}, "name", "gorose44"}).Insert() +db.Table(&user2).Where("uid", 1).Delete() +``` + +## 最终sql构造器, builder构造不同数据库的sql + +目前支持 mysql, sqlite3, postgres, oracle, mssql, clickhouse等符合 `database/sql` 接口支持的数据库驱动 +这一部分, 用户基本无感知, 分理出来, 主要是为了开发者可以自由添加和修改相关驱动以达到个性化的需求 + +## binder, 数据绑定对象 + +这一部分也是用户无感知的, 主要是传入的绑定对象解析和数据绑定, 同样是为了开发者个性化定制而独立出来的 + +## 模块化 + +gorose2.0 完全模块化, 每一个模块都封装了interface接口api, 模块间调用, 都是通过接口, 上层依赖下层 + +- 主模块 + - engin + gorose 初始化配置模块, 可以全局保存并复用 + - session + 真正操作数据库底层模块, 所有的操作, 最终都会走到这里来获取或修改数据 + - orm + 对象关系映射模块, 所有的orm操作, 都在这里完成 + - builder + 构建终极执行的sql模块, 可以构建任何数据库的sql, 但要符合`database/sql`包的接口 +- 子模块 + - driver + 数据库驱动模块, 被engin和builder依赖, 根据驱动来搞事情 + - binder + 结果集绑定模块, 所有的返回结果集都在这里 + +以上主模块, 都相对独立, 可以个性化定制和替换, 只要实现相应模块的接口即可. + + + +## 原版和Pro版本区别(原版没有的功能)+(猜你关心) + +- 更加适合ThinkPHP开发人员 +- 增加Nested Transaction也就是嵌套事务或者事务嵌套,目前在仿TPORM方面唯一一个支持NT功能的框架 +- 缓存支持,原版本功能已经放弃开发,本版在这个部分会使用Redis模块来支持,但是当前功能建议大家使用TuuzGoWeb来解决缓存问题 + +## 故障排查 +- Gorose存在很多问题,有些问题你可能会遇到,下面列出: + - 请尽量不要使用框架的主从模式,无论是TP还是Gorose,他能提供的稳定性,一定是不如你直接去买RDS之类的产品的,不要试图在该花钱的时候省钱 + - 出现锁机制:如果出现锁机制,排查起来请先看慢查询,正常如果时间太长,如果你恰好使用的是我推荐的书写模式,你就能定位超时点,对超时点进行分析即可,老版本在长期使用中确实有出现锁的问题,新版目前没有出现,但是也请大家注意,如果出现了,重启数据库即可解决,如果你对这个功能很不放心,你也可以不使用嵌套查询解决 + diff --git a/README_en.md b/README_en.md new file mode 100644 index 0000000..4e60c0c --- /dev/null +++ b/README_en.md @@ -0,0 +1,447 @@ + + +# GoRose ORM +[![GoDoc](https://godoc.org/github.com/gohouse/gorose/v2?status.svg)](https://godoc.org/github.com/gohouse/gorose/v2) +[![Go Report Card](https://goreportcard.com/badge/github.com/gohouse/gorose/v2)](https://goreportcard.com/report/github.com/gohouse/gorose/v2) +[![GitHub release](https://img.shields.io/github/release/gohouse/gorose.svg)](https://github.com/gohouse/gorose/v2/releases/latest) +[![Gitter](https://badges.gitter.im/gohouse/gorose.svg)](https://gitter.im/gorose/wechat) +![GitHub](https://img.shields.io/github/license/gohouse/gorose?color=blue) +![GitHub All Releases](https://img.shields.io/github/downloads/gohouse/gorose/total?color=blue) + +gorose-orm + +``` + _______ ______ .______ ______ _______. _______ + / _____| / __ \ | _ \ / __ \ / || ____| +| | __ | | | | | |_) | | | | | | (----`| |__ +| | |_ | | | | | | / | | | | \ \ | __| +| |__| | | `--' | | |\ \----.| `--' | .----) | | |____ + \______| \______/ | _| `._____| \______/ |_______/ |_______| +``` + +## translations +[English readme](README_en.md) | +[中文 readme](README.md) + +## document +[2.x doc](https://www.kancloud.cn/fizz/gorose-2/1135835) | +[1.x doc](https://www.kancloud.cn/fizz/gorose/769179) | +[0.x doc](https://gohouse.github.io/gorose/dist/en/index.html) + +## introduction +gorose is a golang orm framework, which is Inspired by laravel's eloquent. +Gorose 2.0 adopts modular architecture, communicates through the API of interface, and strictly relies on the lower layer. Each module can be disassembled, and even can be customized to its preferred appearance. +The module diagram is as follows: +![gorose.2.0.jpg](https://i.loli.net/2019/08/06/7R2GlbwUiFKOrNP.jpg) + +## installation +- go.mod +```bash +require github.com/gohouse/gorose/v2 v2.1.10 +``` +> you should use it like `import "github.com/tobycroft/gorose-pro"` + don't forget the `v2` in the end + +- docker +```bash +docker run -it --rm ababy/gorose sh -c "go run main.go" +``` +> docker image: [ababy/gorose](https://cloud.docker.com/u/ababy/repository/docker/ababy/gorose), The docker image contains the packages and runtime environment necessary for gorose, [view `Dockerfile`](https://github.com/docker-box/gorose/blob/master/golang-alpine/Dockerfile) + +- go get +```bash +go get -u github.com/gohouse/gorose +``` + +## supported drivers +- mysql : https://github.com/go-sql-driver/mysql +- sqlite3 : https://github.com/mattn/go-sqlite3 +- postgres : https://github.com/lib/pq +- oracle : https://github.com/mattn/go-oci8 +- mssql : https://github.com/denisenkom/go-mssqldb +- clickhouse : https://github.com/kshvakov/clickhouse + +## api preview +```go +db.Table().Fields().Distinct().Where().GroupBy().Having().OrderBy().Limit().Offset().Select() +db.Table().Data().Insert() +db.Table().Data().Where().Update() +db.Table().Where().Delete() +``` + +## simple usage example +```go +package main +import ( + "fmt" + "github.com/tobycroft/gorose-pro" + _ "github.com/mattn/go-sqlite3" +) +var err error +var engin *gorose.Engin +func init() { + // Global initialization and reuse of databases + // The engin here needs to be saved globally, using either global variables or singletons + // Configuration & gorose. Config {} is a single database configuration + // If you configure a read-write separation cluster, use & gorose. ConfigCluster {} + engin, err = gorose.Open(&gorose.Config{Driver: "sqlite3", Dsn: "./db.sqlite"}) + // mysql demo, remeber import mysql driver of github.com/go-sql-driver/mysql + // engin, err = gorose.Open(&gorose.Config{Driver: "mysql", Dsn: "root:root@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true"}) +} +func DB() gorose.IOrm { + return engin.NewOrm() +} +func main() { + // Native SQL, return results directly + res,err := DB().Query("select * from users where uid>? limit 2", 1) + fmt.Println(res) + affected_rows,err := DB().Execute("delete from users where uid=?", 1) + fmt.Println(affected_rows, err) + + // orm chan operation, fetch one row + res, err := DB().Table("users").First() + // res's type is map[string]interface{} + fmt.Println(res) + + // rm chan operation, fetch more rows + res2, _ := DB().Table("users").Get() + // res2's type is []map[string]interface{} + fmt.Println(res2) +} +``` + +## usage advise +Gorose provides data object binding (map, struct), while supporting string table names and map data return. It provides great flexibility. + +It is suggested that data binding should be used as a priority to complete query operation, so that the type of data source can be controlled. +Gorose provides default `gorose. Map'and `gorose. Data' types to facilitate initialization of bindings and data + +## Configuration and link initialization +Simple configuration +```go +var configSimple = &gorose.Config{ + Driver: "sqlite3", + Dsn: "./db.sqlite", +} +``` +More configurations, you can configure the cluster, or even configure different databases in a cluster at the same time. The database will randomly select the cluster database to complete the corresponding reading and writing operations, in which master is the writing database, slave is the reading database, you need to do master-slave replication, here only responsible for reading and writing. +```go +var config = &gorose.ConfigCluster{ + Master: []&gorose.Config{}{configSimple} + Slave: []&gorose.Config{}{configSimple} + Prefix: "pre_", + Driver: "sqlite3", +} +``` +Initial usage +```go +var engin *gorose.Engin +engin, err := Open(config) + +if err != nil { + panic(err.Error()) +} +``` + +## Native SQL operation (add, delete, check), session usage +Create user tables of `users` +```sql +DROP TABLE IF EXISTS "users"; +CREATE TABLE "users" ( + "uid" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "age" integer NOT NULL +); + +INSERT INTO "users" VALUES (1, 'gorose', 18); +INSERT INTO "users" VALUES (2, 'goroom', 18); +INSERT INTO "users" VALUES (3, 'fizzday', 18); +``` +define table struct +```go +type Users struct { + Uid int `gorose:"uid"` + Name string `gorose:"name"` + Age int `gorose:"age"` +} +// Set the table name. If not, use struct's name by default +func (u *Users) TableName() string { + return "users" +} +``` +Native query operation +```go +// Here is the structure object to be bound +// If you don't define a structure, you can use map, map example directly +// var u = gorose.Data{} +// var u = gorose.Map{} Both are possible. +var u Users +session := engin.NewSession() +// Here Bind () is used to store results. If you use NewOrm () initialization, you can use NewOrm (). Table (). Query () directly. +_,err := session.Bind(&u).Query("select * from users where uid=? limit 2", 1) +fmt.Println(err) +fmt.Println(u) +fmt.Println(session.LastSql()) +``` +Native inesrt delete update +```go +session.Execute("insert into users(name,age) values(?,?)(?,?)", "gorose",18,"fizzday",19) +session.Execute("update users set name=? where uid=?","gorose",1) +session.Execute("delete from users where uid=?", 1) +``` +## Object Relational Mapping, the Use of ORM + +- 1. Basic Chain Usage + +```go +var u Users +db := engin.NewOrm() +err := db.Table(&u).Fields("name").AddFields("uid","age").Distinct().Where("uid",">",0).OrWhere("age",18). + Group("age").Having("age>1").OrderBy("uid desc").Limit(10).Offset(1).Select() +``` + +- 2. If you don't want to define struct and want to bind map results of a specified type, you can define map types, such as +```go +type user gorose.Map +// Or the following type definitions can be parsed properly +type user2 map[string]interface{} +type users3 []user +type users4 []map[string]string +type users5 []gorose.Map +type users6 []gorose.Data +``` +Start using map binding +```go +db.Table(&user).Select() +db.Table(&users4).Limit(5).Select() +``` +> Note: If the slice data structure is not used, only one piece of data can be obtained. + +--- +The gorose. Data used here is actually the `map [string] interface {}'type. + +And `gorose. Map'is actually a `t. MapString' type. Here comes a `t'package, a golang basic data type conversion package. See http://github.com/gohouse/t for more details. + + +- 3. laravel's `First()`,`Get()`, Used to return the result set +That is to say, you can even pass in the table name directly without passing in various bound structs and maps, and return two parameters, one is the `[] gorose. Map `result set, and the second is `error', which is considered simple and rude. + +Usage is to replace the `Select ()'method above with Get, First, but `Select ()' returns only one parameter. + + +- 4. orm Select Update Insert Delete +```go +db.Table(&user2).Limit(10.Select() +db.Table(&user2).Where("uid", 1).Data(gorose.Data{"name","gorose"}).Update() +db.Table(&user2).Data(gorose.Data{"name","gorose33"}).Insert() +db.Table(&user2).Data([]gorose.Data{{"name","gorose33"},"name","gorose44"}).Insert() +db.Table(&user2).Where("uid", 1).Delete() +``` + +## Final SQL constructor, builder constructs SQL of different databases +Currently supports mysql, sqlite3, postgres, oracle, mssql, Clickhouse and other database drivers that conform to `database/sql` interface support +In this part, users are basically insensitive, sorted out, mainly for developers can freely add and modify related drivers to achieve personalized needs. + +## binder, Data Binding Objects +This part is also user-insensitive, mainly for incoming binding object parsing and data binding, and also for personalized customization by developers. + +## Modularization +Gorose 2.0 is fully modular, each module encapsulates the interface api, calling between modules, through the interface, the upper layer depends on the lower layer + +- Main module + - engin + gorose Initialize the configuration module, which can be saved and reused globally + - session + Really operate the database underlying module, all operations, will eventually come here to obtain or modify data. + - orm + Object relational mapping module, all ORM operations, are done here + - builder + Building the ultimate execution SQL module, you can build any database sql, but to comply with the `database / SQL ` package interface +- sub module + - driver + The database driver module, which is dependent on engin and builder, does things according to the driver + - binder + Result Set Binding Module, where all returned result sets are located + +The above main modules are relatively independent and can be customized and replaced individually, as long as the interface of the corresponding modules is realized. + +## Best Practices +sql +```sql +DROP TABLE IF EXISTS "users"; +CREATE TABLE "users" ( + "uid" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "age" integer NOT NULL +); + +INSERT INTO "users" VALUES (1, 'gorose', 18); +INSERT INTO "users" VALUES (2, 'goroom', 18); +INSERT INTO "users" VALUES (3, 'fizzday', 18); +``` +Actual Code +```go +package main + +import ( + "fmt" + "github.com/tobycroft/gorose-pro" + _ "github.com/mattn/go-sqlite3" +) + +type Users struct { + Uid int64 `gorose:"uid"` + Name string `gorose:"name"` + Age int64 `gorose:"age"` + Xxx interface{} `gorose:"-"` // This field is ignored in ORM +} + +func (u *Users) TableName() string { + return "users" +} + +var err error +var engin *gorose.Engin + +func init() { + // Global initialization and reuse of databases + // The engin here needs to be saved globally, using either global variables or singletons + // Configuration & gorose. Config {} is a single database configuration + // If you configure a read-write separation cluster, use & gorose. ConfigCluster {} + engin, err = gorose.Open(&gorose.Config{Driver: "sqlite3", Dsn: "./db.sqlite"}) +} +func DB() gorose.IOrm { + return engin.NewOrm() +} +func main() { + // A variable DB is defined here to reuse the DB object, and you can use db. LastSql () to get the SQL that was executed last. + // If you don't reuse db, but use DB () directly, you create a new ORM object, which is brand new every time. + // So reusing DB must be within the current session cycle + db := DB() + + // fetch a row + var u Users + // bind result to user{} + err = db.Table(&u).Fields("uid,name,age").Where("age",">",0).OrderBy("uid desc").Select() + if err!=nil { + fmt.Println(err) + } + fmt.Println(u, u.Name) + fmt.Println(db.LastSql()) + + // fetch multi rows + // bind result to []Users, db and context condition parameters are reused here + // If you don't want to reuse, you can use DB() to open a new session, or db.Reset() + // db.Reset() only removes contextual parameter interference, does not change links, DB() will change links. + var u2 []Users + err = db.Table(&u2).Limit(10).Offset(1).Select() + fmt.Println(u2) + + // count + var count int64 + // Here reset clears the parameter interference of the upper query and can count all the data. If it is not clear, the condition is the condition of the upper query. + // At the same time, DB () can be called new, without interference. + count,err = db.Reset().Count() + // or + count, err = DB().Table(&u).Count() + fmt.Println(count, err) +} +``` + +## Advanced Usage + +- Chunk Data Fragmentation, Mass Data Batch Processing (Cumulative Processing) + + ` When a large amount of data needs to be manipulated, the chunk method can be used if it is unreasonable to take it out at one time and then operate it again. + The first parameter of chunk is the amount of data specified for a single operation. According to the volume of business, 100 or 1000 can be selected. + The second parameter of chunk is a callback method for writing normal data processing logic + The goal is to process large amounts of data senselessly + The principle of implementation is that each operation automatically records the current operation position, and the next time the data is retrieved again, the data is retrieved from the current position. + ` + ```go + User := db.Table("users") + User.Fields("id, name").Where("id",">",2).Chunk(2, func(data []gorose.Data) error { + // for _,item := range data { + // fmt.Println(item) + // } + fmt.Println(data) + + // don't forget return error or nil + return nil + }) + + // print result: + // map[id:3 name:gorose] + // map[id:4 name:fizzday] + // map[id:5 name:fizz3] + // map[id:6 name:gohouse] + [map[id:3 name:gorose] map[name:fizzday id:4]] + [map[id:5 name:fizz3] map[id:6 name:gohouse]] + ``` + +- Loop Data fragmentation, mass data batch processing (from scratch) + + ` Similar to chunk method, the implementation principle is that every operation is to fetch data from the beginning. + Reason: When we change data, the result of the change may affect the result of our data taking as where condition, so we can use Loop.` + ```go + User := db.Table("users") + User.Fields("id, name").Where("id",">",2).Loop(2, func(data []gorose.Data) error { + // for _,item := range data { + // fmt.Println(item) + // } + // here run update / delete + + // don't forget return error or nil + return nil + }) + ``` + +- where nested + + ```go + // SELECT * FROM users + // WHERE id > 1 + // and ( name = 'fizz' + // or ( name = 'fizz2' + // and ( name = 'fizz3' or website like 'fizzday%') + // ) + // ) + // and job = 'it' LIMIT 1 + User := db.Table("users") + User.Where("id", ">", 1).Where(func() { + User.Where("name", "fizz").OrWhere(func() { + User.Where("name", "fizz2").Where(func() { + User.Where("name", "fizz3").OrWhere("website", "like", "fizzday%") + }) + }) + }).Where("job", "it").First() + ``` + +## realease log +- v2.1.x: + * update join with auto table prefix + * add query return with []map[string]interface{} +- v2.0.0: new version, new structure + +## Upgrade Guide +### from 2.0.x to 2.1.x +- change `xxx.Join("pre_tablename")` into `xxx.Join("tablename")`,the join table name auto prefix +- change `err:=DB().Bind().Query()` into `res,err:=DB().Query()` with multi return,leave the `Bind()` method as well +### from 1.x to 2.x +install it for new + + +## Jetbrains non-commercial sponsorship +[![](https://www.jetbrains.com/shop/static/images/jetbrains-logo-inv.svg)](https://www.jetbrains.com/?from=gorose) +----- +## pay me a coffee +wechat|alipay|[paypal: click](https://www.paypal.me/fizzday) +---|---|--- +| | + +- pay list + +total | avator +---|--- +¥100 | [![](https://avatars1.githubusercontent.com/u/53846155?s=96&v=4)](https://github.com/sanjinhub) + diff --git a/binder.go b/binder.go new file mode 100644 index 0000000..770c81c --- /dev/null +++ b/binder.go @@ -0,0 +1,311 @@ +package gorose + +import ( + "errors" + "fmt" + "github.com/gohouse/t" + "reflect" +) + +// Map ... +type Map t.MapStringT + +// Data ... +type Data map[string]interface{} + +// BindType ... +type BindType int + +const ( + // OBJECT_STRUCT 结构体 一条数据 (struct) + OBJECT_STRUCT BindType = iota + // OBJECT_STRUCT_SLICE 结构体 多条数据 ([]struct) + OBJECT_STRUCT_SLICE + // OBJECT_MAP map 一条数据 (map[string]interface{}) + OBJECT_MAP + // OBJECT_MAP_SLICE map 多条数据 ([]map[string]interface{}) + OBJECT_MAP_SLICE + // OBJECT_STRING 非结构体 表名字符串 ("users") + OBJECT_STRING + // OBJECT_MAP_T map 一条数据 (map[string]t.Type) + OBJECT_MAP_T + // OBJECT_MAP_SLICE_T map 多条数据 ([]map[string]t.Type) + OBJECT_MAP_SLICE_T + // OBJECT_NIL 默认没有传入任何绑定对象,一般用于query直接返回 + OBJECT_NIL +) + +// BindString ... +var BindString = map[BindType]string{ + OBJECT_STRUCT: "OBJECT_STRUCT", + OBJECT_STRUCT_SLICE: "OBJECT_STRUCT_SLICE", + OBJECT_MAP: "OBJECT_MAP", + OBJECT_MAP_SLICE: "OBJECT_MAP_SLICE", + OBJECT_STRING: "OBJECT_STRING", + OBJECT_MAP_T: "OBJECT_MAP_T", + OBJECT_MAP_SLICE_T: "OBJECT_MAP_SLICE_T", + OBJECT_NIL: "OBJECT_NIL", +} + +// BindType.String ... +func (b BindType) String() string { + return BindString[b] +} + +// Binder ... +type Binder struct { + // Bind是指传入的对象 [slice]map,[slice]struct + // 传入的原始对象 + BindOrigin interface{} + //BindOriginTableName []string + // 解析出来的对象名字, 或者指定的method(TableName)获取到的名字 + BindName string + // 一条结果的反射对象 + BindResult interface{} + // 多条 + BindResultSlice reflect.Value + // 传入结构体解析出来的字段 + BindFields []string + // 传入的对象类型判定 + BindType BindType + // 出入传入得是非slice对象, 则只需要取一条, 取多了也是浪费 + BindLimit int + BindPrefix string + // 多条map结果,传入的是string table时 + BindAll []Data +} + +var _ IBinder = &Binder{} + +// NewBinder ... +func NewBinder(o ...interface{}) *Binder { + var binder = new(Binder) + if len(o) > 0 { + binder.SetBindOrigin(o[0]) + } else { + binder.BindType = OBJECT_NIL + } + return binder +} + +// BindParse ... +func (o *Binder) BindParse(prefix string) error { + if o.GetBindOrigin() == nil { + return nil + } + var BindName string + switch o.GetBindOrigin().(type) { + case string: // 直接传入的是表名 + o.SetBindType(OBJECT_STRING) + BindName = o.GetBindOrigin().(string) + //o.SetBindAll([]Map{}) + + // 传入的是struct或切片 + default: + // 清空字段值,避免手动传入字段污染struct字段 + o.SetBindFields([]string{}) + // make sure dst is an appropriate type + dstVal := reflect.ValueOf(o.GetBindOrigin()) + sliceVal := reflect.Indirect(dstVal) + + switch sliceVal.Kind() { + case reflect.Struct: // struct + o.SetBindType(OBJECT_STRUCT) + BindName = sliceVal.Type().Name() + o.SetBindResult(o.GetBindOrigin()) + //// 默认只查一条 + //o.SetBindLimit(1) + // 解析出字段 + o.parseFields() + // 是否设置了表名 + switch dstVal.Kind() { + case reflect.Ptr, reflect.Struct: + if tn := dstVal.MethodByName("TableName"); tn.IsValid() { + BindName = tn.Call(nil)[0].String() + } + default: + return errors.New("传入的对象有误,示例:var user User,传入 &user{}") + } + case reflect.Map: // map + o.SetBindType(OBJECT_MAP) + //// 默认只查一条 + //o.SetBindLimit(1) + // + o.SetBindResult(o.GetBindOrigin()) + //TODO 检查map的值类型, 是否是t.Type + if sliceVal.Type().Elem() == reflect.ValueOf(map[string]t.Type{}).Type().Elem() { + o.SetBindType(OBJECT_MAP_T) + } + // 是否设置了表名 + if dstVal.Kind() != reflect.Ptr { + return errors.New("传入的不是map指针,如:var user gorose.Map,传入 &user{}") + } + if tn := dstVal.MethodByName("TableName"); tn.IsValid() { + BindName = tn.Call(nil)[0].String() + } + + case reflect.Slice: // []struct,[]map + eltType := sliceVal.Type().Elem() + + switch eltType.Kind() { + case reflect.Map: + o.SetBindType(OBJECT_MAP_SLICE) + o.SetBindResult(reflect.MakeMap(eltType).Interface()) + o.SetBindResultSlice(sliceVal) + //o.SetBindResultSlice(reflect.MakeSlice(sliceVal.Type(),0,0)) + //TODO 检查map的值类型, 是否是t.Type + if eltType.Elem() == reflect.ValueOf(map[string]t.Type{}).Type().Elem() { + o.SetBindType(OBJECT_MAP_SLICE_T) + } + if dstVal.Kind() != reflect.Ptr { + return errors.New("传入的不是map指针,如:var user gorose.Map,传入 &user{}") + } + // 检查设置表名 + r2val := reflect.New(eltType) + if tn := r2val.MethodByName("TableName"); tn.IsValid() { + BindName = tn.Call(nil)[0].String() + } + + case reflect.Struct: + o.SetBindType(OBJECT_STRUCT_SLICE) + BindName = eltType.Name() + br := reflect.New(eltType) + o.SetBindResult(br.Interface()) + o.SetBindResultSlice(sliceVal) + // 解析出字段 + o.parseFields() + + // 是否设置了表名 + switch dstVal.Kind() { + case reflect.Ptr, reflect.Struct: + if tn := br.MethodByName("TableName"); tn.IsValid() { + BindName = tn.Call(nil)[0].String() + } + default: + return errors.New("传入的对象有误,示例:var user User,传入 &user{}") + } + default: + return fmt.Errorf("table只接收 struct,[]struct,map[string]interface{},[]map[string]interface{}的对象和地址, 但是传入的是: %T", o.GetBindOrigin()) + } + // 是否设置了表名 + if tn := dstVal.MethodByName("TableName"); tn.IsValid() { + BindName = tn.Call(nil)[0].String() + } + default: + return fmt.Errorf("table只接收 struct,[]struct,map[string]interface{},[]map[string]interface{}, 但是传入的是: %T", o.GetBindOrigin()) + } + } + + o.SetBindName(prefix + BindName) + o.SetBindPrefix(prefix) + return nil +} + +func (o *Binder) parseFields() { + if len(o.GetBindFields()) == 0 { + o.SetBindFields(getTagName(o.GetBindResult(), TAGNAME)) + } +} + +// ResetBindResultSlice ... +func (o *Binder) ResetBindResultSlice() { + if o.BindType == OBJECT_MAP_SLICE_T { + o.BindResultSlice = reflect.New(o.BindResultSlice.Type()) + } +} + +// SetBindPrefix ... +func (o *Binder) SetBindPrefix(arg string) { + o.BindPrefix = arg +} + +// GetBindPrefix ... +func (o *Binder) GetBindPrefix() string { + return o.BindPrefix +} + +// SetBindOrigin ... +func (o *Binder) SetBindOrigin(arg interface{}) { + o.BindOrigin = arg +} + +// GetBindOrigin ... +func (o *Binder) GetBindOrigin() interface{} { + return o.BindOrigin +} + +// SetBindName ... +func (o *Binder) SetBindName(arg string) { + o.BindName = arg +} + +// GetBindName ... +func (o *Binder) GetBindName() string { + return o.BindName +} + +// SetBindResult ... +func (o *Binder) SetBindResult(arg interface{}) { + o.BindResult = arg +} + +// GetBindResult ... +func (o *Binder) GetBindResult() interface{} { + return o.BindResult +} + +// SetBindResultSlice ... +func (o *Binder) SetBindResultSlice(arg reflect.Value) { + o.BindResultSlice = arg +} + +// GetBindResultSlice ... +func (o *Binder) GetBindResultSlice() reflect.Value { + return o.BindResultSlice +} + +// SetBindFields ... +func (o *Binder) SetBindFields(arg []string) { + o.BindFields = arg +} + +// GetBindFields ... +func (o *Binder) GetBindFields() []string { + return o.BindFields +} + +// SetBindType ... +func (o *Binder) SetBindType(arg BindType) { + o.BindType = arg +} + +// GetBindType ... +func (o *Binder) GetBindType() BindType { + return o.BindType +} + +// SetBindAll ... +func (o *Binder) SetBindAll(arg []Data) { + o.BindAll = arg +} + +// GetBindAll ... +func (o *Binder) GetBindAll() []Data { + return o.BindAll +} + +// ResetBinder ... +func (o *Binder) ResetBinder() { + switch o.BindType { + case OBJECT_STRUCT, OBJECT_MAP, OBJECT_MAP_T: + // 清空结果 + o.SetBindOrigin(nil) + case OBJECT_STRUCT_SLICE, OBJECT_MAP_SLICE, OBJECT_MAP_SLICE_T: + //var rvResult = reflect.ValueOf(o.GetBindResult()) + var rvResult = o.GetBindResultSlice() + // 清空结果 + rvResult.Set(rvResult.Slice(0, 0)) + default: + o.SetBindAll([]Data{}) + } +} diff --git a/binder_interface.go b/binder_interface.go new file mode 100644 index 0000000..908adb5 --- /dev/null +++ b/binder_interface.go @@ -0,0 +1,28 @@ +package gorose + +import "reflect" + +// IBinder 数据绑定对象接口 +type IBinder interface { + SetBindOrigin(arg interface{}) + GetBindOrigin() interface{} + SetBindName(arg string) + GetBindName() string + SetBindResult(arg interface{}) + GetBindResult() interface{} + SetBindResultSlice(arg reflect.Value) + GetBindResultSlice() reflect.Value + SetBindFields(arg []string) + GetBindFields() []string + SetBindType(arg BindType) + GetBindType() BindType + //SetBindLimit(arg int) + //GetBindLimit() int + BindParse(prefix string) error + SetBindPrefix(arg string) + GetBindPrefix() string + ResetBindResultSlice() + SetBindAll(arg []Data) + GetBindAll() []Data + ResetBinder() +} diff --git a/builder.go b/builder.go new file mode 100644 index 0000000..92d1479 --- /dev/null +++ b/builder.go @@ -0,0 +1,6 @@ +package gorose + +// NewBuilder 获取builder +func NewBuilder(driver string) IBuilder { + return NewBuilderDriver().Getter(driver) +} diff --git a/builder_clickhouse.go b/builder_clickhouse.go new file mode 100644 index 0000000..34fc1dd --- /dev/null +++ b/builder_clickhouse.go @@ -0,0 +1,42 @@ +package gorose + +const ( + // DriverClickhouse ... + DriverClickhouse = "clickhouse" +) + +// BuilderClickhouse ... +type BuilderClickhouse struct { + FieldQuotesDefault + //IOrm + driver string +} + +// sqlstr := fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", +// distinct, fields, table, join, where, group, having, order, limit, offset) +// select {distinct} {fields} from {table} {join} {where} {group} {having} {order} {limit} {offset} +// {execute} {table} {data} {where} +func init() { + var builder = &BuilderClickhouse{} + NewBuilderDriver().Register(DriverClickhouse, builder) +} + +// NewBuilderClickhouse ... +func NewBuilderClickhouse() *BuilderClickhouse { + return new(BuilderClickhouse) +} + +// Clone : a new obj +func (b *BuilderClickhouse) Clone() IBuilder { + return &BuilderClickhouse{} +} + +// BuildQuery : build query sql string +func (b *BuilderClickhouse) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderClickhouse()).SetDriver(DriverClickhouse).BuildQuery() +} + +// BuildExecut : build execute sql string +func (b *BuilderClickhouse) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderClickhouse()).SetDriver(DriverClickhouse).BuildExecute(operType) +} diff --git a/builder_default.go b/builder_default.go new file mode 100644 index 0000000..b9f1007 --- /dev/null +++ b/builder_default.go @@ -0,0 +1,608 @@ +package gorose + +import ( + "errors" + "fmt" + "github.com/gohouse/golib/structEngin" + "github.com/gohouse/t" + "reflect" + "strconv" + "strings" +) + +var operator = []string{"=", ">", "<", "!=", "<>", ">=", "<=", "like", "not like", + "in", "not in", "between", "not between", "regexp", "not regexp"} + +// BuilderDefault 默认构造器, 其他的可以继承这个, 重写方法即可 +type BuilderDefault struct { + IOrm + operator []string + placeholder int + driver string + bindValues []interface{} + current IBuilder +} + +// NewBuilderDefault 初始化 +func NewBuilderDefault(o IOrm, current IBuilder) *BuilderDefault { + //onceBuilderDefault.Do(func() { + // builderDefault = new(BuilderDefault) + // builderDefault.operator = operator + // builderDefault.driver = "mysql" + //}) + + builderDefault := new(BuilderDefault) + builderDefault.operator = operator + builderDefault.driver = "mysql" + + builderDefault.IOrm = o + // 每次使用的时候, 重置为0, 方便pg的占位符使用 + builderDefault.placeholder = 0 + + builderDefault.current = current + return builderDefault +} + +// SetDriver 设置驱动, 方便获取占位符使用 +func (b *BuilderDefault) SetDriver(dr string) *BuilderDefault { + b.driver = dr + return b +} + +// SetBindValues ... +func (b *BuilderDefault) SetBindValues(bv interface{}) { + b.bindValues = append(b.bindValues, bv) +} + +// GetBindValues ... +func (b *BuilderDefault) GetBindValues() []interface{} { + return b.bindValues +} + +// GetPlaceholder 获取占位符 +func (b *BuilderDefault) GetPlaceholder() (phstr string) { + switch b.driver { + case "postgres": + withLockContext(func() { + ph := b.placeholder + 1 + phstr = fmt.Sprintf("$%v", ph) + b.placeholder = ph + }) + default: + phstr = "?" + } + return +} + +// BuildQuery 构造query +func (b *BuilderDefault) BuildQuery() (sqlStr string, args []interface{}, err error) { + //b.IOrm = o + join, err := b.BuildJoin() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + where, err := b.BuildWhere() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + sqlStr = fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", + b.BuildDistinct(), b.BuildFields(), b.BuildTable(), join, where, + b.BuildGroup(), b.BuildHaving(), b.BuildOrder(), b.BuildLimit(), b.BuildOffset()) + + //args = b.bindParams + args = b.GetBindValues() + return +} + +// BuilderDefault.BuildExecut : build execute query string +func (b *BuilderDefault) BuildExecute(operType string) (sqlStr string, args []interface{}, err error) { + // insert : {"name":"fizz, "website":"fizzday.net"} or {{"name":"fizz2", "website":"www.fizzday.net"}, {"name":"fizz", "website":"fizzday.net"}}} + // update : {"name":"fizz", "website":"fizzday.net"} + // delete : ... + //b.IOrm = o + var update, insertkey, insertval string + if operType != "delete" { + if b.IOrm.GetData() == nil { + err = errors.New("insert,update请传入数据操作") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + update, insertkey, insertval = b.BuildData(operType) + if update == "" && insertkey == "" { + err = errors.New("传入数据为空") + return + } + } + + var where string + switch operType { + case "insert": + sqlStr = fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", b.BuildTable(), insertkey, insertval) + case "replace": + sqlStr = fmt.Sprintf("REPLACE INTO %s (%s) VALUES %s", b.BuildTable(), insertkey, insertval) + case "update": + where, err = b.BuildWhere() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + if where == "" && b.IOrm.GetForce() == false { + err = errors.New("出于安全考虑, update时where条件不能为空, 如果真的不需要where条件, 请使用Force()(如: db.xxx.Force().Update())") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + sqlStr = fmt.Sprintf("UPDATE %s SET %s%s", b.BuildTable(), update, where) + case "delete": + + where, err = b.BuildWhere() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + if where == "" && b.IOrm.GetForce() == false { + err = errors.New("出于安全考虑, delete时where条件不能为空, 如果真的不需要where条件, 请使用Force()(如: db.xxx.Force().Delete())") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + sqlStr = fmt.Sprintf("DELETE FROM %s%s", b.BuildTable(), where) + } + + args = b.GetBindValues() + return +} + +// BuildData : build inert or update data +func (b *BuilderDefault) BuildData(operType string) (string, string, string) { + data := b.IOrm.GetData() + ref := reflect.Indirect(reflect.ValueOf(data)) + + switch ref.Kind() { + case reflect.Struct: + return b.parseData(operType, structEngin.New().SetExtraCols(b.IOrm.GetExtraCols()).StructContent2Map(data)) + case reflect.Map: + var tmp = []map[string]interface{}{t.New(data).MapStringInterface()} + return b.parseData(operType, tmp) + case reflect.Slice: + switch ref.Type().Elem().Kind() { + case reflect.Struct: + return b.parseData(operType, structEngin.New().SetExtraCols(b.IOrm.GetExtraCols()).StructContent2Map(data)) + case reflect.Map: + return b.parseData(operType, t.New(data).SliceMapStringInterface()) + } + case reflect.String: + return data.(string), "", "" + } + return "", "", "" +} + +// BuildData2 ... +func (b *BuilderDefault) BuildData2(operType string) (string, string, string) { + // insert + var dataFields []string + var dataValues []string + // update or delete + var dataObj []string + + data := b.IOrm.GetData() + + switch data.(type) { + case string: + dataObj = append(dataObj, data.(string)) + case []map[string]interface{}, []Data: // insert multi datas ([]map[string]interface{}) + sliceData := t.New(data).Slice() + for key := range sliceData[0].MapStringT() { + if inArray(key, dataFields) == false { + dataFields = append(dataFields, key) + } + } + for _, itemt := range sliceData { + item := itemt.MapStringT() + var dataValuesSub []string + for _, key := range dataFields { + if item[key] == nil { + dataValuesSub = append(dataValuesSub, "null") + } else { + dataValuesSub = append(dataValuesSub, b.GetPlaceholder()) + b.SetBindValues(item[key]) + } + } + dataValues = append(dataValues, "("+strings.Join(dataValuesSub, ",")+")") + } + case map[string]interface{}, Data: // update or insert (map[string]interface{}) + var dataValuesSub []string + for key, val := range t.New(data).MapStringT() { + if operType == "insert" { + // insert + dataFields = append(dataFields, key) + if val.Interface() == nil { + dataValuesSub = append(dataValuesSub, "null") + } else { + dataValuesSub = append(dataValuesSub, b.GetPlaceholder()) + b.SetBindValues(val.Interface()) + } + } else if operType == "update" { + // update + if val.Interface() == nil { + dataObj = append(dataObj, key+"=null") + } else { + dataObj = append(dataObj, key+"="+b.GetPlaceholder()) + b.SetBindValues(val.Interface()) + } + } + } + if operType == "insert" { + // insert + dataValues = append(dataValues, "("+strings.Join(dataValuesSub, ",")+")") + } + default: + //ref := reflect.Indirect(reflect.ValueOf(data)) + //switch ref.Kind() { + //case reflect.Struct: + // structEngin.New().StructContent2Map(data) + //case reflect.Map: + //case reflect.Slice: + // switch ref.Type().Elem().Kind() { + // case reflect.Struct: + // case reflect.Map: + // } + //} + + return "", "", "" + } + + return strings.Join(dataObj, ","), strings.Join(dataFields, ","), strings.Join(dataValues, ",") +} + +func (b *BuilderDefault) parseData(operType string, data []map[string]interface{}) (string, string, string) { + // insert + var insertFields []string + var insertValues []string + // update or delete + var dataObj []string + + if len(data) == 0 { + return "", "", "" + } + + for key := range data[0] { + if inArray(key, insertFields) == false { + insertFields = append(insertFields, key) + } + } + for _, item := range data { + // 定义插入1条数据的存储 + var insertValuesSub []string + for _, key := range insertFields { + placeholder := b.GetPlaceholder() + if item[key] == nil { + if operType == "insert" || operType == "replace" { + // 放入占位符 + insertValuesSub = append(insertValuesSub, placeholder) + } + // 保存真正的值为null + b.SetBindValues("null") + } else { + if operType == "insert" || operType == "replace" { + // 放入占位符 + insertValuesSub = append(insertValuesSub, placeholder) + } + // 保存真正的值 + b.SetBindValues(item[key]) + } + //insertValues = append(insertValues, "("+strings.Join(insertValuesSub, ",")+")") + // update + dataObj = append(dataObj, fmt.Sprintf("%s = %s", b.current.AddFieldQuotes(key), placeholder)) + } + insertValues = append(insertValues, "("+strings.Join(insertValuesSub, ",")+")") + } + var tmpInsertFields = insertFields[:0] + for _, v := range insertFields { + tmpInsertFields = append(tmpInsertFields, b.current.AddFieldQuotes(v)) + } + return strings.Join(dataObj, ","), strings.Join(tmpInsertFields, ","), strings.Join(insertValues, ",") +} + +// BuildJoin ... +func (b *BuilderDefault) BuildJoin() (s string, err error) { + // 用户传入的join参数+join类型 + var join []interface{} + var returnJoinArr []string + joinArr := b.GetJoin() + + for _, join = range joinArr { + var w string + var ok bool + // 用户传入 join 的where值, 即第二个参数 + var args []interface{} + + if len(join) != 2 { + err = errors.New("join conditions are wrong") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + // 获取真正的用户传入的join参数 + if args, ok = join[1].([]interface{}); !ok { + err = errors.New("join conditions are wrong") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + argsLength := len(args) + var prefix = b.IOrm.GetISession().GetIEngin().GetPrefix() + // 如果表名是 struct,则需要解析出表名 + if argsLength > 1 { + // 只有长度大于2,才有可能使用对象.不然,就是个字符串 + switch args[0].(type) { + case string: + break + default: + rl := reflect.ValueOf(args[0]) + if tn := rl.MethodByName("TableName"); tn.IsValid() { + args[0] = tn.Call(nil)[0].String() + } + } + } + switch argsLength { + case 1: // join字符串 raw + w = fmt.Sprintf("%s%s", prefix, args[0]) + case 2: // join表 + 字符串 + w = fmt.Sprintf("%s%s ON %s", prefix, args[0], args[1]) + case 3: // join表 + (a字段=b字段) + w = fmt.Sprintf("%s%s ON %s = %s", prefix, args[0], args[1], args[2]) + case 4: // join表 + (a字段+关系+b字段) + w = fmt.Sprintf("%s%s ON %s %s %s", prefix, args[0], args[1], args[2], args[3]) + default: + err = errors.New("join format error") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + returnJoinArr = append(returnJoinArr, " "+join[0].(string)+" JOIN "+w) + } + + return strings.Join(returnJoinArr, " "), nil +} + +// BuildWhere ... +func (b *BuilderDefault) BuildWhere() (where string, err error) { + var beforeParseWhere = b.IOrm.GetWhere() + where, err = b.parseWhere(b.IOrm) + b.IOrm.SetWhere(beforeParseWhere) + return If(where == "", "", " WHERE "+where).(string), err +} + +// BuildDistinct ... +func (b *BuilderDefault) BuildDistinct() (dis string) { + return If(b.IOrm.GetDistinct(), "DISTINCT ", "").(string) +} + +// BuildFields ... +func (b *BuilderDefault) BuildFields() string { + if len(b.IOrm.GetFields()) == 0 { + return "*" + } + return strings.Join(b.IOrm.GetFields(), ",") +} + +// BuildTable ... +func (b *BuilderDefault) BuildTable() string { + return b.current.AddFieldQuotes(b.IOrm.GetTable()) +} + +// BuildGroup ... +func (b *BuilderDefault) BuildGroup() string { + return If(b.IOrm.GetGroup() == "", "", " GROUP BY "+b.IOrm.GetGroup()).(string) +} + +// BuildHaving ... +func (b *BuilderDefault) BuildHaving() string { + return If(b.IOrm.GetHaving() == "", "", " HAVING "+b.IOrm.GetHaving()).(string) +} + +// BuildOrder ... +func (b *BuilderDefault) BuildOrder() string { + return If(b.IOrm.GetOrder() == "", "", " ORDER BY "+b.IOrm.GetOrder()).(string) +} + +// BuildLimit ... +func (b *BuilderDefault) BuildLimit() string { + //if b.IOrm.GetUnion() != nil { + // return "" + //} + return If(b.IOrm.GetLimit() == 0, "", " LIMIT "+strconv.Itoa(b.IOrm.GetLimit())).(string) +} + +// BuildOffset ... +func (b *BuilderDefault) BuildOffset() string { + //if b.BuildLimit() == "" { + // return "" + //} + if b.IOrm.GetUnion() != nil { + return "" + } + return If(b.IOrm.GetOffset() == 0, "", " OFFSET "+strconv.Itoa(b.IOrm.GetOffset())).(string) +} + +// parseWhere : parse where condition +func (b *BuilderDefault) parseWhere(ormApi IOrm) (string, error) { + // 取出所有where + wheres := ormApi.GetWhere() + // where解析后存放每一项的容器 + var where []string + + for _, args := range wheres { + // and或者or条件 + var condition = args[0].(string) + // 统计当前数组中有多少个参数 + params := args[1].([]interface{}) + paramsLength := len(params) + + switch paramsLength { + case 3: // 常规3个参数: {"id",">",1} + res, err := b.parseParams(params, ormApi) + if err != nil { + return res, err + } + where = append(where, condition+" "+res) + + case 2: // 常规2个参数: {"id",1} + res, err := b.parseParams(params, ormApi) + if err != nil { + return res, err + } + where = append(where, condition+" "+res) + case 1: // 二维数组或字符串 + switch paramReal := params[0].(type) { + case string: + where = append(where, condition+" ("+paramReal+")") + case map[string]interface{}: // map + var whereArr []string + for key, val := range paramReal { + whereArr = append(whereArr, b.current.AddFieldQuotes(key)+"="+b.GetPlaceholder()) + b.SetBindValues(val) + } + if len(whereArr) != 0 { + where = append(where, condition+" ("+strings.Join(whereArr, " and ")+")") + } + case Data: // map + var whereArr []string + for key, val := range paramReal { + whereArr = append(whereArr, b.current.AddFieldQuotes(key)+"="+b.GetPlaceholder()) + b.SetBindValues(val) + } + if len(whereArr) != 0 { + where = append(where, condition+" ("+strings.Join(whereArr, " and ")+")") + } + case []interface{}: // 一维数组 + var whereArr []string + whereMoreLength := len(paramReal) + switch whereMoreLength { + case 3, 2, 1: + res, err := b.parseParams(paramReal, ormApi) + if err != nil { + return res, err + } + whereArr = append(whereArr, res) + default: + return "", errors.New("where data format is wrong") + } + if len(whereArr) != 0 { + where = append(where, condition+" ("+strings.Join(whereArr, " and ")+")") + } + case [][]interface{}: // 二维数组 + var whereMore []string + for _, arr := range paramReal { // {{"a", 1}, {"id", ">", 1}} + whereMoreLength := len(arr) + switch whereMoreLength { + case 3, 2, 1: + res, err := b.parseParams(arr, ormApi) + if err != nil { + return res, err + } + whereMore = append(whereMore, res) + default: + return "", errors.New("where data format is wrong") + } + } + if len(whereMore) != 0 { + where = append(where, condition+" ("+strings.Join(whereMore, " and ")+")") + } + case func(): + // 清空where,给嵌套的where让路,复用这个节点 + ormApi.SetWhere([][]interface{}{}) + + // 执行嵌套where放入Database struct + paramReal() + // 再解析一遍后来嵌套进去的where + wherenested, err := b.parseWhere(ormApi) + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return "", err + } + // 嵌套的where放入一个括号内 + where = append(where, condition+" ("+wherenested+")") + default: + return "", errors.New("where data format is wrong") + } + } + } + + // 合并where,去掉左侧的空格,and,or并返回 + return strings.TrimLeft( + strings.TrimPrefix( + strings.TrimPrefix( + strings.Trim( + strings.Join(where, " "), + " "), + "and"), + "or"), + " "), nil +} + +/** + * 将where条件中的参数转换为where条件字符串 + * example: {"id",">",1}, {"age", 18} + */ +// parseParams : 将where条件中的参数转换为where条件字符串 +func (b *BuilderDefault) parseParams(args []interface{}, ormApi IOrm) (s string, err error) { + paramsLength := len(args) + argsReal := args + + // 存储当前所有数据的数组 + var paramsToArr []string + + switch paramsLength { + case 3: // 常规3个参数: {"id",">",1} + //if !inArray(argsReal[1], b.GetRegex()) { + if !inArray(argsReal[1], b.GetOperator()) { + err = errors.New("where parameter is wrong") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + //paramsToArr = append(paramsToArr, argsReal[0].(string)) + paramsToArr = append(paramsToArr, b.current.AddFieldQuotes(argsReal[0].(string))) + paramsToArr = append(paramsToArr, argsReal[1].(string)) + + switch strings.Trim(strings.ToLower(t.New(argsReal[1]).String()), " ") { + //case "like", "not like": + // paramsToArr = append(paramsToArr, b.GetPlaceholder()) + // b.SetBindValues(argsReal[2]) + case "in", "not in": + var tmp []string + var ar2 = t.New(argsReal[2]).Slice() + for _, item := range ar2 { + tmp = append(tmp, b.GetPlaceholder()) + b.SetBindValues(t.New(item).Interface()) + } + paramsToArr = append(paramsToArr, "("+strings.Join(tmp, ",")+")") + + case "between", "not between": + var ar2 = t.New(argsReal[2]).Slice() + paramsToArr = append(paramsToArr, b.GetPlaceholder()+" and "+b.GetPlaceholder()) + b.SetBindValues(ar2[0].Interface()) + b.SetBindValues(ar2[1].Interface()) + + default: + paramsToArr = append(paramsToArr, b.GetPlaceholder()) + b.SetBindValues(argsReal[2]) + } + case 2: + paramsToArr = append(paramsToArr, b.current.AddFieldQuotes(argsReal[0].(string))) + paramsToArr = append(paramsToArr, "=") + paramsToArr = append(paramsToArr, b.GetPlaceholder()) + b.SetBindValues(argsReal[1]) + case 1: + paramsToArr = append(paramsToArr, argsReal[0].(string)) + } + + return strings.Join(paramsToArr, " "), nil +} + +// GetOperator ... +func (b *BuilderDefault) GetOperator() []string { + return b.operator +} diff --git a/builder_driver.go b/builder_driver.go new file mode 100644 index 0000000..99301b8 --- /dev/null +++ b/builder_driver.go @@ -0,0 +1,41 @@ +package gorose + +import ( + "sync" +) + +// BuilderDriver ... +type BuilderDriver struct { + builders map[string]IBuilder + b *sync.Map +} + +var builderDriverOnce sync.Once +var builderDriver *BuilderDriver + +// NewBuilderDriver ... +func NewBuilderDriver() *BuilderDriver { + builderDriverOnce.Do(func() { + //builderDriver = &BuilderDriver{builders:make(map[string]IBuilder)} + builderDriver = &BuilderDriver{b: &sync.Map{}} + }) + return builderDriver +} + +// Register ... +func (b *BuilderDriver) Register(driver string, val IBuilder) { + //withLockContext(func() { + // b.builders[driver] = val + //}) + b.b.Store(driver, val) +} + +// Getter ... +func (b *BuilderDriver) Getter(driver string) IBuilder { + + //return b.builders[driver] + if v, ok := b.b.Load(driver); ok { + return v.(IBuilder) + } + return nil +} diff --git a/builder_interface.go b/builder_interface.go new file mode 100644 index 0000000..e8b2abb --- /dev/null +++ b/builder_interface.go @@ -0,0 +1,31 @@ +package gorose + +import ( + "fmt" + "regexp" +) + +// IBuilder ... +type IBuilder interface { + IFieldQuotes + BuildQuery(orm IOrm) (sqlStr string, args []interface{}, err error) + BuildExecute(orm IOrm, operType string) (sqlStr string, args []interface{}, err error) + Clone() IBuilder + //GetIOrm() IOrm +} + +// IFieldQuotes 给系统关键词冲突的字段加引号,如: mysql是反引号, pg是双引号 +type IFieldQuotes interface { + AddFieldQuotes(field string) string +} + +type FieldQuotesDefault struct { +} + +func (FieldQuotesDefault) AddFieldQuotes(field string) string { + reg := regexp.MustCompile(`^\w+$`) + if reg.MatchString(field) { + return fmt.Sprintf("`%s`", field) + } + return field +} diff --git a/builder_mssql.go b/builder_mssql.go new file mode 100644 index 0000000..3c4cd38 --- /dev/null +++ b/builder_mssql.go @@ -0,0 +1,42 @@ +package gorose + +const ( + // DriverMsSql ... + DriverMsSql = "mssql" +) + +// BuilderMsSql ... +type BuilderMsSql struct { + FieldQuotesDefault + //IOrm + driver string +} + +// sqlstr := fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", +// distinct, fields, table, join, where, group, having, order, limit, offset) +// select {distinct} {fields} from {table} {join} {where} {group} {having} {order} {limit} {offset} +// {execute} {table} {data} {where} +func init() { + var builder = &BuilderMsSql{driver: DriverMsSql} + NewBuilderDriver().Register(DriverMsSql, builder) +} + +// NewBuilderMsSql ... +func NewBuilderMsSql() *BuilderMsSql { + return new(BuilderMsSql) +} + +// Clone : a new obj +func (b *BuilderMsSql) Clone() IBuilder { + return &BuilderMsSql{driver: DriverMsSql} +} + +// BuildQuery : build query sql string +func (b *BuilderMsSql) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderMsSql()).SetDriver(b.driver).BuildQuery() +} + +// BuildExecut : build execute sql string +func (b *BuilderMsSql) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderMsSql()).SetDriver(b.driver).BuildExecute(operType) +} diff --git a/builder_mysql.go b/builder_mysql.go new file mode 100644 index 0000000..82ec317 --- /dev/null +++ b/builder_mysql.go @@ -0,0 +1,45 @@ +package gorose + +const ( + // DriverMysql ... + DriverMysql = "mysql" +) + +// BuilderMysql ... +type BuilderMysql struct { + FieldQuotesDefault + //IOrm + driver string +} + +var _ IBuilder = (*BuilderMysql)(nil) + +// sqlstr := fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", +// distinct, fields, table, join, where, group, having, order, limit, offset) +// select {distinct} {fields} from {table} {join} {where} {group} {having} {order} {limit} {offset} +// {execute} {table} {data} {where} +func init() { + NewBuilderDriver().Register(DriverMysql, NewBuilderMysql()) +} + +// NewBuilderMysql ... +func NewBuilderMysql() *BuilderMysql { + return new(BuilderMysql) +} + +// Clone : a new obj +func (b *BuilderMysql) Clone() IBuilder { + return NewBuilderMysql() +} + +// BuildQuery : build query sql string +func (b *BuilderMysql) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + //fmt.Println(o.GetTable(),o.GetWhere()) + sqlStr, args, err = NewBuilderDefault(o, NewBuilderMysql()).SetDriver(DriverMysql).BuildQuery() + return +} + +// BuildExecut : build execute sql string +func (b *BuilderMysql) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderMysql()).SetDriver(DriverMysql).BuildExecute(operType) +} diff --git a/builder_oracle.go b/builder_oracle.go new file mode 100644 index 0000000..9623cde --- /dev/null +++ b/builder_oracle.go @@ -0,0 +1,467 @@ +package gorose + +import ( + "errors" + "fmt" + "github.com/gohouse/golib/structEngin" + "github.com/gohouse/t" + "reflect" + "strconv" + "strings" +) + +const ( + // DriverOracle ... + DriverOracle = "oci8" +) + +var ( + operatorOracle = []string{"=", ">", "<", "!=", "<>", ">=", "<=", "like", "not like", + "intersect", "minus", "union", "||", "in", "not in", "between", "not between"} +) + +// BuilderOracle ... +type BuilderOracle struct { + FieldQuotesDefault + BuilderDefault +} + +// NewBuilderOracle ... +func NewBuilderOracle(o IOrm) *BuilderOracle { + //onceBuilderDefault.Do(func() { + // builderOracle = new(BuilderOracle) + // builderOracle.operator = operatorOracle + //}) + builderOracle := new(BuilderOracle) + builderOracle.operator = operatorOracle + builderOracle.IOrm = o + // 每次使用的时候, 重置为0, 方便pg的占位符使用 + builderOracle.placeholder = 0 + + return builderOracle +} + +func init() { + var builder = &BuilderOracle{} + NewBuilderDriver().Register(DriverOracle, builder) +} + +// Clone : a new obj +func (b *BuilderOracle) Clone() IBuilder { + return &BuilderOracle{} +} + +// SetDriver 设置驱动, 方便获取占位符使用 +func (b *BuilderOracle) SetDriver(dr string) *BuilderOracle { + b.driver = dr + return b +} + +// GetPlaceholder 获取占位符 +func (b *BuilderOracle) GetPlaceholder() (phstr string) { + withLockContext(func() { + ph := b.placeholder + 1 + phstr = fmt.Sprintf(":%v", ph) + b.placeholder = ph + }) + return +} + +// BuildQueryOra ... +func (b *BuilderOracle) BuildQueryOra() (sqlStr string, args []interface{}, err error) { + //b.IOrm = o + join, err := b.BuildJoin() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + where, err := b.BuildWhere() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + if len(b.GetJoin()) > 0 { + b.GetFields() + } + + // 默认情况 + fieldsStr := b.BuildFields() + tableName := b.BuildTable() + sqlStr = fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s", b.BuildDistinct(), fieldsStr, + tableName, join, where, b.BuildLimit(), b.BuildGroup(), b.BuildHaving(), b.BuildOrder()) + + // 批量取数据需嵌套写法 + if b.GetLimit() > 0 { + aliasNameA := "tabA" + aliasNameB := "tabB" + page := b.GetOffset()/b.GetLimit() + 1 + startRow := (page-1)*b.GetLimit() + 1 + endRow := page*b.GetLimit() + 1 + + if fieldsStr == "*" { + fieldsStr = b.GetTable() + ".*, rownum r" + } else { + if b.GetGroup() == "" { + fieldsStr = fieldsStr + ", rownum r" + } + } + + // 没有group by需要1层嵌套, 有group by需要2层嵌套 + // 如果考虑orderby优化,还需要一层嵌套。目前未考虑 + if b.GetGroup() == "" { + sqlStr = fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s", b.BuildDistinct(), fieldsStr, + tableName, join, where, b.BuildLimit(), b.BuildOrder()) + + sqlStr = fmt.Sprintf("select * from (%s) %s where %s.r>=%s", + sqlStr, aliasNameA, aliasNameA, strconv.Itoa(startRow)) + } else { + sqlStr = fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s", b.BuildDistinct(), fieldsStr, + tableName, join, where, b.BuildGroup(), b.BuildHaving(), b.BuildOrder()) + + sqlStr = fmt.Sprintf( + "select * from (select %s, rownum r from (%s) %s where rownum<%s ) %s where %s.r>=%s", + aliasNameA+".*", sqlStr, aliasNameA, strconv.Itoa(endRow), aliasNameB, aliasNameB, + strconv.Itoa(startRow)) + } + } + + //args = b.bindParams + args = b.IOrm.GetBindValues() + return +} + +// BuildExecuteOra ... +func (b *BuilderOracle) BuildExecuteOra(operType string) (sqlStr string, args []interface{}, err error) { + // insert : {"name":"fizz, "website":"fizzday.net"} or {{"name":"fizz2", "website":"www.fizzday.net"}, {"name":"fizz", "website":"fizzday.net"}}} + // update : {"name":"fizz", "website":"fizzday.net"} + // delete : ... + //b.IOrm = o + var update, insertkey, insertval string + if operType != "delete" { + if b.IOrm.GetData() == nil { + err = errors.New("insert,update请传入数据操作") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + update, insertkey, insertval = b.BuildData(operType) + } + + where, err := b.BuildWhere() + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + switch operType { + case "insert": + sqlStr = fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", b.BuildTable(), insertkey, insertval) + case "update": + if where == "" && b.IOrm.GetForce() == false { + err = errors.New("出于安全考虑, update时where条件不能为空, 如果真的不需要where条件, 请使用Force()(如: db.xxx.Force().Update())") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + sqlStr = fmt.Sprintf("UPDATE %s SET %s%s", b.BuildTable(), update, where) + case "delete": + if where == "" && b.IOrm.GetForce() == false { + err = errors.New("出于安全考虑, delete时where条件不能为空, 如果真的不需要where条件, 请使用Force()(如: db.xxx.Force().Delete())") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + sqlStr = fmt.Sprintf("DELETE FROM %s%s", b.BuildTable(), where) + } + + args = b.IOrm.GetBindValues() + return +} + +// BuildData ... +func (b *BuilderOracle) BuildData(operType string) (string, string, string) { + data := b.IOrm.GetData() + ref := reflect.Indirect(reflect.ValueOf(data)) + + switch ref.Kind() { + case reflect.Struct: + return b.parseData(operType, structEngin.New().SetExtraCols(b.IOrm.GetExtraCols()).StructContent2Map(data)) + case reflect.Map: + var tmp = []map[string]interface{}{t.New(data).MapStringInterface()} + return b.parseData(operType, tmp) + case reflect.Slice: + switch ref.Type().Elem().Kind() { + case reflect.Struct: + return b.parseData(operType, structEngin.New().SetExtraCols(b.IOrm.GetExtraCols()).StructContent2Map(data)) + case reflect.Map: + return b.parseData(operType, t.New(data).SliceMapStringInterface()) + } + } + return "", "", "" +} + +// BuildData2 ... +func (b *BuilderOracle) BuildData2(operType string) (string, string, string) { + return b.BuilderDefault.BuildData2(operType) +} + +func (b *BuilderOracle) parseData(operType string, data []map[string]interface{}) (string, string, string) { + // insert + var dataFields []string + var dataValues []string + // update or delete + var dataObj []string + + for key := range data[0] { + if inArray(key, dataFields) == false { + dataFields = append(dataFields, key) + } + } + for _, item := range data { + // 定义1条数据的存储 + var dataValuesSub []string + for _, key := range dataFields { + if item[key] == nil { + // 放入占位符 + dataValuesSub = append(dataValuesSub, b.GetPlaceholder()) + // 保存真正的值为null + b.IOrm.SetBindValues("null") + } else { + // 放入占位符 + dataValuesSub = append(dataValuesSub, b.GetPlaceholder()) + // 保存真正的值 + b.IOrm.SetBindValues(item[key]) + } + // update + dataObj = append(dataObj, fmt.Sprintf("%s=%s", key, b.GetPlaceholder())) + } + dataValues = append(dataValues, "("+strings.Join(dataValuesSub, ",")+")") + } + return strings.Join(dataObj, ","), strings.Join(dataFields, ","), strings.Join(dataValues, ",") +} + +// BuildJoin ... +func (b *BuilderOracle) BuildJoin() (s string, err error) { + return b.BuilderDefault.BuildJoin() +} + +// BuildWhere ... +func (b *BuilderOracle) BuildWhere() (where string, err error) { + var beforeParseWhere = b.IOrm.GetWhere() + where, err = b.parseWhere(b.IOrm) + b.IOrm.SetWhere(beforeParseWhere) + return If(where == "", "", " WHERE "+where).(string), err +} + +// BuildDistinct ... +func (b *BuilderOracle) BuildDistinct() (dis string) { + return b.BuilderDefault.BuildDistinct() +} + +// BuildFields ... +func (b *BuilderOracle) BuildFields() string { + return b.BuilderDefault.BuildFields() +} + +// BuildTable ... +func (b *BuilderOracle) BuildTable() string { + return b.BuilderDefault.BuildTable() +} + +// BuildGroup ... +func (b *BuilderOracle) BuildGroup() string { + return b.BuilderDefault.BuildGroup() +} + +// BuildHaving ... +func (b *BuilderOracle) BuildHaving() string { + return b.BuilderDefault.BuildHaving() +} + +// BuildOrder ... +func (b *BuilderOracle) BuildOrder() string { + return b.BuilderDefault.BuildOrder() +} + +// BuildLimit ... +func (b *BuilderOracle) BuildLimit() string { + //if b.IOrm.GetUnion() != nil { + // return "" + //} + + if b.GetLimit() == 0 { + return "" + } + + page := b.GetOffset()/b.GetLimit() + 1 + endRow := page*b.GetLimit() + 1 + + var limitStr string + if len(b.IOrm.GetWhere()) > 0 { + limitStr = fmt.Sprintf(" and rownum < %d", endRow) + } else { + limitStr = fmt.Sprintf(" where rownum < %d", endRow) + } + return If(b.IOrm.GetLimit() == 0, "", limitStr).(string) +} + +// BuildOffset ... +func (b *BuilderOracle) BuildOffset() string { + return "" +} + +func (b *BuilderOracle) parseWhere(ormApi IOrm) (string, error) { + // 取出所有where + wheres := ormApi.GetWhere() + // where解析后存放每一项的容器 + var where []string + + for _, args := range wheres { + // and或者or条件 + var condition = args[0].(string) + // 统计当前数组中有多少个参数 + params := args[1].([]interface{}) + paramsLength := len(params) + + switch paramsLength { + case 3: // 常规3个参数: {"id",">",1} + res, err := b.parseParams(params, ormApi) + if err != nil { + return res, err + } + where = append(where, condition+" "+res) + + case 2: // 常规2个参数: {"id",1} + res, err := b.parseParams(params, ormApi) + if err != nil { + return res, err + } + where = append(where, condition+" "+res) + case 1: // 二维数组或字符串 + switch paramReal := params[0].(type) { + case string: + where = append(where, condition+" ("+paramReal+")") + case map[string]interface{}: // 一维数组 + var whereArr []string + for key, val := range paramReal { + whereArr = append(whereArr, key+"="+b.GetPlaceholder()) + b.IOrm.SetBindValues(val) + } + where = append(where, condition+" ("+strings.Join(whereArr, " and ")+")") + case [][]interface{}: // 二维数组 + var whereMore []string + for _, arr := range paramReal { // {{"a", 1}, {"id", ">", 1}} + whereMoreLength := len(arr) + switch whereMoreLength { + case 3: + res, err := b.parseParams(arr, ormApi) + if err != nil { + return res, err + } + whereMore = append(whereMore, res) + case 2: + res, err := b.parseParams(arr, ormApi) + if err != nil { + return res, err + } + whereMore = append(whereMore, res) + default: + return "", errors.New("where data format is wrong") + } + } + where = append(where, condition+" ("+strings.Join(whereMore, " and ")+")") + case func(): + // 清空where,给嵌套的where让路,复用这个节点 + ormApi.SetWhere([][]interface{}{}) + + // 执行嵌套where放入Database struct + paramReal() + // 再解析一遍后来嵌套进去的where + wherenested, err := b.parseWhere(ormApi) + if err != nil { + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return "", err + } + // 嵌套的where放入一个括号内 + where = append(where, condition+" ("+wherenested+")") + default: + return "", errors.New("where data format is wrong") + } + } + } + + // 合并where,去掉左侧的空格,and,or并返回 + return strings.TrimLeft( + strings.TrimPrefix( + strings.TrimPrefix( + strings.Trim( + strings.Join(where, " "), + " "), + "and"), + "or"), + " "), nil +} + +func (b *BuilderOracle) parseParams(args []interface{}, ormApi IOrm) (s string, err error) { + paramsLength := len(args) + argsReal := args + + // 存储当前所有数据的数组 + var paramsToArr []string + + switch paramsLength { + case 3: // 常规3个参数: {"id",">",1} + //if !inArray(argsReal[1], b.GetRegex()) { + if !inArray(argsReal[1], b.GetOperator()) { + err = errors.New("where parameter is wrong") + b.IOrm.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + + paramsToArr = append(paramsToArr, argsReal[0].(string)) + paramsToArr = append(paramsToArr, argsReal[1].(string)) + + switch argsReal[1] { + case "like", "not like": + paramsToArr = append(paramsToArr, b.GetPlaceholder()) + b.IOrm.SetBindValues(argsReal[2]) + case "in", "not in": + var tmp []string + var ar2 = t.New(argsReal[2]).Slice() + for _, item := range ar2 { + tmp = append(tmp, b.GetPlaceholder()) + b.IOrm.SetBindValues(t.New(item).Interface()) + } + paramsToArr = append(paramsToArr, "("+strings.Join(tmp, ",")+")") + case "between", "not between": + var ar2 = t.New(argsReal[2]).Slice() + paramsToArr = append(paramsToArr, b.GetPlaceholder()+" and "+b.GetPlaceholder()) + b.IOrm.SetBindValues(ar2[0].Interface()) + b.IOrm.SetBindValues(ar2[1].Interface()) + default: + paramsToArr = append(paramsToArr, b.GetPlaceholder()) + b.IOrm.SetBindValues(argsReal[2]) + } + case 2: + paramsToArr = append(paramsToArr, argsReal[0].(string)) + paramsToArr = append(paramsToArr, "=") + paramsToArr = append(paramsToArr, b.GetPlaceholder()) + b.IOrm.SetBindValues(argsReal[1]) + } + + return strings.Join(paramsToArr, " "), nil +} + +// GetOperator ... +func (b *BuilderOracle) GetOperator() []string { + return b.BuilderDefault.GetOperator() +} + +// 实现接口 +// BuildQuery : build query sql string +func (b *BuilderOracle) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + return NewBuilderOracle(o).SetDriver(DriverOracle).BuildQueryOra() +} + +// BuildExecut : build execute sql string +func (b *BuilderOracle) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderOracle(o).SetDriver(DriverOracle).BuildExecuteOra(operType) +} diff --git a/builder_postgres.go b/builder_postgres.go new file mode 100644 index 0000000..45e22f4 --- /dev/null +++ b/builder_postgres.go @@ -0,0 +1,54 @@ +package gorose + +import ( + "fmt" + "regexp" +) + +const ( + // DriverPostgres ... + DriverPostgres = "postgres" +) + +// BuilderPostgres ... +type BuilderPostgres struct { + //IOrm + driver string +} + +// sqlstr := fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", +// distinct, fields, table, join, where, group, having, order, limit, offset) +// select {distinct} {fields} from {table} {join} {where} {group} {having} {order} {limit} {offset} +// {execute} {table} {data} {where} +func init() { + var builder = &BuilderPostgres{driver: DriverPostgres} + NewBuilderDriver().Register(DriverPostgres, builder) +} + +// NewBuilderPostgres ... +func NewBuilderPostgres() *BuilderPostgres { + return new(BuilderPostgres) +} + +// Clone : a new obj +func (b *BuilderPostgres) Clone() IBuilder { + return &BuilderPostgres{driver: DriverPostgres} +} + +// BuildQuery : build query sql string +func (b *BuilderPostgres) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderPostgres()).SetDriver(b.driver).BuildQuery() +} + +// BuildExecut : build execute sql string +func (b *BuilderPostgres) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderPostgres()).SetDriver(b.driver).BuildExecute(operType) +} + +func (*BuilderPostgres) AddFieldQuotes(field string) string { + reg := regexp.MustCompile(`^\w+$`) + if reg.MatchString(field) { + return fmt.Sprintf(`"%s"`, field) + } + return field +} diff --git a/builder_sqlite3.go b/builder_sqlite3.go new file mode 100644 index 0000000..d730334 --- /dev/null +++ b/builder_sqlite3.go @@ -0,0 +1,42 @@ +package gorose + +const ( + // DriverSqlite3 ... + DriverSqlite3 = "sqlite3" +) + +// BuilderSqlite3 ... +type BuilderSqlite3 struct { + FieldQuotesDefault + //IOrm + driver string +} + +// sqlstr := fmt.Sprintf("SELECT %s%s FROM %s%s%s%s%s%s%s%s", +// distinct, fields, table, join, where, group, having, order, limit, offset) +// select {distinct} {fields} from {table} {join} {where} {group} {having} {order} {limit} {offset} +// {execute} {table} {data} {where} +func init() { + var builder = &BuilderSqlite3{} + NewBuilderDriver().Register(DriverSqlite3, builder) +} + +// NewBuilderSqlite3 ... +func NewBuilderSqlite3() *BuilderSqlite3 { + return new(BuilderSqlite3) +} + +// Clone : a new obj +func (b *BuilderSqlite3) Clone() IBuilder { + return &BuilderSqlite3{driver: DriverSqlite3} +} + +// BuildQuery : build query sql string +func (b *BuilderSqlite3) BuildQuery(o IOrm) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderSqlite3()).SetDriver(b.driver).BuildQuery() +} + +// BuildExecut : build execute sql string +func (b *BuilderSqlite3) BuildExecute(o IOrm, operType string) (sqlStr string, args []interface{}, err error) { + return NewBuilderDefault(o, NewBuilderSqlite3()).SetDriver(b.driver).BuildExecute(operType) +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..af9f50e --- /dev/null +++ b/config.go @@ -0,0 +1,20 @@ +package gorose + +// Config ... +type Config struct { + Driver string `json:"driver"` // 驱动: mysql/sqlite3/oracle/mssql/postgres/clickhouse, 如果集群配置了驱动, 这里可以省略 + // mysql 示例: + // root:root@tcp(localhost:3306)/test?charset=utf8mb4&parseTime=true + Dsn string `json:"dsn"` // 数据库链接 + SetMaxOpenConns int `json:"setMaxOpenConns"` // (连接池)最大打开的连接数,默认值为0表示不限制 + SetMaxIdleConns int `json:"setMaxIdleConns"` // (连接池)闲置的连接数, 默认0 + Prefix string `json:"prefix"` // 表前缀, 如果集群配置了前缀, 这里可以省略 +} + +// ConfigCluster ... +type ConfigCluster struct { + Master []Config // 主 + Slave []Config // 从 + Driver string // 驱动 + Prefix string // 前缀 +} diff --git a/doc/base.md b/doc/base.md new file mode 100644 index 0000000..28a23ee --- /dev/null +++ b/doc/base.md @@ -0,0 +1,102 @@ +# 基础方法 + + +以下两种方法将会贯穿我们的教程始终,请务必区分,根据实际情况来区分 + +Tuuz.Db()方法是我的框架中创建数据库的方法,你可以根据你自己的情况来,后续不再赘述 + +~~~ +看教程中,请仔细看我的文字!!! +看教程中,请仔细看我的文字!!! +看教程中,请仔细看我的文字!!! +看教程中,请仔细看我的文字!!! +看教程中,请仔细看我的文字!!! +看教程中,请仔细看我的文字!!! + +~~~ + +注意观察DB的生成! + +每次查询都创建新的数据库对象 + +```go +func Api_find(group_id, user_id, cid interface{}) gorose.Data { + //Tuuz.Db()方法是我的框架中创建数据库的方法,你可以根据你自己的情况来,后续不再赘述 + db := Tuuz.Db().Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "cid": cid, + } + db.Where(where) + ret, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` + +每次查询使用外部注入的数据库对象,请注意这里的type + +```go +type Interface struct { + Db gorose.IOrm +} + + +func (self *Interface) Api_find(group_id, user_id, cid interface{}) gorose.Data { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "cid": cid, + } + db.Where(where) + ret, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` +这两种方法需要在限定场景和非限定场景中选择 + +请大家务必根据需要来进行选择,这里推荐使用新建数据的方法,能达到最高效率 + +这里选择使用注入方法和新生成方法的原因是如果在Transaction或者Nested Transaction方法下,事务因为是隔离的 +所以不在一个数据流(对象)下的查询是无法查询到隔离内的事务修改的,举个栗子 + +数据库中有一个字段A,里面只有一条数据,数据为123 + +例如在完全无事务的条件下流程是这样的: + +A->find[输出123]->update(A,456)->find[输出456] + +在find使用创建法,update导入法 + +A->开始事务->(不在事务中find[输出123])->update(A,456)->(不在事务中find[输出123])->提交事务->(不在事务中find[输出456]) + +find导入法,update导入法 + +A->开始事务->find[输出123]->update(A,456)->find[输出456]->提交事务->find[输出456] + + +明白了吧?如果你做的是交易所或者资金类的程序,这个概念你必须要搞懂! + + +1.[select方法](./select.md) + +2.[find/first方法](./find.md) + +3.[update方法](./update.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/doc/delete.md b/doc/delete.md new file mode 100644 index 0000000..be348ae --- /dev/null +++ b/doc/delete.md @@ -0,0 +1,36 @@ +# Delete方法 + + +```go + +func Api_delete(qq interface{}) bool { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + _, err := db.Delete() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +``` + +很简单,没啥说的,没有where的时候delete会出错 + + +0.[基础准备](./base.md) + +1.[select方法](./select.md) + +2.[find/first方法](./find.md) + +3.[update方法](./update.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/doc/find.md b/doc/find.md new file mode 100644 index 0000000..3654d0b --- /dev/null +++ b/doc/find.md @@ -0,0 +1,73 @@ +# Find/First查找单条数据的方法 + +基础方法已经在base里面展示过了,这里说个特殊的 + +如果大多数时候希望直接创建法,在做资金查询的时候使用注入法,要怎么做呢? + +很简单,先把注入法写好,再写一个创建法,创建后注入Interface里面即可 + +```go +type Interface struct { +Db gorose.IOrm +} + +func Api_find(group_id, user_id, dj_id interface{}) gorose.Data { + var self Interface + self.Db = tuuz.Db() + return Api_find(group_id, user_id, dj_id) +} + +func (self *Interface) Api_find(group_id, user_id, dj_id interface{}) gorose.Data { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "dj_id": dj_id, + } + db.Where(where) + data, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return data + } +} +``` + + +find方法十分的简单,没有难度,相信大家会很快上手 + +find方法中没有太多骚操作,除此之外还有value拉sum拉之类的方法,大同小异 + +```go +func Api_value_balance(group_id, user_id interface{}) interface{} { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + ret, err := db.Value("balance") + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` + + + +0.[基础准备](./base.md) + +1.[select方法](./select.md) + +3.[update方法](./update.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/doc/intro.md b/doc/intro.md new file mode 100644 index 0000000..0ce43e8 --- /dev/null +++ b/doc/intro.md @@ -0,0 +1,197 @@ +# 先说思想 + +1.为什么开头都叫Api?叫别的行不行? + +受到Golang的限制,首字母大写挎包能调用到,那么前面几个关键字就很重要了, +所以在设计数据库层的时候,这里使用的是单例模式,每个对数据的调用都是API的形式, +所以开头叫Api,你也可以改成别的 + +2.为什么又蛇形又大小写? + +可恶心到很多人,其实蛇形的目的是为了把动作分开, + +例如通过“username字段找1个用户”,Java或者PHP开发喜欢写GetUser,Py开发喜欢get_user这也没问题, +很多朋友是大神喜欢直接写目的,不过后续2开让人接手就很吐了,或者过1年看自己代码就难受了,我一开始开发也是这样的, +后来才开始使用“过程”代替“目的”的命名方式 + + +给个示例: + +例如: +~~~ +Api_find_byQqandPassword() +~~~ + +~~~ +1.这种命名方式请问他是数据库方法还是一个逻辑方法 + +2.他是增删改查中的哪一种? + +3.他返回的是单条数据还是多条数据,我是使用map[string]interface{}来处理还是[]map[string]interface{}来处理? + +4.这个方法主要需要哪两种数据类型才能完成查询? +~~~ + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + +好的如上问题请在看下面的代码前回答 + + + +· + +· + +· + +· + + +```go +func Api_find_byQqandPassword(qq, password interface{}) gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + "password": password, + } + db.Where(where) + ret, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` + +好的,你在对照下Model代码,你看过程式命名方法是不是给后面接手你代码的人带来了很大的便利 + +写代码就像玩魂斗罗,要么一人跳太快给另一人给拖死,要么后面的太慢了把前面的拖死,如果你还没有决定项目规范,GorosePro的这个方案可以作为你的备选呢! + + +那么如果是展示呢?一般在读取数据的时候,find和select后面都应该跟着重点字段,例如这个数据输出,他是什么类型的 +例如这里我需要把可展示的内容列出,那么这里的命名就变成了canShow,因为字段的问题,所以前小后大,请大家注意 + + +```go +func Api_select_canShow() []gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "can_show": 1, + } + db.Where(where) + data, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return data + } +} +``` + + +如果这些案例你能看懂那么接下来这个案例就比较特别,需要使用群id,用户id,道具id来查询对应那条数据的num字段 + +命名是Api_value_num,你会发现在这里没有by了,因为数据太复杂了,请注意,虽然过程命名法可以让后来的人更方便的读懂你的项目, +知道你每个方法期待调用什么类型的数据,但是你可别把所有的查询限定项都写到方法名称中去,这么做虽然方便别人了,但是容易挨揍! + +所以各位用户请掌握好度 + + +```go +func (self *Interface) Api_value_num(group_id, user_id, dj_id interface{}) int64 { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "dj_id": dj_id, + } + db.Where(where) + data, err := db.Value("num") + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return 0 + } else { + if data == nil { + return 0 + } else { + return data.(int64) + } + } +} +``` +同理还有 + +```go +func (self *Interface) Api_sum_byCid(cid interface{}) float64 { + db := self.Db.Table(table) + where := map[string]interface{}{ + "cid": cid, + } + db.Where(where) + ret, err := db.Sum("amount") + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return 0 + } else { + if ret != nil { + return ret.(float64) + } else { + return 0 + } + } +} +``` + +Tuuz.Db()方法是我的框架中创建数据库的方法,你可以根据你自己的情况来,后续不再赘述 + + +以上是命名方法的解说 + +那么我们就要具体案例分析了: + +0.[基础准备](./base.md) + +1.[select方法](./select.md) + +2.[find/first方法](./find.md) + +3.[update方法](./update.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) + +~~~ +好的如果你能平心静气的看到这里你就会发现 + +下划线只是为了分割动作!!!命名方式依旧遵循了标准驼峰,蛇形只是表象而已 + +如果你心平气和的读到这里,那么恭喜你你是一个接受能力很强且不会刚愎自用的人 + +我真诚的邀请您加入我的群:94537310 +~~~ diff --git a/doc/join.md b/doc/join.md new file mode 100644 index 0000000..54175c2 --- /dev/null +++ b/doc/join.md @@ -0,0 +1,37 @@ +# 更多方法 + +## Join方法 +```go +func (self *Interface) Api_join_select(group_id, user_id interface{}) []gorose.Data { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + db.Join("coin on coin.id=cid") + db.Where("amount", ">", 0) + ret, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` + + +0.[基础准备](./base.md) + +1.[select方法](./select.md) + +2.[find/first方法](./find.md) + +3.[update方法](./update.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/doc/model.md b/doc/model.md new file mode 100644 index 0000000..5ebca34 --- /dev/null +++ b/doc/model.md @@ -0,0 +1,70 @@ +## 示例model + +```go +func Api_update_password(qq, password interface{}) bool { + //初始化db并且填写table名称 + db := tuuz.Db().Table(table) + //创建一个where,使用类型是map[string]interface,这个和TP的Array是一样的 + where := map[string]interface{}{ + "qq": qq, + } + //导入where + db.Where(where) + //这里如果是查询就db.dFind()或者db.Get()结束掉了,这里是修改,所以继续 + //使用同样的方法创建同类型数据map + data := map[string]interface{}{ + "password": password, + } + //导入map,使用Data方法 + db.Data(data) + //这里使用db.Update来执行修改,会输出数据库数字和错误 + _, err := db.Update() + //判断错误 + if err != nil { + //记录错误(如果你使用TuuzGoWeb就请记录,你也可使用自动记录,我更偏爱手动) + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} +``` + + +如上方法是一个修改方法,那么如果是输出单条或者多条怎么用呢? + + +单条 +```go +func Api_find(qq interface{}) gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + ret, err := db.First() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` +多条 +```go +func Api_select_canShow() []gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "can_show": 1, + } + db.Where(where) + data, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return data + } +} +``` diff --git a/doc/security.md b/doc/security.md new file mode 100644 index 0000000..38a2cfa --- /dev/null +++ b/doc/security.md @@ -0,0 +1,39 @@ +# 安全相关 + +1.都用上prepare了,基本上不会有注入了,这里说个真实的案例,我最早是从PHP过来的,在PHP那边我对prepare滚瓜烂熟 +但是毕竟golang是新的战场,再加上orm的不完善,另外就是我的业务逻辑很特殊,所以很多时候是需要直接写SQL语句的, +在早期,我使用query语句的时候,将值直接写到了sql语句中,那么懂行的人就知道这是会导致preparestatement降级成emulate +模式运行的罪魁祸首,并且会导致注入,加之当时比比较赶,又刚学Go,就没注意,就写完当天晚上怎么都睡不着,总感觉代码不对 +然后起来看sql这块的,就看到了 +~~~ +db.query("select * from table where user=参数") +~~~ +当然我不是这么写的拉,只是举个栗子,我那个句子大概写满了半个屏幕,问题点大概是上面这样,直接写参数值的 + +我一想,不对啊!PHP人家都有处理方法怎么Go没有,我就到gorose开发那边去问,没问出结果,文档也没有 + +后来我才知道,使用 +~~~ +db.query("select * from table where user=?",[]interface{}{参数}) +~~~ +来查询,所以这里分享给大家 + +当然我的项目没受到这个影响,毕竟不是B类的项目,不涉及¥没人盯 + +其他地方的安全问题还有就是在where查询的时候 +~~~ +db.where("user=参数") +~~~ +也不要这么查询!这样也会导致注入 + +正确的方法应该是 +~~~ +db.where("user","=",参数) +~~~ +或者 +~~~ +db.where("user",参数) +~~~ + +你看对于ORM来说,就是这些简单而又细节的地方可能导致你的程序出现风险,所以安全无小事,其他的安全部分, +你可以参考TP的安全说明,不过数据库这块也差不多就是我上面说的这些内容 diff --git a/doc/select.md b/doc/select.md new file mode 100644 index 0000000..71f4e19 --- /dev/null +++ b/doc/select.md @@ -0,0 +1,88 @@ +# Select方法 + +```go +func Api_select(group_id, user_id interface{}) []gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + data, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return data + } +} +``` + + +如果你看完了上一个技术分享,那么select就很好理解了,select没啥复杂的,就是where然后get,没了 + +说个稍微复杂点的查询 + +```go +func Api_select_have(group_id, user_id interface{}) []gorose.Data { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + db.Where("num", ">", 0) + data, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return data + } +} + +``` +这里就是在where的基础上,再次where,你会发现不是所有的数据都是可以对应上的,很多时候需要做范围查询, +那么这个时候使用单一的where的map就无法正常的将代码写清楚了,那么这个时候,你就可以再次使用where的指定模式 +来再次对范围内容进行限定,如上 + +好的,接下来来一个升级的 + +```go +func (self *Interface) Api_join_select(group_id, user_id interface{}) []gorose.Data { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + db.Join("coin on coin.id=cid") + db.Where("amount", ">", 0) + ret, err := db.Get() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return nil + } else { + return ret + } +} +``` + +这里来一个join方法,join方法是select方法中的骚操作,在Thinkphp中,有model方法来帮助我们做一对多多对一 + +那么用Gorose,we are on our own!难度不大,大家参考下就行了 + +使用数据库ORM很多时候需要我们自己处理数据,不过这也让我们对数据优化可以有更多的认识和提升 + + +0.[基础准备](./base.md) + +2.[find/first方法](./find.md) + +3.[update方法](./update.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/doc/update.md b/doc/update.md new file mode 100644 index 0000000..a993642 --- /dev/null +++ b/doc/update.md @@ -0,0 +1,159 @@ +# Update方法 + + +举两个例子,自己看,实在是很简单,通过XXX来查询XXX + +这两个栗子,都是不需要事务处理的,因为应用场景中,只要if update成功后就可以直接显示成功了 + +因为场景非常简单,所以这里不需要使用“注入法”来使用数据库,直接使用创建法,便捷也快速 + + +```go + +func Api_update_uname(qq, uname interface{}) bool { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + data := map[string]interface{}{ + "uname": uname, + } + db.Data(data) + _, err := db.Update() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} +``` + +```go + +func Api_update_all(qq, uname, password interface{}) bool { + db := tuuz.Db().Table(table) + where := map[string]interface{}{ + "qq": qq, + } + db.Where(where) + data := map[string]interface{}{ + "uname": uname, + "password": password, + } + db.Data(data) + _, err := db.Update() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +``` + +接下来的这个栗子,是资金修改的,所以需要使用导入法 + +(self *Interface)这个没有特殊的意思,名字都可以自己取 + +请注意,一旦涉及到transaction必须使用导入法 + +如果涉及到嵌套事务,一定要使用导入法,否则事务和事务隔离均不能生效 + +```go +//Interface你自己取个名字 +type Interface struct { + //这里面的Db你也可以自己取名字 + Db gorose.IOrm +} +``` + +```go +//这里的self你也可以自己取名字 +func (self *Interface) Api_update(group_id, user_id, balance interface{}) bool { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + } + db.Where(where) + data := map[string]interface{}{ + "balance": balance, + } + db.Data(data) + _, err := db.Update() + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} +``` + +所以非常的简单的!如果你是PHP程序员相信你秒懂 + + +这里说个特殊的Increasement方法,这个方法就很单纯,通过某个字段来递增,然后增量你可以自己定或者使用传入值 + +非常简单,这些值ORM都已经做成可接受interface数据了,所以只要你不作死去导入特殊值例如string之类的, +一般都没问题(就算传入特殊值ORM也会处理) + +```go + +func (self *Interface) Api_incr(group_id, user_id, cid, amount interface{}) bool { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "cid": cid, + } + db.Where(where) + _, err := db.Increment("amount", amount) + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} + +``` +递减也很简单,你也可以使用负值传入递增实现递减的效果,记得ABS后在负数哦~不然给人家撸口小可爱干了你的系统 + +你别怪我框架的问题啊! + +```go + +func (self *Interface) Api_decr(group_id, user_id, dj_id interface{}) bool { + db := self.Db.Table(table) + where := map[string]interface{}{ + "group_id": group_id, + "user_id": user_id, + "dj_id": dj_id, + } + db.Where(where) + _, err := db.Decrement("num", 1) + if err != nil { + Log.Dbrr(err, tuuz.FUNCTION_ALL()) + return false + } else { + return true + } +} +``` + + +0.[基础准备](./base.md) + +1.[select方法](./select.md) + +2.[find/first方法](./find.md) + +4.[delete方法](./delete.md) + +5.[insert方法](./insert.md) + +6.[安全相关](./security.md) diff --git a/engin.go b/engin.go new file mode 100644 index 0000000..9157762 --- /dev/null +++ b/engin.go @@ -0,0 +1,244 @@ +package gorose + +import ( + "database/sql" + "fmt" +) + +// TAGNAME ... +var TAGNAME = "gorose" + +// IGNORE ... +var IGNORE = "-" + +type cluster struct { + master []*sql.DB + masterSize int + slave []*sql.DB + slaveSize int +} + +// Engin ... +type Engin struct { + config *ConfigCluster + driver string + prefix string + dbs *cluster + logger ILogger +} + +var _ IEngin = (*Engin)(nil) + +// NewEngin : init Engin struct pointer +// NewEngin : 初始化 Engin 结构体对象指针 +func NewEngin(conf ...interface{}) (e *Engin, err error) { + engin := new(Engin) + if len(conf) == 0 { + return + } + + // 使用默认的log, 如果自定义了logger, 则只需要调用 Use() 方法即可覆盖 + engin.Use(DefaultLogger()) + + switch conf[0].(type) { + // 传入的是单个配置 + case *Config: + err = engin.bootSingle(conf[0].(*Config)) + // 传入的是集群配置 + case *ConfigCluster: + engin.config = conf[0].(*ConfigCluster) + err = engin.bootCluster() + default: + panic(fmt.Sprint("Open() need *gorose.Config or *gorose.ConfigCluster param, also can empty for build sql string only, but ", + conf, " given")) + } + + return engin, err +} + +// Use ... +func (c *Engin) Use(closers ...func(e *Engin)) { + for _, closer := range closers { + closer(c) + } +} + +// Ping ... +func (c *Engin) Ping() error { + //for _,item := range c.dbs.master { + // + //} + return c.GetQueryDB().Ping() +} + +// TagName 自定义结构体对应的orm字段,默认gorose +func (c *Engin) TagName(arg string) { + //c.tagName = arg + TAGNAME = arg +} + +// IgnoreName 自定义结构体对应的orm忽略字段名字,默认- +func (c *Engin) IgnoreName(arg string) { + //c.ignoreName = arg + IGNORE = arg +} + +// SetPrefix 设置表前缀 +func (c *Engin) SetPrefix(pre string) { + c.prefix = pre +} + +// GetPrefix 获取前缀 +func (c *Engin) GetPrefix() string { + return c.prefix +} + +// GetDriver ... +func (c *Engin) GetDriver() string { + return c.driver +} + +// GetQueryDB : get a slave db for using query operation +// GetQueryDB : 获取一个从库用来做查询操作 +func (c *Engin) GetQueryDB() *sql.DB { + if c.dbs.slaveSize == 0 { + return c.GetExecuteDB() + } + var randint = getRandomInt(c.dbs.slaveSize) + return c.dbs.slave[randint] +} + +// GetExecuteDB : get a master db for using execute operation +// GetExecuteDB : 获取一个主库用来做查询之外的操作 +func (c *Engin) GetExecuteDB() *sql.DB { + if c.dbs.masterSize == 0 { + return nil + } + var randint = getRandomInt(c.dbs.masterSize) + return c.dbs.master[randint] +} + +// GetLogger ... +func (c *Engin) GetLogger() ILogger { + return c.logger +} + +// SetLogger ... +func (c *Engin) SetLogger(lg ILogger) { + c.logger = lg +} + +func (c *Engin) bootSingle(conf *Config) error { + // 如果传入的是单一配置, 则转换成集群配置, 方便统一管理 + var cc = new(ConfigCluster) + cc.Master = append(cc.Master, *conf) + c.config = cc + return c.bootCluster() +} + +func (c *Engin) bootCluster() error { + //fmt.Println(len(c.config.Slave)) + if len(c.config.Slave) > 0 { + for _, item := range c.config.Slave { + if c.config.Driver != "" { + item.Driver = c.config.Driver + } + if c.config.Prefix != "" { + item.Prefix = c.config.Prefix + } + db, err := c.bootReal(item) + if err != nil { + return err + } + if c.dbs == nil { + c.dbs = new(cluster) + } + c.dbs.slave = append(c.dbs.slave, db) + c.dbs.slaveSize++ + c.driver = item.Driver + } + } + var pre, dr string + if len(c.config.Master) > 0 { + for _, item := range c.config.Master { + if c.config.Driver != "" { + item.Driver = c.config.Driver + } + if c.config.Prefix != "" { + item.Prefix = c.config.Prefix + } + db, err := c.bootReal(item) + + if err != nil { + return err + } + if c.dbs == nil { + c.dbs = new(cluster) + } + c.dbs.master = append(c.dbs.master, db) + c.dbs.masterSize = c.dbs.masterSize + 1 + c.driver = item.Driver + //fmt.Println(c.dbs.masterSize) + if item.Prefix != "" { + pre = item.Prefix + } + if item.Driver != "" { + dr = item.Driver + } + } + } + // 如果config没有设置prefix,且configcluster设置了prefix,则使用cluster的prefix + if pre != "" && c.prefix == "" { + c.prefix = pre + } + // 如果config没有设置driver,且configcluster设置了driver,则使用cluster的driver + if dr != "" && c.driver == "" { + c.driver = dr + } + + return nil +} + +// boot sql driver +func (c *Engin) bootReal(dbConf Config) (db *sql.DB, err error) { + //db, err = sql.Open("mysql", "root:root@tcp(localhost:3306)/test?charset=utf8mb4") + // 开始驱动 + db, err = sql.Open(dbConf.Driver, dbConf.Dsn) + if err != nil { + return + } + + // 检查是否可以ping通 + err = db.Ping() + if err != nil { + return + } + + // 连接池设置 + if dbConf.SetMaxOpenConns > 0 { + db.SetMaxOpenConns(dbConf.SetMaxOpenConns) + } + if dbConf.SetMaxIdleConns > 0 { + db.SetMaxIdleConns(dbConf.SetMaxIdleConns) + } + + return +} + +// NewSession 获取session实例 +// 这是一个语法糖, 为了方便使用(engin.NewSession())添加的 +// 添加后会让engin和session耦合, 如果不想耦合, 就删掉此方法 +// 删掉这个方法后,可以使用 gorose.NewSession(gorose.IEngin) +// 通过 gorose.IEngin 依赖注入的方式, 达到解耦的目的 +func (c *Engin) NewSession() ISession { + return NewSession(c) +} + +// NewOrm 获取orm实例 +// 这是一个语法糖, 为了方便使用(engin.NewOrm())添加的 +// 添加后会让engin和 orm 耦合, 如果不想耦合, 就删掉此方法 +// 删掉这个方法后,可以使用 gorose.NewOrm(gorose.NewSession(gorose.IEngin)) +// 通过 gorose.ISession 依赖注入的方式, 达到解耦的目的 +func (c *Engin) NewOrm() IOrm { + return NewOrm(c) +} diff --git a/engin_interface.go b/engin_interface.go new file mode 100644 index 0000000..6dbe59e --- /dev/null +++ b/engin_interface.go @@ -0,0 +1,18 @@ +package gorose + +import "database/sql" + +// IEngin ... +type IEngin interface { + GetExecuteDB() *sql.DB + GetQueryDB() *sql.DB + //EnableSqlLog(e ...bool) + //IfEnableSqlLog() (e bool) + //SetPrefix(pre string) + GetPrefix() (pre string) + //NewSession() ISession + //NewOrm() IOrm + SetLogger(lg ILogger) + GetLogger() ILogger + GetDriver() string +} diff --git a/engin_test.go b/engin_test.go new file mode 100644 index 0000000..99adc18 --- /dev/null +++ b/engin_test.go @@ -0,0 +1,66 @@ +package gorose + +import ( + "github.com/gohouse/t" + "testing" +) + +type aaa t.MapStringT + +func (u *aaa) TableName() string { + return "users" +} + +//type bbb MapRows +type bbb []t.MapStringT + +func (u *bbb) TableName() string { + return "users" +} + +type UsersMap Data + +func (*UsersMap) TableName() string { + return "users" +} + +// 定义map多返回绑定表名,一定要像下边这样,单独定义,否则无法获取对应的 TableName() +type UsersMapSlice []Data + +func (u *UsersMapSlice) TableName() string { + return "users" +} + +type Users struct { + Uid int64 `orm:"uid"` + Name string `orm:"name"` + Age int64 `orm:"age"` + Fi string `orm:"ignore"` +} + +func (Users) TableName() string { + return "users" +} + +type Orders struct { + Id int `orm:"id"` + GoodsName string `orm:"goodsname"` + Price float64 `orm:"price"` +} + +func TestEngin(t *testing.T) { + e := initDB() + e.SetPrefix("pre_") + + t.Log(e.GetPrefix()) + + db := e.GetQueryDB() + + err := db.Ping() + + if err != nil { + t.Error("gorose初始化失败") + } + t.Log("gorose初始化成功") + t.Log(e.GetLogger()) +} diff --git a/err.go b/err.go new file mode 100644 index 0000000..086861d --- /dev/null +++ b/err.go @@ -0,0 +1,103 @@ +package gorose + +import ( + "errors" + "fmt" +) + +// Error ... +type Error uint + +// Lang ... +type Lang uint + +const ( + // CHINESE ... + CHINESE Lang = iota + // ENGLISH ... + ENGLISH + // CHINESE_TRADITIONAL ... + CHINESE_TRADITIONAL +) + +const ( + // ERR_PARAMS_COUNTS ... + ERR_PARAMS_COUNTS Error = iota + // ERR_PARAMS_MISSING ... + ERR_PARAMS_MISSING + // ERR_PARAMS_FORMAT ... + ERR_PARAMS_FORMAT +) + +// Default ... +func (e *Err) Default() map[Error]string { + return map[Error]string{ + ERR_PARAMS_COUNTS: "参数数量有误", + ERR_PARAMS_MISSING: "参数缺失", + ERR_PARAMS_FORMAT: "参数格式错误", + } +} + +var langString = map[Lang]string{ + CHINESE: "chinese", + ENGLISH: "english", + CHINESE_TRADITIONAL: "chinese_traditional", +} + +// String ... +func (l Lang) String() string { + return langString[l] +} + +// Err ... +type Err struct { + lang Lang + err map[Lang]map[Error]string +} + +//var gOnce *sync.Once +var gErr *Err + +func init() { + var tmpLang = make(map[Lang]map[Error]string) + gErr = &Err{err: tmpLang} + gErr.lang = CHINESE + gErr.Register(gErr.Default()) +} + +// NewErr ... +func NewErr() *Err { + return gErr +} + +// SetLang ... +func (e *Err) SetLang(l Lang) { + e.lang = l +} + +// GetLang ... +func (e *Err) GetLang() Lang { + return e.lang +} + +// Register ... +func (e *Err) Register(err map[Error]string) { + e.err[e.GetLang()] = err +} + +// Get ... +func (e *Err) Get(err Error) string { + return e.err[e.GetLang()][err] +} + +// GetErr ... +func GetErr(err Error, args ...interface{}) error { + var argreal string + if len(args) > 0 { + argreal = fmt.Sprint(":", args) + } + return errors.New(fmt.Sprint( + NewErr(). + Get(err), + argreal)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6c97d10 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/tobycroft/gorose-pro + +go 1.16 + +require ( + github.com/gohouse/golib v0.0.0-20210711163732-a5c22059eb75 + github.com/gohouse/t v0.0.0-20201007094014-630049a6bfe9 + github.com/mattn/go-sqlite3 v1.14.8 +) diff --git a/gorose-pro.iml b/gorose-pro.iml new file mode 100644 index 0000000..eacc75a --- /dev/null +++ b/gorose-pro.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/gorose.go b/gorose.go new file mode 100644 index 0000000..942d514 --- /dev/null +++ b/gorose.go @@ -0,0 +1,38 @@ +package gorose + +// GOROSE_IMG ... +const GOROSE_IMG = ` + + ,ad8888ba, 88888888ba + d8"' '"8b 88 "8b +d8' 88 ,8P +88 ,adPPYba, 88aaaaaa8P' ,adPPYba, ,adPPYba, ,adPPYba, +88 88888 a8" "8a 88""""88' a8" "8a I8[ "" a8P_____88 +Y8, 88 8b d8 88 '8b 8b d8 '"Y8ba, 8PP""""""" + Y8a. .a88 "8a, ,a8" 88 '8b "8a, ,a8" aa ]8I "8b, ,aa + '"Y88888P" '"YbbdP"' 88 '8b '"YbbdP"' '"YbbdP"' '"Ybbd8"' + +` + +const ( + // VERSION_TEXT ... + VERSION_TEXT = "\ngolang orm of gorose's version : " + // VERSION_NO ... + VERSION_NO = "v2.2.0" + // VERSION ... + VERSION = VERSION_TEXT + VERSION_NO + GOROSE_IMG +) + +// Open ... +func Open(conf ...interface{}) (engin *Engin, err error) { + // 驱动engin + engin, err = NewEngin(conf...) + if err != nil { + if engin.GetLogger().EnableErrorLog() { + engin.GetLogger().Error(err.Error()) + } + return + } + + return +} diff --git a/gorose_test.go b/gorose_test.go new file mode 100644 index 0000000..d23507e --- /dev/null +++ b/gorose_test.go @@ -0,0 +1,47 @@ +package gorose + +import ( + "fmt" + _ "github.com/mattn/go-sqlite3" +) + +func initDB() *Engin { + e, err := Open(&Config{Driver: "sqlite3", Dsn: "./db.sqlite"}) + + if err != nil { + panic(err.Error()) + } + + e.TagName("orm") + e.IgnoreName("ignore") + + initTable(e) + + return e +} + +func initTable(e *Engin) { + var sql = `CREATE TABLE IF NOT EXISTS "users" ( + "uid" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL default "", + "age" integer NOT NULL default 0 +)` + var s = e.NewSession() + var err error + var aff int64 + + aff, err = s.Execute(sql) + if err != nil { + return + } + if aff == 0 { + return + } + + aff, err = s.Execute("insert into users(name,age) VALUES(?,?),(?,?),(?,?)", + "fizz", 18, "gorose", 19, "fizzday", 20) + if err != nil { + panic(err.Error()) + } + fmt.Println("初始化数据和表成功:", aff) +} diff --git a/imgs/alipay.png b/imgs/alipay.png new file mode 100644 index 0000000..facc730 Binary files /dev/null and b/imgs/alipay.png differ diff --git a/imgs/er.jpg b/imgs/er.jpg new file mode 100644 index 0000000..b70d912 Binary files /dev/null and b/imgs/er.jpg differ diff --git a/imgs/paypal.png b/imgs/paypal.png new file mode 100644 index 0000000..85236dd Binary files /dev/null and b/imgs/paypal.png differ diff --git a/imgs/wechat.png b/imgs/wechat.png new file mode 100644 index 0000000..e7589a4 Binary files /dev/null and b/imgs/wechat.png differ diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..ce0fc5d --- /dev/null +++ b/logger.go @@ -0,0 +1,124 @@ +package gorose + +import ( + "fmt" + "time" +) + +// LogLevel 日志级别 +type LogLevel uint + +const ( + // LOG_SQL ... + LOG_SQL LogLevel = iota + // LOG_SLOW ... + LOG_SLOW + // LOG_ERROR ... + LOG_ERROR +) + +// String ... +func (l LogLevel) String() string { + switch l { + case LOG_SQL: + return "SQL" + case LOG_SLOW: + return "SLOW" + case LOG_ERROR: + return "ERROR" + } + return "" +} + +// LogOption ... +type LogOption struct { + FilePath string + EnableSqlLog bool + // 是否记录慢查询, 默认0s, 不记录, 设置记录的时间阀值, 比如 1, 则表示超过1s的都记录 + EnableSlowLog float64 + EnableErrorLog bool +} + +// Logger ... +type Logger struct { + filePath string + sqlLog bool + slowLog float64 + //infoLog bool + errLog bool +} + +var _ ILogger = (*Logger)(nil) + +//var onceLogger sync.Once +var logger *Logger + +// NewLogger ... +func NewLogger(o *LogOption) *Logger { + //onceLogger.Do(func() { + logger = &Logger{filePath: "./"} + if o.FilePath != "" { + logger.filePath = o.FilePath + } + logger.sqlLog = o.EnableSqlLog + logger.slowLog = o.EnableSlowLog + logger.errLog = o.EnableErrorLog + //}) + return logger +} + +// DefaultLogger ... +func DefaultLogger() func(e *Engin) { + return func(e *Engin) { + e.logger = NewLogger(&LogOption{}) + } +} + +// EnableSqlLog ... +func (l *Logger) EnableSqlLog() bool { + return l.sqlLog +} + +// EnableErrorLog ... +func (l *Logger) EnableErrorLog() bool { + return l.errLog +} + +// EnableSlowLog ... +func (l *Logger) EnableSlowLog() float64 { + return l.slowLog +} + +// Slow ... +func (l *Logger) Slow(sqlStr string, runtime time.Duration) { + if l.EnableSlowLog() > 0 && runtime.Seconds() > l.EnableSlowLog() { + logger.write(LOG_SLOW, "gorose_slow", sqlStr, runtime.String()) + } +} + +// Sql ... +func (l *Logger) Sql(sqlStr string, runtime time.Duration) { + if l.EnableSqlLog() { + logger.write(LOG_SQL, "gorose_sql", sqlStr, runtime.String()) + } +} + +// Error ... +func (l *Logger) Error(msg string) { + if l.EnableErrorLog() { + logger.write(LOG_ERROR, "gorose", msg, "0") + } +} + +func (l *Logger) write(ll LogLevel, filename string, msg string, runtime string) { + now := time.Now() + date := now.Format("20060102") + datetime := now.Format("2006-01-02 15:04:05") + f := readFile(fmt.Sprintf("%s/%v_%v.log", l.filePath, date, filename)) + content := fmt.Sprintf("[%v] [%v] %v --- %v\n", ll.String(), datetime, runtime, msg) + withLockContext(func() { + defer f.Close() + buf := []byte(content) + f.Write(buf) + }) +} diff --git a/logger_interface.go b/logger_interface.go new file mode 100644 index 0000000..d8a4598 --- /dev/null +++ b/logger_interface.go @@ -0,0 +1,38 @@ +package gorose + +import ( + "io" + "time" +) + +// ILogger ... +type ILogger interface { + Sql(sqlStr string, runtime time.Duration) + Slow(sqlStr string, runtime time.Duration) + Error(msg string) + EnableSqlLog() bool + EnableErrorLog() bool + EnableSlowLog() float64 +} + +// 暂时规划 +type ilogger interface { + // Persist 持久化 + Persist(w io.Writer) + // Info 常规日志 + Info(args ...string) + // Error 错误日志 + Error(args ...string) + // Debug 调试日志 + Debug(args ...string) + + Infof(format string, args ...string) + Errorf(format string, args ...string) + Debugf(format string, args ...string) + InfoWithCtx(ctx interface{}, args ...string) + ErrorWithCtx(ctx interface{}, args ...string) + DebugWithCtx(ctx interface{}, args ...string) + InfofWithCtxf(ctx interface{}, format string, args ...string) + ErrorfWithCtxf(ctx interface{}, format string, args ...string) + DebugfWithCtxf(ctx interface{}, format string, args ...string) +} diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 0000000..0788606 --- /dev/null +++ b/logger_test.go @@ -0,0 +1,13 @@ +package gorose + +import ( + "testing" + "time" +) + +func TestDefaultLogger(t *testing.T) { + l := NewLogger(&LogOption{FilePath: "/tmp/gorose.log", EnableErrorLog: true}) + var sqlstr = "select xxx from xxx where a='a' and b=\"33\"" + l.Sql(sqlstr, time.Duration(1<<4)) + t.Log("logger success") +} diff --git a/orm.go b/orm.go new file mode 100644 index 0000000..178d755 --- /dev/null +++ b/orm.go @@ -0,0 +1,402 @@ +package gorose + +import ( + "github.com/gohouse/t" + "strings" +) + +//type TransactionHandlerFunc func(db IOrm) error +// Orm ... +type Orm struct { + ISession + //IBinder + *OrmApi + driver string + bindValues []interface{} +} + +var _ IOrm = (*Orm)(nil) + +// NewOrm ... +func NewOrm(e IEngin) *Orm { + var orm = new(Orm) + orm.SetISession(NewSession(e)) + //orm.IBinder = b + orm.OrmApi = new(OrmApi) + return orm +} + +func NewOrmBuilder() *Orm { + return new(Orm) +} + +// Close ... +func (dba *Orm) Close() { + dba.GetISession().Close() +} + +// ExtraCols 额外的字段 +func (dba *Orm) ExtraCols(args ...string) IOrm { + dba.extraCols = append(dba.extraCols, args...) + return dba +} + +// ResetExtraCols ... +func (dba *Orm) ResetExtraCols() IOrm { + dba.extraCols = []string{} + return dba +} + +// SetBindValues ... +func (dba *Orm) SetBindValues(v interface{}) { + dba.bindValues = append(dba.bindValues, v) +} + +// ClearBindValues ... +func (dba *Orm) ClearBindValues() { + dba.bindValues = []interface{}{} +} + +// GetBindValues ... +func (dba *Orm) GetBindValues() []interface{} { + return dba.bindValues +} + +// GetDriver ... +func (dba *Orm) GetDriver() string { + return dba.driver +} + +// SetISession ... +func (dba *Orm) SetISession(is ISession) { + dba.ISession = is +} + +// GetISession ... +func (dba *Orm) GetISession() ISession { + return dba.ISession +} + +// GetOrmApi ... +func (dba *Orm) GetOrmApi() *OrmApi { + return dba.OrmApi +} + +// Fields : select fields +func (dba *Orm) Table(tab interface{}) IOrm { + dba.GetISession().Bind(tab) + //dba.table = dba.GetISession().GetTableName() + return dba +} + +// Fields : select fields +func (dba *Orm) Fields(fields ...string) IOrm { + dba.fields = fields + return dba +} + +// AddFields : If you already have a query builder instance and you wish to add a column to its existing select clause, you may use the AddFields method: +func (dba *Orm) AddFields(fields ...string) IOrm { + dba.fields = append(dba.fields, fields...) + return dba +} + +// Distinct : select distinct +func (dba *Orm) Distinct() IOrm { + dba.distinct = true + + return dba +} + +// Data : insert or update data +func (dba *Orm) Data(data interface{}) IOrm { + dba.data = data + return dba +} + +// Group : select group by +func (dba *Orm) Group(group string) IOrm { + dba.group = group + return dba +} + +// GroupBy : equals Group() +func (dba *Orm) GroupBy(group string) IOrm { + return dba.Group(group) +} + +// Having : select having +func (dba *Orm) Having(having string) IOrm { + dba.having = having + return dba +} + +// Order : select order by +func (dba *Orm) Order(order string) IOrm { + dba.order = order + return dba +} + +// OrderBy : equal order +func (dba *Orm) OrderBy(order string) IOrm { + return dba.Order(order) +} + +// Limit : select limit +func (dba *Orm) Limit(limit int) IOrm { + dba.limit = limit + return dba +} + +// Offset : select offset +func (dba *Orm) Offset(offset int) IOrm { + dba.offset = offset + return dba +} + +// Page : select page +func (dba *Orm) Page(page int) IOrm { + dba.offset = (page - 1) * dba.GetLimit() + return dba +} + +// Where : query or execute where condition, the relation is and +func (dba *Orm) Where(args ...interface{}) IOrm { + if len(args) == 0 || + t.New(args[0]).Bool() == false { + return dba + } + // 如果只传入一个参数, 则可能是字符串、一维对象、二维数组 + // 重新组合为长度为3的数组, 第一项为关系(and/or), 第二项为具体传入的参数 []interface{} + w := []interface{}{"and", args} + + dba.where = append(dba.where, w) + + return dba +} + +// Where : query or execute where condition, the relation is and +func (dba *Orm) OrWhere(args ...interface{}) IOrm { + // 如果只传入一个参数, 则可能是字符串、一维对象、二维数组 + + // 重新组合为长度为3的数组, 第一项为关系(and/or), 第二项为具体传入的参数 []interface{} + w := []interface{}{"or", args} + + dba.where = append(dba.where, w) + + return dba +} + +// WhereNull ... +func (dba *Orm) WhereNull(arg string) IOrm { + return dba.Where(arg + " IS NULL") +} + +// OrWhereNull ... +func (dba *Orm) OrWhereNull(arg string) IOrm { + return dba.OrWhere(arg + " IS NULL") +} + +// WhereNotNull ... +func (dba *Orm) WhereNotNull(arg string) IOrm { + return dba.Where(arg + " IS NOT NULL") +} + +// OrWhereNotNull ... +func (dba *Orm) OrWhereNotNull(arg string) IOrm { + return dba.OrWhere(arg + " IS NOT NULL") +} + +// WhereRegexp ... +func (dba *Orm) WhereRegexp(arg string, expstr string) IOrm { + return dba.Where(arg, "REGEXP", expstr) +} + +// OrWhereRegexp ... +func (dba *Orm) OrWhereRegexp(arg string, expstr string) IOrm { + return dba.OrWhere(arg, "REGEXP", expstr) +} + +// WhereNotRegexp ... +func (dba *Orm) WhereNotRegexp(arg string, expstr string) IOrm { + return dba.Where(arg, "NOT REGEXP", expstr) +} + +// OrWhereNotRegexp ... +func (dba *Orm) OrWhereNotRegexp(arg string, expstr string) IOrm { + return dba.OrWhere(arg, "NOT REGEXP", expstr) +} + +// WhereIn ... +func (dba *Orm) WhereIn(needle string, hystack []interface{}) IOrm { + return dba.Where(needle, "IN", hystack) +} + +// OrWhereIn ... +func (dba *Orm) OrWhereIn(needle string, hystack []interface{}) IOrm { + return dba.OrWhere(needle, "IN", hystack) +} + +// WhereNotIn ... +func (dba *Orm) WhereNotIn(needle string, hystack []interface{}) IOrm { + return dba.Where(needle, "NOT IN", hystack) +} + +// OrWhereNotIn ... +func (dba *Orm) OrWhereNotIn(needle string, hystack []interface{}) IOrm { + return dba.OrWhere(needle, "NOT IN", hystack) +} + +// WhereBetween ... +func (dba *Orm) WhereBetween(needle string, hystack []interface{}) IOrm { + return dba.Where(needle, "BETWEEN", hystack) +} + +// OrWhereBetween ... +func (dba *Orm) OrWhereBetween(needle string, hystack []interface{}) IOrm { + return dba.OrWhere(needle, "BETWEEN", hystack) +} + +// WhereNotBetween ... +func (dba *Orm) WhereNotBetween(needle string, hystack []interface{}) IOrm { + return dba.Where(needle, "NOT BETWEEN", hystack) +} + +// OrWhereNotBetween ... +func (dba *Orm) OrWhereNotBetween(needle string, hystack []interface{}) IOrm { + return dba.OrWhere(needle, "NOT BETWEEN", hystack) +} + +// Join : select join query +func (dba *Orm) Join(args ...interface{}) IOrm { + dba._joinBuilder("INNER", args) + return dba +} + +// LeftJoin ... +func (dba *Orm) LeftJoin(args ...interface{}) IOrm { + dba._joinBuilder("LEFT", args) + return dba +} + +// RightJoin ... +func (dba *Orm) RightJoin(args ...interface{}) IOrm { + dba._joinBuilder("RIGHT", args) + return dba +} + +// CrossJoin ... +func (dba *Orm) CrossJoin(args ...interface{}) IOrm { + dba._joinBuilder("CROSS", args) + return dba +} + +// _joinBuilder +func (dba *Orm) _joinBuilder(joinType string, args []interface{}) { + dba.join = append(dba.join, []interface{}{joinType, args}) +} + +// Reset orm api and bind values reset to init +func (dba *Orm) Reset() IOrm { + dba.OrmApi = new(OrmApi) + dba.ClearBindValues() + dba.ResetUnion() + dba.ResetTable() + dba.ResetWhere() + dba.ResetExtraCols() + return dba + //return NewOrm(dba.GetIEngin()) +} + +// ResetTable ... +func (dba *Orm) ResetTable() IOrm { + dba.GetISession().SetIBinder(NewBinder()) + return dba +} + +// ResetWhere ... +func (dba *Orm) ResetWhere() IOrm { + dba.where = [][]interface{}{} + return dba +} + +// ResetUnion ... +func (dba *Orm) ResetUnion() IOrm { + dba.GetISession().SetUnion(nil) + return dba +} + +// BuildSql +// operType(select, insert, update, delete) +func (dba *Orm) BuildSql(operType ...string) (a string, b []interface{}, err error) { + // 解析table + dba.table, err = dba.GetISession().GetTableName() + if err != nil { + dba.GetISession().GetIEngin().GetLogger().Error(err.Error()) + return + } + // 解析字段 + // 如果有union操作, 则不需要 + if inArray(dba.GetIBinder().GetBindType(), []interface{}{OBJECT_STRUCT, OBJECT_STRUCT_SLICE}) && + dba.GetUnion() == nil { + dba.fields = getTagName(dba.GetIBinder().GetBindResult(), TAGNAME) + } + if len(operType) == 0 || (len(operType) > 0 && strings.ToLower(operType[0]) == "select") { + //// 根据传入的struct, 设置limit, 有效的节约空间 + //if dba.union == "" { + // var bindType = dba.GetIBinder().GetBindType() + // if bindType == OBJECT_MAP || bindType == OBJECT_STRUCT { + // dba.Limit(1) + // } + //} + a, b, err = NewBuilder(dba.GetISession().GetIEngin().GetDriver()).BuildQuery(dba) + if dba.GetISession().GetTransaction() { + a = a + dba.GetPessimisticLock() + } + } else { + a, b, err = NewBuilder(dba.GetISession().GetIEngin().GetDriver()).BuildExecute(dba, strings.ToLower(operType[0])) + // 重置强制获取更新或插入的字段, 防止复用时感染 + dba.ResetExtraCols() + } + // 如果是事务, 因为需要复用单一对象, 故参数会产生感染 + // 所以, 在这里做一下数据绑定重置操作 + if dba.GetISession().GetTransaction() { + dba.Reset() + } + // 这里统一清理一下绑定的数据吧, 万一要复用了, 造成绑定数据感染, 就尴尬了 + dba.ClearBindValues() + return +} + +// Transaction ... +func (dba *Orm) Transaction(closers ...func(db IOrm) error) (err error) { + err = dba.ISession.Begin() + if err != nil { + dba.GetIEngin().GetLogger().Error(err.Error()) + return err + } + + for _, closer := range closers { + err = closer(dba) + if err != nil { + dba.GetIEngin().GetLogger().Error(err.Error()) + _ = dba.ISession.Rollback() + return + } + } + return dba.ISession.Commit() +} + +// SharedLock 共享锁 +// select * from xxx lock in share mode +func (dba *Orm) SharedLock() *Orm { + dba.pessimisticLock = " lock in share mode" + return dba +} + +// LockForUpdate +// select * from xxx for update +func (dba *Orm) LockForUpdate() *Orm { + dba.pessimisticLock = " for update" + return dba +} diff --git a/orm_api.go b/orm_api.go new file mode 100644 index 0000000..5aa0851 --- /dev/null +++ b/orm_api.go @@ -0,0 +1,96 @@ +package gorose + +// OrmApi ... +type OrmApi struct { + table string + fields []string + where [][]interface{} + order string + limit int + offset int + join [][]interface{} + distinct bool + //union string + group string + having string + data interface{} + force bool + extraCols []string + // 悲观锁 + pessimisticLock string +} + +// GetTable ... +func (o *Orm) GetTable() string { + return o.table +} + +// GetFields ... +func (o *Orm) GetFields() []string { + return o.fields +} + +// SetWhere ... +func (o *Orm) SetWhere(arg [][]interface{}) { + o.where = arg +} + +// GetWhere ... +func (o *Orm) GetWhere() [][]interface{} { + return o.where +} + +// GetOrder ... +func (o *Orm) GetOrder() string { + return o.order +} + +// GetLimit ... +func (o *Orm) GetLimit() int { + return o.limit +} + +// GetOffset ... +func (o *Orm) GetOffset() int { + return o.offset +} + +// GetJoin ... +func (o *Orm) GetJoin() [][]interface{} { + return o.join +} + +// GetDistinct ... +func (o *Orm) GetDistinct() bool { + return o.distinct +} + +// GetGroup ... +func (o *Orm) GetGroup() string { + return o.group +} + +// GetHaving ... +func (o *Orm) GetHaving() string { + return o.having +} + +// GetData ... +func (o *Orm) GetData() interface{} { + return o.data +} + +// GetForce ... +func (o *Orm) GetForce() bool { + return o.force +} + +// GetExtraCols ... +func (o *Orm) GetExtraCols() []string { + return o.extraCols +} + +// GetPessimisticLock ... +func (o *Orm) GetPessimisticLock() string { + return o.pessimisticLock +} diff --git a/orm_api_interface.go b/orm_api_interface.go new file mode 100644 index 0000000..c9debaa --- /dev/null +++ b/orm_api_interface.go @@ -0,0 +1,21 @@ +package gorose + +// IOrmApi ... +type IOrmApi interface { + GetTable() string + GetFields() []string + SetWhere(arg [][]interface{}) + GetWhere() [][]interface{} + GetOrder() string + GetLimit() int + GetOffset() int + GetJoin() [][]interface{} + GetDistinct() bool + GetGroup() string + GetHaving() string + GetData() interface{} + ExtraCols(args ...string) IOrm + ResetExtraCols() IOrm + GetExtraCols() []string + GetPessimisticLock() string +} diff --git a/orm_execute.go b/orm_execute.go new file mode 100644 index 0000000..b5b74ee --- /dev/null +++ b/orm_execute.go @@ -0,0 +1,147 @@ +package gorose + +import ( + "errors" + "github.com/gohouse/t" + "reflect" +) + +// Insert : insert data and get affected rows +func (dba *Orm) Insert(data ...interface{}) (int64, error) { + return dba.exec("insert", data...) +} + +// insertGetId : insert data and get id +func (dba *Orm) InsertGetId(data ...interface{}) (int64, error) { + _, err := dba.Insert(data...) + if err != nil { + return 0, err + } + return dba.GetISession().LastInsertId(), nil +} + +// Update : update data +func (dba *Orm) Update(data ...interface{}) (int64, error) { + return dba.exec("update", data...) +} + +// Replace : replace data and get affected rows +func (dba *Orm) Replace(data ...interface{}) (int64, error) { + return dba.exec("replace", data...) +} + +// Force 强制执行没有where的删除和修改 +func (dba *Orm) Force() IOrm { + dba.force = true + return dba +} + +// Delete : delete data +func (dba *Orm) Delete() (int64, error) { + return dba.exec("delete") +} + +// Delete : delete data +func (dba *Orm) exec(operType string, data ...interface{}) (int64, error) { + if operType == "insert" || operType == "update" { + if dba.GetData() == nil { + if len(data) > 0 { + dba.Data(data[0]) + } else { + return 0, GetErr(ERR_PARAMS_MISSING, "Data()") + } + } + + //if dba.GetISession().GetIBinder() == nil { + // 如果这里是默认值, 则需要对其进行table处理 + //if dba.GetISession().GetIBinder().GetBindType() == OBJECT_NIL { + // if dba.GetData() != nil { + // dba.Table(dba.GetData()) + // } else { + // return 0, GetErr(ERR_PARAMS_MISSING, "Data() or Table()") + // } + //} + rl := reflect.ValueOf(dba.GetData()) + rl2 := reflect.Indirect(rl) + + switch rl2.Kind() { + case reflect.Struct, reflect.Ptr: + //return 0, errors.New("传入的结构体必须是对象的地址") + if tn := rl2.MethodByName("TableName"); tn.IsValid() { + dba.Table(dba.GetData()) + } + case reflect.Map: + if tn := rl2.MethodByName("TableName"); tn.IsValid() { + dba.Table(dba.GetData()) + } + if tn := rl.MethodByName("TableName"); tn.IsValid() { + dba.Table(dba.GetData()) + } + case reflect.Slice: + r2 := rl2.Type().Elem() + r2val := reflect.New(r2) + switch r2val.Kind() { + case reflect.Struct, reflect.Ptr: + if tn := r2val.MethodByName("TableName"); tn.IsValid() { + dba.Table(dba.GetData()) + } + case reflect.Map: + if tn := r2val.MethodByName("TableName"); tn.IsValid() { + dba.Table(dba.GetData()) + } + default: + return 0, errors.New("表名有误") + } + } + + } + // 构建sql + sqlStr, args, err := dba.BuildSql(operType) + if err != nil { + return 0, err + } + + return dba.GetISession().Execute(sqlStr, args...) +} + +// Increment : auto Increment +1 default +// we can define step (such as 2, 3, 6 ...) if give the second params +// we can use this method as decrement with the third param as "-" +// orm.Increment("top") , orm.Increment("top", 2, "-")=orm.Decrement("top",2) +func (dba *Orm) Increment(args ...interface{}) (int64, error) { + argLen := len(args) + var field string + var mode = "+" + var value = "1" + switch argLen { + case 1: + field = t.New(args[0]).String() + case 2: + field = t.New(args[0]).String() + value = t.New(args[1]).String() + case 3: + field = t.New(args[0]).String() + value = t.New(args[1]).String() + mode = t.New(args[2]).String() + default: + return 0, errors.New("参数数量只允许1个,2个或3个") + } + dba.Data(field + "=" + field + mode + value) + return dba.Update() +} + +// Decrement : auto Decrement -1 default +// we can define step (such as 2, 3, 6 ...) if give the second params +func (dba *Orm) Decrement(args ...interface{}) (int64, error) { + arglen := len(args) + switch arglen { + case 1: + args = append(args, 1) + args = append(args, "-") + case 2: + args = append(args, "-") + default: + return 0, errors.New("Decrement参数个数有误") + } + return dba.Increment(args...) +} diff --git a/orm_execute_interface.go b/orm_execute_interface.go new file mode 100644 index 0000000..478c444 --- /dev/null +++ b/orm_execute_interface.go @@ -0,0 +1,20 @@ +package gorose + +// IOrmExecute ... +type IOrmExecute interface { + GetForce() bool + // insert,insertGetId + Insert(data ...interface{}) (int64, error) + InsertGetId(data ...interface{}) (int64, error) + Update(data ...interface{}) (int64, error) + // updateOrInsert + Replace(data ...interface{}) (int64, error) + // increment,decrement + // 在操作过程中你还可以指定额外的列进行更新: + Increment(args ...interface{}) (int64, error) + Decrement(args ...interface{}) (int64, error) + // delete + Delete() (int64, error) + //LastInsertId() int64 + Force() IOrm +} diff --git a/orm_execute_test.go b/orm_execute_test.go new file mode 100644 index 0000000..935ed3d --- /dev/null +++ b/orm_execute_test.go @@ -0,0 +1,101 @@ +package gorose + +import ( + "fmt" + "testing" +) + +func TestOrm_Update(t *testing.T) { + db := DB() + + var u = []Users{{ + Name: "gorose2", + Age: 19, + }} + + aff, err := db.Force().Update(&u) + if err != nil { + t.Error(err.Error()) + } + t.Log(aff, db.LastSql()) +} + +func TestOrm_Update2(t *testing.T) { + db := DB() + + //var u = []Users{{ + // Name: "gorose2", + // Age: 11, + //}} + + aff, err := db.Table("users").Where("uid", 1).Update() + if err != nil { + //t.Error(err.Error()) + t.Log(err.Error()) + return + } + t.Log(aff, db.LastSql()) +} + +func TestOrm_UpdateMap(t *testing.T) { + db := DB() + + //var u = []UsersMap{{"name": "gorose2", "age": 19}} + var u = UsersMap{"name": "gorose2", "age": 19} + + aff, err := db.Force().Update(&u) + if err != nil { + t.Error(err.Error()) + } + t.Log(aff, db.LastSql()) +} + +func TestTrans(t *testing.T) { + var db = DB() + var db2 = DB() + var res Users + db.Begin() + db2.Table(&res).Select() + t.Log(res) + db.Commit() + t.Log(res) +} + +func Test_Transaction(t *testing.T) { + var db = DB() + // 一键事务, 自动回滚和提交, 我们只需要关注业务即可 + err := db.Transaction( + func(db IOrm) error { + //db.Table("users").Limit(2).SharedLock().Get() + //fmt.Println(db.LastSql()) + _, err := db.Table("users").Where("uid", 2).Update(Data{"name": "gorose2"}) + fmt.Println(db.LastSql()) + if err != nil { + return err + } + _, err = db.Insert(&UsersMap{"name": "gorose2", "age": 0}) + fmt.Println(db.LastSql()) + if err != nil { + return err + } + return nil + }, + func(db IOrm) error { + _, err := db.Table("users").Where("uid", 3).Update(Data{"name": "gorose3"}) + fmt.Println(db.LastSql()) + if err != nil { + return err + } + _, err = db.Insert(&UsersMap{"name": "gorose2", "age": 0}) + fmt.Println(db.LastSql()) + if err != nil { + return err + } + return nil + }, + ) + if err != nil { + t.Error(err.Error()) + } + t.Log("事务测试通过") +} diff --git a/orm_interface.go b/orm_interface.go new file mode 100644 index 0000000..4c32513 --- /dev/null +++ b/orm_interface.go @@ -0,0 +1,72 @@ +package gorose + +// IOrm ... +type IOrm interface { + IOrmApi + IOrmQuery + IOrmExecute + IOrmSession + //ISession + Close() + BuildSql(operType ...string) (string, []interface{}, error) + Table(tab interface{}) IOrm + // fields=select + Fields(fields ...string) IOrm + AddFields(fields ...string) IOrm + // distinct 方法允许你强制查询返回不重复的结果集: + Distinct() IOrm + Data(data interface{}) IOrm + // groupBy, orderBy, having + Group(group string) IOrm + GroupBy(group string) IOrm + Having(having string) IOrm + Order(order string) IOrm + OrderBy(order string) IOrm + Limit(limit int) IOrm + Offset(offset int) IOrm + Page(page int) IOrm + // join(=innerJoin),leftJoin,rightJoin,crossJoin + Join(args ...interface{}) IOrm + LeftJoin(args ...interface{}) IOrm + RightJoin(args ...interface{}) IOrm + CrossJoin(args ...interface{}) IOrm + // `Where`,`OrWhere`,`WhereNull / WhereNotNull`,`WhereIn / WhereNotIn / OrWhereIn / OrWhereNotIn`,`WhereBetween / WhereBetwee / OrWhereBetween / OrWhereNotBetween` + Where(args ...interface{}) IOrm + OrWhere(args ...interface{}) IOrm + WhereNull(arg string) IOrm + OrWhereNull(arg string) IOrm + WhereNotNull(arg string) IOrm + OrWhereNotNull(arg string) IOrm + WhereRegexp(arg string, expstr string) IOrm + OrWhereRegexp(arg string, expstr string) IOrm + WhereNotRegexp(arg string, expstr string) IOrm + OrWhereNotRegexp(arg string, expstr string) IOrm + WhereIn(needle string, hystack []interface{}) IOrm + OrWhereIn(needle string, hystack []interface{}) IOrm + WhereNotIn(needle string, hystack []interface{}) IOrm + OrWhereNotIn(needle string, hystack []interface{}) IOrm + WhereBetween(needle string, hystack []interface{}) IOrm + OrWhereBetween(needle string, hystack []interface{}) IOrm + WhereNotBetween(needle string, hystack []interface{}) IOrm + OrWhereNotBetween(needle string, hystack []interface{}) IOrm + // truncate + //Truncate() + GetDriver() string + //GetIBinder() IBinder + SetBindValues(v interface{}) + GetBindValues() []interface{} + ClearBindValues() + Transaction(closers ...func(db IOrm) error) (err error) + Reset() IOrm + ResetTable() IOrm + ResetWhere() IOrm + GetISession() ISession + GetOrmApi() *OrmApi + // 悲观锁使用 + // sharedLock(lock in share mode) 不会阻塞其它事务读取被锁定行记录的值 + SharedLock() *Orm + // 此外你还可以使用 lockForUpdate 方法。“for update”锁避免选择行被其它共享锁修改或删除: + // 会阻塞其他锁定性读对锁定行的读取(非锁定性读仍然可以读取这些记录,lock in share mode 和 for update 都是锁定性读) + LockForUpdate() *Orm + //ResetUnion() IOrm +} diff --git a/orm_query.go b/orm_query.go new file mode 100644 index 0000000..9d9e1db --- /dev/null +++ b/orm_query.go @@ -0,0 +1,442 @@ +package gorose + +import ( + "github.com/gohouse/t" + "math" + "reflect" + "strings" +) + +// Select : select one or more rows , relation limit set +func (dba *Orm) Select() error { + switch dba.GetIBinder().GetBindType() { + case OBJECT_STRUCT, OBJECT_MAP, OBJECT_MAP_T: + dba.Limit(1) + } + // 构建sql + sqlStr, args, err := dba.BuildSql() + if err != nil { + return err + } + + // 执行查询 + _, err = dba.GetISession().Query(sqlStr, args...) + return err +} + +// First : select one row , relation limit set +func (dba *Orm) First() (result Data, err error) { + dba.GetIBinder().SetBindType(OBJECT_STRING) + err = dba.Limit(1).Select() + if err != nil { + return + } + res := dba.GetISession().GetBindAll() + if len(res) > 0 { + result = res[0] + } + return +} + +func (dba *Orm) Find() (result Data, err error) { + return dba.First() +} + +// Get : select more rows , relation limit set +func (dba *Orm) Get() (result []Data, err error) { + dba.GetIBinder().SetBindType(OBJECT_STRING) + tabname := dba.GetISession().GetIBinder().GetBindName() + prefix := dba.GetISession().GetIBinder().GetBindPrefix() + tabname2 := strings.TrimPrefix(tabname, prefix) + dba.ResetTable() + dba.Table(tabname2) + err = dba.Select() + result = dba.GetISession().GetBindAll() + return +} + +// Count : select count rows +func (dba *Orm) Count(args ...string) (int64, error) { + fields := "*" + if len(args) > 0 { + fields = args[0] + } + count, err := dba._unionBuild("count", fields) + if count == nil { + return 0, err + } + return t.New(count).Int64(), err +} + +// Sum : select sum field +func (dba *Orm) Sum(sum string) (interface{}, error) { + return dba._unionBuild("sum", sum) +} + +// Avg : select avg field +func (dba *Orm) Avg(avg string) (interface{}, error) { + return dba._unionBuild("avg", avg) +} + +// Max : select max field +func (dba *Orm) Max(max string) (interface{}, error) { + return dba._unionBuild("max", max) +} + +// Min : select min field +func (dba *Orm) Min(min string) (interface{}, error) { + return dba._unionBuild("min", min) +} + +// _unionBuild : build union select real +func (dba *Orm) _unionBuild(union, field string) (interface{}, error) { + fields := union + "(" + field + ") as " + union + dba.fields = []string{fields} + + res, err := dba.First() + if r, ok := res[union]; ok { + return r, err + } + return 0, err +} + +//func (dba *Orm) _unionBuild_bak(union, field string) (interface{}, error) { +// var tmp interface{} +// +// dba.union = union + "(" + field + ") as " + union +// // 缓存fields字段,暂时由union占用 +// fieldsTmp := dba.fields +// dba.fields = []string{dba.union} +// dba.GetISession().SetUnion(true) +// +// // 构建sql +// sqls, args, err := dba.BuildSql() +// if err != nil { +// return tmp, err +// } +// +// // 执行查询 +// _, err = dba.GetISession().Query(sqls, args...) +// if err != nil { +// return tmp, err +// } +// +// // 重置union, 防止复用的时候感染 +// dba.union = "" +// // 返还fields +// dba.fields = fieldsTmp +// +// // 语法糖获取union值 +// if dba.GetISession().GetUnion() != nil { +// tmp = dba.GetISession().GetUnion() +// // 获取之后, 释放掉 +// dba.GetISession().SetUnion(nil) +// } +// +// return tmp, nil +//} + +// Pluck 获取一列数据, 第二个字段可以指定另一个字段的值作为这一列数据的key +func (dba *Orm) Pluck(field string, fieldKey ...string) (v interface{}, err error) { + var resMap = make(map[interface{}]interface{}, 0) + var resSlice = make([]interface{}, 0) + + res, err := dba.Get() + + if err != nil { + return + } + + if len(res) > 0 { + for _, val := range res { + if len(fieldKey) > 0 { + resMap[val[fieldKey[0]]] = val[field] + } else { + resSlice = append(resSlice, val[field]) + } + } + } + if len(fieldKey) > 0 { + v = resMap + } else { + v = resSlice + } + return +} + +// Pluck_bak ... +func (dba *Orm) Pluck_bak(field string, fieldKey ...string) (v interface{}, err error) { + var binder = dba.GetISession().GetIBinder() + var resMap = make(map[interface{}]interface{}, 0) + var resSlice = make([]interface{}, 0) + + err = dba.Select() + if err != nil { + return + } + + switch binder.GetBindType() { + case OBJECT_MAP, OBJECT_MAP_T, OBJECT_STRUCT: // row + var key, val interface{} + if len(fieldKey) > 0 { + key, err = dba.Value(fieldKey[0]) + if err != nil { + return + } + val, err = dba.Value(field) + if err != nil { + return + } + resMap[key] = val + } else { + v, err = dba.Value(field) + if err != nil { + return + } + } + case OBJECT_MAP_SLICE, OBJECT_MAP_SLICE_T: + for _, item := range t.New(binder.GetBindResultSlice().Interface()).Slice() { + val := item.MapInterfaceT() + if len(fieldKey) > 0 { + resMap[val[fieldKey[0]].Interface()] = val[field].Interface() + } else { + resSlice = append(resSlice, val[field].Interface()) + } + } + case OBJECT_STRUCT_SLICE: // rows + var brs = binder.GetBindResultSlice() + for i := 0; i < brs.Len(); i++ { + val := reflect.Indirect(brs.Index(i)) + if len(fieldKey) > 0 { + mapkey := dba._valueFromStruct(val, fieldKey[0]) + mapVal := dba._valueFromStruct(val, field) + resMap[mapkey] = mapVal + } else { + resSlice = append(resSlice, dba._valueFromStruct(val, field)) + } + } + case OBJECT_STRING: + res := dba.GetISession().GetBindAll() + if len(res) > 0 { + for _, val := range res { + if len(fieldKey) > 0 { + resMap[val[fieldKey[0]]] = val[field] + } else { + resSlice = append(resSlice, val[field]) + } + } + } + } + if len(fieldKey) > 0 { + v = resMap + } else { + v = resSlice + } + return +} + +// Type is get a row of a field value +func (dba *Orm) Value(field string) (v interface{}, err error) { + res, err := dba.First() + if v, ok := res[field]; ok { + return v, err + } + return +} + +// Value_bak ... +func (dba *Orm) Value_bak(field string) (v interface{}, err error) { + dba.Limit(1) + err = dba.Select() + if err != nil { + return + } + var binder = dba.GetISession().GetIBinder() + switch binder.GetBindType() { + case OBJECT_MAP, OBJECT_MAP_SLICE, OBJECT_MAP_SLICE_T, OBJECT_MAP_T: + v = reflect.ValueOf(binder.GetBindResult()).MapIndex(reflect.ValueOf(field)).Interface() + case OBJECT_STRUCT, OBJECT_STRUCT_SLICE: + bindResult := reflect.Indirect(reflect.ValueOf(binder.GetBindResult())) + v = dba._valueFromStruct(bindResult, field) + case OBJECT_STRING: + res := dba.GetISession().GetBindAll() + if len(res) > 0 { + v = res[0][field] + } + } + return +} +func (dba *Orm) _valueFromStruct(bindResult reflect.Value, field string) (v interface{}) { + ostype := bindResult.Type() + for i := 0; i < ostype.NumField(); i++ { + tag := ostype.Field(i).Tag.Get(TAGNAME) + if tag == field || ostype.Field(i).Name == field { + v = bindResult.FieldByName(ostype.Field(i).Name).Interface() + } + } + return +} + +// Chunk : 分块处理数据,当要处理很多数据的时候, 我不需要知道具体是多少数据, 我只需要每次取limit条数据, +// 然后不断的增加offset去取更多数据, 从而达到分块处理更多数据的目的 +//TODO 后续增加 gorotine 支持, 提高批量数据处理效率, 预计需要增加获取更多链接的支持 +func (dba *Orm) Chunk(limit int, callback func([]Data) error) (err error) { + var page = 1 + var tabname = dba.GetISession().GetIBinder().GetBindName() + prefix := dba.GetISession().GetIBinder().GetBindPrefix() + tabname2 := strings.TrimPrefix(tabname, prefix) + // 先执行一条看看是否报错, 同时设置指定的limit, offset + result, err := dba.Table(tabname2).Limit(limit).Page(page).Get() + if err != nil { + return + } + for len(result) > 0 { + if err = callback(result); err != nil { + break + } + page++ + // 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的 + // 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来 + dba.ClearBindValues() + result, _ = dba.Page(page).Get() + } + return +} + +// ChunkStruct : 同Chunk,只不过不用返回map, 而是绑定数据到传入的对象上 +// 这里一定要传入绑定struct +func (dba *Orm) ChunkStruct(limit int, callback func() error) (err error) { + var page = 0 + //var tableName = dba.GetISession().GetIBinder().GetBindName() + // 先执行一条看看是否报错, 同时设置指定的limit, offset + err = dba.Limit(limit).Offset(page * limit).Select() + if err != nil { + return + } + switch dba.GetIBinder().GetBindType() { + case OBJECT_STRUCT, OBJECT_MAP, OBJECT_MAP_T: + var ibinder = dba.GetIBinder() + var result = ibinder.GetBindResult() + for result != nil { + if err = callback(); err != nil { + break + } + page++ + // 清空结果 + //result = nil + var rfRes = reflect.ValueOf(result) + rfRes.Set(reflect.Zero(rfRes.Type())) + // 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的 + // 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来 + dba.ClearBindValues() + _ = dba.Table(ibinder.GetBindOrigin()).Offset(page * limit).Select() + result = dba.GetIBinder().GetBindResultSlice() + } + case OBJECT_STRUCT_SLICE, OBJECT_MAP_SLICE, OBJECT_MAP_SLICE_T: + var ibinder = dba.GetIBinder() + var result = ibinder.GetBindResultSlice() + for result.Interface() != nil { + if err = callback(); err != nil { + break + } + page++ + // 清空结果 + result.Set(result.Slice(0, 0)) + // 清理绑定数据, 进行下一次操作, 因为绑定数据是每一次执行的时候都会解析并保存的 + // 而第二次以后执行的, 都会再次解析并保存, 数据结构是slice, 故会累积起来 + dba.ClearBindValues() + _ = dba.Table(ibinder.GetBindOrigin()).Offset(page * limit).Select() + result = dba.GetIBinder().GetBindResultSlice() + } + } + return +} + +// Loop : 同chunk, 不过, 这个是循环的取前limit条数据, 为什么是循环取这些数据呢 +// 因为, 我们考虑到一种情况, 那就是where条件如果刚好是要修改的值, +// 那么最后的修改结果因为offset的原因, 只会修改一半, 比如: +// DB().Where("age", 18) ===> DB().Data(gorose.Data{"age":19}).Where().Update() +func (dba *Orm) Loop(limit int, callback func([]Data) error) (err error) { + var page = 0 + var tabname = dba.GetISession().GetIBinder().GetBindName() + prefix := dba.GetISession().GetIBinder().GetBindPrefix() + tabname2 := strings.TrimPrefix(tabname, prefix) + // 先执行一条看看是否报错, 同时设置指定的limit + result, err := dba.Table(tabname2).Limit(limit).Get() + if err != nil { + return + } + for len(result) > 0 { + if err = callback(result); err != nil { + break + } + page++ + // 同chunk + dba.ClearBindValues() + result, _ = dba.Get() + } + return +} + +// Paginate 自动分页 +// @param limit 每页展示数量 +// @param current_page 当前第几页, 从1开始 +// 以下是laravel的Paginate返回示例 +//{ +// "total": 50, +// "per_page": 15, +// "current_page": 1, +// "lastPage": 4, +// "first_page_url": "http://laravel.app?page=1", +// "lastPage_url": "http://laravel.app?page=4", +// "nextPage_url": "http://laravel.app?page=2", +// "prevPage_url": null, +// "path": "http://laravel.app", +// "from": 1, +// "to": 15, +// "data":[ +// { +// // Result Object +// }, +// { +// // Result Object +// } +// ] +//} +func (dba *Orm) Paginate(page ...int) (res Data, err error) { + if len(page) > 0 { + dba.Page(page[0]) + } + var limit = dba.GetLimit() + if limit == 0 { + limit = 15 + } + var offset = dba.GetOffset() + var currentPage = int(math.Ceil(float64(offset+1) / float64(limit))) + //dba.ResetUnion() + // 获取结果 + resData, err := dba.Get() + if err != nil { + return + } + // 统计总量 + dba.offset = 0 + count, err := dba.Count() + var lastPage = int(math.Ceil(float64(count) / float64(limit))) + var nextPage = currentPage + 1 + var prevPage = currentPage - 1 + res = Data{ + "total": count, + "per_page": limit, + "current_page": currentPage, + "last_page": lastPage, + "first_page_url": 1, + "last_page_url": lastPage, + "next_page_url": If(nextPage > lastPage, nil, nextPage), + "prev_page_url": If(prevPage < 1, nil, prevPage), + //"data": dba.GetIBinder().GetBindResultSlice().Interface(), + "data": resData, + } + + return +} diff --git a/orm_query_interface.go b/orm_query_interface.go new file mode 100644 index 0000000..dbdd70c --- /dev/null +++ b/orm_query_interface.go @@ -0,0 +1,35 @@ +package gorose + +// IOrmQuery ... +type IOrmQuery interface { + // 获取数据, 依据传入的绑定对象, 选择查询一条或多条数据并绑定到传入对象上 + // 当绑定对象传入的是string类型时, 返回多条结果集, 需要使用 Get() 来获取最终结果 + Select() error + // 获取一条结果并返回, 只有当传入的table对象是字符串时生效 + First() (Data, error) + Find() (Data, error) + // 获取多条结果并返回, 只有当传入的table对象是字符串时生效 + Get() ([]Data, error) + // 如果你不需要完整的一行,可以使用 value 方法从结果中获取单个值,该方法会直接返回指定列的值: + Value(field string) (v interface{}, err error) + // 如果想要获取包含单个列值的数组,可以使用 pluck 方法 + // 还可以在返回数组中为列值指定自定义键(该自定义键必须是该表的其它字段列名,否则会报错) + Pluck(field string, fieldKey ...string) (v interface{}, err error) + // 查询构建器还提供了多个聚合方法,如count, max, min, avg 和 sum,你可以在构造查询之后调用这些方法: + Count(args ...string) (int64, error) + Sum(sum string) (interface{}, error) + Avg(avg string) (interface{}, error) + Max(max string) (interface{}, error) + Min(min string) (interface{}, error) + // 分页, 返回分页需要的基本数据 + Paginate(page ...int) (res Data, err error) + // 组块结果集 + // 如果你需要处理成千上万或者更多条数据库记录,可以考虑使用 chunk 方法,该方法一次获取结果集的一小块, + // 然后传递每一小块数据到闭包函数进行处理,该方法在编写处理大量数据库记录的 Artisan 命令的时候非常有用。 + // 例如,我们可以将处理全部 users 表数据分割成一次处理 100 条记录的小组块 + // 你可以通过从闭包函数中返回 err 来终止组块的运行 + Chunk(limit int, callback func([]Data) error) (err error) + // 跟Chunk类似,只不过callback的是传入的结构体 + ChunkStruct(limit int, callback func() error) (err error) + Loop(limit int, callback func([]Data) error) (err error) +} diff --git a/orm_query_test.go b/orm_query_test.go new file mode 100644 index 0000000..ee2553b --- /dev/null +++ b/orm_query_test.go @@ -0,0 +1,358 @@ +package gorose + +import ( + "errors" + "fmt" + "testing" + "time" +) + +func TestOrm_BuildSql2(t *testing.T) { + db := DB() + var u = "age=age+1,num=num+1" + var wheres interface{} + wheres = [][]interface{}{{"a", ">", "b"}, {"a", "b"}, {"a is null"}} + sqlstr, a, b := db.Force().Table("users").Data(u).Where(wheres).BuildSql("update") + + t.Log(sqlstr, a, b) +} + +func TestOrm_BuildSql3(t *testing.T) { + db := DB() + var u = "age=age+1,num=num+1" + var wheres interface{} + wheres = [][]interface{}{{"a", ">", "b"}, {"a", "b"}} + sqlstr, a, b := db.Force().Table(Users{}).Data(u).Where(wheres).BuildSql("update") + + t.Log(sqlstr, a, b) +} + +func TestOrm_BuildSql4(t *testing.T) { + db := DB() + //var wheres interface{} + //wheres = [][]interface{}{{"a", ">", "b"},{"lock",1}} + wheres := Data{"lock": 1, "`date`": 1} + obj := db.Table(Users{}).Where(wheres).Where(func() { + db.Where("c", 2).OrWhere("lock", ">", 4) + }).Data(wheres) + + sqlstr, a, b := obj.BuildSql() + t.Log(sqlstr, a, b) + + sqlstr, a, b = obj.BuildSql("update") + t.Log(sqlstr, a, b) + + sqlstr, a, b = obj.BuildSql("insert") + t.Log(sqlstr, a, b) +} + +func TestOrm_BuildSql5(t *testing.T) { + //ticker := time.NewTicker(100*time.Millisecond) + go func(t *testing.T) { + for { + //<-ticker.C + db := DB() + sqlstr, a, b := db.Table("users").Where("uid", ">", 1).BuildSql() + //c,d := db.Table("users").Get() + //t.Log(db.LastSql()) + count, d := db.First() + + t.Log(sqlstr, a, b) + t.Log(count, d) + t.Log(db.LastSql()) + } + }(t) + time.Sleep(500 * time.Millisecond) +} + +func TestOrm_BuildSql6(t *testing.T) { + var db = DB() + sqlstr, a, b := db.Table("users3").Limit(2).Offset(2).BuildSql() + t.Log(sqlstr, a, b) + + sqlstr, a, b = db.Table("users2").Limit(2).Offset(2).BuildSql() + t.Log(sqlstr, a, b) + + var u = Users{ + Uid: 1111, + Name: "2", + Age: 3, + } + res, err := db.Table("xxx").Where("xx", "xx").Update(&u) + t.Log(db.LastSql(), res, err) +} + +func TestOrm_First(t *testing.T) { + res, err := DB().Table(Users{}).Where("uid", 1).First() + if err != nil { + t.Error(err.Error()) + } + t.Log(res) +} + +func TestOrm_Select(t *testing.T) { + db := DB() + var err error + + var u = []Users{} + err = db.Table(&u).Select() + t.Log(err, u, db.LastSql()) + + var u2 = Users{} + err = db.Table(&u2).Select() + t.Log(err, u2, db.LastSql()) + + var u3 Users + err = db.Table(&u3).Select() + t.Log(err, u3, db.LastSql()) + + var u4 []Users + err = db.Table(&u4).Limit(2).Select() + t.Log(err, u4, db.LastSql()) + if err != nil { + t.Error(err.Error()) + } + t.Log(u, u2, u3, u4) +} + +func TestOrm_Select2(t *testing.T) { + db := DB() + var err error + + var u = []UsersMap{} + err = db.Table(&u).Limit(2).Select() + if err != nil { + t.Error(err.Error()) + } + t.Log(u) + + var u3 = UsersMap{} + err = db.Table(&u3).Limit(1).Select() + if err != nil { + t.Error(err.Error()) + } + t.Log(u) +} + +type Users2 struct { + Name string `orm:"name"` + Age int `orm:"age"` + Uid int `orm:"uid"` + Fi string `orm:"ignore"` +} + +func (u *Users2) TableName() string { + return "users" +} +func TestOrm_Get2(t *testing.T) { + db := DB() + var err error + var u []Users2 + + res, err := db.Table("users").Where("uid", ">", 2). + //Where("1","=","1"). + Where("1 = 1"). + Limit(2).Get() + //res, err := db.Table(&u).Where("uid", ">", 0).Limit(2).Get() + fmt.Println(db.LastSql()) + if err != nil { + t.Error(err.Error()) + } + t.Log(res, u) +} + +func TestOrm_Get(t *testing.T) { + orm := DB() + + var u = UsersMap{} + ormObj := orm.Table(&u).Join("b", "a.id", "=", "b.id"). + RightJoin("userinfo d on a.id=d.id"). + Fields("a.uid,a.age"). + Order("uid desc"). + Where("a", 1). + WhereNull("bb"). + WhereNotNull("cc"). + WhereIn("dd", []interface{}{1, 2}). + OrWhereNotIn("ee", []interface{}{1, 2}). + WhereBetween("ff", []interface{}{11, 21}). + WhereNotBetween("ff", []interface{}{1, 2}). + Where("a", "like", "%3%"). + OrWhere(func() { + orm.Where("c", 3).OrWhere(func() { + orm.Where("d", ">", 4) + }) + }).Where("e", 5).Limit(5).Offset(2) + s, a, err := ormObj.BuildSql() + + if err != nil { + t.Error(err.Error()) + } + t.Log(s, a, u) +} + +func TestOrm_Pluck(t *testing.T) { + orm := DB() + + //var u = UsersMapSlice{} + //var u []Users + ormObj := orm.Table("users") + //res,err := ormObj.Pluck("name", "uid") + res, err := ormObj.Limit(5).Pluck("name", "uid") + if err != nil { + t.Error(err.Error()) + } + t.Log(res, orm.LastSql()) +} + +func TestOrm_Value(t *testing.T) { + db := DB() + + //var u = UsersMap{} + //var u = UsersMapSlice{} + //var u Users + //var u []Users + //ormObj := db.Table(&u) + //ormObj := db.Table("users") + ormObj := db.Table(Users{}) + res, err := ormObj.Limit(5).Value("uid") + if err != nil { + t.Error(err.Error()) + } + t.Log(res, db.LastSql()) +} + +func TestOrm_Count(t *testing.T) { + db := DB() + + //var u = UsersMap{} + //ormObj := db.Table(&u) + ormObj := db.Table("users") + + res, err := ormObj.Count() + if err != nil { + t.Error(err.Error()) + } + t.Log(res, db.LastSql()) +} + +func TestOrm_Count2(t *testing.T) { + var u Users + var count int64 + count, err := DB().Table(&u).Count() + if err != nil { + t.Error(err.Error()) + } + t.Log(count) +} + +func TestOrm_Chunk(t *testing.T) { + orm := DB() + + var u = []UsersMap{} + err := orm.Table(&u).Chunk(1, func(data []Data) error { + for _, item := range data { + t.Log(item["name"]) + } + return errors.New("故意停止,防止数据过多,浪费时间") + //return nil + }) + if err != nil && err.Error() != "故意停止,防止数据过多,浪费时间" { + t.Error(err.Error()) + } + t.Log("Chunk() success") +} + +func TestOrm_Chunk2(t *testing.T) { + orm := DB() + + var u []Users + var i int + err := orm.Table(&u).ChunkStruct(2, func() error { + //for _, item := range u { + t.Log(u) + //} + if i == 2 { + return errors.New("故意停止,防止数据过多,浪费时间") + } + i++ + return nil + }) + if err != nil && err.Error() != "故意停止,防止数据过多,浪费时间" { + t.Error(err.Error()) + } + t.Log("ChunkStruct() success") +} + +func TestOrm_Loop(t *testing.T) { + db := DB() + + var u = []UsersMap{} + //aff,err := db.Table(&u).Force().Data(Data{"age": 18}).Update() + //fmt.Println(aff,err) + err := db.Table(&u).Where("age", 18).Loop(2, func(data []Data) error { + for _, item := range data { + _, err := DB().Table(&u).Data(Data{"age": 19}).Where("uid", item["uid"]).Update() + if err != nil { + t.Error(err.Error()) + } + } + return errors.New("故意停止,防止数据过多,浪费时间") + //return nil + }) + if err != nil && err.Error() != "故意停止,防止数据过多,浪费时间" { + t.Error(err.Error()) + } + t.Log("Loop() success") +} + +func TestOrm_Paginate(t *testing.T) { + db := DB() + + var u []Users + res, err := db.Table(&u).Limit(2).Paginate() + if err != nil { + t.Error(err.Error()) + } + t.Log(res, u) + t.Log(db.LastSql()) +} + +func TestOrm_Paginate2(t *testing.T) { + db := DB() + + var u []Users + res, err := db.Table(&u).Where("uid", ">", 1).Limit(2).Paginate(3) + if err != nil { + t.Error(err.Error()) + } + t.Log(res, u) + t.Log(db.LastSql()) +} + +func TestOrm_Sum(t *testing.T) { + db := DB() + + var u Users + //res, err := db.Table(Users{}).First() + res, err := db.Table(&u).Where(Data{"uid": 1}).Sum("age") + if err != nil { + t.Error(err.Error()) + } + //fmt.Printf("%#v\n",res) + t.Log(res, u) + t.Log(db.LastSql()) +} + +func BenchmarkNewOrm(b *testing.B) { + engin := initDB() + for i := 0; i < b.N; i++ { + engin.NewOrm().Table("users").First() + } +} + +func BenchmarkNewOrm2(b *testing.B) { + engin := initDB() + for i := 0; i < b.N; i++ { + engin.NewOrm().Table("users").First() + } +} diff --git a/orm_session_interface.go b/orm_session_interface.go new file mode 100644 index 0000000..7e5df8c --- /dev/null +++ b/orm_session_interface.go @@ -0,0 +1,23 @@ +package gorose + +// IOrmSession ... +type IOrmSession interface { + //Close() + //Table(bind interface{}) IOrm + //Bind(bind interface{}) ISession + Begin() (err error) + Rollback() (err error) + Commit() (err error) + //Transaction(closer ...func(session ISession) error) (err error) + Query(sqlstring string, args ...interface{}) ([]Data, error) + Execute(sqlstring string, args ...interface{}) (int64, error) + //GetMasterDriver() string + //GetSlaveDriver() string + LastInsertId() int64 + LastSql() string + //SetIBinder(b IBinder) + //GetTableName() (string, error) + GetIBinder() IBinder + SetUnion(u interface{}) + GetUnion() interface{} +} diff --git a/orm_test.go b/orm_test.go new file mode 100644 index 0000000..e64e401 --- /dev/null +++ b/orm_test.go @@ -0,0 +1,62 @@ +package gorose + +import ( + "testing" +) + +func DB() IOrm { + return initDB().NewOrm() +} +func TestNewOrm(t *testing.T) { + orm := DB() + orm.Close() +} +func TestOrm_AddFields(t *testing.T) { + orm := DB() + //var u = Users{} + var fieldStmt = orm.Table("users").Fields("a").Where("m", 55) + a, b, err := fieldStmt.AddFields("b").Where("d", 1).BuildSql() + if err != nil { + t.Error(err.Error()) + } + t.Log(a, b) + + fieldStmt.Reset() + d, e, err := fieldStmt.Fields("a").AddFields("c").Where("d", 2).BuildSql() + if err != nil { + t.Error(err.Error()) + } + t.Log(d, e) +} + +func TestOrm_BuildSql(t *testing.T) { + var u = Users{ + Name: "gorose2", + Age: 19, + } + + //aff, err := db.Force().Data(&u) + a, b, err := DB().Table(&u).Where("age", ">", 1).Data(&u).BuildSql("update") + if err != nil { + t.Error(err.Error()) + } + t.Log(a, b) +} + +func TestOrm_BuildSql_where(t *testing.T) { + var u = Users{ + Name: "gorose2", + Age: 19, + } + + var db = DB() + a, b, err := db.Table(&u).Where("age", ">", 1).Where(func() { + db.Where("name", "like", "%fizz%").OrWhere(func() { + db.Where("age", ">", 10).Where("uid", ">", 2) + }) + }).Limit(2).Offset(2).BuildSql() + if err != nil { + t.Error(err.Error()) + } + t.Log(a, b) +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..bb90ba5 --- /dev/null +++ b/session.go @@ -0,0 +1,547 @@ +package gorose + +import ( + "database/sql" + "errors" + "fmt" + "github.com/gohouse/t" + "reflect" + "strconv" + "strings" + "sync/atomic" + "time" +) + +const beginStatus = 0 + +// Session ... +type Session struct { + IEngin + IBinder + master *sql.DB + tx *sql.Tx + slave *sql.DB + lastInsertId int64 + sqlLogs []string + lastSql string + union interface{} + transaction bool + tx_index int64 + err error +} + +var _ ISession = (*Session)(nil) + +// NewSession : 初始化 Session +func NewSession(e IEngin) *Session { + + var s = new(Session) + s.IEngin = e + // 初始化 IBinder + s.SetIBinder(NewBinder()) + + s.master = e.GetExecuteDB() + s.slave = e.GetQueryDB() + //初始化数据 + s.tx_index = beginStatus + + return s +} + +func (s *Session) Close() { + s.master.Close() + s.slave.Close() +} + +// GetIEngin 获取engin +func (s *Session) GetIEngin() IEngin { + return s.IEngin +} + +// GetDriver 获取驱动 +func (s *Session) SetIEngin(ie IEngin) { + s.IEngin = ie +} + +// Bind : 传入绑定结果的对象, 参数一为对象, 可以是 struct, gorose.MapRow 或对应的切片 +// 如果是做非query操作,第一个参数也可以仅仅指定为字符串表名 +func (s *Session) Bind(tab interface{}) ISession { + //fmt.Println(tab, NewBinder(tab)) + //s.SetIBinder(NewBinder(tab)) + s.GetIBinder().SetBindOrigin(tab) + s.err = s.IBinder.BindParse(s.GetIEngin().GetPrefix()) + return s +} + +// GetBinder 获取绑定对象 +func (s *Session) GetErr() error { + return s.err +} + +// GetBinder 获取绑定对象 +func (s *Session) SetIBinder(ib IBinder) { + s.IBinder = ib +} + +// GetBinder 获取绑定对象 +func (s *Session) GetIBinder() IBinder { + return s.IBinder +} + +// GetBinder 获取绑定对象 +func (s *Session) ResetBinderResult() { + _ = s.IBinder.BindParse(s.GetIEngin().GetPrefix()) +} + +// GetTableName 获取解析后的名字, 提供给orm使用 +// 为什么要在这里重复添加该方法, 而不是直接继承 IBinder 的方法呢? +// 是因为, 这里涉及到表前缀的问题, 只能通过session来传递, 所以IOrm就可以选择直接继承 +func (s *Session) GetTableName() (string, error) { + //err := s.IBinder.BindParse(s.GetIEngin().GetPrefix()) + //fmt.Println(s.GetIBinder()) + return s.GetIBinder().GetBindName(), s.err +} + +// Begin ... +func (s *Session) Begin() (err error) { + s.SetTransaction(true) + if s.tx == nil { + s.tx, err = s.master.Begin() + } else { + num := atomic.AddInt64(&s.tx_index, 1) + err = s.SavePoint("sp_" + strconv.FormatInt(num, 10)) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } else { + } + } + return +} + +// Rollback ... +func (s *Session) Rollback() (err error) { + if s.tx != nil && s.transaction == true { + if atomic.LoadInt64(&s.tx_index) == beginStatus { + s.SetTransaction(false) + err = s.tx.Rollback() + s.tx = nil + } else { + num := atomic.LoadInt64(&s.tx_index) + err = s.RollbackTo("sp_" + strconv.FormatInt(num, 10)) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } else { + atomic.AddInt64(&s.tx_index, -1) + } + } + } + return +} + +// RollbackTo ... +func (s *Session) RollbackTo(savepoint string) (err error) { + if s.tx != nil && s.transaction == true { + _, err = s.tx.Exec("rollback to " + savepoint) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + } + return +} + +func (s *Session) SavePoint(savepoint string) (err error) { + if s.tx != nil && s.transaction == true { + _, err = s.tx.Exec("savepoint " + savepoint) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + } + return +} + +// Commit ... +func (s *Session) Commit() (err error) { + if s.tx != nil { + if atomic.LoadInt64(&s.tx_index) == beginStatus { + s.SetTransaction(false) + err = s.tx.Commit() + s.tx = nil + } else { + atomic.AddInt64(&s.tx_index, -1) + } + } else { + s.SetTransaction(false) + } + return +} + +// Transaction ... +func (s *Session) Transaction(closers ...func(ses ISession) error) (err error) { + err = s.Begin() + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return err + } + + for _, closer := range closers { + err = closer(s) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + _ = s.Rollback() + return + } + } + return s.Commit() +} + +// Query ... +func (s *Session) Query(sqlstring string, args ...interface{}) (result []Data, err error) { + // 记录开始时间 + start := time.Now() + //withRunTimeContext(func() { + if s.err != nil { + err = s.err + s.GetIEngin().GetLogger().Error(err.Error()) + } + // 记录sqlLog + s.lastSql = fmt.Sprint(sqlstring, ", ", args) + //if s.IfEnableSqlLog() { + // s.sqlLogs = append(s.sqlLogs, s.lastSql) + //} + + var stmt *sql.Stmt + // 如果是事务, 则从主库中读写 + if s.tx == nil { + stmt, err = s.slave.Prepare(sqlstring) + } else { + stmt, err = s.tx.Prepare(sqlstring) + } + + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + + defer stmt.Close() + rows, err := stmt.Query(args...) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + + // make sure we always close rows + defer rows.Close() + + err = s.scan(rows) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + //}, func(duration time.Duration) { + // //if duration.Seconds() > 1 { + // // s.GetIEngin().GetLogger().Slow(s.LastSql(), duration) + // //} else { + // // s.GetIEngin().GetLogger().Sql(s.LastSql(), duration) + // //} + //}) + + timeduration := time.Since(start) + //if timeduration.Seconds() > 1 { + s.GetIEngin().GetLogger().Slow(s.LastSql(), timeduration) + //} else { + s.GetIEngin().GetLogger().Sql(s.LastSql(), timeduration) + //} + + result = s.GetIBinder().GetBindAll() + return +} + +// Execute ... +func (s *Session) Execute(sqlstring string, args ...interface{}) (rowsAffected int64, err error) { + // 记录开始时间 + start := time.Now() + //withRunTimeContext(func() { + // err = s.GetIBinder().BindParse(s.GetIEngin().GetPrefix()) + if s.err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + s.lastSql = fmt.Sprint(sqlstring, ", ", args) + //// 记录sqlLog + //if s.IfEnableSqlLog() { + // s.sqlLogs = append(s.sqlLogs, s.lastSql) + //} + + var operType = strings.ToLower(sqlstring[0:6]) + if operType == "select" { + err = errors.New("Execute does not allow select operations, please use Query") + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + + var stmt *sql.Stmt + if s.tx == nil { + stmt, err = s.master.Prepare(sqlstring) + } else { + stmt, err = s.tx.Prepare(sqlstring) + } + + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + + //var err error + defer stmt.Close() + result, err := stmt.Exec(args...) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return + } + + if operType == "insert" || operType == "replace" { + // get last insert id + lastInsertId, err := result.LastInsertId() + if err == nil { + s.lastInsertId = lastInsertId + } else { + s.GetIEngin().GetLogger().Error(err.Error()) + } + } + // get rows affected + rowsAffected, err = result.RowsAffected() + timeduration := time.Since(start) + //}, func(duration time.Duration) { + if timeduration.Seconds() > 1 { + s.GetIEngin().GetLogger().Slow(s.LastSql(), timeduration) + } else { + s.GetIEngin().GetLogger().Sql(s.LastSql(), timeduration) + } + //}) + return +} + +// LastInsertId ... +func (s *Session) LastInsertId() int64 { + return s.lastInsertId +} + +// LastSql ... +func (s *Session) LastSql() string { + return s.lastSql +} + +func (s *Session) scan(rows *sql.Rows) (err error) { + // 如果不需要绑定, 则需要初始化一下binder + if s.GetIBinder() == nil { + s.SetIBinder(NewBinder()) + } + // 检查实多维数组还是一维数组 + switch s.GetBindType() { + case OBJECT_STRING: + err = s.scanAll(rows) + case OBJECT_STRUCT, OBJECT_STRUCT_SLICE: + err = s.scanStructAll(rows) + //case OBJECT_MAP, OBJECT_MAP_T: + // err = s.scanMap(rows, s.GetBindResult()) + case OBJECT_MAP, OBJECT_MAP_T, OBJECT_MAP_SLICE, OBJECT_MAP_SLICE_T: + err = s.scanMapAll(rows) + case OBJECT_NIL: + err = s.scanAll(rows) + default: + err = errors.New("Bind value error") + } + return +} + +//func (s *Session) scanMap(rows *sql.Rows, dst interface{}) (err error) { +// return s.scanMapAll(rows, dst) +//} + +func (s *Session) scanMapAll(rows *sql.Rows) (err error) { + var columns []string + // 获取查询的所有字段 + if columns, err = rows.Columns(); err != nil { + return + } + count := len(columns) + + for rows.Next() { + // 定义要绑定的结果集 + values := make([]interface{}, count) + scanArgs := make([]interface{}, count) + for i := 0; i < count; i++ { + scanArgs[i] = &values[i] + } + // 获取结果 + _ = rows.Scan(scanArgs...) + + // 定义预设的绑定对象 + //fmt.Println(reflect.TypeOf(s.GetBindResult()).Kind()) + var bindResultTmp = reflect.MakeMap(reflect.Indirect(reflect.ValueOf(s.GetBindResult())).Type()) + //// 定义union操作的map返回 + //var unionTmp = map[string]interface{}{} + for i, col := range columns { + var v interface{} + val := values[i] + if b, ok := val.([]byte); ok { + v = string(b) + } else { + v = val + } + // 如果是union操作就不需要绑定数据直接返回, 否则就绑定数据 + //TODO 这里可能有点问题, 比如在group时, 返回的结果不止一条, 这里直接返回的就是第一条 + // 默认其实只是取了第一条, 满足常规的 union 操作(count,sum,max,min,avg)而已 + // 后边需要再行完善, 以便group时使用 + // 具体完善方法: 就是这里断点去掉, 不直接绑定union, 新增一个map,将结果放在map中,在方法最后统一返回 + if s.GetUnion() != nil { + s.union = v + return + // 以下上通用解决方法 + //unionTmp[col] = v + //s.union = unionTmp + } else { + br := reflect.Indirect(reflect.ValueOf(s.GetBindResult())) + switch s.GetBindType() { + case OBJECT_MAP_T, OBJECT_MAP_SLICE_T: // t.T类型 + // 绑定到一条数据结果对象上,方便其他地方的调用,永远存储最新一条 + br.SetMapIndex(reflect.ValueOf(col), reflect.ValueOf(t.New(v))) + // 跟上一行干的事是一样的, 只不过防止上一行的数据被后续的数据改变, 而无法提供给下边多条数据报错的需要 + if s.GetBindType() == OBJECT_MAP_SLICE || s.GetBindType() == OBJECT_MAP_SLICE_T { + + bindResultTmp.SetMapIndex(reflect.ValueOf(col), reflect.ValueOf(t.New(v))) + } + default: // 普通类型map[string]interface{}, 具体代码注释参照 上一个 case + br.SetMapIndex(reflect.ValueOf(col), reflect.ValueOf(v)) + if s.GetBindType() == OBJECT_MAP_SLICE || s.GetBindType() == OBJECT_MAP_SLICE_T { + bindResultTmp.SetMapIndex(reflect.ValueOf(col), reflect.ValueOf(v)) + } + } + } + } + // 如果是union操作就不需要绑定数据直接返回, 否则就绑定数据 + if s.GetUnion() == nil { + // 如果是多条数据集, 就插入到对应的结果集slice上 + if s.GetBindType() == OBJECT_MAP_SLICE || s.GetBindType() == OBJECT_MAP_SLICE_T { + s.GetBindResultSlice().Set(reflect.Append(s.GetBindResultSlice(), bindResultTmp)) + } + } + } + return +} + +// ScanAll scans all sql result rows into a slice of structs. +// It reads all rows and closes rows when finished. +// dst should be a pointer to a slice of the appropriate type. +// The new results will be appended to any existing data in dst. +func (s *Session) scanStructAll(rows *sql.Rows) error { + // check if there is data waiting + //if !rows.Next() { + // if err := rows.Err(); err != nil { + // s.GetIEngin().GetLogger().Error(err.Error()) + // return err + // } + // return sql.ErrNoRows + //} + var sfs = structForScan(s.GetBindResult()) + for rows.Next() { + if s.GetUnion() != nil { + var union interface{} + err := rows.Scan(&union) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return err + } + s.union = union + return err + } + // scan it + //fmt.Printf("%#v \n",structForScan(s.GetBindResult())) + err := rows.Scan(sfs...) + if err != nil { + s.GetIEngin().GetLogger().Error(err.Error()) + return err + } + // 如果是union操作就不需要绑定数据直接返回, 否则就绑定数据 + if s.GetUnion() == nil { + // 如果是多条数据集, 就插入到对应的结果集slice上 + if s.GetBindType() == OBJECT_STRUCT_SLICE { + // add to the result slice + s.GetBindResultSlice().Set(reflect.Append(s.GetBindResultSlice(), + reflect.Indirect(reflect.ValueOf(s.GetBindResult())))) + } + } + } + + return rows.Err() +} + +func (s *Session) scanAll(rows *sql.Rows) (err error) { + var columns []string + // 获取查询的所有字段 + if columns, err = rows.Columns(); err != nil { + return + } + count := len(columns) + + var result = []Data{} + for rows.Next() { + // 定义要绑定的结果集 + values := make([]interface{}, count) + scanArgs := make([]interface{}, count) + for i := 0; i < count; i++ { + scanArgs[i] = &values[i] + } + // 获取结果 + _ = rows.Scan(scanArgs...) + + // 定义预设的绑定对象 + var resultTmp = Data{} + //// 定义union操作的map返回 + //var unionTmp = map[string]interface{}{} + for i, col := range columns { + var v interface{} + val := values[i] + if b, ok := val.([]byte); ok { + v = string(b) + } else { + v = val + } + if s.GetUnion() != nil { + s.union = v + return + // 以下上通用解决方法 + //unionTmp[col] = v + //s.union = unionTmp + } + resultTmp[col] = v + } + result = append(result, resultTmp) + } + s.IBinder.SetBindAll(result) + return +} + +// SetUnion ... +func (s *Session) SetUnion(u interface{}) { + s.union = u +} + +// GetUnion ... +func (s *Session) GetUnion() interface{} { + return s.union +} + +// SetTransaction ... +func (s *Session) SetTransaction(b bool) { + s.transaction = b +} + +// GetTransaction 提供给 orm 使用的, 方便reset操作 +func (s *Session) GetTransaction() bool { + return s.transaction +} diff --git a/session_interface.go b/session_interface.go new file mode 100644 index 0000000..c74bbc8 --- /dev/null +++ b/session_interface.go @@ -0,0 +1,29 @@ +package gorose + +// ISession ... +type ISession interface { + Close() + //Table(bind interface{}) IOrm + Bind(bind interface{}) ISession + Begin() (err error) + Rollback() (err error) + Commit() (err error) + Transaction(closer ...func(session ISession) error) (err error) + Query(sqlstring string, args ...interface{}) ([]Data, error) + Execute(sqlstring string, args ...interface{}) (int64, error) + //GetDriver() string + GetIEngin() IEngin + LastInsertId() int64 + LastSql() string + //SetIBinder(b IBinder) + GetTableName() (string, error) + SetIBinder(ib IBinder) + GetIBinder() IBinder + SetUnion(u interface{}) + GetUnion() interface{} + SetTransaction(b bool) + GetTransaction() bool + //ResetBinder() + GetBindAll() []Data + GetErr() error +} diff --git a/session_test.go b/session_test.go new file mode 100644 index 0000000..1c12b74 --- /dev/null +++ b/session_test.go @@ -0,0 +1,175 @@ +package gorose + +import ( + "errors" + "testing" +) + +func initSession() ISession { + return initDB().NewSession() +} + +func TestSession_Query(t *testing.T) { + var s = initSession() + //var user []Users + res, err := s.Query("select * from users where name=?", "gorose2") + if err != nil { + t.Error(err.Error()) + } + //t.Log(res) + t.Log(res, s.LastSql()) +} + +func TestSession_Query3(t *testing.T) { + var s = initSession() + var o []Users + //var o []map[string]interface{} + //var o []gorose.Data + res, err := s.Bind(&o).Query("select * from users limit 2") + if err != nil { + t.Error(err.Error()) + } + t.Log(res) + t.Log(o, s.LastSql()) +} + +func TestSession_Execute(t *testing.T) { + var sql = `CREATE TABLE IF NOT EXISTS "orders" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "goodsname" TEXT NOT NULL default "", + "price" decimal default "0.00" + )` + var s = initSession() + var err error + var aff int64 + + aff, err = s.Execute(sql) + if err != nil { + t.Error(err.Error()) + } + if aff == 0 { + return + } + + aff, err = s.Execute("insert into orders(goodsname,price) VALUES(?,?),(?,?)", + "goods1", 1.23, "goods2", 3.23) + if err != nil { + t.Error(err.Error()) + } + t.Log(aff) +} + +func TestSession_Query_struct(t *testing.T) { + var s = initSession() + var err error + // defer s.Close() + + var user []Users + _, err = s.Bind(&user).Query("select * from users limit ?", 2) + if err != nil { + t.Error(err.Error()) + } + t.Log("多条struct绑定:", user) + + var user2 Users + _, err = s.Bind(&user2).Query("select * from users limit ?", 2) + if err != nil { + t.Error(err.Error()) + } + t.Log("一条struct绑定:", user2) +} + +//type UserMap map[string]interface{} + +func TestSession_Query_map(t *testing.T) { + var s = initSession() + var err error + + var user2 = aaa{} + _, err = s.Bind(&user2).Query("select * from users limit ?", 2) + if err != nil { + t.Error(err.Error()) + } + t.Log("一条map绑定:", user2) + t.Log("一条map绑定的uid为:", user2["uid"]) + t.Log(s.LastSql()) + + var user = bbb{} + _, err = s.Bind(&user).Query("select * from users limit ?", 2) + if err != nil { + t.Error(err.Error()) + } + t.Log("多条map绑定:", user) + t.Log("多条map绑定:", user[0]["age"].Int()) + t.Log(s.LastSql()) +} + +func TestSession_Bind(t *testing.T) { + var s = initSession() + var err error + + var user2 = aaa{} + _, err = s.Bind(&user2).Query("select * from users limit ?", 2) + + if err != nil { + t.Error(err.Error()) + } + t.Log("session bind success") +} + +func TestSession_Transaction(t *testing.T) { + var s = initSession() + // 一键事务, 自动回滚和提交, 我们只需要关注业务即可 + err := s.Transaction(trans1, trans2) + if err != nil { + t.Error(err.Error()) + } + t.Log("session transaction success") +} + +func trans1(s ISession) error { + var err error + var aff int64 + aff, err = s.Execute("update users set name=?,age=? where uid=?", + "gorose3", 21, 3) + if err != nil { + return err + } + if aff == 0 { + return errors.New("fail") + } + + aff, err = s.Execute("update users set name=?,age=? where uid=?", + "gorose2", 20, 2) + if err != nil { + return err + } + if aff == 0 { + return errors.New("fail") + } + + return nil +} +func trans2(s ISession) error { + var err error + var aff int64 + aff, err = s.Execute("update users set name=?,age=? where uid=?", + "gorose3", 21, 3) + if err != nil { + return err + } + if aff == 0 { + return errors.New("fail") + } + + aff, err = s.Execute("update users set name=?,age=? where uid=?", + "gorose2", 20, 2) + if err != nil { + return err + } + if aff == 0 { + return errors.New("fail") + } + + return nil +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..09ecfc4 --- /dev/null +++ b/util.go @@ -0,0 +1,136 @@ +package gorose + +import ( + "fmt" + "github.com/gohouse/t" + "log" + "math/rand" + "os" + "path" + "reflect" + "strings" + "sync" + "time" +) + +func getRandomInt(num int) int { + rand.Seed(time.Now().UnixNano()) + return rand.Intn(num) +} + +func structForScan(u interface{}) []interface{} { + val := reflect.Indirect(reflect.ValueOf(u)) + v := make([]interface{}, 0) + for i := 0; i < val.NumField(); i++ { + valueField := val.Field(i) + if val.Type().Field(i).Tag.Get(TAGNAME) != IGNORE { + if valueField.CanAddr() { + v = append(v, valueField.Addr().Interface()) + } else { + //v[i] = valueField + v = append(v, valueField) + } + } + } + return v +} + +// StructToMap ... +func StructToMap(obj interface{}) map[string]interface{} { + ty := reflect.TypeOf(obj) + v := reflect.ValueOf(obj) + + var data = make(map[string]interface{}) + for i := 0; i < ty.NumField(); i++ { + data[ty.Field(i).Name] = v.Field(i).Interface() + } + return data +} + +// getTagName 获取结构体中Tag的值,如果没有tag则返回字段值 +func getTagName(structName interface{}, tagstr string) []string { + // 获取type + tag := reflect.TypeOf(structName) + // 如果是反射Ptr类型, 就获取他的 element type + if tag.Kind() == reflect.Ptr { + tag = tag.Elem() + } + + // 判断是否是struct + if tag.Kind() != reflect.Struct { + log.Println("Check type error not Struct") + return nil + } + // 获取字段数量 + fieldNum := tag.NumField() + result := make([]string, 0, fieldNum) + for i := 0; i < fieldNum; i++ { + // tag 名字 + tagName := tag.Field(i).Tag.Get(tagstr) + if tagName != IGNORE { + // tag为-时, 不解析 + if tagName == "-" || tagName == "" { + // 字段名字 + tagName = tag.Field(i).Name + } + result = append(result, tagName) + } + } + return result +} + +// If : ternary operator (三元运算) +// condition:比较运算 +// trueVal:运算结果为真时的值 +// falseVal:运算结果为假时的值 +// return: 由于不知道传入值的类型, 所有, 必须在接收结果时, 指定对应的值类型 +func If(condition bool, trueVal, falseVal interface{}) interface{} { + if condition { + return trueVal + } + return falseVal +} + +func addQuotes(data interface{}, sep string) string { + ret := t.New(data).String() + ret = strings.Replace(ret, `\`, `\\`, -1) + ret = strings.Replace(ret, `"`, `\"`, -1) + ret = strings.Replace(ret, `'`, `\'`, -1) + return fmt.Sprintf("%s%s%s", sep, ret, sep) +} + +// InArray :给定元素值 是否在 指定的数组中 +func inArray(needle, hystack interface{}) bool { + nt := t.New(needle) + for _, item := range t.New(hystack).Slice() { + if strings.ToLower(nt.String()) == strings.ToLower(item.String()) { + return true + } + } + return false +} + +func withLockContext(fn func()) { + var mu sync.Mutex + mu.Lock() + defer mu.Unlock() + fn() +} + +func withRunTimeContext(closer func(), callback func(time.Duration)) { + // 记录开始时间 + start := time.Now() + closer() + timeduration := time.Since(start) + //log.Println("执行完毕,用时:", timeduration.Seconds(),timeduration.Seconds()>1.1) + callback(timeduration) +} + +func readFile(filepath string) *os.File { + file, err := os.OpenFile(filepath, os.O_WRONLY|os.O_APPEND, 0666) + if err != nil && os.IsNotExist(err) { + _ = os.MkdirAll(path.Dir(filepath), os.ModePerm) + file, _ = os.Create(filepath) + } + return file +} diff --git a/util_test.go b/util_test.go new file mode 100644 index 0000000..ef19bf1 --- /dev/null +++ b/util_test.go @@ -0,0 +1,40 @@ +package gorose + +import ( + "testing" + "time" +) + +func TestStructToMap(t *testing.T) { + user := Users{Uid: 1, Name: "gorose"} + data := StructToMap(user) + t.Log(data) +} + +func TestIf(t *testing.T) { + closer := func() { + time.Sleep(1 * time.Second) + } + withRunTimeContext(closer, func(td time.Duration) { + t.Log("用时:", td, td.Seconds() > 1) + }) +} + +//func TestStructToMap2(t *testing.T) { +// var u Users +// //res := structForScan(&u) +// res := structForScan(reflect.ValueOf(&u).Interface()) +// for _, item := range res { +// err := varBindValue.BindVal(item, 1234) +// if err != nil { +// t.Error(err.Error()) +// } +// } +// t.Log(res, u) +//} +func Test_getRandomInt(t *testing.T) { + t.Log(getRandomInt(2)) + t.Log(getRandomInt(3)) + t.Log(getRandomInt(2)) + t.Log(getRandomInt(3)) +}