Better preloads

This commit is contained in:
Hein 2025-11-21 10:41:58 +02:00
parent 7e76977dcc
commit a931b8cdd2
4 changed files with 74 additions and 2 deletions

View File

@ -43,6 +43,11 @@ type PreloadOption struct {
Updatable *bool `json:"updateable"` // if true, the relation can be updated
ComputedQL map[string]string `json:"computed_ql"` // Computed columns as SQL expressions
Recursive bool `json:"recursive"` // if true, preload recursively up to 5 levels
// Relationship keys from XFiles - used to build proper foreign key filters
PrimaryKey string `json:"primary_key"` // Primary key of the related table
RelatedKey string `json:"related_key"` // For child tables: column in child that references parent
ForeignKey string `json:"foreign_key"` // For parent tables: column in current table that references parent
}
type FilterOption struct {

View File

@ -756,11 +756,42 @@ func ConvertToNumericType(value string, kind reflect.Kind) (interface{}, error)
// 2. Bun tag name (if exists)
// 3. Gorm tag name (if exists)
// 4. JSON tag name (if exists)
//
// Supports recursive field paths using dot notation (e.g., "MAL.MAL.DEF")
// For nested fields, it traverses through each level of the struct hierarchy
func GetRelationModel(model interface{}, fieldName string) interface{} {
if model == nil || fieldName == "" {
return nil
}
// Split the field name by "." to handle nested/recursive relations
fieldParts := strings.Split(fieldName, ".")
// Start with the current model
currentModel := model
// Traverse through each level of the field path
for _, part := range fieldParts {
if part == "" {
continue
}
currentModel = getRelationModelSingleLevel(currentModel, part)
if currentModel == nil {
return nil
}
}
return currentModel
}
// getRelationModelSingleLevel gets the model type for a single level field (non-recursive)
// This is a helper function used by GetRelationModel to handle one level at a time
func getRelationModelSingleLevel(model interface{}, fieldName string) interface{} {
if model == nil || fieldName == "" {
return nil
}
modelType := reflect.TypeOf(model)
if modelType == nil {
return nil

View File

@ -560,11 +560,33 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
// applyPreloadWithRecursion applies a preload with support for ComputedQL and recursive preloading
func (h *Handler) applyPreloadWithRecursion(query common.SelectQuery, preload common.PreloadOption, model interface{}, depth int) common.SelectQuery {
// Log relationship keys if they're specified (from XFiles)
if preload.RelatedKey != "" || preload.ForeignKey != "" || preload.PrimaryKey != "" {
logger.Debug("Preload %s has relationship keys - PK: %s, RelatedKey: %s, ForeignKey: %s",
preload.Relation, preload.PrimaryKey, preload.RelatedKey, preload.ForeignKey)
// Build a WHERE clause using the relationship keys if needed
// Note: Bun's PreloadRelation typically handles the relationship join automatically via struct tags
// However, if the relationship keys are explicitly provided from XFiles, we can use them
// to add additional filtering or validation
if preload.RelatedKey != "" && preload.Where == "" {
// For child tables: ensure the child's relatedkey column will be matched
// The actual parent value is dynamic and handled by Bun's preload mechanism
// We just log this for visibility
logger.Debug("Child table %s will be filtered by %s matching parent's primary key",
preload.Relation, preload.RelatedKey)
}
if preload.ForeignKey != "" && preload.Where == "" {
// For parent tables: ensure the parent's primary key matches the current table's foreign key
logger.Debug("Parent table %s will be filtered by primary key matching current table's %s",
preload.Relation, preload.ForeignKey)
}
}
// Apply the preload
query = query.PreloadRelation(preload.Relation, func(sq common.SelectQuery) common.SelectQuery {
// Get the related model for column operations
relationParts := strings.Split(preload.Relation, ",")
relatedModel := reflection.GetRelationModel(model, relationParts[0])
relatedModel := reflection.GetRelationModel(model, preload.Relation)
if relatedModel == nil {
logger.Warn("Could not get related model for preload: %s", preload.Relation)
// relatedModel = model // fallback to parent model

View File

@ -919,6 +919,20 @@ func (h *Handler) addXFilesPreload(xfile *XFiles, options *ExtendedRequestOption
// Set recursive flag
preloadOpt.Recursive = xfile.Recursive
// Extract relationship keys for proper foreign key filtering
if xfile.PrimaryKey != "" {
preloadOpt.PrimaryKey = xfile.PrimaryKey
logger.Debug("X-Files: Set primary key for %s: %s", relationPath, xfile.PrimaryKey)
}
if xfile.RelatedKey != "" {
preloadOpt.RelatedKey = xfile.RelatedKey
logger.Debug("X-Files: Set related key for %s: %s", relationPath, xfile.RelatedKey)
}
if xfile.ForeignKey != "" {
preloadOpt.ForeignKey = xfile.ForeignKey
logger.Debug("X-Files: Set foreign key for %s: %s", relationPath, xfile.ForeignKey)
}
// Add the preload option
options.Preload = append(options.Preload, preloadOpt)