gf/database/gdb/gdb_model_with.go

318 lines
9.5 KiB
Go
Raw Normal View History

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gdb
2021-02-08 17:57:21 +08:00
import (
2021-09-23 19:29:20 +08:00
"database/sql"
2021-02-08 17:57:21 +08:00
"fmt"
"reflect"
2021-11-13 23:23:55 +08:00
"github.com/gogf/gf/v2/errors/gcode"
2021-10-11 21:41:56 +08:00
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/os/gstructs"
2021-10-11 21:41:56 +08:00
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
2021-02-08 17:57:21 +08:00
)
2021-08-27 20:16:29 +08:00
// With creates and returns an ORM model based on metadata of given object.
// It also enables model association operations feature on given `object`.
// It can be called multiple times to add one or more objects to model and enable
// their mode association operations feature.
// For example, if given struct definition:
// type User struct {
// gmeta.Meta `orm:"table:user"`
// Id int `json:"id"`
// Name string `json:"name"`
// UserDetail *UserDetail `orm:"with:uid=id"`
// UserScores []*UserScores `orm:"with:uid=id"`
// }
// We can enable model association operations on attribute `UserDetail` and `UserScores` by:
// db.With(User{}.UserDetail).With(User{}.UserDetail).Scan(xxx)
// Or:
// db.With(UserDetail{}).With(UserDetail{}).Scan(xxx)
2021-05-02 22:35:47 +08:00
// Or:
// db.With(UserDetail{}, UserDetail{}).Scan(xxx)
func (m *Model) With(objects ...interface{}) *Model {
model := m.getModel()
2021-05-02 22:35:47 +08:00
for _, object := range objects {
if m.tables == "" {
m.tablesInit = m.db.GetCore().QuotePrefixTableName(
getTableNameFromOrmTag(object),
)
m.tables = m.tablesInit
2021-05-02 22:35:47 +08:00
return model
}
model.withArray = append(model.withArray, object)
}
return model
}
2021-02-08 17:57:21 +08:00
// WithAll enables model association operations on all objects that have "with" tag in the struct.
func (m *Model) WithAll() *Model {
model := m.getModel()
model.withAll = true
return model
}
// doWithScanStruct handles model association operations feature for single struct.
func (m *Model) doWithScanStruct(pointer interface{}) error {
var (
err error
allowedTypeStrArray = make([]string, 0)
)
currentStructFieldMap, err := gstructs.FieldMap(gstructs.FieldMapInput{
2021-08-03 22:21:20 +08:00
Pointer: pointer,
PriorityTagArray: nil,
RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
2021-08-03 22:21:20 +08:00
})
if err != nil {
return err
}
// It checks the with array and automatically calls the ScanList to complete association querying.
if !m.withAll {
2021-08-27 20:16:29 +08:00
for _, field := range currentStructFieldMap {
for _, withItem := range m.withArray {
withItemReflectValueType, err := gstructs.StructType(withItem)
if err != nil {
return err
}
var (
fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]")
withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]")
)
2021-08-03 22:21:20 +08:00
// It does select operation if the field type is in the specified "with" type array.
if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 {
allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr)
}
}
}
}
2021-08-27 20:16:29 +08:00
for _, field := range currentStructFieldMap {
var (
fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]")
parsedTagOutput = m.parseWithTagInFieldStruct(field)
)
if parsedTagOutput.With == "" {
continue
}
2021-08-27 20:16:29 +08:00
// It just handlers "with" type attribute struct, so it ignores other struct types.
if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) {
continue
}
array := gstr.SplitAndTrim(parsedTagOutput.With, "=")
if len(array) == 1 {
2021-08-27 20:16:29 +08:00
// It also supports using only one column name
// if both tables associates using the same column name.
array = append(array, parsedTagOutput.With)
}
var (
2021-08-27 20:16:29 +08:00
model *Model
fieldKeys []string
relatedSourceName = array[0]
relatedTargetName = array[1]
relatedTargetValue interface{}
)
// Find the value of related attribute from `pointer`.
2021-08-27 20:16:29 +08:00
for attributeName, attributeValue := range currentStructFieldMap {
if utils.EqualFoldWithoutChars(attributeName, relatedTargetName) {
relatedTargetValue = attributeValue.Value.Interface()
break
}
}
2021-08-27 20:16:29 +08:00
if relatedTargetValue == nil {
2021-07-20 23:02:02 +08:00
return gerror.NewCodef(
gcode.CodeInvalidParameter,
2021-08-27 20:16:29 +08:00
`cannot find the target related value of name "%s" in with tag "%s" for attribute "%s.%s"`,
relatedTargetName, parsedTagOutput.With, reflect.TypeOf(pointer).Elem(), field.Name(),
)
}
bindToReflectValue := field.Value
2021-08-27 20:16:29 +08:00
if bindToReflectValue.Kind() != reflect.Ptr && bindToReflectValue.CanAddr() {
bindToReflectValue = bindToReflectValue.Addr()
}
// It automatically retrieves struct field names from current attribute struct/slice.
if structType, err := gstructs.StructType(field.Value); err != nil {
return err
} else {
fieldKeys = structType.FieldKeys()
}
// Recursively with feature checks.
model = m.db.With(field.Value)
if m.withAll {
model = model.WithAll()
} else {
model = model.With(m.withArray...)
}
if parsedTagOutput.Where != "" {
model = model.Where(parsedTagOutput.Where)
}
if parsedTagOutput.Order != "" {
model = model.Order(parsedTagOutput.Order)
}
2021-08-27 20:16:29 +08:00
err = model.Fields(fieldKeys).Where(relatedSourceName, relatedTargetValue).Scan(bindToReflectValue)
2021-09-23 19:29:20 +08:00
// It ignores sql.ErrNoRows in with feature.
if err != nil && err != sql.ErrNoRows {
2021-02-08 17:57:21 +08:00
return err
}
}
return nil
}
// doWithScanStructs handles model association operations feature for struct slice.
// Also see doWithScanStruct.
func (m *Model) doWithScanStructs(pointer interface{}) error {
2021-07-08 21:02:36 +08:00
if v, ok := pointer.(reflect.Value); ok {
pointer = v.Interface()
}
var (
err error
allowedTypeStrArray = make([]string, 0)
)
currentStructFieldMap, err := gstructs.FieldMap(gstructs.FieldMapInput{
2021-08-03 22:21:20 +08:00
Pointer: pointer,
PriorityTagArray: nil,
RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag,
2021-08-03 22:21:20 +08:00
})
if err != nil {
return err
}
// It checks the with array and automatically calls the ScanList to complete association querying.
if !m.withAll {
2021-08-27 20:16:29 +08:00
for _, field := range currentStructFieldMap {
for _, withItem := range m.withArray {
withItemReflectValueType, err := gstructs.StructType(withItem)
if err != nil {
return err
}
var (
fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]")
withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]")
)
// It does select operation if the field type is in the specified with type array.
if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 {
allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr)
2021-02-08 17:57:21 +08:00
}
}
}
}
2021-08-27 20:16:29 +08:00
for fieldName, field := range currentStructFieldMap {
var (
fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]")
parsedTagOutput = m.parseWithTagInFieldStruct(field)
)
if parsedTagOutput.With == "" {
continue
}
if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) {
continue
}
array := gstr.SplitAndTrim(parsedTagOutput.With, "=")
if len(array) == 1 {
// It supports using only one column name
// if both tables associates using the same column name.
array = append(array, parsedTagOutput.With)
}
var (
2021-08-27 20:16:29 +08:00
model *Model
fieldKeys []string
relatedSourceName = array[0]
relatedTargetName = array[1]
relatedTargetValue interface{}
)
// Find the value slice of related attribute from `pointer`.
2021-11-16 17:21:13 +08:00
for attributeName := range currentStructFieldMap {
2021-08-27 20:16:29 +08:00
if utils.EqualFoldWithoutChars(attributeName, relatedTargetName) {
relatedTargetValue = ListItemValuesUnique(pointer, attributeName)
break
}
}
2021-08-27 20:16:29 +08:00
if relatedTargetValue == nil {
2021-07-20 23:02:02 +08:00
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`cannot find the related value for attribute name "%s" of with tag "%s"`,
2021-08-27 20:16:29 +08:00
relatedTargetName, parsedTagOutput.With,
)
}
// It automatically retrieves struct field names from current attribute struct/slice.
if structType, err := gstructs.StructType(field.Value); err != nil {
return err
} else {
fieldKeys = structType.FieldKeys()
}
// Recursively with feature checks.
model = m.db.With(field.Value)
if m.withAll {
model = model.WithAll()
} else {
model = model.With(m.withArray...)
}
if parsedTagOutput.Where != "" {
model = model.Where(parsedTagOutput.Where)
}
if parsedTagOutput.Order != "" {
model = model.Order(parsedTagOutput.Order)
}
2021-08-27 20:16:29 +08:00
err = model.Fields(fieldKeys).
Where(relatedSourceName, relatedTargetValue).
ScanList(pointer, fieldName, parsedTagOutput.With)
2021-09-23 19:29:20 +08:00
// It ignores sql.ErrNoRows in with feature.
if err != nil && err != sql.ErrNoRows {
return err
}
}
2021-02-08 17:57:21 +08:00
return nil
}
type parseWithTagInFieldStructOutput struct {
With string
Where string
Order string
}
func (m *Model) parseWithTagInFieldStruct(field gstructs.Field) (output parseWithTagInFieldStructOutput) {
var (
match []string
ormTag = field.Tag(OrmTagForStruct)
)
// with tag.
match, _ = gregex.MatchString(
fmt.Sprintf(`%s\s*:\s*([^,]+),{0,1}`, OrmTagForWith),
ormTag,
)
if len(match) > 1 {
output.With = match[1]
}
if len(match) > 2 {
output.Where = gstr.Trim(match[2])
}
// where string.
match, _ = gregex.MatchString(
fmt.Sprintf(`%s\s*:.+,\s*%s:\s*([^,]+),{0,1}`, OrmTagForWith, OrmTagForWithWhere),
ormTag,
)
if len(match) > 1 {
output.Where = gstr.Trim(match[1])
}
// order string.
match, _ = gregex.MatchString(
fmt.Sprintf(`%s\s*:.+,\s*%s:\s*([^,]+),{0,1}`, OrmTagForWith, OrmTagForWithOrder),
ormTag,
)
if len(match) > 1 {
output.Order = gstr.Trim(match[1])
}
return
}