Skip to content

Commit d27d313

Browse files
committed
feat(database): 新增ORM批量操作和预加载功能支持
- 添加OrmTagForChunkSize和OrmTagForBatchMinRows常量定义 - 实现Preload方法用于启用模型关联操作的预加载功能 - 添加parseWithTagInFieldStructOutput结构体支持分块操作相关字段 - 实现分块大小和批量最小行数的解析逻辑 - 新增typeFieldCacheManager类型用于缓存字段索引提升性能 - 添加字段缓存管理器和相关的缓存获取、构建、清理方法
1 parent 91f9864 commit d27d313

File tree

4 files changed

+221
-9
lines changed

4 files changed

+221
-9
lines changed

database/gdb/gdb_func.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ const (
6666
OrmTagForWithOrder = "order"
6767
OrmTagForWithUnscoped = "unscoped"
6868
OrmTagForDo = "do"
69+
OrmTagForChunkSize = "chunkSize"
70+
OrmTagForBatchMinRows = "batchMinRows"
6971
)
7072

7173
var (

database/gdb/gdb_model.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type Model struct {
5656
softTimeOption SoftTimeOption // SoftTimeOption is the option to customize soft time feature for Model.
5757
shardingConfig ShardingConfig // ShardingConfig for database/table sharding feature.
5858
shardingValue any // Sharding value for sharding feature.
59+
preload bool // preload enables batch recursive scanning for association operations (Solving N+1 problem).
5960
}
6061

6162
// ModelHandler is a function that handles given Model and returns a new Model that is custom modified.

database/gdb/gdb_model_with.go

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ package gdb
88

99
import (
1010
"database/sql"
11+
"errors"
1112
"reflect"
1213

1314
"github.com/gogf/gf/v2/errors/gcode"
1415
"github.com/gogf/gf/v2/errors/gerror"
1516
"github.com/gogf/gf/v2/internal/utils"
1617
"github.com/gogf/gf/v2/os/gstructs"
1718
"github.com/gogf/gf/v2/text/gstr"
19+
"github.com/gogf/gf/v2/util/gconv"
1820
"github.com/gogf/gf/v2/util/gutil"
1921
)
2022

@@ -65,6 +67,16 @@ func (m *Model) WithAll() *Model {
6567
return model
6668
}
6769

70+
// Preload enables model association operations on all objects that have "with" tag in the struct.
71+
func (m *Model) Preload(enable ...bool) *Model {
72+
model := m.getModel()
73+
m.preload = true
74+
if len(enable) > 0 {
75+
model.preload = enable[0]
76+
}
77+
return m
78+
}
79+
6880
// doWithScanStruct handles model association operations feature for single struct.
6981
func (m *Model) doWithScanStruct(pointer any) error {
7082
if len(m.withArray) == 0 && !m.withAll {
@@ -306,32 +318,46 @@ func (m *Model) doWithScanStructs(pointer any) error {
306318
Where(relatedSourceName, relatedTargetValue).
307319
ScanList(pointer, fieldName, parsedTagOutput.With)
308320
// It ignores sql.ErrNoRows in with feature.
309-
if err != nil && err != sql.ErrNoRows {
321+
if err != nil && !errors.Is(err, sql.ErrNoRows) {
310322
return err
311323
}
312324
}
313325
return nil
314326
}
315327

316328
type parseWithTagInFieldStructOutput struct {
317-
With string
318-
Where string
319-
Order string
320-
Unscoped string
329+
With string
330+
Where string
331+
Order string
332+
Unscoped string
333+
Chunked bool
334+
ChunkSize int
335+
BatchMinRows int
321336
}
322337

323338
func (m *Model) parseWithTagInFieldStruct(field gstructs.Field) (output parseWithTagInFieldStructOutput) {
324339
var (
325-
ormTag = field.Tag(OrmTagForStruct)
326-
data = make(map[string]string)
327-
array []string
328-
key string
340+
ormTag = field.Tag(OrmTagForStruct)
341+
data = make(map[string]string)
342+
array []string
343+
key string
344+
ifChunkSize bool
345+
ifBatchMinRows bool
329346
)
330347
for _, v := range gstr.SplitAndTrim(ormTag, ",") {
348+
v = gstr.Trim(v)
349+
if v == "" {
350+
continue
351+
}
331352
array = gstr.Split(v, ":")
332353
if len(array) == 2 {
333354
key = array[0]
334355
data[key] = gstr.Trim(array[1])
356+
if key == OrmTagForChunkSize {
357+
ifChunkSize = true
358+
} else if key == OrmTagForBatchMinRows {
359+
ifBatchMinRows = true
360+
}
335361
} else {
336362
if key == OrmTagForWithOrder {
337363
// supporting multiple order fields
@@ -345,5 +371,10 @@ func (m *Model) parseWithTagInFieldStruct(field gstructs.Field) (output parseWit
345371
output.Where = data[OrmTagForWithWhere]
346372
output.Order = data[OrmTagForWithOrder]
347373
output.Unscoped = data[OrmTagForWithUnscoped]
374+
output.Chunked = ifChunkSize && ifBatchMinRows
375+
if output.Chunked {
376+
output.ChunkSize = gconv.Int(data[OrmTagForChunkSize])
377+
output.BatchMinRows = gconv.Int(data[OrmTagForBatchMinRows])
378+
}
348379
return
349380
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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+
"reflect"
12+
"strings"
13+
14+
"github.com/gogf/gf/v2/container/gmap"
15+
"github.com/gogf/gf/v2/container/gtype"
16+
)
17+
18+
var typeFieldCacheChecker = func(v *fieldCacheItem) bool { return v == nil }
19+
20+
type typeFieldCacheManager struct {
21+
cache *gmap.KVMap[typeFieldCacheKey, *fieldCacheItem]
22+
}
23+
24+
type typeFieldCacheKey struct {
25+
typ reflect.Type
26+
bindToAttrName string
27+
relationAttrName string
28+
}
29+
30+
func newTypeFieldCacheManager() *typeFieldCacheManager {
31+
return &typeFieldCacheManager{
32+
cache: gmap.NewKVMapWithChecker[typeFieldCacheKey, *fieldCacheItem](typeFieldCacheChecker, true),
33+
}
34+
}
35+
36+
var fieldCacheInstance = newTypeFieldCacheManager()
37+
38+
type fieldCacheItem struct {
39+
// Deterministic field index (can be safely cached)
40+
bindToAttrIndex int // Field index of bound attribute (e.g. UserDetail), -1 for embedded fields
41+
bindToAttrIndexPath []int // Full index path for embedded fields (e.g. []int{1, 2})
42+
relationAttrIndex int // Field index of relation attribute (e.g. User, -1 means none)
43+
relationAttrIndexPath []int // Full index path for embedded relation attribute
44+
isPointerElem bool // Whether array element is pointer type
45+
bindToAttrKind reflect.Kind // Type of bound attribute
46+
47+
// Field name mapping (supports case-insensitive lookup)
48+
fieldNameMap map[string]string // lowercase -> OriginalName
49+
fieldIndexMap map[string]int // FieldName -> Index
50+
}
51+
52+
func (m *typeFieldCacheManager) getOrSet(
53+
arrayItemType reflect.Type,
54+
bindToAttrName string,
55+
relationAttrName string,
56+
) (*fieldCacheItem, error) {
57+
e := gtype.New(nil, true)
58+
cacheKey := typeFieldCacheKey{
59+
typ: arrayItemType,
60+
bindToAttrName: bindToAttrName,
61+
relationAttrName: relationAttrName,
62+
}
63+
v := m.cache.GetOrSetFuncLock(cacheKey, func() *fieldCacheItem {
64+
item, err := m.buildCacheItem(arrayItemType, bindToAttrName, relationAttrName)
65+
if err != nil {
66+
e.Set(err)
67+
return nil
68+
}
69+
return item
70+
})
71+
if e.Val() != nil {
72+
return nil, e.Val().(error)
73+
}
74+
return v, nil
75+
}
76+
77+
func (m *typeFieldCacheManager) buildCacheItem(
78+
arrayItemType reflect.Type,
79+
bindToAttrName string,
80+
relationAttrName string,
81+
) (*fieldCacheItem, error) {
82+
structType := arrayItemType
83+
isPointerElem := false
84+
if structType.Kind() == reflect.Pointer {
85+
structType = structType.Elem()
86+
isPointerElem = true
87+
}
88+
89+
if structType.Kind() != reflect.Struct {
90+
return nil, fmt.Errorf("arrayItemType must be struct or pointer to struct, got: %s", arrayItemType.Kind())
91+
}
92+
93+
numField := structType.NumField()
94+
cache := &fieldCacheItem{
95+
relationAttrIndex: -1,
96+
isPointerElem: isPointerElem,
97+
fieldNameMap: make(map[string]string, numField),
98+
fieldIndexMap: make(map[string]int, numField),
99+
}
100+
101+
// Iterate all fields, build field mapping
102+
for i := 0; i < numField; i++ {
103+
field := structType.Field(i)
104+
fieldName := field.Name
105+
106+
cache.fieldIndexMap[fieldName] = i
107+
cache.fieldNameMap[strings.ToLower(fieldName)] = fieldName
108+
}
109+
110+
// Find bindToAttrName field index
111+
cache.bindToAttrIndex, cache.bindToAttrIndexPath = m.findFieldIndex(structType, cache.fieldIndexMap, cache.fieldNameMap, bindToAttrName)
112+
if cache.bindToAttrIndex < 0 && len(cache.bindToAttrIndexPath) == 0 {
113+
return nil, fmt.Errorf(`field "%s" not found in type %s`, bindToAttrName, arrayItemType.String())
114+
}
115+
116+
// Set bindToAttrKind based on the found field
117+
var bindToField reflect.StructField
118+
if cache.bindToAttrIndex >= 0 {
119+
bindToField = structType.Field(cache.bindToAttrIndex)
120+
} else {
121+
bindToField = structType.FieldByIndex(cache.bindToAttrIndexPath)
122+
}
123+
cache.bindToAttrKind = bindToField.Type.Kind()
124+
125+
// Find relationAttrName field index (optional)
126+
if relationAttrName != "" {
127+
cache.relationAttrIndex, cache.relationAttrIndexPath = m.findFieldIndex(structType, cache.fieldIndexMap, cache.fieldNameMap, relationAttrName)
128+
// Note: if still not found, keep -1, indicating that arrayElemValue itself should be used
129+
}
130+
131+
return cache, nil
132+
}
133+
134+
// findFieldIndex finds the field index or index path for a given field name
135+
// Returns: index (>=0 for direct field, -1 for embedded field), indexPath (nil for direct field, actual path for embedded field)
136+
func (m *typeFieldCacheManager) findFieldIndex(
137+
structType reflect.Type,
138+
fieldIndexMap map[string]int,
139+
fieldNameMap map[string]string,
140+
fieldName string,
141+
) (int, []int) {
142+
// First, try direct lookup
143+
if idx, ok := fieldIndexMap[fieldName]; ok {
144+
return idx, nil
145+
}
146+
147+
// Second, try case-insensitive lookup
148+
lowerFieldName := strings.ToLower(fieldName)
149+
if originalName, ok := fieldNameMap[lowerFieldName]; ok {
150+
return fieldIndexMap[originalName], nil
151+
}
152+
153+
// Third, try using FieldByName for embedded fields
154+
field, ok := structType.FieldByName(fieldName)
155+
if !ok {
156+
return -1, nil // Not found
157+
}
158+
159+
// For embedded fields, field.Index contains the full path
160+
if len(field.Index) == 1 {
161+
// Direct field (shouldn't normally happen here since we already checked fieldIndexMap)
162+
return field.Index[0], nil
163+
} else {
164+
// Embedded field - return the full index path
165+
return -1, field.Index
166+
}
167+
}
168+
169+
// clear clears all cache (used for testing or hot updates)
170+
func (m *typeFieldCacheManager) clear() {
171+
m.cache.Clear()
172+
}
173+
174+
// ClearFieldCache clears field cache (for external calls)
175+
// Used for testing or application hot update scenarios
176+
func ClearFieldCache() {
177+
fieldCacheInstance.clear()
178+
}

0 commit comments

Comments
 (0)