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