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 }