github.com/gogf/gf@v1.16.9/database/gdb/gdb_model_with.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gdb
     8  
     9  import (
    10  	"fmt"
    11  	"github.com/gogf/gf/errors/gcode"
    12  	"reflect"
    13  
    14  	"github.com/gogf/gf/errors/gerror"
    15  	"github.com/gogf/gf/internal/structs"
    16  	"github.com/gogf/gf/internal/utils"
    17  	"github.com/gogf/gf/text/gregex"
    18  	"github.com/gogf/gf/text/gstr"
    19  )
    20  
    21  // With creates and returns an ORM model based on metadata of given object.
    22  // It also enables model association operations feature on given `object`.
    23  // It can be called multiple times to add one or more objects to model and enable
    24  // their mode association operations feature.
    25  // For example, if given struct definition:
    26  // type User struct {
    27  //	 gmeta.Meta `orm:"table:user"`
    28  // 	 Id         int           `json:"id"`
    29  //	 Name       string        `json:"name"`
    30  //	 UserDetail *UserDetail   `orm:"with:uid=id"`
    31  //	 UserScores []*UserScores `orm:"with:uid=id"`
    32  // }
    33  // We can enable model association operations on attribute `UserDetail` and `UserScores` by:
    34  //     db.With(User{}.UserDetail).With(User{}.UserDetail).Scan(xxx)
    35  // Or:
    36  //     db.With(UserDetail{}).With(UserDetail{}).Scan(xxx)
    37  // Or:
    38  //     db.With(UserDetail{}, UserDetail{}).Scan(xxx)
    39  func (m *Model) With(objects ...interface{}) *Model {
    40  	model := m.getModel()
    41  	for _, object := range objects {
    42  		if m.tables == "" {
    43  			m.tablesInit = m.db.GetCore().QuotePrefixTableName(
    44  				getTableNameFromOrmTag(object),
    45  			)
    46  			m.tables = m.tablesInit
    47  			return model
    48  		}
    49  		model.withArray = append(model.withArray, object)
    50  	}
    51  	return model
    52  }
    53  
    54  // WithAll enables model association operations on all objects that have "with" tag in the struct.
    55  func (m *Model) WithAll() *Model {
    56  	model := m.getModel()
    57  	model.withAll = true
    58  	return model
    59  }
    60  
    61  // doWithScanStruct handles model association operations feature for single struct.
    62  func (m *Model) doWithScanStruct(pointer interface{}) error {
    63  	var (
    64  		err                 error
    65  		allowedTypeStrArray = make([]string, 0)
    66  	)
    67  	currentStructFieldMap, err := structs.FieldMap(structs.FieldMapInput{
    68  		Pointer:          pointer,
    69  		PriorityTagArray: nil,
    70  		RecursiveOption:  structs.RecursiveOptionEmbeddedNoTag,
    71  	})
    72  	if err != nil {
    73  		return err
    74  	}
    75  	// It checks the with array and automatically calls the ScanList to complete association querying.
    76  	if !m.withAll {
    77  		for _, field := range currentStructFieldMap {
    78  			for _, withItem := range m.withArray {
    79  				withItemReflectValueType, err := structs.StructType(withItem)
    80  				if err != nil {
    81  					return err
    82  				}
    83  				var (
    84  					fieldTypeStr                = gstr.TrimAll(field.Type().String(), "*[]")
    85  					withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]")
    86  				)
    87  				// It does select operation if the field type is in the specified "with" type array.
    88  				if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 {
    89  					allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr)
    90  				}
    91  			}
    92  		}
    93  	}
    94  	for _, field := range currentStructFieldMap {
    95  		var (
    96  			fieldTypeStr    = gstr.TrimAll(field.Type().String(), "*[]")
    97  			parsedTagOutput = m.parseWithTagInFieldStruct(field)
    98  		)
    99  		if parsedTagOutput.With == "" {
   100  			continue
   101  		}
   102  		// It just handlers "with" type attribute struct, so it ignores other struct types.
   103  		if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) {
   104  			continue
   105  		}
   106  		array := gstr.SplitAndTrim(parsedTagOutput.With, "=")
   107  		if len(array) == 1 {
   108  			// It also supports using only one column name
   109  			// if both tables associates using the same column name.
   110  			array = append(array, parsedTagOutput.With)
   111  		}
   112  		var (
   113  			model              *Model
   114  			fieldKeys          []string
   115  			relatedSourceName  = array[0]
   116  			relatedTargetName  = array[1]
   117  			relatedTargetValue interface{}
   118  		)
   119  		// Find the value of related attribute from `pointer`.
   120  		for attributeName, attributeValue := range currentStructFieldMap {
   121  			if utils.EqualFoldWithoutChars(attributeName, relatedTargetName) {
   122  				relatedTargetValue = attributeValue.Value.Interface()
   123  				break
   124  			}
   125  		}
   126  		if relatedTargetValue == nil {
   127  			return gerror.NewCodef(
   128  				gcode.CodeInvalidParameter,
   129  				`cannot find the target related value of name "%s" in with tag "%s" for attribute "%s.%s"`,
   130  				relatedTargetName, parsedTagOutput.With, reflect.TypeOf(pointer).Elem(), field.Name(),
   131  			)
   132  		}
   133  		bindToReflectValue := field.Value
   134  		if bindToReflectValue.Kind() != reflect.Ptr && bindToReflectValue.CanAddr() {
   135  			bindToReflectValue = bindToReflectValue.Addr()
   136  		}
   137  
   138  		// It automatically retrieves struct field names from current attribute struct/slice.
   139  		if structType, err := structs.StructType(field.Value); err != nil {
   140  			return err
   141  		} else {
   142  			fieldKeys = structType.FieldKeys()
   143  		}
   144  
   145  		// Recursively with feature checks.
   146  		model = m.db.With(field.Value)
   147  		if m.withAll {
   148  			model = model.WithAll()
   149  		} else {
   150  			model = model.With(m.withArray...)
   151  		}
   152  		if parsedTagOutput.Where != "" {
   153  			model = model.Where(parsedTagOutput.Where)
   154  		}
   155  		if parsedTagOutput.Order != "" {
   156  			model = model.Order(parsedTagOutput.Order)
   157  		}
   158  
   159  		err = model.Fields(fieldKeys).Where(relatedSourceName, relatedTargetValue).Scan(bindToReflectValue)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  	}
   165  	return nil
   166  }
   167  
   168  // doWithScanStructs handles model association operations feature for struct slice.
   169  // Also see doWithScanStruct.
   170  func (m *Model) doWithScanStructs(pointer interface{}) error {
   171  	if v, ok := pointer.(reflect.Value); ok {
   172  		pointer = v.Interface()
   173  	}
   174  
   175  	var (
   176  		err                 error
   177  		allowedTypeStrArray = make([]string, 0)
   178  	)
   179  	currentStructFieldMap, err := structs.FieldMap(structs.FieldMapInput{
   180  		Pointer:          pointer,
   181  		PriorityTagArray: nil,
   182  		RecursiveOption:  structs.RecursiveOptionEmbeddedNoTag,
   183  	})
   184  	if err != nil {
   185  		return err
   186  	}
   187  	// It checks the with array and automatically calls the ScanList to complete association querying.
   188  	if !m.withAll {
   189  		for _, field := range currentStructFieldMap {
   190  			for _, withItem := range m.withArray {
   191  				withItemReflectValueType, err := structs.StructType(withItem)
   192  				if err != nil {
   193  					return err
   194  				}
   195  				var (
   196  					fieldTypeStr                = gstr.TrimAll(field.Type().String(), "*[]")
   197  					withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]")
   198  				)
   199  				// It does select operation if the field type is in the specified with type array.
   200  				if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 {
   201  					allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr)
   202  				}
   203  			}
   204  		}
   205  	}
   206  
   207  	for fieldName, field := range currentStructFieldMap {
   208  		var (
   209  			fieldTypeStr    = gstr.TrimAll(field.Type().String(), "*[]")
   210  			parsedTagOutput = m.parseWithTagInFieldStruct(field)
   211  		)
   212  		if parsedTagOutput.With == "" {
   213  			continue
   214  		}
   215  		if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) {
   216  			continue
   217  		}
   218  		array := gstr.SplitAndTrim(parsedTagOutput.With, "=")
   219  		if len(array) == 1 {
   220  			// It supports using only one column name
   221  			// if both tables associates using the same column name.
   222  			array = append(array, parsedTagOutput.With)
   223  		}
   224  		var (
   225  			model              *Model
   226  			fieldKeys          []string
   227  			relatedSourceName  = array[0]
   228  			relatedTargetName  = array[1]
   229  			relatedTargetValue interface{}
   230  		)
   231  		// Find the value slice of related attribute from `pointer`.
   232  		for attributeName, _ := range currentStructFieldMap {
   233  			if utils.EqualFoldWithoutChars(attributeName, relatedTargetName) {
   234  				relatedTargetValue = ListItemValuesUnique(pointer, attributeName)
   235  				break
   236  			}
   237  		}
   238  		if relatedTargetValue == nil {
   239  			return gerror.NewCodef(
   240  				gcode.CodeInvalidParameter,
   241  				`cannot find the related value for attribute name "%s" of with tag "%s"`,
   242  				relatedTargetName, parsedTagOutput.With,
   243  			)
   244  		}
   245  
   246  		// It automatically retrieves struct field names from current attribute struct/slice.
   247  		if structType, err := structs.StructType(field.Value); err != nil {
   248  			return err
   249  		} else {
   250  			fieldKeys = structType.FieldKeys()
   251  		}
   252  
   253  		// Recursively with feature checks.
   254  		model = m.db.With(field.Value)
   255  		if m.withAll {
   256  			model = model.WithAll()
   257  		} else {
   258  			model = model.With(m.withArray...)
   259  		}
   260  		if parsedTagOutput.Where != "" {
   261  			model = model.Where(parsedTagOutput.Where)
   262  		}
   263  		if parsedTagOutput.Order != "" {
   264  			model = model.Order(parsedTagOutput.Order)
   265  		}
   266  
   267  		err = model.Fields(fieldKeys).
   268  			Where(relatedSourceName, relatedTargetValue).
   269  			ScanList(pointer, fieldName, parsedTagOutput.With)
   270  		if err != nil {
   271  			return err
   272  		}
   273  	}
   274  	return nil
   275  }
   276  
   277  type parseWithTagInFieldStructOutput struct {
   278  	With  string
   279  	Where string
   280  	Order string
   281  }
   282  
   283  func (m *Model) parseWithTagInFieldStruct(field *structs.Field) (output parseWithTagInFieldStructOutput) {
   284  	var (
   285  		match  []string
   286  		ormTag = field.Tag(OrmTagForStruct)
   287  	)
   288  	// with tag.
   289  	match, _ = gregex.MatchString(
   290  		fmt.Sprintf(`%s\s*:\s*([^,]+),{0,1}`, OrmTagForWith),
   291  		ormTag,
   292  	)
   293  	if len(match) > 1 {
   294  		output.With = match[1]
   295  	}
   296  	if len(match) > 2 {
   297  		output.Where = gstr.Trim(match[2])
   298  	}
   299  	// where string.
   300  	match, _ = gregex.MatchString(
   301  		fmt.Sprintf(`%s\s*:.+,\s*%s:\s*([^,]+),{0,1}`, OrmTagForWith, OrmTagForWithWhere),
   302  		ormTag,
   303  	)
   304  	if len(match) > 1 {
   305  		output.Where = gstr.Trim(match[1])
   306  	}
   307  	// order string.
   308  	match, _ = gregex.MatchString(
   309  		fmt.Sprintf(`%s\s*:.+,\s*%s:\s*([^,]+),{0,1}`, OrmTagForWith, OrmTagForWithOrder),
   310  		ormTag,
   311  	)
   312  	if len(match) > 1 {
   313  		output.Order = gstr.Trim(match[1])
   314  	}
   315  	return
   316  }