Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add public BuildCondition method for associations #7008

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

greencoda
Copy link

@greencoda greencoda commented May 8, 2024

  • Do only one thing
  • Non breaking API changes
  • Tested

What did this pull request do?

This PR proposes to add a public variant of the buildCondition() method, as it would allow dynamic query creation, for cases when the user would like do additional filtering, ordering or limiting before calling Find() or Count() on the transaction statement.

User Case Description

I'm creating a connector between GORM and GraphQL, where associated fields on a model are returned as the result of a pagination function, which allows the user to apply ORDER, LIMIT and WHERE conditions before returning a page of associated records.

As the association struct does not allow for setting these before calling the Find() and Count() methods, having access to this private buildCondition() method is the next best way to be able to construct this query.

@jinzhu
Copy link
Member

jinzhu commented Jun 17, 2024

Hello @greencoda,

You should be able to call db.Order("xxx").Association("XXX").Find(&xxx), or perhaps you could implement Scopes support for Association mode?

@greencoda
Copy link
Author

Hi @jinzhu

My usecase is in building a paged result set for GraphQL resolver, and I need to run the Count() method both before and after the filtering conditions are applied:

func findAssociatedModels[M any](ctx context.Context, resolver *Resolver, obj any, associatedObj M, associationColumn string, objPath string, first *int, after *int, filterParams []filterParam, sortParams []sortParam) (M, int, int, error) {
	var (
		total       int64
		filtered    int64
		association = resolver.db.Model(&obj).Association(associationColumn)
	)

	if association.Error != nil {
		return *new(M), 0, 0, association.Error
	}

	stmt := buildCondition(association)

	err := stmt.Count(&total).Error
	if err != nil {
		return *new(M), 0, 0, err
	}

	stmt, appliedFilterCount := applyFiltering(stmt, filterParams)
	if appliedFilterCount > 0 {
		err = stmt.Count(&filtered).Error
		if err != nil {
			return *new(M), 0, 0, err
		}
	} else {
		filtered = total
	}

	if first != nil {
		stmt = stmt.Limit(*first)
	}

	if after != nil {
		stmt = stmt.Offset(*after)
	}

	for _, sort := range sortParams {
		stmt = stmt.Order(sort.field + " " + formatSortDirection(sort.direction))
	}

	for _, preload := range resolver.preloadableFieldCollector.CollectPreloads(ctx, objPath) {
		stmt = stmt.Preload(preload)
	}

	err = stmt.Find(&associatedObj).Error
	if err != nil {
		return *new(M), 0, 0, err
	}

	return associatedObj, int(total), int(filtered), nil
}

This is my whole method, where the buildCondition function is copied from GORM's code as-is, since it's not exposed:

func buildCondition(association *gorm.Association) *gorm.DB {
	var (
		queryConds = association.Relationship.ToQueryConditions(association.DB.Statement.Context, association.DB.Statement.ReflectValue)
		modelValue = reflect.New(association.Relationship.FieldSchema.ModelType).Interface()
		tx         = association.DB.Model(modelValue)
	)

	if association.Relationship.JoinTable != nil {
		if !tx.Statement.Unscoped && len(association.Relationship.JoinTable.QueryClauses) > 0 {
			joinStmt := gorm.Statement{DB: tx, Context: tx.Statement.Context, Schema: association.Relationship.JoinTable, Table: association.Relationship.JoinTable.Table, Clauses: map[string]clause.Clause{}}
			for _, queryClause := range association.Relationship.JoinTable.QueryClauses {
				joinStmt.AddClause(queryClause)
			}
			joinStmt.Build("WHERE")
			if len(joinStmt.SQL.String()) > 0 {
				tx.Clauses(clause.Expr{SQL: strings.Replace(joinStmt.SQL.String(), "WHERE ", "", 1), Vars: joinStmt.Vars})
			}
		}

		tx = tx.Session(&gorm.Session{QueryFields: true}).Clauses(clause.From{Joins: []clause.Join{{
			Table: clause.Table{Name: association.Relationship.JoinTable.Table},
			ON:    clause.Where{Exprs: queryConds},
		}}})
	} else {
		tx.Clauses(clause.Where{Exprs: queryConds})
	}

	return tx
}

It would be nicer if we didn't have to maintain a copy of this code, and we could use the one that's already implemented in GORM's code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants