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 }