https://github.com/bigpigeon/toyorm
golang orm
https://github.com/bigpigeon/toyorm
database golang
Last synced: 5 months ago
JSON representation
golang orm
- Host: GitHub
- URL: https://github.com/bigpigeon/toyorm
- Owner: bigpigeon
- License: mit
- Created: 2018-01-10T07:01:38.000Z (over 8 years ago)
- Default Branch: master
- Last Pushed: 2019-06-30T11:58:11.000Z (almost 7 years ago)
- Last Synced: 2024-06-20T06:56:29.138Z (about 2 years ago)
- Topics: database, golang
- Language: Go
- Homepage: https://bigpigeon.org/toyorm
- Size: 733 KB
- Stars: 226
- Watchers: 6
- Forks: 9
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: License
Awesome Lists containing this project
README
# Toyorm
this is powerful sql orm library for Golang, have some funny features
[](https://travis-ci.org/bigpigeon/toyorm)
[](https://codecov.io/gh/bigpigeon/toyorm)
[](https://goreportcard.com/report/github.com/bigpigeon/toyorm)
[](https://godoc.org/github.com/bigpigeon/toyorm)
[](https://gitter.im/toyorm/toyorm)
---
- [A Simple Example](#a-simple-example)
- [Website Example](#website-example)
- [Database connection](#database-connection)
- [Model definition](#model-definition)
- [example](#example)
- [type translate](#type-translate)
- [Bind models](#bind-models)
- [Sql Operation](#sql-operation)
- [create table](#create-table)
- [drop table](#drop-table)
- [insert/save data](#insertsave-data)
- [update](#update)
- [find](#find)
- [delete](#delete)
- [ToyBrick](#toybrick)
- [Where condition](#where-condition)
- [usage](#usage)
- [SearchExpr](#searchexpr)
- [example](#example-1)
- [Transaction](#transaction)
- [Debug](#debug)
- [IgnoreMode](#ignoremode)
- [BindFields](#bindfields)
- [Scope](#scope)
- [Template](#template)
- [Custom insert](#custom-insert)
- [Custom find](#custom-find)
- [Custom update](#custom-update)
- [Placeholder](#placeholder)
- [Thread safe](#thread-safe)
- [Preload](#preload)
- [Preload example](#preload-example)
- [One to one](#one-to-one)
- [Belong to](#belong-to)
- [One to many](#one-to-many)
- [Many to many](#many-to-many)
- [Load preload](#load-preload)
- [Join](#join)
- [Join Example](#join-example)
- [Model Define](#model-define)
- [Join in Find](#join-in-find)
- [Preload On Join](#preload-on-join)
- [Result](#result)
- [Selector](#selector)
- [Collection](#collection)
- [ToyCollection](#toycollection)
- [CollectionBrick](#collectionbrick)
- [Selector](#selector-1)
- [id generator](#id-generator)
- [sql action](#sql-action)
- [collection example](#collection-example)
- [toy-doctor](#toy-doctor)
---
### Support database
[sqlite3](https://www.sqlite.org/) [mysql](https://www.mysql.com/) [postgresql](https://www.postgresql.org/)
### Go version
version go-1.9
---
### A Simple Example
[here](examples/simple_example)
---
### Website Example
[here](examples/website)
---
### Database connection
import database driver
```golang
// if database is mysql
_ "github.com/go-sql-driver/mysql"
// if database is sqlite3
_ "github.com/mattn/go-sqlite3"
// when database is postgres
_ "github.com/lib/pq"
```
create a toy
```golang
// if database is mysql, make sure your mysql have toyorm_example schema
toy, err = toyorm.Open("mysql", "root:@tcp(localhost:3306)/toyorm_example?charset=utf8&parseTime=True")
// if database is sqlite3
toy,err = toyorm.Open("sqlite3", "toyorm_test.db")
// when database is postgres
toy, err = toyorm.Open("postgres", "user=postgres dbname=toyorm sslmode=disable")
```
### Model definition
##### example
```golang
type Extra map[string]interface{}
func (e Extra) Scan(value interface{}) error {
switch v := value.(type) {
case string:
return json.Unmarshal([]byte(v), e)
case []byte:
return json.Unmarshal(v, e)
default:
return errors.New("not support type")
}
}
func (e Extra) Value() (driver.Value, error) {
return json.Marshal(e)
}
type UserDetail struct {
ID int `toyorm:"primary key;auto_increment"`
UserID uint `toyorm:"index"`
MainPage string
Extra Extra `toyorm:"type:VARCHAR(1024)"`
}
type Blog struct {
toyorm.ModelDefault
UserID uint `toyorm:"index"`
Title string `toyorm:"index"`
Content string
}
type User struct {
toyorm.ModelDefault
Name string `toyorm:"unique index"`
Age int
Sex string
Detail *UserDetail
Friends []*User
Blog []Blog
}
```
##### type translate
if sql type is not match, toyorm will ignore it in Sql Operation
you can use **\** field tag to specified their sql type
the following type will auto translate to sql type
Go Type | sql type
--------|-----------
bool | BOOLEAN
int8,int16,int32,uint8,uint16,uint32| INTEGER
int64,uint64,int,uint| BIGINT
float32,float64| FLOAT
string | VARCHAR(255)
time.Time | TIMESTAMP
[]byte | VARCHAR(255)
sql.NullBool | BOOLEAN
sql.NullInt64 | BIGINT
sql.NullFloat64 | FLOAT
sql.NullString | VARCHAR(255)
sql.RawBytes | VARCHAR(255)
**special fields**
1. special fields have some process in handlers, do not try to change it type or set it value
Field Name| Type | Description
----------|-----------|------------
CreatedAt |time.Time | generate when element be create
UpdatedAt |time.Time | generate when element be update/create
DeletedAt |*time.Time | delete mode is soft
Cas |int | (not support for sqlite3)save operation will failure when it's value modified by third party e.g in postgres:cas=1 insert xxx conflict(id) update cas = 2 where cas = 1
**field tags**
1. tag format can be \ or \
2. the following is special tag
Key |Value |Description
--------------|------------------------|-----------
index | void or string | use for optimization when search condition have this field,if you want make a combined,just set same index name with fields
unique index | void or string | have unique limit index, other same as index
primary key | void | allow multiple primary key,but some operation not support
\- | void | ignore this field in sql
type | string | sql type
column | string | sql column name
auto_increment| void | recommend, if your table primary key have auto_increment attribute must add it
autoincrement | void | same as auto_increment
foreign key | void | to add foreign key feature when create table
alias | string | change field name with toyorm
join | string | to select related field when call brick.Join
belong to | string | to select related field when call brick.Preload with BelongTo container
one to one | string | to select related field when call brick.Preload with OneToOne container
one to many | string | to select related field when call brick.Preload with OneToMany container
default | srring | default value in database
other custom TAG will append to end of CREATE TABLE field
#### Bind models
1. model kind must be a struct or a point with struct
2. the model is what information that toyorm know about the table
```golang
brick := toy.Model(&User{})
// or
brick := toy.Model(User{})
```
### Sql Operation
---
#### create table
```golang
var err error
_, err = toy.Model(&User{}).Debug().CreateTable()
// CREATE TABLE user (id BIGINT AUTO_INCREMENT,created_at TIMESTAMP NULL,updated_at TIMESTAMP NULL,deleted_at TIMESTAMP NULL,name VARCHAR(255),age BIGINT ,sex VARCHAR(255) , PRIMARY KEY(id))
// CREATE INDEX idx_user_deletedat ON user(deleted_at)
// CREATE UNIQUE INDEX udx_user_name ON user(name)
_, err =toy.Model(&UserDetail{}).Debug().CreateTable()
// CREATE TABLE user_detail (id BIGINT AUTO_INCREMENT,user_id BIGINT,main_page Text,extra VARCHAR(1024), PRIMARY KEY(id))
// CREATE INDEX idx_user_detail_userid ON user_detail(user_id)
_, err =toy.Model(&Blog{}).Debug().CreateTable()
// CREATE TABLE blog (id BIGINT AUTO_INCREMENT,created_at TIMESTAMP NULL,updated_at TIMESTAMP NULL,deleted_at TIMESTAMP NULL,user_id BIGINT,title VARCHAR(255),content VARCHAR(255) , PRIMARY KEY(id))
// CREATE INDEX idx_blog_deletedat ON blog(deleted_at)
// CREATE INDEX idx_blog_userid ON blog(user_id)
// CREATE INDEX idx_blog_title ON blog(title)
```
#### drop table
```golang
var err error
_, err =toy.Model(&User{}).Debug().DropTable()
// DROP TABLE user
_, err =toy.Model(&UserDetail{}).Debug().DropTable()
// DROP TABLE user_detail
_, err =toy.Model(&Blog{}).Debug().DropTable()
// DROP TABLE blog
```
#### insert/save data
// insert with autoincrement will set id to source data
```golang
user := &User{
Name: "bigpigeon",
Age: 18,
Sex: "male",
}
_, err = toy.Model(&User{}).Debug().Insert(&user)
// INSERT INTO user(created_at,updated_at,name,age,sex) VALUES(?,?,?,?,?) , args:[]interface {}{time.Time{wall:0xbe8df5112e7f07c8, ext:210013499, loc:(*time.Location)(0x141af80)}, time.Time{wall:0xbe8df5112e7f1768, ext:210017044, loc:(*time.Location)(0x141af80)}, "bigpigeon", 18, "male"}
// print user format with json
/* {
"ID": 1,
"CreatedAt": "2018-01-11T20:47:00.780077+08:00",
"UpdatedAt": "2018-01-11T20:47:00.780081+08:00",
"DeletedAt": null,
"Name": "bigpigeon",
"Age": 18,
"Sex": "male",
"Detail": null,
"Friends": null,
"Blog": null
}*/
```
// save data use "REPLACE INTO" when primary key exist
```golang
users := []User{
{
ModelDefault: toyorm.ModelDefault{ID: 1},
Name: "bigpigeon",
Age: 18,
Sex: "male",
},
{
Name: "fatpigeon",
Age: 27,
Sex: "male",
},
}
_, err = toy.Model(&User{}).Debug().Save(&user)
// SELECT id,created_at FROM user WHERE id IN (?), args:[]interface {}{0x1}
// REPLACE INTO user(id,created_at,updated_at,name,age,sex) VALUES(?,?,?,?,?,?) , args:[]interface {}{0x1, time.Time{wall:0x0, ext:63651278036, loc:(*time.Location)(nil)}, time.Time{wall:0xbe8dfb5511465918, ext:302600558, loc:(*time.Location)(0x141af80)}, "bigpigeon", 18, "male"}
// INSERT INTO user(created_at,updated_at,name,age,sex) VALUES(?,?,?,?,?) , args:[]interface {}{time.Time{wall:0xbe8dfb551131b7d8, ext:301251230, loc:(*time.Location)(0x141af80)}, time.Time{wall:0xbe8dfb5511465918, ext:302600558, loc:(*time.Location)(0x141af80)}, "fatpigeon", 27, "male"}
```
#### update
```golang
toy.Model(&User{}).Debug().Update(&User{
Age: 4,
})
// UPDATE user SET updated_at=?,age=? WHERE deleted_at IS NULL, args:[]interface {}{time.Time{wall:0xbe8df4eb81b6c050, ext:233425327, loc:(*time.Location)(0x141af80)}, 4}
```
#### find
find one
```golang
var user User
_, err = toy.Model(&User{}).Debug().Find(&user}
// SELECT id,created_at,updated_at,deleted_at,name,age,sex FROM user WHERE deleted_at IS NULL LIMIT 1, args:[]interface {}(nil)
// print user format with json
/* {
"ID": 1,
"CreatedAt": "2018-01-11T12:47:01Z",
"UpdatedAt": "2018-01-11T12:47:01Z",
"DeletedAt": null,
"Name": "bigpigeon",
"Age": 4,
"Sex": "male",
"Detail": null,
"Friends": null,
"Blog": null
}*/
```
find multiple
```golang
var users []User
_, err = brick.Debug().Find(&users)
fmt.Printf("find users %s\n", JsonEncode(&users))
// SELECT id,created_at,updated_at,deleted_at,name,age,sex FROM user WHERE deleted_at IS NULL, args:[]interface {}(nil)
```
#### delete
delete with primary key
```golang
_, err = brick.Debug().Delete(&user)
// UPDATE user SET deleted_at=? WHERE id IN (?), args:[]interface {}{(*time.Time)(0xc4200f0520), 0x1}
```
delete with condition
```golang
_, err = brick.Debug().Where(toyorm.ExprEqual, Offsetof(User{}.Name), "bigpigeon").DeleteWithConditions()
// UPDATE user SET deleted_at=? WHERE name = ?, args:[]interface {}{(*time.Time)(0xc4200dbfa0), "bigpigeon"}
```
### ToyBrick
-----
use **toy.Model** will create a ToyBrick, you need use it to build grammar and operate the database
#### Where condition
affective update/find/delete operation
##### usage
where will clean old conditions and make new one
brick.Where(, , [value])
whereGroup add multiple condition with same expr
brick.WhereGroup(, )
conditions will copy conditions and clean old conditions
brick.Conditions()
or & and condition will use or/and to link new condition when current condition is not nil
brick.Or().Condition(, , [value])
brick.Or().ConditionGroup(, )
brick.And().Condition(, , [value])
or & and conditions will use or/and to link new conditions
brick.Or().Conditions()
brick.And().Conditions()
##### SearchExpr
SearchExpr | to sql | example
------------------|--------------|:----------------
ExprAnd | AND | brick.WhereGroup(ExprAnd, Product{Name:"food one", Count: 4}) // WHERE name = "food one" AND Count = 4
ExprOr | OR | brick.WhereGroup(ExprOr, Product{Name:"food one", Count: 4}) // WHERE name = "food one" OR Count = "4"
ExprEqual | = | brick.Where(ExprEqual, OffsetOf(Product{}.Name), "food one") // WHERE name = "find one"
ExprNotEqual | <> | brick.Where(ExprNotEqual, OffsetOf(Product{}.Name), "food one") // WHERE name <> "find one"
ExprGreater | > | brick.Where(ExprGreater, OffsetOf(Product{}.Count), 3) // WHERE count > 3
ExprGreaterEqual | >= | brick.Where(ExprGreaterEqual, OffsetOf(Product{}.Count), 3) // WHERE count >= 3
ExprLess | < | brick.Where(ExprLess, OffsetOf(Product{}.Count), 3) // WHERE count < 3
ExprLessEqual | <= | brick.Where(ExprLessEqual, OffsetOf(Product{}.Count), 3) // WHERE count <= 3
ExprBetween | Between | brick.Where(ExprBetween, OffsetOf(Product{}.Count), [2]int{2,3}) // WHERE count BETWEEN 2 AND 3
ExprNotBetween | NOT Between | brick.Where(ExprNotBetween, OffsetOf(Product{}.Count), [2]int{2,3}) // WHERE count NOT BETWEEN 2 AND 3
ExprIn | IN | brick.Where(ExprIn, OffsetOf(Product{}.Count), []int{1, 2, 3}) // WHERE count IN (1,2,3)
ExprNotIn | NOT IN | brick.Where(ExprNotIn, OffsetOf(Product{}.Count), []int{1, 2, 3}) // WHERE count NOT IN (1,2,3)
ExprLike | LIKE | brick.Where(ExprLike, OffsetOf(Product{}.Name), "one") // WHERE name LIKE "one"
ExprNotLike | NOT LIKE | brick.Where(ExprNotLike, OffsetOf(Product{}.Name), "one") // WHERE name NOT LIKE "one"
ExprNull | IS NULL | brick.Where(ExprNull, OffsetOf(Product{}.DeletedAt)) // WHERE DeletedAt IS NULL
ExprNotNull | IS NOT NULL | brick.Where(ExprNotNull, OffsetOf(Product{}.DeletedAt)) // WHERE DeletedAt IS NOT NULL
##### example
single condition
```golang
brick = brick.Where(toyorm.ExprEqual, Offsetof(Product{}.Tag), "food")
or use string
brick = brick.Where("=", Offsetof(Product{}.Tag), "food")
// WHERE tag = "food"
```
combination condition
```golang
brick = brick.Where(toyorm.ExprEqual, Offsetof(Product{}.Count), 2).And().
Condition(toyorm.ExprGreater, Offsetof(Product{}.Price), 3).Or().
Condition(toyorm.ExprEqual, Offsetof(Product{}.Count), 4)
or use string
brick = brick.Where("=", Offsetof(Product{}.Count), 2).And().
Condition(">", Offsetof(Product{}.Price), 3).Or().
Condition("=", Offsetof(Product{}.Count), 4)
// WHERE count = 2 and price > 3 or count = 4
```
priority condition
```golang
brick.Where(toyorm.ExprGreater, Offsetof(Product{}.Price), 3).And().Conditions(
brick.Where(toyorm.ExprEqual, Offsetof(Product{}.Count), 2).Or().
Condition(toyorm.ExprEqual, Offsetof(Product{}.Count), 1).Search
)
or use string
brick.Where(">", Offsetof(Product{}.Price), 3).And().Conditions(
brick.Where("=", Offsetof(Product{}.Count), 2).Or().
Condition("=", Offsetof(Product{}.Count), 1).Search
)
// WHERE price > 3 and (count = 2 or count = 1)
```
```golang
brick.Conditions(
brick.Where(toyorm.ExprEqual, Offsetof(Product{}.Count), 2).Or().
Condition(toyorm.ExprEqual, Offsetof(Product{}.Count), 1).Search,
).And().Conditions(
brick.Where(toyorm.ExprEqual, Offsetof(Product{}.Price), 3).Or().
Condition(toyorm.ExprEqual, Offsetof(Product{}.Price), 4).Search,
)
or use string
brick.Conditions(
brick.Where("=", Offsetof(Product{}.Count), 2).Or().
Condition("=", Offsetof(Product{}.Count), 1).Search,
).And().Conditions(
brick.Where("=", Offsetof(Product{}.Price), 3).Or().
Condition("=", Offsetof(Product{}.Price), 4).Search,
)
// WHERE (count = ? OR count = ?) AND (price = ? OR price = ?)
```
limit & offset
```golang
brick := brick.Offset(2).Limit(2)
// LIMIT 2 OFFSET 2
```
order by
```golang
brick = brick.OrderBy(Offsetof(Product{}.Name))
// ORDER BY name
```
order by desc
```golang
brick = brick.OrderBy(brick.ToDesc(Offsetof(Product{}.Name)))
// ORDER BY name DESC
```
#### Template Field
sometimes the condition of sql is not a normal field, use TempField to wrapper normal field
```golang
brick = brick.Where("=", brick.TempField(Offsetof(Product{}.Name), "LOWER(%s)"), "name")
// WHERE LOWER(name) = ?
```
#### Transaction
---
start a transaction
```golang
brick = brick.Begin()
```
rollback all sql action
```golang
err = brick.Rollback()
```
commit all sql action
```golang
err = brick.Commit()
```
#### Debug
if Set debug all sql action will have log
```golang
brick = brick.Debug()
```
#### IgnoreMode
when I Update or Search with struct that have some zero value, did I update it ?
use IgnoreMode to differentiate what zero value should update
```golang
brick = brick.IgnoreMode(toyorm.Mode("Update"), toyorm.IgnoreZero ^ toyorm.IgnoreZeroLen)
// ignore all zeor value but excloud zero len slice
// now field = []int(nil) will ignore when update
// but field = []int{} will update when update
// now field = map[int]int(nil) will ignore when update
// but field = map[int]int{} will update when update
```
In default
Operation | Mode | affect
----------|------------|--------
Insert | IgnoreNo | brick.Insert()
Replace | IgnoreNo | brick.Replace()
Condition | IgnoreZero | brick.WhereGroup(ExprAnd/ExprOr, )
Update | IgnoreZero | brick.Update()
**All of IgnoreMode**
mode | effective
------------------|---------------
IgnoreFalse | ignore field type is bool and value is false
IgnoreZeroInt | ignore field type is int/uint/uintptr(incloud their 16,32,64 bit type) and value is 0
IgnoreZeroFloat | ignore field type is float32/float64 and value is 0.0
IgnoreZeroComplex | ignore field type is complex64/complex128 and value is 0 + 0i
IgnoreNilString | ignore field type is string and value is ""
IgnoreNilPoint | ignore field type is point/map/slice and value is nil
IgnoreZeroLen | ignore field type is map/array/slice and len = 0
IgnoreNullStruct | ignore field type is struct and value is zero value struct e.g type A struct{A string,B int}, A{"", 0} will be ignore
IgnoreNil | ignore with IgnoreNilPoint and IgnoreZeroLen
IgnoreZero | ignore all of the above
#### BindFields
if bind a not null fields, the IgnoreMode will failure
```golang
{
var p Product
result, err := brick.BindDefaultFields(Offsetof(p.Price), Offsetof(p.UpdatedAt)).Update(&Product{
Price: 0,
})
// process error
...
}
var products []Product
result, err = brick.Find(&products)
// process error
...
for _, p := range products {
fmt.Printf("product name %s, price %v\n", p.Name, p.Price)
}
```
#### Scope
use scope to do some custom operation
```golang
// desc all order by fields
brick.Scope(func(t *ToyBrick) *ToyBrick{
newOrderBy := make([]*ModelFields, len(t.orderBy))
for i, f := range t.orderBy {
newOrderBy = append(newOrderBy, t.ToDesc(f))
}
newt := *t
newt.orderBy = newOrderBy
return &newt
})
```
#### Template
use template exec to replace default exec
now template only support Insert/Save/Update/Find
##### Custom insert
```golang
data := Product{
Name: "bag",
Price: 9999,
Count: 2,
Tag: "container",
}
result, err := brick.Template("INSERT INTO $ModelName($Columns) Values($Values)").Insert(&data)
// INSERT INTO product(created_at,updated_at,deleted_at,name,price,count,tag) Values(?,?,?,?,?,?,?) args:["2018-04-01T17:05:48.927499+08:00","2018-04-01T17:05:48.927499+08:00",null,"bag",9999,2,"container"]
```
##### Custom find
```golang
var data Product
// if driver is mysql use "USE INDEX" replace "INDEXED BY"
result, err := brick.Template("SELECT $Columns FROM $ModelName INDEXED BY idx_product_name $Conditions").
Where("=", Offsetof(Product{}.Name), "bag").Find(&data)
// SELECT id,created_at,updated_at,deleted_at,name,price,count,tag FROM product INDEXED BY idx_product_name WHERE deleted_at IS NULL AND name = ? LIMIT 1 args:["bag"]
```
##### Custom update
set count = count + 2
```golang
result, err := brick.Template(fmt.Sprintf("UPDATE $ModelName SET $Values,$FN-Count = $0x%x + ? $Conditions", Offsetof(Product{}.Count)), 2).
Where("=", Offsetof(Product{}.Name), "bag").Update(&Product{Price: 200})
// UPDATE product SET updated_at = ?,price = ?,count = count + ? WHERE deleted_at IS NULL AND name = ? args:["2018-04-01T17:50:35.205377+08:00",200,2,"bag"]
```
##### Placeholder
follower placeholder use in template example
two special placeholder
1. $FN- will convert struct field name to table field name e.g $FN-Name => name
2. $0x will convert struct field offset to table field name e.g $0x58 => Count
action \\ placeholder | $ModelName | $Columns | $Values | $Conditions
-----------------------|------------|-------------|------------------| -----------|
Find | product | id,data,... | - | WHERE ... ORDER BY ... GROUP BY ... LIMIT ... OFFSET ...
Insert | product | id,data,... | ?,?,... | WHERE ... ORDER BY ... GROUP BY ... LIMIT ... OFFSET ...
Save | product | id,data,... | ?,?,... | WHERE ... ORDER BY ... GROUP BY ... LIMIT ... OFFSET ...
Update | product | id,data,... | id = ?,data = ?,... | WHERE ... ORDER BY ... GROUP BY ... LIMIT ... OFFSET ...
#### Thread safe
Thread safe if you comply with the following agreement
1. make sure **ToyBrick** object is read only, if you want to change it, create a new one
2. do not use **append** to change ToyBrick's slice data,use **make** and **copy** to clone new slice
#### Preload
---
preload need have relation field and container field
relations field is used to link the main record and sub record
container field is used to hold sub record
##### Preload example
[here](examples/preload_example)
##### One to one
relation field at sub model
relation field name must be main model type name + main model primary key name
```golang
type User struct {
toyorm.ModelDefault
// container field
Detail *UserDetail
}
type UserDetail struct {
ID int `toyorm:"primary key;auto_increment"`
// relation field
UserID uint `toyorm:"index"`
MainPage string `toyorm:"type:Text"`
}
// load preload
brick = toy.Model(&User{}).Debug().Preload(OffsetOf(User.Detail)).Enter()
```
##### Belong to
relation field at main model
relation field name must be container field name + sub model primary key name
```golang
type User struct {
toyorm.ModelDefault
// container field
Detail *UserDetail
// relation field
DetailID int `toyorm:"index"`
}
type UserDetail struct {
ID int `toyorm:"primary key;auto_increment"`
MainPage string `toyorm:"type:Text"`
}
```
##### One to many
relation field at sub model
relation field name must be main model type name + main model primary key name
```golang
type User struct {
toyorm.ModelDefault
// container field
Blog []Blog
}
type Blog struct {
toyorm.ModelDefault
// relation field
UserID uint `toyorm:"index"`
Title string `toyorm:"index"`
Content string
}
```
##### Many to many
many to many not need to specified the relation ship,it relation field at middle model
```
type User struct {
toyorm.ModelDefault
// container field
Friends []*User
}
```
##### Load preload
when you finish model definition, it time to load preload
```golang
// create a main brick
brick = toy.Model(&User{})
// create a sub brick
subBrick := brick.Preload(OffsetOf(User.Blog))
// you can editing any attribute what you want, just like editing it on main model
subBrick = subBrick.Where(ExprEqual, OffsetOf(Blog.Title), "my blog")
// finished change ,use Enter() go back the main brick
brick = subBrick.Enter()
```
if you not like relation field name rule,use custom module to create it
```golang
// one to one custom
brick.CustomOneToOnePreload(, , [sub model struct])
// belong to custom
brick.CustomBelongToPreload(, , [sub model struct])
// one to many
brick.CustomOneToManyPreload(, , [sub model struct])
// many to many
brick.CustomManyToManyPreload(, , , , [sub model struct])
```
or use tag declaration
```golang
type UserDetail struct {
ID int `toyorm:"primary key;auto_increment"`
MainID uint32 `toyorm:"index;one to one:Detail"` // declaration the container field
MainPage string `toyorm:"type:Text"`
Extra Extra `toyorm:"type:VARCHAR(1024)"`
}
type Blog struct {
toyorm.ModelDefault
MainID uint32 `toyorm:"index;one to many:Blog"` // declaration the container field
Title string `toyorm:"index"`
Content string
}
type User struct {
toyorm.ModelDefault
Name string `toyorm:"unique index"`
Age int
Sex string
Detail *UserDetail
Friends []*User
Blog []Blog
}
// now custom relation field is MainID
brick = brick.Preload(Offsetof(User{}.Blog)).Enter()
brick = brick.Preload(Offsetof(User{}.Detail)).Enter()
```
#### Join
---
different association query is join query
##### Join Example
[here](examples/join_example)
##### Model Define
use join tag to association related field/ join tag value must same as container field name
```golang
type Extra map[string]interface{}
func (e *Extra) Scan(value interface{}) error {
switch v := value.(type) {
case string:
return json.Unmarshal([]byte(v), e)
case []byte:
return json.Unmarshal(v, e)
default:
return errors.New("not support type")
}
}
func (e Extra) Value() (driver.Value, error) {
return json.Marshal(e)
}
type Color struct {
Name string `toyorm:"primary key;join:ColorDetail"`
Code int32
}
type Comment struct {
toyorm.ModelDefault
ProductDetailProductID uint32 `toyorm:"index"`
Data string `toyorm:"type:VARCHAR(1024)"`
}
type ProductDetail struct {
ProductID uint32 `toyorm:"primary key;join:Detail"`
Title string
CustomPage string `toyorm:"type:text"`
Extra Extra `toyorm:"type:VARCHAR(2048)"`
Color string `toyorm:"join:ColorDetail"`
ColorJoin Color `toyorm:"alias:ColorDetail"`
Comment []Comment
}
type Product struct {
ID uint32 `toyorm:"primary key;auto_increment;join:Detail"`
CreatedAt time.Time `toyorm:"NULL"`
DeletedAt *time.Time `toyorm:"NULL"`
Name string
Count int
Price float64
Detail *ProductDetail
}
```
##### Join in Find
Swap on Join just like Enter on Preload, can back to previous data
```golang
brick := toy.Model(&tab).Debug().
Join(Offsetof(tab.Detail)).
Join(Offsetof(detailTab.ColorJoin)).Swap().Swap()
var scanData []Product
result, err = brick.Find(&scanData)
// SELECT m.id,m.created_at,m.deleted_at,m.name,m.count,m.price,m_0.product_id,m_0.title,m_0.custom_page,m_0.extra,m_0.color,m_0_0.name,m_0_0.code FROM `product` as `m` JOIN `product_detail` AS `m_0` ON m.id = m_0.product_id JOIN `color` AS `m_0_0` ON m_0.color = m_0_0.name WHERE m.deleted_at IS NULL
```
use Join to switch current model and add conditions
```golang
// where Product.Name = "clean stick" or Color.Name = "black"
brick := toy.Model(&tab).Debug().Where("=", Offsetof(tab.Name), "clean stick").
Join(Offsetof(tab.Detail)).
Join(Offsetof(detailTab.ColorJoin)).Or().Condition("=", Offsetof(colorTab.Name), "black").
Swap().Swap()
var scanData []Product
result, err = brick.Find(&scanData)
// SELECT m.id,m.created_at,m.deleted_at,m.name,m.count,m.price,m_0.product_id,m_0.title,m_0.custom_page,m_0.extra,m_0.color,m_0_0.name,m_0_0.code FROM `product` as `m` JOIN `product_detail` AS `m_0` ON m.id = m_0.product_id JOIN `color` AS `m_0_0` ON m_0.color = m_0_0.name WHERE m.deleted_at IS NULL AND (m.name = ? OR m_0_0.name = ?) args:["clean stick","black"]
```
set order just like add conditions
```golang
// where Product.Name = "clean stick" or Color.Name = "black"
brick := toy.Model(&tab).Debug().
Join(Offsetof(tab.Detail)).
Join(Offsetof(detailTab.ColorJoin)).OrderBy(Offsetof(colorTab.Name)).
Swap().Swap()
var scanData []Product
result, err = brick.Find(&scanData)
// SELECT m.id,m.created_at,m.deleted_at,m.name,m.count,m.price,m_0.product_id,m_0.title,m_0.custom_page,m_0.extra,m_0.color,m_0_0.name,m_0_0.code FROM `product` as `m` JOIN `product_detail` AS `m_0` ON m.id = m_0.product_id JOIN `color` AS `m_0_0` ON m_0.color = m_0_0.name WHERE m.deleted_at IS NULL ORDER BY m_0_0.name
```
also can set GroupBy but here not example
##### Preload On Join
Preload method also work on Join mode
```golang
brick := toy.Model(&tab).Debug().
Join(Offsetof(tab.Detail)).Preload(Offsetof(detailTab.Comment)).Enter().
Join(Offsetof(detailTab.ColorJoin)).Swap().Swap()
var scanData []Product
result, err = brick.Find(&scanData)
// SELECT m.id,m.created_at,m.deleted_at,m.name,m.count,m.price,m_0.product_id,m_0.title,m_0.custom_page,m_0.extra,m_0.color,m_0_0.name,m_0_0.code FROM `product` as `m` JOIN `product_detail` AS `m_0` ON m.id = m_0.product_id JOIN `color` AS `m_0_0` ON m_0.color = m_0_0.name WHERE m.deleted_at IS NULL
// SELECT id,created_at,updated_at,deleted_at,product_detail_product_id,data FROM `comment` WHERE deleted_at IS NULL AND product_detail_product_id IN (?,?,?) args:[1,2,3]
```
### Custom Table Name
custom your table name with different platform
```golang
type User struct {
ID uint32 `toyorm:"primary key"`
Name string `toyorm:"index"`
Platform string `toyorm:"-"`
}
func (u *User) TableName() string {
return "user_" + u.Platform
}
brick := toy.Model(&User{Platform:"p1"}).Debug()
brick.CreateTable()
// CREATE TABLE user_p1 (id BIGINT AUTO_INCREMENT,name VARCHAR(255), PRIMARY KEY(id))
brick := toy.Model(&User{Platform:"p2"}).Debug()
brick.CreateTable()
// CREATE TABLE user_p2 (id BIGINT AUTO_INCREMENT,name VARCHAR(255), PRIMARY KEY(id))
```
table name method also work on Preload and Join
```golang
type UserDetail struct {
ID uint32 `toyorm:"primary key"`
UserID uint32
Data string
}
func (u *UserDetail) TableName() string {
return "user_detail_" + u.Platform
}
type User struct {
ID uint32 `toyorm:"primary key"`
Name string `toyorm:"index"`
Detail *UserDetail
Platform string `toyorm:"-"`
}
func (u *User) TableName() string {
return "user_" + u.Platform
}
brick := toy.Model(&User{Platform:"p1", Detail:&UserDetail{Platform:"p1"}}).Debug().
Preload(Offsetof(User{}.UserDetail)).Enter()
brick.CreateTable()
// CREATE TABLE user_p1 (id BIGINT AUTO_INCREMENT,name VARCHAR(255), PRIMARY KEY(id))
// CREATE TABLE user_detail_p1 (id BIGINT AUTO_INCREMENT, user_id BIGINT, data VARCHAR(255), PRIMARY KEY(id))
```
in one to many or many to many mode , need to set it's first element val
```golang
type Address struct {
ID uint32 `toyorm:"primary key"`
UserID uint32
Addr string
Platform string `toyorm:"-"`
}
func (a *Address) TableName() string {
return "address_" + a.Platform
}
type User struct {
ID uint32 `toyorm:"primary key"`
Name string `toyorm:"index"`
Addresses []Address
Platform string `toyorm:"-"`
}
func (u *User) TableName() string {
return "user_" + u.Platform
}
brick := toy.Model(&User{Platform:"p1", Addresses:[]Address{{Platform:"p1"}}}).Debug().
Preload(Offsetof(User{}.Addresses)).Enter()
brick.CreateTable()
// CREATE TABLE user_p1 (id BIGINT AUTO_INCREMENT,name VARCHAR(255), PRIMARY KEY(id))
// CREATE TABLE address_p1 (id BIGINT AUTO_INCREMENT, user_id BIGINT, addr VARCHAR(255), PRIMARY KEY(id))
```
### Result
**use Report to view sql action**
report format
insert
```golang
user := User{
Detail: &UserDetail{
MainPage: "some html code with you page",
Extra: Extra{"title": "my blog"},
},
Blog: []Blog{
{Title: "how to write a blog", Content: "first ..."},
{Title: "blog introduction", Content: "..."},
},
Friends: []*User{
{
Detail: &UserDetail{
MainPage: "some html code with you page",
Extra: Extra{},
},
Blog: []Blog{
{Title: "some python tech", Content: "first ..."},
{Title: "my eleme_union_meal usage", Content: "..."},
},
Name: "fatpigeon",
Age: 18,
Sex: "male",
},
},
Name: "bigpigeon",
Age: 18,
Sex: "male",
}
result, err = brick.Save(&user)
// error process ...
fmt.Printf("report:\n%s\n", result.Report())
/*
// [0, ] means affected the 0 element
// [0-0, ] means affected the 0 element the 0 sub element
report:
[0, ] INSERT INTO user(created_at,updated_at,deleted_at,name,age,sex) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.012285+08:00","2018-02-28T17:31:20.012285+08:00",null,"bigpigeon",18,"male"]
preload Detail
[0-, ] INSERT INTO user_detail(user_id,main_page,extra) VALUES(?,?,?) args:[1,"some html code with you page",{"title":"my blog"}]
preload Blog
[0-0, ] INSERT INTO blog(created_at,updated_at,deleted_at,user_id,title,content) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.013968+08:00","2018-02-28T17:31:20.013968+08:00",null,1,"how to write a blog","first ..."]
[0-1, ] INSERT INTO blog(created_at,updated_at,deleted_at,user_id,title,content) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.013968+08:00","2018-02-28T17:31:20.013968+08:00",null,1,"blog introduction","..."]
preload Friends
[0-0, ] INSERT INTO user(created_at,updated_at,deleted_at,name,age,sex) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.015207+08:00","2018-02-28T17:31:20.015207+08:00",null,"fatpigeon",18,"male"]
preload Detail
[0-0-, ] INSERT INTO user_detail(user_id,main_page,extra) VALUES(?,?,?) args:[2,"some html code with you page",{}]
preload Blog
[0-0-0, ] INSERT INTO blog(created_at,updated_at,deleted_at,user_id,title,content) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.016389+08:00","2018-02-28T17:31:20.016389+08:00",null,2,"some python tech","first ..."]
[0-0-1, ] INSERT INTO blog(created_at,updated_at,deleted_at,user_id,title,content) VALUES(?,?,?,?,?,?) args:["2018-02-28T17:31:20.016389+08:00","2018-02-28T17:31:20.016389+08:00",null,2,"my eleme_union_meal usage","..."]
*/
```
find
```golang
brick := brick.Preload(Offsetof(User{}.Friends)).
Preload(Offsetof(User{}.Detail)).Enter().
Preload(Offsetof(User{}.Blog)).Enter().
Enter()
var users []User
result, err = brick.Find(&users)
// some error process
...
// print the report
fmt.Printf("report:\n%s\n", result.Report())
// report log
/*
report:
[0, 1, ] SELECT id,created_at,updated_at,deleted_at,name,age,sex FROM user WHERE deleted_at IS NULL args:null
preload Detail
[0-, 1-, ] SELECT id,user_id,main_page,extra FROM user_detail WHERE user_id IN (?,?) args:[2,1]
preload Blog
[0-0, 0-1, 1-0, 1-1, ] SELECT id,created_at,updated_at,deleted_at,user_id,title,content FROM blog WHERE deleted_at IS NULL AND user_id IN (?,?) args:[1,2]
preload Friends
[0-0, ] SELECT id,created_at,updated_at,deleted_at,name,age,sex FROM user WHERE deleted_at IS NULL AND id IN (?) args:[2]
preload Detail
[0-0-, ] SELECT id,user_id,main_page,extra FROM user_detail WHERE user_id IN (?) args:[2]
preload Blog
[0-0-0, 0-0-1, ] SELECT id,created_at,updated_at,deleted_at,user_id,title,content FROM blog WHERE deleted_at IS NULL AND user_id IN (?) args:[2]
*/
```
**use Err to view sql action error**
```golang
var users []struct {
ID uint32
Age bool
Detail *UserDetail
Blog []Blog
}
result, err = brick.Find(&users)
if err != nil {
panic(err)
}
if err := result.Err(); err != nil {
fmt.Printf("error:\n%s\n", err)
}
/*
error:
SELECT id,age FROM user WHERE deleted_at IS NULL args:null errors(
[0]sql: Scan error on column index 1: sql/driver: couldn't convert "18" into type bool
[1]sql: Scan error on column index 1: sql/driver: couldn't convert "18" into type bool
)
*/
```
#### Selector
toyorm support following selector
operation \\ selector | OffsetOf | Name string | map\[OffsetOf\]interface{} | map\[string\]interface{} | struct |
--------------------|----------|-----------------|--------------------------------|--------------------------|--------|
Update | no | no | yes | yes | yes
Insert | no | no | yes | yes | yes
Save | no | no | yes | yes | yes
Where & Conditions | yes | yes | no | no | no
WhereGroup & ConditionGroup | no | no | yes | yes | yes
BindFields | yes | yes | no | no | no
Preload & Custom Preload | yes | yes | no | no | no
OrderBy | yes | yes | no | no | no
Find | no | no | no | no | yes
## Collection
collection provide multiple database operation
### ToyCollection
ToyCollection is basic of the collection , it like Toy
```toyorm
toyCollection, err = toyorm.OpenCollection("sqlite3", []string{"", ""}...)
```
### CollectionBrick
CollectionBrick use to build grammar and operate the database, like ToyBrick
```toyorm
brick := toyCollection.Model(&User{})
```
#### Selector
Selector use to select database when Insert/Save
CollectionBrick has default selector **dbPrimaryKeySelector**
you can custom db selector
```toyorm
func idSelector(n int, keys ...interface{}) int {
sum := 0
for _, k := range keys {
switch val := k.(type) {
case int:
sum += val
case int32:
sum += int(val)
case uint:
sum += int(val)
case uint32:
sum += int(val)
default:
panic("primary key type not match")
}
}
return sum % n
}
brick = brick.Selector(idSelector)
```
#### id generator
in this mode,Field tag **auto_increment** was invalid
you need create id generator
```golang
// a context id generator
type IDGenerator map[*toyorm.Model]chan int
func (g IDGenerator) CollectionIDGenerate(ctx *toyorm.CollectionContext) error {
if g[ctx.Brick.Model] == nil {
idGenerate := make(chan int)
go func() {
current := 1
for {
// if have redis, use redis-cli
idGenerate <- current
current++
}
}()
g[ctx.Brick.Model] = idGenerate
}
primaryKey := ctx.Brick.Model.GetOnePrimary()
for _, record := range ctx.Result.Records.GetRecords() {
if field := record.Field(primaryKey.Name()); field.IsValid() == false || toyorm.IsZero(field) {
v := <-g[ctx.Brick.Model]
record.SetField(primaryKey.Name(), reflect.ValueOf(v))
}
}
return nil
}
// set id genetor to model context
toy.SetModelHandlers("Save", brick.Model, toyorm.CollectionHandlersChain{idGenerate.CollectionIDGenerate})
// set id genetor to all preload models context
for _, pBrick := range brick.MapPreloadBrick {
toy.SetModelHandlers("Save", pBrick.Model, toyorm.CollectionHandlersChain{idGenerate.CollectionIDGenerate})
}
```
#### sql action
toy collection sql action is same as Toy
insert
```golang
users := []User{
{Name: "Turing"},
{Name: "Shannon"},
{Name: "Ritchie"},
{Name: "Jobs"},
}
result, err = userBrick.Insert(&users)
// error process
// view report log
fmt.Printf("report:\n%s", result.Report())
```
find
```golang
var jobs User
result, err = userBrick.Where(toyorm.ExprEqual, Offsetof(User{}.Name), "Jobs").Find(&jobs)
// error process
// view report log
fmt.Printf("report:\n%s", result.Report())
```
delete
```golang
result, err = userBrick.Delete(&jobs)
// error process
// view report log
fmt.Printf("delete report:\n%s\n", result.Report())
```
### collection example
[here](examples/collection_example)
## toy-doctor
parameter check within ToyBrick method call
[toy-doctor](https://github.com/bigpigeon/toy-doctor)