@@ -45,6 +45,10 @@ func (b *Builder) selectWriteTo(w Writer) error { | |||
} | |||
} | |||
if !b.cond.IsValid() { | |||
return nil | |||
} | |||
if _, err := fmt.Fprint(w, " WHERE "); err != nil { | |||
return err | |||
} | |||
@@ -25,7 +25,9 @@ func And(conds ...Cond) Cond { | |||
func (and condAnd) WriteTo(w Writer) error { | |||
for i, cond := range and { | |||
_, isOr := cond.(condOr) | |||
if isOr { | |||
_, isExpr := cond.(expr) | |||
wrap := isOr || isExpr | |||
if wrap { | |||
fmt.Fprint(w, "(") | |||
} | |||
@@ -34,7 +36,7 @@ func (and condAnd) WriteTo(w Writer) error { | |||
return err | |||
} | |||
if isOr { | |||
if wrap { | |||
fmt.Fprint(w, ")") | |||
} | |||
@@ -1,11 +1,12 @@ | |||
package core | |||
import ( | |||
"bytes" | |||
"encoding/gob" | |||
"errors" | |||
"fmt" | |||
"strings" | |||
"time" | |||
"bytes" | |||
"encoding/gob" | |||
) | |||
const ( | |||
@@ -55,11 +56,10 @@ func encodeIds(ids []PK) (string, error) { | |||
return buf.String(), err | |||
} | |||
func decodeIds(s string) ([]PK, error) { | |||
pks := make([]PK, 0) | |||
dec := gob.NewDecoder(bytes.NewBufferString(s)) | |||
dec := gob.NewDecoder(strings.NewReader(s)) | |||
err := dec.Decode(&pks) | |||
return pks, err | |||
@@ -11,4 +11,5 @@ database: | |||
test: | |||
override: | |||
# './...' is a relative pattern which means all subdirectories | |||
- go test -v -race | |||
- go test -v -race | |||
- go test -v -race --dbtype=sqlite3 |
@@ -79,6 +79,10 @@ func (col *Column) String(d Dialect) string { | |||
} | |||
} | |||
if col.Default != "" { | |||
sql += "DEFAULT " + col.Default + " " | |||
} | |||
if d.ShowCreateNull() { | |||
if col.Nullable { | |||
sql += "NULL " | |||
@@ -87,10 +91,6 @@ func (col *Column) String(d Dialect) string { | |||
} | |||
} | |||
if col.Default != "" { | |||
sql += "DEFAULT " + col.Default + " " | |||
} | |||
return sql | |||
} | |||
@@ -99,6 +99,10 @@ func (col *Column) StringNoPk(d Dialect) string { | |||
sql += d.SqlType(col) + " " | |||
if col.Default != "" { | |||
sql += "DEFAULT " + col.Default + " " | |||
} | |||
if d.ShowCreateNull() { | |||
if col.Nullable { | |||
sql += "NULL " | |||
@@ -107,10 +111,6 @@ func (col *Column) StringNoPk(d Dialect) string { | |||
} | |||
} | |||
if col.Default != "" { | |||
sql += "DEFAULT " + col.Default + " " | |||
} | |||
return sql | |||
} | |||
@@ -44,6 +44,9 @@ func convertTime(dest *NullTime, src interface{}) error { | |||
} | |||
*dest = NullTime(t) | |||
return nil | |||
case time.Time: | |||
*dest = NullTime(s) | |||
return nil | |||
case nil: | |||
default: | |||
return fmt.Errorf("unsupported driver -> Scan pair: %T -> %T", src, dest) | |||
@@ -32,13 +32,10 @@ proposed functionality. | |||
We appreciate any bug reports, but especially ones with self-contained | |||
(doesn't depend on code outside of xorm), minimal (can't be simplified | |||
further) test cases. It's especially helpful if you can submit a pull | |||
request with just the failing test case (you'll probably want to | |||
pattern it after the tests in | |||
[base.go](https://github.com/go-xorm/tests/blob/master/base.go) AND | |||
[benchmark.go](https://github.com/go-xorm/tests/blob/master/benchmark.go). | |||
request with just the failing test case(you can find some example test file like [session_get_test.go](https://github.com/go-xorm/xorm/blob/master/session_get_test.go)). | |||
If you implements a new database interface, you maybe need to add a <databasename>_test.go file. | |||
For example, [mysql_test.go](https://github.com/go-xorm/tests/blob/master/mysql/mysql_test.go) | |||
If you implements a new database interface, you maybe need to add a test_<databasename>.sh file. | |||
For example, [mysql_test.go](https://github.com/go-xorm/xorm/blob/master/test_mysql.sh) | |||
### New functionality | |||
@@ -28,6 +28,8 @@ Xorm is a simple and powerful ORM for Go. | |||
* SQL Builder support via [github.com/go-xorm/builder](https://github.com/go-xorm/builder) | |||
* Automatical Read/Write seperatelly | |||
# Drivers Support | |||
Drivers for Go's sql package which currently support database/sql includes: | |||
@@ -48,6 +50,13 @@ Drivers for Go's sql package which currently support database/sql includes: | |||
# Changelog | |||
* **v0.6.4** | |||
* Automatical Read/Write seperatelly | |||
* Query/QueryString/QueryInterface and action with Where/And | |||
* Get support non-struct variables | |||
* BufferSize on Iterate | |||
* fix some other bugs. | |||
* **v0.6.3** | |||
* merge tests to main project | |||
* add `Exist` function | |||
@@ -61,13 +70,6 @@ Drivers for Go's sql package which currently support database/sql includes: | |||
* add Scan features to Get | |||
* add QueryString method | |||
* **v0.6.0** | |||
* remove support for ql | |||
* add query condition builder support via [github.com/go-xorm/builder](https://github.com/go-xorm/builder), so `Where`, `And`, `Or` | |||
methods can use `builder.Cond` as parameter | |||
* add Sum, SumInt, SumInt64 and NotIn methods | |||
* some bugs fixed | |||
[More changes ...](https://github.com/go-xorm/manual-en-US/tree/master/chapter-16) | |||
# Installation | |||
@@ -106,15 +108,36 @@ type User struct { | |||
err := engine.Sync2(new(User)) | |||
``` | |||
* `Query` runs a SQL string, the returned results is `[]map[string][]byte`, `QueryString` returns `[]map[string]string`. | |||
* Create Engine Group | |||
```Go | |||
dataSourceNameSlice := []string{masterDataSourceName, slave1DataSourceName, slave2DataSourceName} | |||
engineGroup, err := xorm.NewEngineGroup(driverName, dataSourceNameSlice) | |||
``` | |||
```Go | |||
masterEngine, err := xorm.NewEngine(driverName, masterDataSourceName) | |||
slave1Engine, err := xorm.NewEngine(driverName, slave1DataSourceName) | |||
slave2Engine, err := xorm.NewEngine(driverName, slave2DataSourceName) | |||
engineGroup, err := xorm.NewEngineGroup(masterEngine, []*Engine{slave1Engine, slave2Engine}) | |||
``` | |||
Then all place where `engine` you can just use `engineGroup`. | |||
* `Query` runs a SQL string, the returned results is `[]map[string][]byte`, `QueryString` returns `[]map[string]string`, `QueryInterface` returns `[]map[string]interface{}`. | |||
```Go | |||
results, err := engine.Query("select * from user") | |||
results, err := engine.Where("a = 1").Query() | |||
results, err := engine.QueryString("select * from user") | |||
results, err := engine.Where("a = 1").QueryString() | |||
results, err := engine.QueryInterface("select * from user") | |||
results, err := engine.Where("a = 1").QueryInterface() | |||
``` | |||
* `Execute` runs a SQL string, it returns `affected` and `error` | |||
* `Exec` runs a SQL string, it returns `affected` and `error` | |||
```Go | |||
affected, err := engine.Exec("update user set age = ? where name = ?", age, name) | |||
@@ -125,62 +148,76 @@ affected, err := engine.Exec("update user set age = ? where name = ?", age, name | |||
```Go | |||
affected, err := engine.Insert(&user) | |||
// INSERT INTO struct () values () | |||
affected, err := engine.Insert(&user1, &user2) | |||
// INSERT INTO struct1 () values () | |||
// INSERT INTO struct2 () values () | |||
affected, err := engine.Insert(&users) | |||
// INSERT INTO struct () values (),(),() | |||
affected, err := engine.Insert(&user1, &users) | |||
// INSERT INTO struct1 () values () | |||
// INSERT INTO struct2 () values (),(),() | |||
``` | |||
* Query one record from database | |||
* `Get` query one record from database | |||
```Go | |||
has, err := engine.Get(&user) | |||
// SELECT * FROM user LIMIT 1 | |||
has, err := engine.Where("name = ?", name).Desc("id").Get(&user) | |||
// SELECT * FROM user WHERE name = ? ORDER BY id DESC LIMIT 1 | |||
var name string | |||
has, err := engine.Where("id = ?", id).Cols("name").Get(&name) | |||
// SELECT name FROM user WHERE id = ? | |||
var id int64 | |||
has, err := engine.Where("name = ?", name).Cols("id").Get(&id) | |||
has, err := engine.SQL("select id from user").Get(&id) | |||
// SELECT id FROM user WHERE name = ? | |||
var valuesMap = make(map[string]string) | |||
has, err := engine.Where("id = ?", id).Get(&valuesMap) | |||
// SELECT * FROM user WHERE id = ? | |||
var valuesSlice = make([]interface{}, len(cols)) | |||
has, err := engine.Where("id = ?", id).Cols(cols...).Get(&valuesSlice) | |||
// SELECT col1, col2, col3 FROM user WHERE id = ? | |||
``` | |||
* Check if one record exist on table | |||
* `Exist` check if one record exist on table | |||
```Go | |||
has, err := testEngine.Exist(new(RecordExist)) | |||
// SELECT * FROM record_exist LIMIT 1 | |||
has, err = testEngine.Exist(&RecordExist{ | |||
Name: "test1", | |||
}) | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
has, err = testEngine.Where("name = ?", "test1").Exist(&RecordExist{}) | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
has, err = testEngine.SQL("select * from record_exist where name = ?", "test1").Exist() | |||
// select * from record_exist where name = ? | |||
has, err = testEngine.Table("record_exist").Exist() | |||
// SELECT * FROM record_exist LIMIT 1 | |||
has, err = testEngine.Table("record_exist").Where("name = ?", "test1").Exist() | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
``` | |||
* Query multiple records from database, also you can use join and extends | |||
* `Find` query multiple records from database, also you can use join and extends | |||
```Go | |||
var users []User | |||
err := engine.Where("name = ?", name).And("age > 10").Limit(10, 0).Find(&users) | |||
// SELECT * FROM user WHERE name = ? AND age > 10 limit 0 offset 10 | |||
// SELECT * FROM user WHERE name = ? AND age > 10 limit 10 offset 0 | |||
type Detail struct { | |||
Id int64 | |||
@@ -193,14 +230,14 @@ type UserDetail struct { | |||
} | |||
var users []UserDetail | |||
err := engine.Table("user").Select("user.*, detail.*") | |||
err := engine.Table("user").Select("user.*, detail.*"). | |||
Join("INNER", "detail", "detail.user_id = user.id"). | |||
Where("user.name = ?", name).Limit(10, 0). | |||
Find(&users) | |||
// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 0 offset 10 | |||
// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 10 offset 0 | |||
``` | |||
* Query multiple records and record by record handle, there are two methods Iterate and Rows | |||
* `Iterate` and `Rows` query multiple records and record by record handle, there are two methods Iterate and Rows | |||
```Go | |||
err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
@@ -209,6 +246,13 @@ err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
}) | |||
// SELECT * FROM user | |||
err := engine.BufferSize(100).Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
user := bean.(*User) | |||
return nil | |||
}) | |||
// SELECT * FROM user Limit 0, 100 | |||
// SELECT * FROM user Limit 101, 100 | |||
rows, err := engine.Rows(&User{Name:name}) | |||
// SELECT * FROM user | |||
defer rows.Close() | |||
@@ -218,7 +262,7 @@ for rows.Next() { | |||
} | |||
``` | |||
* Update one or more records, default will update non-empty and non-zero fields except when you use Cols, AllCols and so on. | |||
* `Update` update one or more records, default will update non-empty and non-zero fields except when you use Cols, AllCols and so on. | |||
```Go | |||
affected, err := engine.Id(1).Update(&user) | |||
@@ -243,21 +287,39 @@ affected, err := engine.Id(1).AllCols().Update(&user) | |||
// UPDATE user SET name=?,age=?,salt=?,passwd=?,updated=? Where id = ? | |||
``` | |||
* Delete one or more records, Delete MUST have condition | |||
* `Delete` delete one or more records, Delete MUST have condition | |||
```Go | |||
affected, err := engine.Where(...).Delete(&user) | |||
// DELETE FROM user Where ... | |||
affected, err := engine.Id(2).Delete(&user) | |||
affected, err := engine.ID(2).Delete(&user) | |||
// DELETE FROM user Where id = ? | |||
``` | |||
* Count records | |||
* `Count` count records | |||
```Go | |||
counts, err := engine.Count(&user) | |||
// SELECT count(*) AS total FROM user | |||
``` | |||
* `Sum` sum functions | |||
```Go | |||
agesFloat64, err := engine.Sum(&user, "age") | |||
// SELECT sum(age) AS total FROM user | |||
agesInt64, err := engine.SumInt(&user, "age") | |||
// SELECT sum(age) AS total FROM user | |||
sumFloat64Slice, err := engine.Sums(&user, "age", "score") | |||
// SELECT sum(age), sum(score) FROM user | |||
sumInt64Slice, err := engine.SumsInt(&user, "age", "score") | |||
// SELECT sum(age), sum(score) FROM user | |||
``` | |||
* Query conditions builder | |||
```Go | |||
@@ -265,6 +327,59 @@ err := engine.Where(builder.NotIn("a", 1, 2).And(builder.In("b", "c", "d", "e")) | |||
// SELECT id, name ... FROM user WHERE a NOT IN (?, ?) AND b IN (?, ?, ?) | |||
``` | |||
* Multiple operations in one go routine, no transation here but resue session memory | |||
```Go | |||
session := engine.NewSession() | |||
defer session.Close() | |||
user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} | |||
if _, err := session.Insert(&user1); err != nil { | |||
return err | |||
} | |||
user2 := Userinfo{Username: "yyy"} | |||
if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { | |||
return err | |||
} | |||
if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { | |||
return err | |||
} | |||
return nil | |||
``` | |||
* Transation should on one go routine. There is transaction and resue session memory | |||
```Go | |||
session := engine.NewSession() | |||
defer session.Close() | |||
// add Begin() before any action | |||
if err := session.Begin(); err != nil { | |||
// if returned then will rollback automatically | |||
return err | |||
} | |||
user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} | |||
if _, err := session.Insert(&user1); err != nil { | |||
return err | |||
} | |||
user2 := Userinfo{Username: "yyy"} | |||
if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { | |||
return err | |||
} | |||
if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { | |||
return err | |||
} | |||
// add Commit() after all actions | |||
return session.Commit() | |||
``` | |||
# Cases | |||
* [studygolang](http://studygolang.com/) - [github.com/studygolang/studygolang](https://github.com/studygolang/studygolang) | |||
@@ -115,12 +115,33 @@ type User struct { | |||
err := engine.Sync2(new(User)) | |||
``` | |||
* `Query` 最原始的也支持SQL语句查询,返回的结果类型为 []map[string][]byte。`QueryString` 返回 []map[string]string | |||
* 创建Engine组 | |||
```Go | |||
dataSourceNameSlice := []string{masterDataSourceName, slave1DataSourceName, slave2DataSourceName} | |||
engineGroup, err := xorm.NewEngineGroup(driverName, dataSourceNameSlice) | |||
``` | |||
```Go | |||
masterEngine, err := xorm.NewEngine(driverName, masterDataSourceName) | |||
slave1Engine, err := xorm.NewEngine(driverName, slave1DataSourceName) | |||
slave2Engine, err := xorm.NewEngine(driverName, slave2DataSourceName) | |||
engineGroup, err := xorm.NewEngineGroup(masterEngine, []*Engine{slave1Engine, slave2Engine}) | |||
``` | |||
所有使用 `engine` 都可以简单的用 `engineGroup` 来替换。 | |||
* `Query` 最原始的也支持SQL语句查询,返回的结果类型为 []map[string][]byte。`QueryString` 返回 []map[string]string, `QueryInterface` 返回 `[]map[string]interface{}`. | |||
```Go | |||
results, err := engine.Query("select * from user") | |||
results, err := engine.Where("a = 1").Query() | |||
results, err := engine.QueryString("select * from user") | |||
results, err := engine.Where("a = 1").QueryString() | |||
results, err := engine.QueryInterface("select * from user") | |||
results, err := engine.Where("a = 1").QueryInterface() | |||
``` | |||
* `Exec` 执行一个SQL语句 | |||
@@ -129,67 +150,81 @@ results, err := engine.QueryString("select * from user") | |||
affected, err := engine.Exec("update user set age = ? where name = ?", age, name) | |||
``` | |||
* 插入一条或者多条记录 | |||
* `Insert` 插入一条或者多条记录 | |||
```Go | |||
affected, err := engine.Insert(&user) | |||
// INSERT INTO struct () values () | |||
affected, err := engine.Insert(&user1, &user2) | |||
// INSERT INTO struct1 () values () | |||
// INSERT INTO struct2 () values () | |||
affected, err := engine.Insert(&users) | |||
// INSERT INTO struct () values (),(),() | |||
affected, err := engine.Insert(&user1, &users) | |||
// INSERT INTO struct1 () values () | |||
// INSERT INTO struct2 () values (),(),() | |||
``` | |||
* 查询单条记录 | |||
* `Get` 查询单条记录 | |||
```Go | |||
has, err := engine.Get(&user) | |||
// SELECT * FROM user LIMIT 1 | |||
has, err := engine.Where("name = ?", name).Desc("id").Get(&user) | |||
// SELECT * FROM user WHERE name = ? ORDER BY id DESC LIMIT 1 | |||
var name string | |||
has, err := engine.Where("id = ?", id).Cols("name").Get(&name) | |||
// SELECT name FROM user WHERE id = ? | |||
var id int64 | |||
has, err := engine.Where("name = ?", name).Cols("id").Get(&id) | |||
has, err := engine.SQL("select id from user").Get(&id) | |||
// SELECT id FROM user WHERE name = ? | |||
var valuesMap = make(map[string]string) | |||
has, err := engine.Where("id = ?", id).Get(&valuesMap) | |||
// SELECT * FROM user WHERE id = ? | |||
var valuesSlice = make([]interface{}, len(cols)) | |||
has, err := engine.Where("id = ?", id).Cols(cols...).Get(&valuesSlice) | |||
// SELECT col1, col2, col3 FROM user WHERE id = ? | |||
``` | |||
* 检测记录是否存在 | |||
* `Exist` 检测记录是否存在 | |||
```Go | |||
has, err := testEngine.Exist(new(RecordExist)) | |||
// SELECT * FROM record_exist LIMIT 1 | |||
has, err = testEngine.Exist(&RecordExist{ | |||
Name: "test1", | |||
}) | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
has, err = testEngine.Where("name = ?", "test1").Exist(&RecordExist{}) | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
has, err = testEngine.SQL("select * from record_exist where name = ?", "test1").Exist() | |||
// select * from record_exist where name = ? | |||
has, err = testEngine.Table("record_exist").Exist() | |||
// SELECT * FROM record_exist LIMIT 1 | |||
has, err = testEngine.Table("record_exist").Where("name = ?", "test1").Exist() | |||
// SELECT * FROM record_exist WHERE name = ? LIMIT 1 | |||
``` | |||
* 查询多条记录,当然可以使用Join和extends来组合使用 | |||
* `Find` 查询多条记录,当然可以使用Join和extends来组合使用 | |||
```Go | |||
var users []User | |||
err := engine.Where("name = ?", name).And("age > 10").Limit(10, 0).Find(&users) | |||
// SELECT * FROM user WHERE name = ? AND age > 10 limit 0 offset 10 | |||
// SELECT * FROM user WHERE name = ? AND age > 10 limit 10 offset 0 | |||
type Detail struct { | |||
Id int64 | |||
@@ -206,10 +241,10 @@ err := engine.Table("user").Select("user.*, detail.*") | |||
Join("INNER", "detail", "detail.user_id = user.id"). | |||
Where("user.name = ?", name).Limit(10, 0). | |||
Find(&users) | |||
// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 0 offset 10 | |||
// SELECT user.*, detail.* FROM user INNER JOIN detail WHERE user.name = ? limit 10 offset 0 | |||
``` | |||
* 根据条件遍历数据库,可以有两种方式: Iterate and Rows | |||
* `Iterate` 和 `Rows` 根据条件遍历数据库,可以有两种方式: Iterate and Rows | |||
```Go | |||
err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
@@ -218,6 +253,13 @@ err := engine.Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
}) | |||
// SELECT * FROM user | |||
err := engine.BufferSize(100).Iterate(&User{Name:name}, func(idx int, bean interface{}) error { | |||
user := bean.(*User) | |||
return nil | |||
}) | |||
// SELECT * FROM user Limit 0, 100 | |||
// SELECT * FROM user Limit 101, 100 | |||
rows, err := engine.Rows(&User{Name:name}) | |||
// SELECT * FROM user | |||
defer rows.Close() | |||
@@ -227,7 +269,7 @@ for rows.Next() { | |||
} | |||
``` | |||
* 更新数据,除非使用Cols,AllCols函数指明,默认只更新非空和非0的字段 | |||
* `Update` 更新数据,除非使用Cols,AllCols函数指明,默认只更新非空和非0的字段 | |||
```Go | |||
affected, err := engine.Id(1).Update(&user) | |||
@@ -252,20 +294,39 @@ affected, err := engine.Id(1).AllCols().Update(&user) | |||
// UPDATE user SET name=?,age=?,salt=?,passwd=?,updated=? Where id = ? | |||
``` | |||
* 删除记录,需要注意,删除必须至少有一个条件,否则会报错。要清空数据库可以用EmptyTable | |||
* `Delete` 删除记录,需要注意,删除必须至少有一个条件,否则会报错。要清空数据库可以用EmptyTable | |||
```Go | |||
affected, err := engine.Where(...).Delete(&user) | |||
// DELETE FROM user Where ... | |||
affected, err := engine.ID(2).Delete(&user) | |||
// DELETE FROM user Where id = ? | |||
``` | |||
* 获取记录条数 | |||
* `Count` 获取记录条数 | |||
```Go | |||
counts, err := engine.Count(&user) | |||
// SELECT count(*) AS total FROM user | |||
``` | |||
* `Sum` 求和函数 | |||
```Go | |||
agesFloat64, err := engine.Sum(&user, "age") | |||
// SELECT sum(age) AS total FROM user | |||
agesInt64, err := engine.SumInt(&user, "age") | |||
// SELECT sum(age) AS total FROM user | |||
sumFloat64Slice, err := engine.Sums(&user, "age", "score") | |||
// SELECT sum(age), sum(score) FROM user | |||
sumInt64Slice, err := engine.SumsInt(&user, "age", "score") | |||
// SELECT sum(age), sum(score) FROM user | |||
``` | |||
* 条件编辑器 | |||
```Go | |||
@@ -273,6 +334,59 @@ err := engine.Where(builder.NotIn("a", 1, 2).And(builder.In("b", "c", "d", "e")) | |||
// SELECT id, name ... FROM user WHERE a NOT IN (?, ?) AND b IN (?, ?, ?) | |||
``` | |||
* 在一个Go程中多次操作数据库,但没有事务 | |||
```Go | |||
session := engine.NewSession() | |||
defer session.Close() | |||
user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} | |||
if _, err := session.Insert(&user1); err != nil { | |||
return err | |||
} | |||
user2 := Userinfo{Username: "yyy"} | |||
if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { | |||
return err | |||
} | |||
if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { | |||
return err | |||
} | |||
return nil | |||
``` | |||
* 在一个Go程中有事务 | |||
```Go | |||
session := engine.NewSession() | |||
defer session.Close() | |||
// add Begin() before any action | |||
if err := session.Begin(); err != nil { | |||
// if returned then will rollback automatically | |||
return err | |||
} | |||
user1 := Userinfo{Username: "xiaoxiao", Departname: "dev", Alias: "lunny", Created: time.Now()} | |||
if _, err := session.Insert(&user1); err != nil { | |||
return err | |||
} | |||
user2 := Userinfo{Username: "yyy"} | |||
if _, err := session.Where("id = ?", 2).Update(&user2); err != nil { | |||
return err | |||
} | |||
if _, err := session.Exec("delete from userinfo where username = ?", user2.Username); err != nil { | |||
return err | |||
} | |||
// add Commit() after all actions | |||
return session.Commit() | |||
``` | |||
# 案例 | |||
* [Go语言中文网](http://studygolang.com/) - [github.com/studygolang/studygolang](https://github.com/studygolang/studygolang) | |||
@@ -0,0 +1,26 @@ | |||
// Copyright 2017 The Xorm Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
// +build go1.8 | |||
package xorm | |||
import "context" | |||
// PingContext tests if database is alive | |||
func (engine *Engine) PingContext(ctx context.Context) error { | |||
session := engine.NewSession() | |||
defer session.Close() | |||
return session.PingContext(ctx) | |||
} | |||
// PingContext test if database is ok | |||
func (session *Session) PingContext(ctx context.Context) error { | |||
if session.isAutoClose { | |||
defer session.Close() | |||
} | |||
session.engine.logger.Infof("PING DATABASE %v", session.engine.DriverName()) | |||
return session.DB().PingContext(ctx) | |||
} |
@@ -209,10 +209,10 @@ func convertAssign(dest, src interface{}) error { | |||
if src == nil { | |||
dv.Set(reflect.Zero(dv.Type())) | |||
return nil | |||
} else { | |||
dv.Set(reflect.New(dv.Type().Elem())) | |||
return convertAssign(dv.Interface(), src) | |||
} | |||
dv.Set(reflect.New(dv.Type().Elem())) | |||
return convertAssign(dv.Interface(), src) | |||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: | |||
s := asString(src) | |||
i64, err := strconv.ParseInt(s, 10, dv.Type().Bits()) | |||
@@ -8,7 +8,6 @@ import ( | |||
"errors" | |||
"fmt" | |||
"net/url" | |||
"sort" | |||
"strconv" | |||
"strings" | |||
@@ -765,13 +764,18 @@ var ( | |||
"YES": true, | |||
"ZONE": true, | |||
} | |||
// DefaultPostgresSchema default postgres schema | |||
DefaultPostgresSchema = "public" | |||
) | |||
type postgres struct { | |||
core.Base | |||
schema string | |||
} | |||
func (db *postgres) Init(d *core.DB, uri *core.Uri, drivername, dataSourceName string) error { | |||
db.schema = DefaultPostgresSchema | |||
return db.Base.Init(d, db, uri, drivername, dataSourceName) | |||
} | |||
@@ -923,7 +927,7 @@ func (db *postgres) IsColumnExist(tableName, colName string) (bool, error) { | |||
func (db *postgres) GetColumns(tableName string) ([]string, map[string]*core.Column, error) { | |||
// FIXME: the schema should be replaced by user custom's | |||
args := []interface{}{tableName, "public"} | |||
args := []interface{}{tableName, db.schema} | |||
s := `SELECT column_name, column_default, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_precision_radix , | |||
CASE WHEN p.contype = 'p' THEN true ELSE false END AS primarykey, | |||
CASE WHEN p.contype = 'u' THEN true ELSE false END AS uniquekey | |||
@@ -1024,8 +1028,7 @@ WHERE c.relkind = 'r'::char AND c.relname = $1 AND s.table_schema = $2 AND f.att | |||
} | |||
func (db *postgres) GetTables() ([]*core.Table, error) { | |||
// FIXME: replace public to user customrize schema | |||
args := []interface{}{"public"} | |||
args := []interface{}{db.schema} | |||
s := fmt.Sprintf("SELECT tablename FROM pg_tables WHERE schemaname = $1") | |||
db.LogSQL(s, args) | |||
@@ -1050,8 +1053,7 @@ func (db *postgres) GetTables() ([]*core.Table, error) { | |||
} | |||
func (db *postgres) GetIndexes(tableName string) (map[string]*core.Index, error) { | |||
// FIXME: replace the public schema to user specify schema | |||
args := []interface{}{"public", tableName} | |||
args := []interface{}{db.schema, tableName} | |||
s := fmt.Sprintf("SELECT indexname, indexdef FROM pg_indexes WHERE schemaname=$1 AND tablename=$2") | |||
db.LogSQL(s, args) | |||
@@ -1117,10 +1119,6 @@ func (vs values) Get(k string) (v string) { | |||
return vs[k] | |||
} | |||
func errorf(s string, args ...interface{}) { | |||
panic(fmt.Errorf("pq: %s", fmt.Sprintf(s, args...))) | |||
} | |||
func parseURL(connstr string) (string, error) { | |||
u, err := url.Parse(connstr) | |||
if err != nil { | |||
@@ -1131,46 +1129,18 @@ func parseURL(connstr string) (string, error) { | |||
return "", fmt.Errorf("invalid connection protocol: %s", u.Scheme) | |||
} | |||
var kvs []string | |||
escaper := strings.NewReplacer(` `, `\ `, `'`, `\'`, `\`, `\\`) | |||
accrue := func(k, v string) { | |||
if v != "" { | |||
kvs = append(kvs, k+"="+escaper.Replace(v)) | |||
} | |||
} | |||
if u.User != nil { | |||
v := u.User.Username() | |||
accrue("user", v) | |||
v, _ = u.User.Password() | |||
accrue("password", v) | |||
} | |||
i := strings.Index(u.Host, ":") | |||
if i < 0 { | |||
accrue("host", u.Host) | |||
} else { | |||
accrue("host", u.Host[:i]) | |||
accrue("port", u.Host[i+1:]) | |||
} | |||
if u.Path != "" { | |||
accrue("dbname", u.Path[1:]) | |||
return escaper.Replace(u.Path[1:]), nil | |||
} | |||
q := u.Query() | |||
for k := range q { | |||
accrue(k, q.Get(k)) | |||
} | |||
sort.Strings(kvs) // Makes testing easier (not a performance concern) | |||
return strings.Join(kvs, " "), nil | |||
return "", nil | |||
} | |||
func parseOpts(name string, o values) { | |||
func parseOpts(name string, o values) error { | |||
if len(name) == 0 { | |||
return | |||
return fmt.Errorf("invalid options: %s", name) | |||
} | |||
name = strings.TrimSpace(name) | |||
@@ -1179,31 +1149,36 @@ func parseOpts(name string, o values) { | |||
for _, p := range ps { | |||
kv := strings.Split(p, "=") | |||
if len(kv) < 2 { | |||
errorf("invalid option: %q", p) | |||
return fmt.Errorf("invalid option: %q", p) | |||
} | |||
o.Set(kv[0], kv[1]) | |||
} | |||
return nil | |||
} | |||
func (p *pqDriver) Parse(driverName, dataSourceName string) (*core.Uri, error) { | |||
db := &core.Uri{DbType: core.POSTGRES} | |||
o := make(values) | |||
var err error | |||
if strings.HasPrefix(dataSourceName, "postgresql://") || strings.HasPrefix(dataSourceName, "postgres://") { | |||
dataSourceName, err = parseURL(dataSourceName) | |||
db.DbName, err = parseURL(dataSourceName) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} else { | |||
o := make(values) | |||
err = parseOpts(dataSourceName, o) | |||
if err != nil { | |||
return nil, err | |||
} | |||
db.DbName = o.Get("dbname") | |||
} | |||
parseOpts(dataSourceName, o) | |||
db.DbName = o.Get("dbname") | |||
if db.DbName == "" { | |||
return nil, errors.New("dbname is empty") | |||
} | |||
/*db.Schema = o.Get("schema") | |||
if len(db.Schema) == 0 { | |||
db.Schema = "public" | |||
}*/ | |||
return db, nil | |||
} |
@@ -47,6 +47,23 @@ type Engine struct { | |||
disableGlobalCache bool | |||
tagHandlers map[string]tagHandler | |||
engineGroup *EngineGroup | |||
} | |||
// BufferSize sets buffer size for iterate | |||
func (engine *Engine) BufferSize(size int) *Session { | |||
session := engine.NewSession() | |||
session.isAutoClose = true | |||
return session.BufferSize(size) | |||
} | |||
// CondDeleted returns the conditions whether a record is soft deleted. | |||
func (engine *Engine) CondDeleted(colName string) builder.Cond { | |||
if engine.dialect.DBType() == core.MSSQL { | |||
return builder.IsNull{colName} | |||
} | |||
return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) | |||
} | |||
// ShowSQL show SQL statement or not on logger if log level is great than INFO | |||
@@ -79,6 +96,11 @@ func (engine *Engine) SetLogger(logger core.ILogger) { | |||
engine.dialect.SetLogger(logger) | |||
} | |||
// SetLogLevel sets the logger level | |||
func (engine *Engine) SetLogLevel(level core.LogLevel) { | |||
engine.logger.SetLevel(level) | |||
} | |||
// SetDisableGlobalCache disable global cache or not | |||
func (engine *Engine) SetDisableGlobalCache(disable bool) { | |||
if engine.disableGlobalCache != disable { | |||
@@ -201,6 +223,11 @@ func (engine *Engine) SetDefaultCacher(cacher core.Cacher) { | |||
engine.Cacher = cacher | |||
} | |||
// GetDefaultCacher returns the default cacher | |||
func (engine *Engine) GetDefaultCacher() core.Cacher { | |||
return engine.Cacher | |||
} | |||
// NoCache If you has set default cacher, and you want temporilly stop use cache, | |||
// you can use NoCache() | |||
func (engine *Engine) NoCache() *Session { | |||
@@ -736,6 +763,13 @@ func (engine *Engine) OrderBy(order string) *Session { | |||
return session.OrderBy(order) | |||
} | |||
// Prepare enables prepare statement | |||
func (engine *Engine) Prepare() *Session { | |||
session := engine.NewSession() | |||
session.isAutoClose = true | |||
return session.Prepare() | |||
} | |||
// Join the join_operator should be one of INNER, LEFT OUTER, CROSS etc - this will be prepended to JOIN | |||
func (engine *Engine) Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session { | |||
session := engine.NewSession() | |||
@@ -757,7 +791,8 @@ func (engine *Engine) Having(conditions string) *Session { | |||
return session.Having(conditions) | |||
} | |||
func (engine *Engine) unMapType(t reflect.Type) { | |||
// UnMapType removes the datbase mapper of a type | |||
func (engine *Engine) UnMapType(t reflect.Type) { | |||
engine.mutex.Lock() | |||
defer engine.mutex.Unlock() | |||
delete(engine.Tables, t) | |||
@@ -914,7 +949,7 @@ func (engine *Engine) mapType(v reflect.Value) (*core.Table, error) { | |||
} | |||
if pStart > -1 { | |||
if !strings.HasSuffix(k, ")") { | |||
return nil, errors.New("cannot match ) charactor") | |||
return nil, fmt.Errorf("field %s tag %s cannot match ) charactor", col.FieldName, key) | |||
} | |||
ctx.tagName = k[:pStart] | |||
@@ -1341,24 +1376,24 @@ func (engine *Engine) Exec(sql string, args ...interface{}) (sql.Result, error) | |||
} | |||
// Query a raw sql and return records as []map[string][]byte | |||
func (engine *Engine) Query(sql string, paramStr ...interface{}) (resultsSlice []map[string][]byte, err error) { | |||
func (engine *Engine) Query(sqlorArgs ...interface{}) (resultsSlice []map[string][]byte, err error) { | |||
session := engine.NewSession() | |||
defer session.Close() | |||
return session.Query(sql, paramStr...) | |||
return session.Query(sqlorArgs...) | |||
} | |||
// QueryString runs a raw sql and return records as []map[string]string | |||
func (engine *Engine) QueryString(sqlStr string, args ...interface{}) ([]map[string]string, error) { | |||
func (engine *Engine) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { | |||
session := engine.NewSession() | |||
defer session.Close() | |||
return session.QueryString(sqlStr, args...) | |||
return session.QueryString(sqlorArgs...) | |||
} | |||
// QueryInterface runs a raw sql and return records as []map[string]interface{} | |||
func (engine *Engine) QueryInterface(sqlStr string, args ...interface{}) ([]map[string]interface{}, error) { | |||
func (engine *Engine) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { | |||
session := engine.NewSession() | |||
defer session.Close() | |||
return session.QueryInterface(sqlStr, args...) | |||
return session.QueryInterface(sqlorArgs...) | |||
} | |||
// Insert one or more records | |||
@@ -1564,24 +1599,39 @@ func (engine *Engine) formatTime(sqlTypeName string, t time.Time) (v interface{} | |||
return | |||
} | |||
// Unscoped always disable struct tag "deleted" | |||
func (engine *Engine) Unscoped() *Session { | |||
session := engine.NewSession() | |||
session.isAutoClose = true | |||
return session.Unscoped() | |||
// GetColumnMapper returns the column name mapper | |||
func (engine *Engine) GetColumnMapper() core.IMapper { | |||
return engine.ColumnMapper | |||
} | |||
// CondDeleted returns the conditions whether a record is soft deleted. | |||
func (engine *Engine) CondDeleted(colName string) builder.Cond { | |||
if engine.dialect.DBType() == core.MSSQL { | |||
return builder.IsNull{colName} | |||
} | |||
return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) | |||
// GetTableMapper returns the table name mapper | |||
func (engine *Engine) GetTableMapper() core.IMapper { | |||
return engine.TableMapper | |||
} | |||
// BufferSize sets buffer size for iterate | |||
func (engine *Engine) BufferSize(size int) *Session { | |||
// GetTZLocation returns time zone of the application | |||
func (engine *Engine) GetTZLocation() *time.Location { | |||
return engine.TZLocation | |||
} | |||
// SetTZLocation sets time zone of the application | |||
func (engine *Engine) SetTZLocation(tz *time.Location) { | |||
engine.TZLocation = tz | |||
} | |||
// GetTZDatabase returns time zone of the database | |||
func (engine *Engine) GetTZDatabase() *time.Location { | |||
return engine.DatabaseTZ | |||
} | |||
// SetTZDatabase sets time zone of the database | |||
func (engine *Engine) SetTZDatabase(tz *time.Location) { | |||
engine.DatabaseTZ = tz | |||
} | |||
// Unscoped always disable struct tag "deleted" | |||
func (engine *Engine) Unscoped() *Session { | |||
session := engine.NewSession() | |||
session.isAutoClose = true | |||
return session.BufferSize(size) | |||
return session.Unscoped() | |||
} |
@@ -0,0 +1,194 @@ | |||
// Copyright 2017 The Xorm Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
package xorm | |||
import ( | |||
"github.com/go-xorm/core" | |||
) | |||
// EngineGroup defines an engine group | |||
type EngineGroup struct { | |||
*Engine | |||
slaves []*Engine | |||
policy GroupPolicy | |||
} | |||
// NewEngineGroup creates a new engine group | |||
func NewEngineGroup(args1 interface{}, args2 interface{}, policies ...GroupPolicy) (*EngineGroup, error) { | |||
var eg EngineGroup | |||
if len(policies) > 0 { | |||
eg.policy = policies[0] | |||
} else { | |||
eg.policy = RoundRobinPolicy() | |||
} | |||
driverName, ok1 := args1.(string) | |||
conns, ok2 := args2.([]string) | |||
if ok1 && ok2 { | |||
engines := make([]*Engine, len(conns)) | |||
for i, conn := range conns { | |||
engine, err := NewEngine(driverName, conn) | |||
if err != nil { | |||
return nil, err | |||
} | |||
engine.engineGroup = &eg | |||
engines[i] = engine | |||
} | |||
eg.Engine = engines[0] | |||
eg.slaves = engines[1:] | |||
return &eg, nil | |||
} | |||
master, ok3 := args1.(*Engine) | |||
slaves, ok4 := args2.([]*Engine) | |||
if ok3 && ok4 { | |||
master.engineGroup = &eg | |||
for i := 0; i < len(slaves); i++ { | |||
slaves[i].engineGroup = &eg | |||
} | |||
eg.Engine = master | |||
eg.slaves = slaves | |||
return &eg, nil | |||
} | |||
return nil, ErrParamsType | |||
} | |||
// Close the engine | |||
func (eg *EngineGroup) Close() error { | |||
err := eg.Engine.Close() | |||
if err != nil { | |||
return err | |||
} | |||
for i := 0; i < len(eg.slaves); i++ { | |||
err := eg.slaves[i].Close() | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
// Master returns the master engine | |||
func (eg *EngineGroup) Master() *Engine { | |||
return eg.Engine | |||
} | |||
// Ping tests if database is alive | |||
func (eg *EngineGroup) Ping() error { | |||
if err := eg.Engine.Ping(); err != nil { | |||
return err | |||
} | |||
for _, slave := range eg.slaves { | |||
if err := slave.Ping(); err != nil { | |||
return err | |||
} | |||
} | |||
return nil | |||
} | |||
// SetColumnMapper set the column name mapping rule | |||
func (eg *EngineGroup) SetColumnMapper(mapper core.IMapper) { | |||
eg.Engine.ColumnMapper = mapper | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].ColumnMapper = mapper | |||
} | |||
} | |||
// SetDefaultCacher set the default cacher | |||
func (eg *EngineGroup) SetDefaultCacher(cacher core.Cacher) { | |||
eg.Engine.SetDefaultCacher(cacher) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].SetDefaultCacher(cacher) | |||
} | |||
} | |||
// SetLogger set the new logger | |||
func (eg *EngineGroup) SetLogger(logger core.ILogger) { | |||
eg.Engine.SetLogger(logger) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].SetLogger(logger) | |||
} | |||
} | |||
// SetLogLevel sets the logger level | |||
func (eg *EngineGroup) SetLogLevel(level core.LogLevel) { | |||
eg.Engine.SetLogLevel(level) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].SetLogLevel(level) | |||
} | |||
} | |||
// SetMapper set the name mapping rules | |||
func (eg *EngineGroup) SetMapper(mapper core.IMapper) { | |||
eg.Engine.SetMapper(mapper) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].SetMapper(mapper) | |||
} | |||
} | |||
// SetMaxIdleConns set the max idle connections on pool, default is 2 | |||
func (eg *EngineGroup) SetMaxIdleConns(conns int) { | |||
eg.Engine.db.SetMaxIdleConns(conns) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].db.SetMaxIdleConns(conns) | |||
} | |||
} | |||
// SetMaxOpenConns is only available for go 1.2+ | |||
func (eg *EngineGroup) SetMaxOpenConns(conns int) { | |||
eg.Engine.db.SetMaxOpenConns(conns) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].db.SetMaxOpenConns(conns) | |||
} | |||
} | |||
// SetPolicy set the group policy | |||
func (eg *EngineGroup) SetPolicy(policy GroupPolicy) *EngineGroup { | |||
eg.policy = policy | |||
return eg | |||
} | |||
// SetTableMapper set the table name mapping rule | |||
func (eg *EngineGroup) SetTableMapper(mapper core.IMapper) { | |||
eg.Engine.TableMapper = mapper | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].TableMapper = mapper | |||
} | |||
} | |||
// ShowExecTime show SQL statement and execute time or not on logger if log level is great than INFO | |||
func (eg *EngineGroup) ShowExecTime(show ...bool) { | |||
eg.Engine.ShowExecTime(show...) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].ShowExecTime(show...) | |||
} | |||
} | |||
// ShowSQL show SQL statement or not on logger if log level is great than INFO | |||
func (eg *EngineGroup) ShowSQL(show ...bool) { | |||
eg.Engine.ShowSQL(show...) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].ShowSQL(show...) | |||
} | |||
} | |||
// Slave returns one of the physical databases which is a slave according the policy | |||
func (eg *EngineGroup) Slave() *Engine { | |||
switch len(eg.slaves) { | |||
case 0: | |||
return eg.Engine | |||
case 1: | |||
return eg.slaves[0] | |||
} | |||
return eg.policy.Slave(eg) | |||
} | |||
// Slaves returns all the slaves | |||
func (eg *EngineGroup) Slaves() []*Engine { | |||
return eg.slaves | |||
} |
@@ -0,0 +1,116 @@ | |||
// Copyright 2017 The Xorm Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
package xorm | |||
import ( | |||
"math/rand" | |||
"sync" | |||
"time" | |||
) | |||
// GroupPolicy is be used by chosing the current slave from slaves | |||
type GroupPolicy interface { | |||
Slave(*EngineGroup) *Engine | |||
} | |||
// GroupPolicyHandler should be used when a function is a GroupPolicy | |||
type GroupPolicyHandler func(*EngineGroup) *Engine | |||
// Slave implements the chosen of slaves | |||
func (h GroupPolicyHandler) Slave(eg *EngineGroup) *Engine { | |||
return h(eg) | |||
} | |||
// RandomPolicy implmentes randomly chose the slave of slaves | |||
func RandomPolicy() GroupPolicyHandler { | |||
var r = rand.New(rand.NewSource(time.Now().UnixNano())) | |||
return func(g *EngineGroup) *Engine { | |||
return g.Slaves()[r.Intn(len(g.Slaves()))] | |||
} | |||
} | |||
// WeightRandomPolicy implmentes randomly chose the slave of slaves | |||
func WeightRandomPolicy(weights []int) GroupPolicyHandler { | |||
var rands = make([]int, 0, len(weights)) | |||
for i := 0; i < len(weights); i++ { | |||
for n := 0; n < weights[i]; n++ { | |||
rands = append(rands, i) | |||
} | |||
} | |||
var r = rand.New(rand.NewSource(time.Now().UnixNano())) | |||
return func(g *EngineGroup) *Engine { | |||
var slaves = g.Slaves() | |||
idx := rands[r.Intn(len(rands))] | |||
if idx >= len(slaves) { | |||
idx = len(slaves) - 1 | |||
} | |||
return slaves[idx] | |||
} | |||
} | |||
func RoundRobinPolicy() GroupPolicyHandler { | |||
var pos = -1 | |||
var lock sync.Mutex | |||
return func(g *EngineGroup) *Engine { | |||
var slaves = g.Slaves() | |||
lock.Lock() | |||
defer lock.Unlock() | |||
pos++ | |||
if pos >= len(slaves) { | |||
pos = 0 | |||
} | |||
return slaves[pos] | |||
} | |||
} | |||
func WeightRoundRobinPolicy(weights []int) GroupPolicyHandler { | |||
var rands = make([]int, 0, len(weights)) | |||
for i := 0; i < len(weights); i++ { | |||
for n := 0; n < weights[i]; n++ { | |||
rands = append(rands, i) | |||
} | |||
} | |||
var pos = -1 | |||
var lock sync.Mutex | |||
return func(g *EngineGroup) *Engine { | |||
var slaves = g.Slaves() | |||
lock.Lock() | |||
defer lock.Unlock() | |||
pos++ | |||
if pos >= len(rands) { | |||
pos = 0 | |||
} | |||
idx := rands[pos] | |||
if idx >= len(slaves) { | |||
idx = len(slaves) - 1 | |||
} | |||
return slaves[idx] | |||
} | |||
} | |||
// LeastConnPolicy implements GroupPolicy, every time will get the least connections slave | |||
func LeastConnPolicy() GroupPolicyHandler { | |||
return func(g *EngineGroup) *Engine { | |||
var slaves = g.Slaves() | |||
connections := 0 | |||
idx := 0 | |||
for i := 0; i < len(slaves); i++ { | |||
openConnections := slaves[i].DB().Stats().OpenConnections | |||
if i == 0 { | |||
connections = openConnections | |||
idx = i | |||
} else if openConnections <= connections { | |||
connections = openConnections | |||
idx = i | |||
} | |||
} | |||
return slaves[idx] | |||
} | |||
} |
@@ -12,3 +12,11 @@ import "time" | |||
func (engine *Engine) SetConnMaxLifetime(d time.Duration) { | |||
engine.db.SetConnMaxLifetime(d) | |||
} | |||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused. | |||
func (eg *EngineGroup) SetConnMaxLifetime(d time.Duration) { | |||
eg.Engine.SetConnMaxLifetime(d) | |||
for i := 0; i < len(eg.slaves); i++ { | |||
eg.slaves[i].SetConnMaxLifetime(d) | |||
} | |||
} |
@@ -23,4 +23,6 @@ var ( | |||
ErrNeedDeletedCond = errors.New("Delete need at least one condition") | |||
// ErrNotImplemented not implemented | |||
ErrNotImplemented = errors.New("Not implemented") | |||
// ErrConditionType condition type unsupported | |||
ErrConditionType = errors.New("Unsupported conditon type") | |||
) |
@@ -0,0 +1,103 @@ | |||
// Copyright 2017 The Xorm Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. | |||
package xorm | |||
import ( | |||
"database/sql" | |||
"reflect" | |||
"time" | |||
"github.com/go-xorm/core" | |||
) | |||
// Interface defines the interface which Engine, EngineGroup and Session will implementate. | |||
type Interface interface { | |||
AllCols() *Session | |||
Alias(alias string) *Session | |||
Asc(colNames ...string) *Session | |||
BufferSize(size int) *Session | |||
Cols(columns ...string) *Session | |||
Count(...interface{}) (int64, error) | |||
CreateIndexes(bean interface{}) error | |||
CreateUniques(bean interface{}) error | |||
Decr(column string, arg ...interface{}) *Session | |||
Desc(...string) *Session | |||
Delete(interface{}) (int64, error) | |||
Distinct(columns ...string) *Session | |||
DropIndexes(bean interface{}) error | |||
Exec(string, ...interface{}) (sql.Result, error) | |||
Exist(bean ...interface{}) (bool, error) | |||
Find(interface{}, ...interface{}) error | |||
Get(interface{}) (bool, error) | |||
GroupBy(keys string) *Session | |||
ID(interface{}) *Session | |||
In(string, ...interface{}) *Session | |||
Incr(column string, arg ...interface{}) *Session | |||
Insert(...interface{}) (int64, error) | |||
InsertOne(interface{}) (int64, error) | |||
IsTableEmpty(bean interface{}) (bool, error) | |||
IsTableExist(beanOrTableName interface{}) (bool, error) | |||
Iterate(interface{}, IterFunc) error | |||
Limit(int, ...int) *Session | |||
NoAutoCondition(...bool) *Session | |||
NotIn(string, ...interface{}) *Session | |||
Join(joinOperator string, tablename interface{}, condition string, args ...interface{}) *Session | |||
Omit(columns ...string) *Session | |||
OrderBy(order string) *Session | |||
Ping() error | |||
Query(sqlOrAgrs ...interface{}) (resultsSlice []map[string][]byte, err error) | |||
QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) | |||
QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) | |||
Rows(bean interface{}) (*Rows, error) | |||
SetExpr(string, string) *Session | |||
SQL(interface{}, ...interface{}) *Session | |||
Sum(bean interface{}, colName string) (float64, error) | |||
SumInt(bean interface{}, colName string) (int64, error) | |||
Sums(bean interface{}, colNames ...string) ([]float64, error) | |||
SumsInt(bean interface{}, colNames ...string) ([]int64, error) | |||
Table(tableNameOrBean interface{}) *Session | |||
Unscoped() *Session | |||
Update(bean interface{}, condiBeans ...interface{}) (int64, error) | |||
UseBool(...string) *Session | |||
Where(interface{}, ...interface{}) *Session | |||
} | |||
// EngineInterface defines the interface which Engine, EngineGroup will implementate. | |||
type EngineInterface interface { | |||
Interface | |||
Before(func(interface{})) *Session | |||
Charset(charset string) *Session | |||
CreateTables(...interface{}) error | |||
DBMetas() ([]*core.Table, error) | |||
Dialect() core.Dialect | |||
DropTables(...interface{}) error | |||
DumpAllToFile(fp string, tp ...core.DbType) error | |||
GetColumnMapper() core.IMapper | |||
GetDefaultCacher() core.Cacher | |||
GetTableMapper() core.IMapper | |||
GetTZDatabase() *time.Location | |||
GetTZLocation() *time.Location | |||
NewSession() *Session | |||
NoAutoTime() *Session | |||
Quote(string) string | |||
SetDefaultCacher(core.Cacher) | |||
SetLogLevel(core.LogLevel) | |||
SetMapper(core.IMapper) | |||
SetTZDatabase(tz *time.Location) | |||
SetTZLocation(tz *time.Location) | |||
ShowSQL(show ...bool) | |||
Sync(...interface{}) error | |||
Sync2(...interface{}) error | |||
StoreEngine(storeEngine string) *Session | |||
TableInfo(bean interface{}) *Table | |||
UnMapType(reflect.Type) | |||
} | |||
var ( | |||
_ Interface = &Session{} | |||
_ EngineInterface = &Engine{} | |||
_ EngineInterface = &EngineGroup{} | |||
) |
@@ -76,6 +76,7 @@ func (session *Session) Init() { | |||
session.afterDeleteBeans = make(map[interface{}]*[]func(interface{}), 0) | |||
session.beforeClosures = make([]func(interface{}), 0) | |||
session.afterClosures = make([]func(interface{}), 0) | |||
session.stmtCache = make(map[uint32]*core.Stmt) | |||
session.afterProcessors = make([]executedProcessor, 0) | |||
@@ -262,13 +263,13 @@ func (session *Session) canCache() bool { | |||
return true | |||
} | |||
func (session *Session) doPrepare(sqlStr string) (stmt *core.Stmt, err error) { | |||
func (session *Session) doPrepare(db *core.DB, sqlStr string) (stmt *core.Stmt, err error) { | |||
crc := crc32.ChecksumIEEE([]byte(sqlStr)) | |||
// TODO try hash(sqlStr+len(sqlStr)) | |||
var has bool | |||
stmt, has = session.stmtCache[crc] | |||
if !has { | |||
stmt, err = session.DB().Prepare(sqlStr) | |||
stmt, err = db.Prepare(sqlStr) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -461,6 +462,10 @@ func (session *Session) slice2Bean(scanResults []interface{}, fields []string, b | |||
hasAssigned = true | |||
if len(bs) > 0 { | |||
if fieldType.Kind() == reflect.String { | |||
fieldValue.SetString(string(bs)) | |||
continue | |||
} | |||
if fieldValue.CanAddr() { | |||
err := json.Unmarshal(bs, fieldValue.Addr().Interface()) | |||
if err != nil { | |||
@@ -34,27 +34,27 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti | |||
sd, err := strconv.ParseInt(sdata, 10, 64) | |||
if err == nil { | |||
x = time.Unix(sd, 0) | |||
session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(0) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} else { | |||
session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(0) err key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} | |||
} else if len(sdata) > 19 && strings.Contains(sdata, "-") { | |||
x, err = time.ParseInLocation(time.RFC3339Nano, sdata, parseLoc) | |||
session.engine.logger.Debugf("time(1) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
if err != nil { | |||
x, err = time.ParseInLocation("2006-01-02 15:04:05.999999999", sdata, parseLoc) | |||
session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(2) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} | |||
if err != nil { | |||
x, err = time.ParseInLocation("2006-01-02 15:04:05.9999999 Z07:00", sdata, parseLoc) | |||
session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(3) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} | |||
} else if len(sdata) == 19 && strings.Contains(sdata, "-") { | |||
x, err = time.ParseInLocation("2006-01-02 15:04:05", sdata, parseLoc) | |||
session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(4) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} else if len(sdata) == 10 && sdata[4] == '-' && sdata[7] == '-' { | |||
x, err = time.ParseInLocation("2006-01-02", sdata, parseLoc) | |||
session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(5) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} else if col.SQLType.Name == core.Time { | |||
if strings.Contains(sdata, " ") { | |||
ssd := strings.Split(sdata, " ") | |||
@@ -68,7 +68,7 @@ func (session *Session) str2Time(col *core.Column, data string) (outTime time.Ti | |||
st := fmt.Sprintf("2006-01-02 %v", sdata) | |||
x, err = time.ParseInLocation("2006-01-02 15:04:05", st, parseLoc) | |||
session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
//session.engine.logger.Debugf("time(6) key[%v]: %+v | sdata: [%v]\n", col.FieldName, x, sdata) | |||
} else { | |||
outErr = fmt.Errorf("unsupported time format %v", sdata) | |||
return | |||
@@ -10,6 +10,7 @@ import ( | |||
"reflect" | |||
"github.com/go-xorm/builder" | |||
"github.com/go-xorm/core" | |||
) | |||
// Exist returns true if the record exist otherwise return false | |||
@@ -35,10 +36,18 @@ func (session *Session) Exist(bean ...interface{}) (bool, error) { | |||
return false, err | |||
} | |||
sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) | |||
if session.engine.dialect.DBType() == core.MSSQL { | |||
sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s WHERE %s", tableName, condSQL) | |||
} else { | |||
sqlStr = fmt.Sprintf("SELECT * FROM %s WHERE %s LIMIT 1", tableName, condSQL) | |||
} | |||
args = condArgs | |||
} else { | |||
sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) | |||
if session.engine.dialect.DBType() == core.MSSQL { | |||
sqlStr = fmt.Sprintf("SELECT top 1 * FROM %s", tableName) | |||
} else { | |||
sqlStr = fmt.Sprintf("SELECT * FROM %s LIMIT 1", tableName) | |||
} | |||
args = []interface{}{} | |||
} | |||
} else { | |||
@@ -5,6 +5,7 @@ | |||
package xorm | |||
import ( | |||
"database/sql" | |||
"errors" | |||
"reflect" | |||
"strconv" | |||
@@ -79,6 +80,13 @@ func (session *Session) nocacheGet(beanKind reflect.Kind, table *core.Table, bea | |||
return false, nil | |||
} | |||
switch bean.(type) { | |||
case sql.NullInt64, sql.NullBool, sql.NullFloat64, sql.NullString: | |||
return true, rows.Scan(&bean) | |||
case *sql.NullInt64, *sql.NullBool, *sql.NullFloat64, *sql.NullString: | |||
return true, rows.Scan(bean) | |||
} | |||
switch beanKind { | |||
case reflect.Struct: | |||
fields, err := rows.Columns() | |||
@@ -400,7 +400,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { | |||
return 0, err | |||
} | |||
handleAfterInsertProcessorFunc(bean) | |||
defer handleAfterInsertProcessorFunc(bean) | |||
if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { | |||
session.cacheInsert(table, tableName) | |||
@@ -445,7 +445,7 @@ func (session *Session) innerInsert(bean interface{}) (int64, error) { | |||
if err != nil { | |||
return 0, err | |||
} | |||
handleAfterInsertProcessorFunc(bean) | |||
defer handleAfterInsertProcessorFunc(bean) | |||
if cacher := session.engine.getCacher2(table); cacher != nil && session.statement.UseCache { | |||
session.cacheInsert(table, tableName) | |||
@@ -8,17 +8,92 @@ import ( | |||
"fmt" | |||
"reflect" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"github.com/go-xorm/builder" | |||
"github.com/go-xorm/core" | |||
) | |||
func (session *Session) genQuerySQL(sqlorArgs ...interface{}) (string, []interface{}, error) { | |||
if len(sqlorArgs) > 0 { | |||
switch sqlorArgs[0].(type) { | |||
case string: | |||
return sqlorArgs[0].(string), sqlorArgs[1:], nil | |||
case *builder.Builder: | |||
return sqlorArgs[0].(*builder.Builder).ToSQL() | |||
case builder.Builder: | |||
bd := sqlorArgs[0].(builder.Builder) | |||
return bd.ToSQL() | |||
default: | |||
return "", nil, ErrUnSupportedType | |||
} | |||
} | |||
if session.statement.RawSQL != "" { | |||
return session.statement.RawSQL, session.statement.RawParams, nil | |||
} | |||
if len(session.statement.TableName()) <= 0 { | |||
return "", nil, ErrTableNotFound | |||
} | |||
var columnStr = session.statement.ColumnStr | |||
if len(session.statement.selectStr) > 0 { | |||
columnStr = session.statement.selectStr | |||
} else { | |||
if session.statement.JoinStr == "" { | |||
if columnStr == "" { | |||
if session.statement.GroupByStr != "" { | |||
columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) | |||
} else { | |||
columnStr = session.statement.genColumnStr() | |||
} | |||
} | |||
} else { | |||
if columnStr == "" { | |||
if session.statement.GroupByStr != "" { | |||
columnStr = session.statement.Engine.Quote(strings.Replace(session.statement.GroupByStr, ",", session.engine.Quote(","), -1)) | |||
} else { | |||
columnStr = "*" | |||
} | |||
} | |||
} | |||
if columnStr == "" { | |||
columnStr = "*" | |||
} | |||
} | |||
condSQL, condArgs, err := builder.ToSQL(session.statement.cond) | |||
if err != nil { | |||
return "", nil, err | |||
} | |||
args := append(session.statement.joinArgs, condArgs...) | |||
sqlStr, err := session.statement.genSelectSQL(columnStr, condSQL) | |||
if err != nil { | |||
return "", nil, err | |||
} | |||
// for mssql and use limit | |||
qs := strings.Count(sqlStr, "?") | |||
if len(args)*2 == qs { | |||
args = append(args, args...) | |||
} | |||
return sqlStr, args, nil | |||
} | |||
// Query runs a raw sql and return records as []map[string][]byte | |||
func (session *Session) Query(sqlStr string, args ...interface{}) ([]map[string][]byte, error) { | |||
func (session *Session) Query(sqlorArgs ...interface{}) ([]map[string][]byte, error) { | |||
if session.isAutoClose { | |||
defer session.Close() | |||
} | |||
sqlStr, args, err := session.genQuerySQL(sqlorArgs...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return session.queryBytes(sqlStr, args...) | |||
} | |||
@@ -114,11 +189,16 @@ func rows2Strings(rows *core.Rows) (resultsSlice []map[string]string, err error) | |||
} | |||
// QueryString runs a raw sql and return records as []map[string]string | |||
func (session *Session) QueryString(sqlStr string, args ...interface{}) ([]map[string]string, error) { | |||
func (session *Session) QueryString(sqlorArgs ...interface{}) ([]map[string]string, error) { | |||
if session.isAutoClose { | |||
defer session.Close() | |||
} | |||
sqlStr, args, err := session.genQuerySQL(sqlorArgs...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
rows, err := session.queryRows(sqlStr, args...) | |||
if err != nil { | |||
return nil, err | |||
@@ -162,11 +242,16 @@ func rows2Interfaces(rows *core.Rows) (resultsSlice []map[string]interface{}, er | |||
} | |||
// QueryInterface runs a raw sql and return records as []map[string]interface{} | |||
func (session *Session) QueryInterface(sqlStr string, args ...interface{}) ([]map[string]interface{}, error) { | |||
func (session *Session) QueryInterface(sqlorArgs ...interface{}) ([]map[string]interface{}, error) { | |||
if session.isAutoClose { | |||
defer session.Close() | |||
} | |||
sqlStr, args, err := session.genQuerySQL(sqlorArgs...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
rows, err := session.queryRows(sqlStr, args...) | |||
if err != nil { | |||
return nil, err | |||
@@ -47,9 +47,16 @@ func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Row | |||
} | |||
if session.isAutoCommit { | |||
var db *core.DB | |||
if session.engine.engineGroup != nil { | |||
db = session.engine.engineGroup.Slave().DB() | |||
} else { | |||
db = session.DB() | |||
} | |||
if session.prepareStmt { | |||
// don't clear stmt since session will cache them | |||
stmt, err := session.doPrepare(sqlStr) | |||
stmt, err := session.doPrepare(db, sqlStr) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -61,7 +68,7 @@ func (session *Session) queryRows(sqlStr string, args ...interface{}) (*core.Row | |||
return rows, nil | |||
} | |||
rows, err := session.DB().Query(sqlStr, args...) | |||
rows, err := db.Query(sqlStr, args...) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -171,7 +178,7 @@ func (session *Session) exec(sqlStr string, args ...interface{}) (sql.Result, er | |||
} | |||
if session.prepareStmt { | |||
stmt, err := session.doPrepare(sqlStr) | |||
stmt, err := session.doPrepare(session.DB(), sqlStr) | |||
if err != nil { | |||
return nil, err | |||
} | |||
@@ -242,10 +242,23 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 | |||
var autoCond builder.Cond | |||
if !session.statement.noAutoCondition && len(condiBean) > 0 { | |||
var err error | |||
autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) | |||
if err != nil { | |||
return 0, err | |||
if c, ok := condiBean[0].(map[string]interface{}); ok { | |||
autoCond = builder.Eq(c) | |||
} else { | |||
ct := reflect.TypeOf(condiBean[0]) | |||
k := ct.Kind() | |||
if k == reflect.Ptr { | |||
k = ct.Elem().Kind() | |||
} | |||
if k == reflect.Struct { | |||
var err error | |||
autoCond, err = session.statement.buildConds(session.statement.RefTable, condiBean[0], true, true, false, true, false) | |||
if err != nil { | |||
return 0, err | |||
} | |||
} else { | |||
return 0, ErrConditionType | |||
} | |||
} | |||
} | |||
@@ -160,6 +160,9 @@ func (statement *Statement) And(query interface{}, args ...interface{}) *Stateme | |||
case string: | |||
cond := builder.Expr(query.(string), args...) | |||
statement.cond = statement.cond.And(cond) | |||
case map[string]interface{}: | |||
cond := builder.Eq(query.(map[string]interface{})) | |||
statement.cond = statement.cond.And(cond) | |||
case builder.Cond: | |||
cond := query.(builder.Cond) | |||
statement.cond = statement.cond.And(cond) | |||
@@ -181,6 +184,9 @@ func (statement *Statement) Or(query interface{}, args ...interface{}) *Statemen | |||
case string: | |||
cond := builder.Expr(query.(string), args...) | |||
statement.cond = statement.cond.Or(cond) | |||
case map[string]interface{}: | |||
cond := builder.Eq(query.(map[string]interface{})) | |||
statement.cond = statement.cond.Or(cond) | |||
case builder.Cond: | |||
cond := query.(builder.Cond) | |||
statement.cond = statement.cond.Or(cond) | |||
@@ -901,8 +907,12 @@ func (statement *Statement) genDelIndexSQL() []string { | |||
func (statement *Statement) genAddColumnStr(col *core.Column) (string, []interface{}) { | |||
quote := statement.Engine.Quote | |||
sql := fmt.Sprintf("ALTER TABLE %v ADD %v;", quote(statement.TableName()), | |||
sql := fmt.Sprintf("ALTER TABLE %v ADD %v", quote(statement.TableName()), | |||
col.String(statement.Engine.dialect)) | |||
if statement.Engine.dialect.DBType() == core.MYSQL && len(col.Comment) > 0 { | |||
sql += " COMMENT '" + col.Comment + "'" | |||
} | |||
sql += ";" | |||
return sql, []interface{}{} | |||
} | |||
@@ -462,16 +462,16 @@ | |||
"revisionTime": "2016-11-01T11:13:14Z" | |||
}, | |||
{ | |||
"checksumSHA1": "9SXbj96wb1PgppBZzxMIN0axbFQ=", | |||
"checksumSHA1": "HsUSlgz1VKEEiZdkXY5qdLzexWU=", | |||
"path": "github.com/go-xorm/builder", | |||
"revision": "c8871c857d2555fbfbd8524f895be5386d3d8836", | |||
"revisionTime": "2017-05-19T03:21:30Z" | |||
"revision": "488224409dd8aa2ce7a5baf8d10d55764a913738", | |||
"revisionTime": "2018-01-16T06:54:19Z" | |||
}, | |||
{ | |||
"checksumSHA1": "HMavuxvDhKOwmbbFnYt9hfT6jE0=", | |||
"checksumSHA1": "7JjlvSpGfLa49MHElks8NGBUfFA=", | |||
"path": "github.com/go-xorm/core", | |||
"revision": "da1adaf7a28ca792961721a34e6e04945200c890", | |||
"revisionTime": "2017-09-09T08:56:53Z" | |||
"revision": "cb1d0ca71f42d3ee1bf4aba7daa16099bc31a7e9", | |||
"revisionTime": "2017-12-21T01:38:49Z" | |||
}, | |||
{ | |||
"checksumSHA1": "k52lEKLp8j5M+jFpe+3u+bIFpxQ=", | |||
@@ -480,10 +480,10 @@ | |||
"revisionTime": "2016-08-11T02:11:45Z" | |||
}, | |||
{ | |||
"checksumSHA1": "+KmPfckyKvrUZPIHBYHylg/7V8o=", | |||
"checksumSHA1": "eGBz6F3I/0naVUclZ6GZWc3EzQo=", | |||
"path": "github.com/go-xorm/xorm", | |||
"revision": "29d4a0330a00b9be468b70e3fb0f74109348c358", | |||
"revisionTime": "2017-09-30T01:26:13Z" | |||
"revision": "d4149d1eee0c2c488a74a5863fd9caf13d60fd03", | |||
"revisionTime": "2018-01-22T13:32:35Z" | |||
}, | |||
{ | |||
"checksumSHA1": "1ft/4j5MFa7C9dPI9whL03HSUzk=", | |||