配置ORM
type ORMdemoController struct {
beego.Controller
}
func (this * ORMdemoController) Get(){
//注冊數據驅動
orm.RegisterDriver("mysql", orm.DRMySQL) // mysql、sqlite3、postgres 這三種是beego默認已經注冊過的,所以可以無需設置
//注冊數據庫 ORM 必須注冊一個別名為 default 的數據庫,作為默認使用
//五個參數:1、數據庫別名;2、數據庫驅動;3、數據庫賬戶:密碼@鏈接地址/數據庫名稱;4、最大空閑連接數;5、最大數據連接
orm.RegisterDataBase("default", "mysql", "root:passwd@tcp(127.0.0.1:3306)/db_user?charset=utf8")
//設置數據庫時區
//orm.DefaultTimeLoc = time.UTC
//注冊模型
orm.RegisterModel(new(UserTable))
//自動創建表:1、默認數據;2、是否開啟創建表;3、是否更新表
orm.RunSyncdb("default", true, true)
this.Ctx.WriteString("表創建成功")
}
創建Model
- 定義model時注意事項
1、我們定義結構體作為表,必須要有主鍵。
2、當 Field 類型為 int, int32, int64, uint, uint32, uint64 時,可以設置字段為自增健。
3、當模型定義里沒有主鍵時,符合int類型且名稱為 Id 的 Field 將被視為自增健。
4、屬性的首字母最好是大寫,設置屬性為公開訪問性。
5、未定義其他規則時,自動生表時,所有字段都為NOT NULL,id為自增主鍵,其他都有其類型默認值
type UserTable struct {
User int //添加 int型字段ID,作為主鍵
Pwd string
RealName string
Age string
IdCard string
Email string
Tel string
}
- 使用結構體tag進行表的詳細屬性設置
type UserTable struct {
ID int `orm:"pk;auto;column(id)"` //設置主鍵自增長 字段名為 id
User string `orm:"size(15);column(user)"` //設置長度為15 字段名為 user
Pwd string `orm:"size(20);column(pwd)"` //設置長度為20 字段名為 pwd
RealName string `orm:"size(10);column(realname)"` //設置長度為10 字段名為 realname
Age string `orm:column(age)"` //設置字段名為age
IdCard string `orm:"size(18);column(idcard)"` //設置長度為18 字段名為 idcard
Email string `orm:"size(100);column(email)"` //設置長度為100 字段名為 email
Tel string `orm:"size(11);column(tel)"` //設置長度為11 字段名為 tel
}
- 表名
beego中表名默認使用駝峰:UserTable
遇到大寫會增加 _,原名稱中的下劃線保留:UserTable->user_table
//自定義表名
func (u *UserTable) TableName() string {
return "user" //表名被改為user
}
//使用RegisterModelWithPrefix為表名設置前綴:prefix_user_table
orm.RegisterModelWithPrefix("prefix_", new(UserTable))
- 關聯關系定義
//一對一,rel(one): 創建對應關系的字段:表_id,即對應主鍵,有唯一約束
type User struct {
......
Profile *Profile `orm:"rel(one)"`
}
//反向一對一,reverse(one): 不會創建字段,可選tag
type Profile struct {
......
User *User `orm:"reverse(one)"`
}
//一對多,rel(fk): 創建對應關系的字段:表名_id,即對應主鍵,沒有約束
type Post struct {
......
User *User `orm:"rel(fk)"`
}
//反向一對多,reverse(many): 不會創建字段,寫在關系為多的類里
type User struct {
......
Post []*Post `orm:"reverse(many)"`
}
//多對多,rel(m2m): 不會創建字段,聲明處的表名,為自動創建的表的表名前綴(post)
type Post struct {
......
Tags []*Tag `orm:"rel(m2m)"`
}
//反向多對多,reverse(many): 不會創建字段,聲明處的表名,為自動創建的表的表名后綴+s(tags)
type Tag struct {
......
Posts []*Post `orm:"reverse(many)"`
}
ps:反向一對多和反向多對多,關鍵詞一樣
ORM的使用
- 一些基本操作
type Ormer interface {
Read(interface{}, …string) error
ReadOrCreate(interface{}, string, …string) (bool, int64, error)
Insert(interface{}) (int64, error)
InsertMulti(int, interface{}) (int64, error)
Update(interface{}, …string) (int64, error)
Delete(interface{}) (int64, error)
LoadRelated(interface{}, string, …interface{}) (int64, error)
QueryM2M(interface{}, string) QueryM2Mer
QueryTable(interface{}) QuerySeter
Using(string) error
Begin() error
Commit() error
Rollback() error
Raw(string, …interface{}) RawSeter
Driver() Driver
}
- 基本操作符
- 基本的CRUD
type UserTable struct {......}
func Create(param interface{}) (int, error) {
return orm.NewOrm().Insert(param)
}
func Update(param interface{}, fields ...string) (int, error) {
return orm.NewOrm().Update(param, fields...)
}
func Delete(param interface{}, cols ...string) (int, error) {
return orm.NewOrm().Delete(param, cols...)
}
func Read(md interface{}, cols ...string) error {
return orm.NewOrm().Read(md, cols...)
}
ORM源碼分析
主要流程
啟動應用時,完成orm相關配置的注冊:數據庫、model、rel
使用時,實例化orm對象、關聯關系處理、進行sql解析
- 注冊ORM相關配置
//注冊數據庫 ORM 必須注冊一個別名為 default 的數據庫,作為默認使用
//五個參數:1、數據庫別名;2、數據庫驅動;3、數據庫賬戶:密碼@鏈接地址/數據庫名稱;4、最大空閑連接數;5、最大數據連接
orm.RegisterDataBase("default", "mysql", "root:passwd@tcp(127.0.0.1:3306)/db_user?charset=utf8")
//注冊模型
orm.RegisterModel(new(UserTable))
//自動創建表:1、默認數據;2、是否開啟創建表;3、是否更新表
orm.RunSyncdb("default", true, true)
注冊操作做了啥
- 注冊db
//使用當前驅動的連接配置信息(數據庫賬戶:密碼@鏈接地址/數據庫名稱),設置數據庫連接參數
func RegisterDataBase(aliasName, driverName, dataSource string, params ...int) error {
......
}
- 注冊model
//===== github.com/astaxie/beego/orm/orm.go =====
func RegisterModel(models ...interface{}) {
......
RegisterModelWithPrefix("", models...)
}
func RegisterModelWithPrefix(prefix string, models ...interface{}) {
......
for _, model := range models {
registerModel(prefix, model, true)
}
}
//RegisterModel 和 RegisterModelWithPrefix 都是對 registerModel 的封裝
func registerModel(PrefixOrSuffix string, model interface{}, isPrefix bool) {
//通過反射獲取模型信息
val := reflect.ValueOf(model)
typ := reflect.Indirect(val).Type()
......
//處理表名和前綴,完整路徑
table := getTableName(val)
if PrefixOrSuffix != "" {
if isPrefix {
table = PrefixOrSuffix + table
} else {
table = table + PrefixOrSuffix
}
}
// models's fullname is pkgpath + struct name
name := getFullName(typ)
if _, ok := modelCache.getByFullName(name); ok {
fmt.Printf("<orm.RegisterModel> model `%s` repeat register, must be unique\n", name)
os.Exit(2)
}
if _, ok := modelCache.get(table); ok {
fmt.Printf("<orm.RegisterModel> table name `%s` repeat register, must be unique\n", table)
os.Exit(2)
}
//通過反射判斷是否有id字段,設置主鍵
mi := newModelInfo(val)
if mi.fields.pk == nil {
outFor:
for _, fi := range mi.fields.fieldsDB {
if strings.ToLower(fi.name) == "id" {
switch fi.addrValue.Elem().Kind() {
case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
fi.auto = true
fi.pk = true
mi.fields.pk = fi
break outFor
}
}
}
if mi.fields.pk == nil {
fmt.Printf("<orm.RegisterModel> `%s` needs a primary key field, default is to use 'id' if not set\n", name)
os.Exit(2)
}
}
//處理完成的model信息,設置到model緩存中
mi.table = table
mi.pkg = typ.PkgPath()
mi.model = model
mi.manual = true
modelCache.set(table, mi)
}
ORM初始化過程
以這樣一段orm操作作為示例:
func main() {
o := orm.NewOrm()
user := User{Name: "user"}
u := User{Id: user.Id}
err = o.Read(&u)
fmt.Println(err)
}
- 初始化orm
NewOrm時,先調用BootStrap方法完成模型信息的加載,然后調用Using選中默認數據庫:
源碼github.com/astaxie/beego/orm/orm.go
type orm struct {
alias *alias
db dbQuerier
isTx bool
}
func NewOrm() Ormer {
//orm的構造函數一開始就會執行啟動引導
BootStrap() // execute only once
//選擇一個默認的數據庫驅動
o := new(orm)
err := o.Using("default")
if err != nil {
panic(err)
}
return o
}
BootStrap方法:加載緩存中的model實例,把model中定義的字段跟數據表中的字段進行綁定關聯,無緩存會進行首次初始化。
Using方法:讀取緩存的數據庫信息,賦值給 orm 的 alias 屬性,之后就可以進行正常的crud操作了
//====== orm/orm.go ======
func (o *orm) Using(name string) error {
......
//將數據庫緩存信息賦值給 orm 的 alias 屬性
if al, ok := dataBaseCache.get(name); ok {
o.alias = al
if Debug {//debug模式下開啟sql日志
o.db = newDbQueryLog(al, al.DB)
} else {
o.db = al.DB
}
} else {
return fmt.Errorf("<Ormer.Using> unknown db alias name `%s`", name)
}
return nil
}
//============ orm/models_boot.go ============
func BootStrap() {
//對model的操作緩存進行加鎖,保證只會執行一次
modelCache.Lock()
defer modelCache.Unlock()
if modelCache.done {
return
}
bootStrap()
modelCache.done = true
}
func bootStrap() {
......
var (
err error
models map[string]*modelInfo
)
//遍歷模型的字段
models = modelCache.all()
for _, mi := range models {
for _, fi := range mi.fields.columns {
//如果有設置rel或者reverse關系
if fi.rel || fi.reverse {
elm := fi.addrValue.Type().Elem()
if fi.fieldType == RelReverseMany || fi.fieldType == RelManyToMany {
elm = elm.Elem()
}
//檢查關聯的模型是否注冊,并獲取模型信息
name := getFullName(elm)
mii, ok := modelCache.getByFullName(name)
if !ok || mii.pkg != elm.PkgPath() {
err = fmt.Errorf("can not find rel in field `%s`, `%s` may be miss register", fi.fullName, elm.String())
goto end
}
fi.relModelInfo = mii
switch fi.fieldType {
case RelManyToMany:
//如果是多對多關系,且聲明了中間表,則根據中間表獲取關聯表信息;并為當前字段設置模型關聯信息
if fi.relThrough != "" {
if i := strings.LastIndex(fi.relThrough, "."); i != -1 && len(fi.relThrough) > (i+1) {
pn := fi.relThrough[:i]
rmi, ok := modelCache.getByFullName(fi.relThrough)
if !ok || pn != rmi.pkg {
err = fmt.Errorf("field `%s` wrong rel_through value `%s` cannot find table", fi.fullName, fi.relThrough)
goto end
}
fi.relThroughModelInfo = rmi
fi.relTable = rmi.table
} else {
err = fmt.Errorf("field `%s` wrong rel_through value `%s`", fi.fullName, fi.relThrough)
goto end
}
} else {
//未主動聲明中間表關系,創建新的關聯模型實例,判斷關聯表是否被注冊
i := newM2MModelInfo(mi, mii)
if fi.relTable != "" {
i.table = fi.relTable
}
if v := modelCache.set(i.table, i); v != nil {
err = fmt.Errorf("the rel table name `%s` already registered, cannot be use, please change one", fi.relTable)
goto end
}
fi.relTable = i.table
fi.relThroughModelInfo = i
}
fi.relThroughModelInfo.isThrough = true
}
}
}
}
//后面是一些模板代碼
//遍歷字段關聯,自動生成字段反向關聯信息
models = modelCache.all()
for _, mi := range models {
for _, fi := range mi.fields.fieldsRel {......}
}
//遍歷字段關聯,檢查反向多對多關聯關系設置
models = modelCache.all()
for _, mi := range models {
for _, fi := range mi.fields.fieldsRel {......}
}
//遍歷字段反向關聯,檢查字段是否在模型中設置
models = modelCache.all()
for _, mi := range models {
for _, fi := range mi.fields.fieldsReverse {.....}
}
}
- 當我們使用QueryTable設置表時
func (o *orm) QueryTable(ptrStructOrTableName interface{}) (qs QuerySeter) {
var name string
if table, ok := ptrStructOrTableName.(string); ok {
//如果是指針結構體或表名,根據命名策略,到模型緩存中獲取模型
name = nameStrategyMap[defaultNameStrategy](table)
if mi, ok := modelCache.get(name); ok {
qs = newQuerySet(o, mi)
}
} else {
//否則,通過反射獲取到表名,再根據表名獲取緩存中的模型
name = getFullName(indirectType(reflect.TypeOf(ptrStructOrTableName)))
if mi, ok := modelCache.getByFullName(name); ok {
qs = newQuerySet(o, mi)
}
}
//最后返回一個QuerySet
return
}
- QuerySeter
QuerySeter接口定義了一系列的查詢方法:
//====== orm/types.go ======
type QuerySeter interface {
// 篩選條件 where
Filter(string, ...interface{}) QuerySeter
// 原生過濾語句
// qs.FilterRaw("user_id IN (SELECT id FROM profile WHERE age>=18)")
FilterRaw(string, string) QuerySeter
// 排除篩選條件 where not in
Exclude(string, ...interface{}) QuerySeter
// 設置單個篩選條件
SetCond(*Condition) QuerySeter
// 獲取指定的篩選條件
GetCond() *Condition
// 分頁
Limit(limit interface{}, args ...interface{}) QuerySeter
Offset(offset interface{}) QuerySeter
// 分組
GroupBy(exprs ...string) QuerySeter
// 排序
OrderBy(exprs ...string) QuerySeter
// 模型關聯查詢
RelatedSel(params ...interface{}) QuerySeter
// 去重
Distinct() QuerySeter
// 給構造器設置FOR UPDATE
ForUpdate() QuerySeter
// 計數
Count() (int64, error)
// 是否存在
Exist() bool
// 更新
Update(values Params) (int64, error)
// 刪除
Delete() (int64, error)
// 返回一個插入queryer
PrepareInsert() (Inserter, error)
// 查詢多條數據
All(container interface{}, cols ...string) (int64, error)
// 查詢單條數據
One(container interface{}, cols ...string) error
// 查詢多條結果并將結果存入字符串類型的map指針變量中
Values(results *[]Params, exprs ...string) (int64, error)
// 查詢多條結果并將結果存入接口類型的map指針變量中
ValuesList(results *[]ParamsList, exprs ...string) (int64, error)
// 將所有結果集存入map變量中,沒有字段名
ValuesFlat(result *ParamsList, expr string) (int64, error)
// ptrStruct存放查詢結果的指針map變量, keyCol查詢字段名,valueCol字段值
RowsToMap(result *Params, keyCol, valueCol string) (int64, error)
// ptrStruct存放查詢結果的結構體指針變量, keyCol查詢字段名,valueCol字段值
RowsToStruct(ptrStruct interface{}, keyCol, valueCol string) (int64, error)
}
- QuerySeter接口的實現
//====== orm/orm_queryset.go ======
type querySet struct {
mi *modelInfo
cond *Condition
related []string
relDepth int
limit int64
offset int64
groups []string
orders []string
distinct bool
forupdate bool
orm *orm
ctx context.Context
forContext bool
}
func newQuerySet(orm *orm, mi *modelInfo) QuerySeter {
o := new(querySet)
o.mi = mi
o.orm = orm
return o
}
//這里就以 All 方法的實現說明一下,其他的類似
func (o *querySet) All(container interface{}, cols ...string) (int64, error) {
//All最終會調用DbBaser接口提供的ReadBatch方法
return o.orm.alias.DbBaser.ReadBatch(o.orm.db, o, o.mi, o.cond, container, o.orm.alias.TZ, cols)
}
- dbBaser 接口
//====== orm/types.go ======
type dbBaser interface {
Read(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string, bool) error
Insert(dbQuerier, *modelInfo, reflect.Value, *time.Location) (int64, error)
InsertOrUpdate(dbQuerier, *modelInfo, reflect.Value, *alias, ...string) (int64, error)
InsertMulti(dbQuerier, *modelInfo, reflect.Value, int, *time.Location) (int64, error)
InsertValue(dbQuerier, *modelInfo, bool, []string, []interface{}) (int64, error)
InsertStmt(stmtQuerier, *modelInfo, reflect.Value, *time.Location) (int64, error)
Update(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string) (int64, error)
Delete(dbQuerier, *modelInfo, reflect.Value, *time.Location, []string) (int64, error)
ReadBatch(dbQuerier, *querySet, *modelInfo, *Condition, interface{}, *time.Location, []string) (int64, error)
......
}
- DbBaser 接口 ReadBatch 方法的實現
//====== orm/db.go ======
//ReadBatch方法的實現
func (d *dbBase) ReadBatch(q dbQuerier, qs *querySet, mi *modelInfo, cond *Condition, container interface{}, tz *time.Location, cols []string) (int64, error) {
val := reflect.ValueOf(container)
ind := reflect.Indirect(val)
errTyp := true
one := true
isPtr := true
//通過反射判斷容器類型,標記是否指針類型
if val.Kind() == reflect.Ptr {
fn := ""
if ind.Kind() == reflect.Slice {
one = false
typ := ind.Type().Elem()
switch typ.Kind() {
case reflect.Ptr:
fn = getFullName(typ.Elem())
case reflect.Struct:
isPtr = false
fn = getFullName(typ)
}
} else {
fn = getFullName(ind.Type())
}
errTyp = fn != mi.fullName
}
if errTyp {
if one {
panic(fmt.Errorf("wrong object type `%s` for rows scan, need *%s", val.Type(), mi.fullName))
} else {
panic(fmt.Errorf("wrong object type `%s` for rows scan, need *[]*%s or *[]%s", val.Type(), mi.fullName, mi.fullName))
}
}
//從querySeter中獲取分頁信息
rlimit := qs.limit
offset := qs.offset
Q := d.ins.TableQuote()
var tCols []string
if len(cols) > 0 {
//判斷模型是否有關聯關系
hasRel := len(qs.related) > 0 || qs.relDepth > 0
tCols = make([]string, 0, len(cols))
var maps map[string]bool
if hasRel {
maps = make(map[string]bool)
}
//從模型信息獲取所有字段
for _, col := range cols {
if fi, ok := mi.fields.GetByAny(col); ok {
tCols = append(tCols, fi.column)
if hasRel {
maps[fi.column] = true
}
} else {
return 0, fmt.Errorf("wrong field/column name `%s`", col)
}
}
//如果有關聯關系,從模型信息中讀取關聯字段,并賦值給tCols,完成數據庫字段映射
if hasRel {
for _, fi := range mi.fields.fieldsDB {
if fi.fieldType&IsRelField > 0 {
if !maps[fi.column] {
tCols = append(tCols, fi.column)
}
}
}
}
} else {
tCols = mi.fields.dbcols
}
colsNum := len(tCols)
sep := fmt.Sprintf("%s, T0.%s", Q, Q)
sels := fmt.Sprintf("T0.%s%s%s", Q, strings.Join(tCols, sep), Q)
//初始dbTables對象,解析關聯關系
tables := newDbTables(mi, d.ins)
tables.parseRelated(qs.related, qs.relDepth)
//構建sql語句各節點:where、groupBy、limit、join
//后續深入sql組裝
where, args := tables.getCondSQL(cond, false, tz)
groupBy := tables.getGroupSQL(qs.groups)
orderBy := tables.getOrderSQL(qs.orders)
limit := tables.getLimitSQL(mi, offset, rlimit)
join := tables.getJoinSQL()
for _, tbl := range tables.tables {
if tbl.sel {
colsNum += len(tbl.mi.fields.dbcols)
sep := fmt.Sprintf("%s, %s.%s", Q, tbl.index, Q)
sels += fmt.Sprintf(", %s.%s%s%s", tbl.index, Q, strings.Join(tbl.mi.fields.dbcols, sep), Q)
}
}
//組裝sql
sqlSelect := "SELECT"
if qs.distinct {
sqlSelect += " DISTINCT"
}
query := fmt.Sprintf("%s %s FROM %s%s%s T0 %s%s%s%s%s", sqlSelect, sels, Q, mi.table, Q, join, where, groupBy, orderBy, limit)
if qs.forupdate {
query += " FOR UPDATE"
}
d.ins.ReplaceMarks(&query)
var rs *sql.Rows
var err error
if qs != nil && qs.forContext {
rs, err = q.QueryContext(qs.ctx, query, args...)
if err != nil {
return 0, err
}
} else {
rs, err = q.Query(query, args...)
if err != nil {
return 0, err
}
}
refs := make([]interface{}, colsNum)
for i := range refs {
var ref interface{}
refs[i] = &ref
}
defer rs.Close()
slice := ind
var cnt int64
//如果存在下一個結果行
//通過反射獲取字段的類型信息,并設置給data結構體的屬性
//遍歷dbTables實例,循環tables和models
//通過反射獲取字段發名稱、值、索引
for rs.Next() {
//if嵌套看著頭大,就不傷害大家的眼睛了
if one && cnt == 0 || !one {......}
}
//如果存在數據行,將slice賦值給ind,否則根據ind的類型創建一個空切片并賦值
if !one {
if cnt > 0 {
ind.Set(slice)
} else {
// when a result is empty and container is nil
// to set a empty container
if ind.IsNil() {
ind.Set(reflect.MakeSlice(ind.Type(), 0, 0))
}
}
}
return cnt, nil
}
- getOrderSQL方法,組裝排序sql
//====== orm/db_tables.go ======
func (t *dbTables) getOrderSQL(orders []string) (orderSQL string) {
if len(orders) == 0 {
return
}
Q := t.base.TableQuote()
//根據orders切片長度,生成一個新的切片用于保存sql
orderSqls := make([]string, 0, len(orders))
for _, order := range orders {
//設置排序方式,以'-'開頭為降序,否則為升序
asc := "ASC"
if order[0] == '-' {
asc = "DESC"
order = order[1:]
}
//根據ExprSep分隔符常量,將order字符串拆分為切片
exprs := strings.Split(order, ExprSep)
//解析模型字段
index, _, fi, suc := t.parseExprs(t.mi, exprs)
if !suc {
panic(fmt.Errorf("unknown field/column name `%s`", strings.Join(exprs, ExprSep)))
}
//將解析結果格式化為字符串后追加到orderSqls切片
orderSqls = append(orderSqls, fmt.Sprintf("%s.%s%s%s %s", index, Q, fi.column, Q, asc))
}
//將sql結果以逗號拼接為支付串后,組裝"ORDER BY "
orderSQL = fmt.Sprintf("ORDER BY %s ", strings.Join(orderSqls, ", "))
return
}