Skip to content

Conversation

@vzaica
Copy link

@vzaica vzaica commented Nov 7, 2025

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

This PR covers #3871 feature request.

What did this pull request do?

This PR adds two new query helper methods — WhereHas and WhereDoesntHave.
These methods make it possible to filter query results based on related models, similar to how this is done in frameworks like Laravel Eloquent.

Problem

Currently, GORM lacks convenient methods for filtering records by the existence (or absence) of related entities.
For example, there is no built-in way to express queries like:

DB.WhereHas("Toys", DB.Where("type = ?", "car")).Find(&users)

or the inverse case using WhereDoesntHave.

As a result, developers must write verbose raw joins or subqueries, which reduces readability and reusability.

Solution

This PR introduces:

  • WhereHas(association string, args ...interface{}).
    Filters parent records that have related models matching the scope.
  • WhereDoesntHave(association string, args ...interface{})
    Filters parent records that do not have related models matching the scope.

These methods apply only to query building (e.g., SELECT) and do not affect update/insert logic.

Implementation notes

  • Implemented as query scopes that inject appropriate subqueries into WHERE EXISTS (...) or WHERE NOT EXISTS (...) clauses.
  • Tested against nested associations and multiple joins.
  • Does not modify core model update/insert logic.

User Case Description

Assume the following model structure:

type User struct {
	gorm.Model
	Name      string
	Pets      []*Pet
	Toys      []Toy   `gorm:"polymorphic:Owner"`
}

type Pet struct {
	gorm.Model
	UserID *uint
	Name   string
	Toy    Toy `gorm:"polymorphic:Owner;"`
}

type Toy struct {
	gorm.Model
	Name      string
	Type      string
	OwnerID   string
	OwnerType string
}

1. Basic WhereHas

Find all users who have pets:

var users []User
DB.WhereHas("Pets").Find(&users)

2. Basic WhereDoesntHave

Find all users who don’t have pets:

var users []User
DB.WhereDoesntHave("Pets").Find(&users)

3. Conditional WhereHas

Find all users who have toys of a specific type:

var users []User
DB.WhereHas("Toys", DB.Where("type = ?", "car")).Find(&users)

4. Nested WhereHas

Find all users who have pets without toys:

var users []User
DB.WhereHas("Pets", DB.WhereDoesntHave("Toy")).Find(&users)

@propel-code-bot
Copy link
Contributor

propel-code-bot bot commented Nov 7, 2025

Add WhereHas / WhereDoesntHave helpers for relationship-based filtering

Introduces first-class support for "exists / not-exists" style filtering on associations. New helper methods (WhereHas, WhereDoesntHave) can be chained like other query builders and internally translate into WHERE EXISTS (subquery) / WHERE NOT EXISTS (subquery) clauses. Implementation required a new callback (callbacks/where_has.go), Statement field plumbing, and integration into the main query builder, plus an extensive test-suite that covers HasOne, HasMany, BelongsTo, Many2Many, and nested use cases.

Key Changes

• Created callbacks/where_has.go – core logic for building EXISTS / NOT EXISTS subqueries for each schema.RelationshipType
• Extended Statement struct with []whereHasCondition and propagated it through cloning, build, and condition methods
• Added chainable API methods WhereHas and WhereDoesntHave in chainable_api.go
• Hooked new logic into callbacks/query.go so conditions are injected before SQL build
• Enhanced BuildCondition to merge nested WhereHas state when sub-DB scopes are used
• Added >150 lines of integration tests in tests/query_where_has_test.go

Affected Areas

callbacks/where_has.go (new)
callbacks/query.go
chainable_api.go
statement.go
• unit/integration tests

This summary was automatically generated by @propel-code-bot

@propel-code-bot propel-code-bot bot changed the title add whereHas/whereDoesntHave methods for Query Add WhereHas and WhereDoesntHave relational filters to query API Nov 7, 2025
Comment on lines +190 to +192
if len(inlineConds) > 0 {
existsQuery = existsQuery.Where(inlineConds[0], inlineConds[1:]...)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[BestPractice]

Array bounds safety issue: applyConds assumes that if inlineConds has length > 0, it can safely access inlineConds[0] and pass the rest as variadic args. However, there's a subtle issue - if inlineConds has only one element, inlineConds[1:]... will be an empty slice, which is fine. But for better readability and explicit handling:

Suggested Change
Suggested change
if len(inlineConds) > 0 {
existsQuery = existsQuery.Where(inlineConds[0], inlineConds[1:]...)
}
if len(inlineConds) > 0 {
if len(inlineConds) == 1 {
existsQuery = existsQuery.Where(inlineConds[0])
} else {
existsQuery = existsQuery.Where(inlineConds[0], inlineConds[1:]...)
}
}

Committable suggestion

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Context for Agents
[**BestPractice**]

Array bounds safety issue: `applyConds` assumes that if `inlineConds` has length > 0, it can safely access `inlineConds[0]` and pass the rest as variadic args. However, there's a subtle issue - if `inlineConds` has only one element, `inlineConds[1:]...` will be an empty slice, which is fine. But for better readability and explicit handling:

<details>
<summary>Suggested Change</summary>

```suggestion
	if len(inlineConds) > 0 {
		if len(inlineConds) == 1 {
			existsQuery = existsQuery.Where(inlineConds[0])
		} else {
			existsQuery = existsQuery.Where(inlineConds[0], inlineConds[1:]...)
		}
	}
```

⚡ **Committable suggestion**

Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

</details>

File: callbacks/where_has.go
Line: 192

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part of the code is inspired by the code in the file callbacks/preload.go (line 288) and this code is correct, does not cause errors, and I think readability is better than the proposed version.

@propel-code-bot propel-code-bot bot changed the title Add WhereHas and WhereDoesntHave relational filters to query API Add WhereHas and WhereDoesntHave helpers for relationship filtering Nov 9, 2025
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.

1 participant