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 }