feat: add query relation#1463
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #1463 +/- ##
==========================================
- Coverage 69.19% 68.51% -0.69%
==========================================
Files 370 385 +15
Lines 29338 32519 +3181
==========================================
+ Hits 20300 22279 +1979
- Misses 8106 9156 +1050
- Partials 932 1084 +152 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Introduce contracts and regenerate mocks for the unified relation rework: Relation/RelationWriter/PivotQuery, ModelWithRelations, RelationCallback/MorphRelationCallback/PivotCallback. Add the corresponding OrmRelation* / OrmEagerLoad* errors and remove the obsolete Association contract. (cherry picked from commit 1a39fa5)
Introduce a morph map under database/orm/morphmap to register and resolve polymorphic class aliases, plus the contracts/database/orm/morph.go and database/orm/morph.go entry points used by relation queries to translate between *_type column values and registered model types. (cherry picked from commit 06b7cb8)
Replace the old Preload-based With() pipeline with an eager-load engine that parses dotted paths, batches per-relation queries, and hydrates results back onto the parent model. Add OfMany / LatestOfMany / OldestOfMany aggregate selectors usable inside With() callbacks. Update Cursor() to run eager loads per-row and Load() to forward variadic args to With(). (cherry picked from commit 65dedf6)
Add the read-side relation engine: relation.go resolves Relations() definitions into typed Has-One/Has-Many/Belongs-To/MorphMany/etc. relations with shared key inference; new_relation.go exposes the Related() helper for ad-hoc relation queries; queries_relationships.go provides Has, OrHas, DoesntHave, HasMorph and WhereRelation existence/absence builders driven by Conditions.relations. (cherry picked from commit 611d2ca)
Add the relation write API (Save/Push/Associate/Dissociate/Attach/Detach/Sync/ Toggle) implemented over the unified relation resolver, plus a PivotQuery builder for filtering and updating pivot rows including pivot-defined created_at/updated_at columns. Introduce contracts.db.SyncResult to report attached/detached/updated ids per Sync call. (cherry picked from commit ee0fcde)
…ework Add tests covering With/Has/HasMorph/Load/PivotQuery/Sync flows against the real database drivers, refresh tests/mock_config.go and tests go.mod/go.sum for the new dependencies, and document the many-to-many API in docs/orm-many-to-many.md. (cherry picked from commit 185c0a8)
(cherry picked from commit 9f6456e)
f5a4bed to
da8db93
Compare
|
Hey @LinboLen Is the PR ready to be reviewed, please? And FYI, CI failed. |
|
is tested pass on my local dev environment. currently is finished state. I will fix the ci failed. this pr not use gorm associate any more. use |
Remove GORM relation tags from test model struct fields and implement Relations() methods instead. This aligns with the new relation system that forbids GORM tags and requires explicit relation declarations. Changes: - Add `gorm:"-"` tags to all relation fields - Implement Relations() for User, Role, Address, and Book models - Use contractsorm relation types (HasOne, HasMany, BelongsTo, Many2Many, MorphOne, MorphMany) Fixes CI test failures in TestWithSuite.
|
fixed all test case. and removed association code in query. the codecov is low. adding more test case |
a2128fa to
c01e6cf
Compare
c01e6cf to
eda8bab
Compare
|
some of api are real database read and write. already covered in another test case. so the codecov is down for now and can't add this coverage test |
|
So sorry, I missed the messages. I will review the PR. |
There was a problem hiding this comment.
Pull request overview
This PR adds Goravel’s metadata-driven query relation system and custom eager loading support, replacing GORM association/preload behavior with framework-managed relation declarations and loaders.
Changes:
- Adds relation read/write APIs, relation descriptors, eager-load parsing/loading, morph map support, pivot query support, and one-of-many helpers.
- Updates ORM contracts, mocks, errors, and test models to use declarative
Relations(). - Adds extensive unit/integration tests for relation resolution, SQL generation, eager loading, relation writers, morph maps, and
Withbehavior.
Reviewed changes
Copilot reviewed 57 out of 59 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/with_test.go | Adds integration coverage for With, Without, WithOnly, nested eager loads, callbacks, columns, and chunking. |
| tests/models.go | Migrates test models to declarative Relations() and ignores relation fields for GORM. |
| tests/mock_config.go | Adds mock config support for eager-load chunk size. |
| tests/go.mod | Bumps indirect dst dependency. |
| tests/go.sum | Updates checksum entries for dst. |
| testing/mock/mock.go | Removes obsolete ORM association mock factory. |
| mocks/database/schema/ColumnType.go | Removes generated mock no longer needed. |
| mocks/database/orm/RelationCallback.go | Adds generated mock for relation callbacks. |
| mocks/database/orm/Relation.go | Adds generated mock for relation contracts. |
| mocks/database/orm/PivotQuery.go | Adds generated mock for pivot query callbacks. |
| mocks/database/orm/PivotCallback.go | Adds generated mock for pivot callbacks. |
| mocks/database/orm/Orm.go | Regenerates ORM mock with Related and Relation. |
| mocks/database/orm/MorphRelationCallback.go | Adds generated mock for morph relation callbacks. |
| mocks/database/orm/ModelWithRelations.go | Adds generated mock for relation-declaring models. |
| mocks/database/orm/ModelWithMorphClass.go | Adds generated mock for morph-class models. |
| mocks/database/orm/Association.go | Removes obsolete association mock. |
| errors/list.go | Adds centralized ORM relation/eager-load/morph errors. |
| database/orm/orm.go | Adds facade-level Related and Relation methods. |
| database/orm/morphmap/morphmap.go | Adds process-wide morph alias registry. |
| database/orm/morphmap/morphmap_test.go | Tests morph map registration and lookup behavior. |
| database/orm/morph.go | Adds public morph map helper functions. |
| database/orm/model.go | Removes exported GORM association constant. |
| database/gorm/row.go | Applies eager loads after row scanning. |
| database/gorm/row_test.go | Adds row error behavior tests. |
| database/gorm/relation_writer.go | Adds relation writer delegating to relation write operations. |
| database/gorm/relation_writer_test.go | Tests relation writer binding and delegation paths. |
| database/gorm/relation_test.go | Tests relation descriptor resolution and forbidden tag handling. |
| database/gorm/relation_sql_test.go | Pins generated SQL for relation queries and subqueries. |
| database/gorm/relation_sql_capture_test.go | Adds opt-in SQL capture helper test. |
| database/gorm/query.go | Wires relation query APIs, eager loads, aggregates, and removes association-specific behavior. |
| database/gorm/queries_relationships_test.go | Tests relationship query queueing and helper parsing. |
| database/gorm/pivot_query.go | Adds pivot query callback implementation. |
| database/gorm/pivot_query_test.go | Tests pivot query SQL and descriptor wiring. |
| database/gorm/one_of_many.go | Adds one-of-many eager-load query rewrite. |
| database/gorm/one_of_many_test.go | Tests one-of-many joins and shortcut APIs. |
| database/gorm/new_relation.go | Adds Related query builders for each relation kind. |
| database/gorm/new_relation_test.go | Tests Related query behavior and morph resolution. |
| database/gorm/event_extra_test.go | Adds supplemental event behavior tests. |
| database/gorm/eager_loader_test.go | Tests eager-load helpers, assignment, and pivot hydration helpers. |
| database/gorm/eager_load_parse.go | Adds parser for With argument shapes. |
| database/gorm/eager_load_parse_test.go | Tests eager-load argument parsing and chunking helpers. |
| database/gorm/conditions.go | Extends query conditions with relation/eager-load state. |
| database/gorm/build_relations_test.go | Tests building relation existence and aggregate subqueries. |
| contracts/database/orm/relation_writer.go | Adds relation write-side contract. |
| contracts/database/orm/relation_test.go | Tests relation kind constants and implementations. |
| contracts/database/orm/orm.go | Extends ORM/query contracts for relation APIs and eager-load shapes. |
| contracts/database/orm/morph.go | Adds morph-class contract. |
| contracts/database/db/db.go | Adds sync result contract for pivot operations. |
| return r.freshSession(). | ||
| Table(desc.pivotTable). | ||
| Select(selectArgs[0], selectArgs[1:]...). | ||
| Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(pivotParentCol)), chunk) |
| // Build pivot data map: key = relatedID, value = map of pivot column values to hydrate. | ||
| var pivotDataByRelatedID map[string]map[string]any | ||
| if pivotPlan != nil { | ||
| pivotDataByRelatedID = make(map[string]map[string]any, len(pivotRows)) |
| return r.freshSession(). | ||
| Table(desc.pivotTable). | ||
| Select(selectArgs[0], selectArgs[1:]...). | ||
| Where(fmt.Sprintf("%s.%s IN ?", quoteIdent(desc.pivotTable), quoteIdent(pivotParentCol)), chunk). | ||
| Where(fmt.Sprintf("%s.%s = ?", quoteIdent(desc.pivotTable), quoteIdent(desc.morphTypeColumn)), desc.morphValue) |
| // Build pivot data map: key = relatedID, value = map of pivot column values to hydrate. | ||
| var pivotDataByRelatedID map[string]map[string]any | ||
| if pivotPlan != nil { | ||
| pivotDataByRelatedID = make(map[string]map[string]any, len(pivotRows)) | ||
| for _, p := range pivotRows { |
| v := r.config.GetInt("database.eager_load_chunk_size", defaultEagerLoadChunkSize) | ||
| if v == 0 { | ||
| return defaultEagerLoadChunkSize | ||
| } | ||
| return v |
hwbrzzl
left a comment
There was a problem hiding this comment.
Thanks for the amazing PR. But it's so big that it is hard to be reviewed. I'm not sure how much the core logic is. Is it possible to split it into several PRs via AI? I just reviewed the contracts and tests for now, I left some questions. I'm glad to have a deep discussion on them with you.
|
|
||
| type Query interface { | ||
| // Association gets an association instance by name. | ||
| Association(association string) Association |
There was a problem hiding this comment.
Will Relation replace this function? It's a break change.
There was a problem hiding this comment.
there are no need Association any more.
There was a problem hiding this comment.
Can both of them be kept for now? We don't want to have such a break change in the next major version.
| return &UserFactory{} | ||
| } | ||
|
|
||
| func (r *User) Relations() map[string]contractsorm.Relation { |
There was a problem hiding this comment.
Is this function required in the new implementation?
There was a problem hiding this comment.
yes it's required for define relation. no need gorm relation annotation
| House *House `gorm:"polymorphic:Houseable"` | ||
| Phones []*Phone `gorm:"polymorphic:Phoneable"` | ||
| Roles []*Role `gorm:"many2many:role_user"` | ||
| Address *Address `gorm:"-"` |
There was a problem hiding this comment.
Is gorm:"-" required when defining the relation?
There was a problem hiding this comment.
Yes, gorm:"-" IS required when defining relations in the new system.
- The new relation system uses Relations() method - Relations are now defined programmatically via the Relations() method that returns a map of relation definitions, not
through struct tags. - Without gorm:"-", GORM will try to persist these fields - If you don't add gorm:"-", GORM will treat relation fields (like Books []*Book, Address *Address) as regular
database columns and try to:
- Create columns for them in migrations
- Serialize/deserialize them during queries
- This will cause errors since these are complex types that can't be stored directly - The tag tells GORM to ignore these fields - The gorm:"-" tag instructs GORM to skip these fields entirely during its normal operations. The framework's relation system
then handles loading these fields separately through the Relations() definitions. - This is a breaking change from the old system - Previously, you'd use tags like:
- gorm:"polymorphic:Houseable"
- gorm:"many2many:role_user"
Now you use:
- gorm:"-" on the struct field
- Define the relation in Relations() method
Example from the codebase:
type User struct {
Model
Name string
Address *Address gorm:"-" // ← Required!
Books []*Book gorm:"-" // ← Required!
}
func (r *User) Relations() map[string]contractsorm.Relation {
return map[string]contractsorm.Relation{
"Address": contractsorm.HasOne{Related: &Address{}},
"Books": contractsorm.HasMany{Related: &Book{}},
}
}
There was a problem hiding this comment.
Yes, I got it. but I think it's a bit redundant when defining a relation. Can the GORM tag be removed directly, or use another new tag to replace it? For example, The relations are defined in the model currently. Can we ignore the tag based on the relation names?
| for driver, query := range s.queries { | ||
| s.Run(driver, func() { | ||
| user := &User{Name: "rel_bt_user"} | ||
| s.Nil(query.Query().Create(&user)) |
There was a problem hiding this comment.
&user == &&User{Name: "rel_bt_user"}, is it expected?
| ) | ||
| } | ||
|
|
||
| func (s *QueriesRelationshipsTestSuite) TestSQL_OrHas() { |
There was a problem hiding this comment.
Could you add some specific tests for Or** functions?
| Address: &Address{ | ||
| Name: "association_find_address", | ||
| }, |
There was a problem hiding this comment.
Can't user create the relation like this when using the new logic? It's a break change.
There was a problem hiding this comment.
yes. implement with laravel style. not gorm style
There was a problem hiding this comment.
Em, most of users are using this feature. The impact is enormous. We should find a solution to adapt it.
The OR logic means all users should match: - alice: doesn't have target_role (matches 2nd condition) - bob: doesn't have target_book (matches 1st condition) - carol: doesn't have either (matches both conditions)
|
建议不要拆分,理由:
|
Summary
Has/WhereHas, morph existence checks, aggregate subselects, and framework-managed eager loading across direct, nested, polymorphic, many-to-many, and through relations.Relation(...)andRelated(...)ORM builders so applications can read and write relationships through a typed API, including association writes, pivot-table operations,OfManyhelpers, and morph map resolution.Relations()model contract.Closes #963
Why
This PR fills in the missing ORM relationship workflow that users expect from Laravel-style APIs: declaring relations in one place, querying parents by related records, eager loading nested relations, and mutating associations without dropping down to raw GORM behavior. The new contracts make relationship behavior explicit at the framework layer instead of relying on scattered struct-tag conventions and custom query code.
The broader relation rework also enables relationship writers and polymorphic helpers to share the same metadata pipeline, which is why this branch includes eager-loader internals, pivot query support, morph maps, and new integration tests. That combination is what makes features like
WhereHas,HasMorph,With("Books.Author"),Relation(parent, "Roles"), and through-relation queries behave consistently across drivers.