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