Repository navigation
Hibernate 7: native domain binding through Hibernate's annotation binder - #16568
borinquenkid wants to merge 174 commits into
Conversation
Converts the 24 Java sources of the spring-orm subproject (the Hibernate 7 copies of Spring's HibernateTemplate, HibernateTransactionManager, LocalSessionFactoryBean/Builder, SessionFactoryUtils, SpringBeanContainer, the exception translators, OpenSessionInViewInterceptor and the TransactionResources pair) to statically compiled Groovy under src/main/groovy, and adds the Groovy plugin, the Groovy dependency and the code-style plugin to its build so the new sources are compiled and Checkstyle/CodeNarc-checked like every other framework module. Maintainers asked in #16351 for the Java-to-Groovy migration to land one Gradle project at a time. This is the first slice: it transplants the end state of the 9.0.x-java-to-groovy branch for this project (original commits 2416bdd..0c2e2c2) onto 9.0.x unchanged, so that the native domain binding work can be rebuilt on top of Groovy sources. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Converts the 207 Java main sources of grails-data-hibernate7-core (HibernateDatastore, GrailsHibernateTemplate, the criteria builder, the query package, the cfg/domainbinding subtree with its binders, generators and second passes, the connections, proxy, access, support and exceptions packages) to statically compiled Groovy, and adds two Spock specs written while reviewing the conversion. The behaviour is unchanged: the module's own 3657-test suite passes on the converted sources, and the review of the original branch caught and fixed the Groovy semantics that differ from Java (reference identity in equals(), Optional.map null-collapse, closure spread, isX/getX accessor ambiguity). Maintainers asked in #16351 for the Java-to-Groovy migration to land one Gradle project at a time. This transplants the end state of the 9.0.x-java-to-groovy branch for this project (original commits 98c59ed..25dde13) onto 9.0.x unchanged. It depends on the Groovy spring-orm subproject committed just before it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Converts the 35 Java main sources, 19 Java tests and 29 Java test fixtures of the dbmigration-core subproject (the liquibase-hibernate fork: the HibernateDatabase family, the snapshot and diff generators, the custom metadata factory and the harness) to statically compiled Groovy. The HibernateDatabase classes are annotated with @pojo as well as @CompileStatic, because their public getProperty(String) API would otherwise become the GroovyObject property hook and hijack dynamic property access in the dbmigration plugin's Spock specs. Lombok is dropped from the build, since the fixtures no longer use it, and jboss-logging is added as compileOnly because Groovydoc resolves the documented Hibernate types with Class.forName and hibernate-core exposes it at runtime only. The RAT exclusions in gradle/rat-root-config.gradle that name the forked third-party files are repointed from the old .java paths to the new .groovy paths, so the files stay excluded and the rat task stays green. Maintainers asked in #16351 for the Java-to-Groovy migration to land one Gradle project at a time. This transplants the end state of the 9.0.x-java-to-groovy branch for this project (original commits 44f4279..e2ba1e6) onto 9.0.x unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…o Groovy Converts the 5 Java sources of the Hibernate 7 Grails plugin subproject (HibernatePersistenceContextInterceptor, GrailsOpenSessionInViewInterceptor, AggregatePersistenceContextInterceptor and its abstract multi-data-source base, SessionFactoryAwarePersistenceContextInterceptor) to statically compiled Groovy. With this commit no Java source is left under grails-data-hibernate7; the dbmigration, boot-plugin and docs subprojects were already pure Groovy and are unchanged. Maintainers asked in #16351 for the Java-to-Groovy migration to land one Gradle project at a time. This transplants the end state of the 9.0.x-java-to-groovy branch for this project (original commits ee295ea..06fb6a1) onto 9.0.x unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
IdentityEnumTypeSpec gains the IdentityEnumType UserType contract features (adapted to the position-based nullSafeGet/nullSafeSet and getSqlType API, plus an integer-id case), GroovyProxyInterceptorLogicSpec gains the missing isInitialized feature, and DatabaseMultiTenantServiceRoutingSpec and MultiTenantFinderDispatchSpec are added. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Hibernate 7 copy was @ignore'd and weakened to assert only A.count() == 1 because of a NullPointerException tracked in issue 10604. That failure no longer occurs, and the original assertions (an exception is thrown, the entity has errors, and field1 reports a field error) pass on Hibernate 7. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nyToManySpec The Hibernate 7 copy built its entities with plain constructors instead of saving the department through the @CurrentTenant data service as the Hibernate 5 original does. Restore the service path; it passes on Hibernate 7. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…he default The mapWith guard joined its two comparisons with OR, which is true for any non-null value, so a class declaring mapWith = 'hibernate' (or the default strategy) skipped the transform along with classes mapped by other datastores. Use AND so only a different strategy skips it, and cover both accepted values. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rnate 7 NullValueEqualSpec skipped its 'not equal' feature on Hibernate 7 although it passes, and PagedResultSpec was skipped on Hibernate 7 only because it asserted the exact class name PagedResultList while Hibernate subtypes it. Lift the first skip and make the second assertion match any *PagedResultList; the exact HibernatePagedResultList type stays covered by HibernateGormStaticApiSpec. The two Hibernate 7 forks that existed to work around those gates (NullValueEqualSpec and PagedResultSpecHibernate) were verbatim copies and are removed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Behaviour specs cannot see a renamed column or table because the same binder writes and reads it. Record the H2 tables, columns, keys and indexes for a small domain (hasMany, belongsTo, enum, single-table inheritance, element collection, mapping DSL), plus each entity's Hibernate contributor and bound data source, so a binder refactor that moves any of them fails a test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ty list GrailsDomainBinder ignored the MetadataBuildingContext Hibernate hands to an AdditionalMappingContributor and built its own MetadataBuildingContextRootImpl from internal classes. Use the supplied context instead. The binder also pulled its entities from the mapping context itself, twice, and HibernateMappingContext.getHibernatePersistentEntities(dataSourceName) set the data source on every entity as a side effect of a getter. The getter is now a pure filter; HibernateMappingContextConfiguration computes the entity list once, after the additional classes have been added, sets the data source on each, and passes the list to the binder. The unused three-argument constructor is removed and the test-only re-run takes an explicit entity list. Entity tagging and the physical schema are unchanged (MappingSnapshotSpec). AdditionalClassBindingSpec covers entities registered through annotatedClasses, which only exist once the session factory is being built. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The merged default discriminator column is named class instead of DTYPE (#16471). Regenerate the snapshot; nothing else in it changes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ated JPA class Generate, with ByteBuddy, a class whose fields carry the annotations Hibernate's annotation binder understands, from an already-built GrailsHibernatePersistentEntity: @entity and @table on the class, @id on the identifier, and @column on each basic property. The field set is exactly the entity's persistent properties. Column facets are not re-derived: the generator runs the constraint binders the domain binder uses on a scratch Column and reads the result back. Properties that map several columns are rejected with a clear message until they are supported. The generator is not wired into the domain binder yet. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…est domain class Add a differential spec that scans the TCK and Hibernate 7 test domain classes, groups them by association so each group boots as its own datastore, and checks that the column facets the generator decides equal what the binder bound on the Hibernate Column and Property. 1237 properties on 696 entities match; groups that cannot boot alone (deliberately invalid mappings, multi-datasource entities) and properties the generator does not support yet are reported, not hidden. The comparison found four things the generator now handles: - a table-per-hierarchy subclass has no identifier column of its own, so a non-root entity is rejected until inheritance is supported; - an identifier column is never nullable; - Bean Validation constraints (@notblank, @digits, @SiZe) become DDL when Hibernate applies them after binding, so they are copied onto the generated field instead of being mirrored; - a backtick-quoted column name stays quoted. The generator now exposes supports() and columnFacets(), and rejects a property kind it does not support by name instead of describing it wrongly. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ts in GrailsDomainGenerator The generator now emits @Version for the version property, @Enumerated or @type(IdentityEnumType) for enums following the enum binder's own rules, @ColumnDefault, @ColumnTransformer and @comment for the column extras, and @DynamicInsert, @DynamicUpdate, @batchsize and a table @comment at class level. Class-level decisions are exposed as an EntityFacets record next to the column facets. The differential spec compares all of it with the binder on 696 entities and 1896 properties, and found no mismatch. It now also compares the enum style and the entity facets with the bound PersistentClass and table, and skips entities whose persistent class belongs to another entity (an embedded class). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…enerator A derived property is bound as a Formula with no column, so the generator emits @formula and no @column. An explicit type (PropertyConfig.type, Mapping.userTypes, typeParams, or an enum with a type) becomes @type for a UserType class with its parameters, or @JdbcTypeCode for a registered legacy name such as text, resolved with Hibernate's own type registry instead of a hand-kept table. Anything else (serializable, converter names, custom marshaller types) stays unsupported and is named in the exception. The differential spec now compares formulas and the bound BasicValue's type name, parameters and JDBC type, and a generated class is bound by Hibernate's own annotation binder to confirm the annotations mean what the binder bound. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The binder installs one of GORM's own generator classes on the identifier. A Hibernate IdGeneratorType annotation cannot name them (the GORM generators configure themselves, and GrailsNativeGenerator inherits an annotation-based contract that rejects any annotation but Hibernate's own NativeGenerator), so the generated id carries a GrailsIdGenerator marker with the strategy and the identity params, except assigned (plain @id) and uuid/uuid2 (@uuidgenerator, the same class). The code that binds the generated class installs the generator with the same GrailsSequenceWrapper call the binder uses, so the classes and parameters are identical by construction. The differential spec compares the class and key parameters of the generator the binder installed on every root entity, and a spec binds generated classes with Hibernate's own annotation binder and compares them with the binder's. An entity without a simple identifier is now rejected instead of generating a class with no @id. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nerator Hibernate's annotation binder derives a hierarchy from extends, so the generator now builds the classes of a hierarchy together (generateAll): each subclass is generated against its superclass and all of them are loaded into one class loader, which a Hibernate registry can be given once. The root states @Inheritance, @DiscriminatorColumn or @DiscriminatorFormula, @DiscriminatorOptions for a non-inserted discriminator and @DiscriminatorValue; a subclass states only its value and its own class-level facets, never a table. Every decision is the binder's own accessor or constraint class; the differential spec now compares root and subclass entities (kind, superclass, table, abstractness, discriminator) with the bound PersistentClass and stays at zero mismatches. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A joined root states @Inheritance(JOINED) and its table; each subclass states its own table and a @PrimaryKeyJoinColumn named like the identifier column, as JoinedSubClassBinder names the key. The differential spec now compares the key column with the bound JoinedSubclass and the Hibernate class kind per strategy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nerator A table-per-class root states @Inheritance(TABLE_PER_CLASS) and each subclass its own table. The generated class is abstract exactly when the entity is, which is how Hibernate's annotation binder learns that an abstract root's table is abstract; the facets report the binder's own isTableAbstract for roots. Specs read abstract and three-level hierarchies back through Hibernate's annotation binder. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…binder in the differential The facet comparison shows what the generator decides; this also binds the generated classes of every describable hierarchy with Hibernate's own annotation binder and compares each PersistentClass (kind, superclass, table, abstractness, properties, discriminator, key column) with the one the domain binder built. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An embedded property becomes an @Embedded field of a generated @embeddable shared by every owner that embeds the same type. The owner states each column with an @AttributeOverride because the column name (path prefix) and the nullability (parent property, table-per-hierarchy owner, enclosing components) depend on the owner; the rules are ComponentBinder's and ColumnBinder's own, run on a scratch Column. The differential now compares every embedded component leaf with the binder and reads every describable entity (not only hierarchies) back through Hibernate's annotation binder, including the column facets the binder states, so an override that drops a length or a precision is caught. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…enerator A hasMany of simple values or enums becomes an @ElementCollection field whose generic signature names the element, with @CollectionTable and its key @joincolumn, the element @column, @OrderColumn for a list, @MapKeyColumn for a map, and the fetch, batch size and cache the mapping states. The table name, column names and facets come from the binder's own classes (TableForManyCalculator, the join table accessors, the column config binder); only the few decisions that live inside a second-pass method are mirrored. The differential compares each collection with the bound Hibernate Collection and reads it back through Hibernate's annotation binder. The comparison found binder defects the generator deliberately does not copy: an owner with more than one unidirectional collection gets a non-updatable key, which makes Hibernate write no rows (pinned by a pending feature in BasicCollectionKeyDefectSpec); a SortedSet and a map of enums cannot be bound; a map's table keeps an unused column. Collections the binder or Hibernate's annotations cannot describe (extra lazy, SortedSet, map of enums, embedded collections) are rejected by name. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ainGenerator The tenant id is an ordinary column, so it is described like a simple property. The tenant filter the binder adds to a multi-tenant entity is written as @filter on the entity and one @FilterDef carrying the parameter, not as Hibernate's @TenantID: that one also sets the tenant on insert and filters every query through a resolver, while GORM enables the filter itself per session and treats the tenant id as a normal writable property. Hibernate refuses a second @FilterDef of the same name, so generateAll puts the single definition on the first multi-tenant entity of the call. The differential boots multi-tenant groups in discriminator mode (GORM only gives an entity a tenant id there), compares each class's own filter and the global definition with the binder's and reads both back through Hibernate's annotation binder. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CollectionKeyColumnUpdater made the key of every collection not updatable once its owner had more than one unidirectional to-many property. For an entity collection the key is a foreign key in the target table and that rule keeps several collections from all updating it, but a collection of basic values or enums lives in a table of its own, and Hibernate writes no rows at all for a collection whose key is not updatable. An owner with two such collections saved and reloaded with both of them empty, silently. Keep the existing rule for entity collections and always leave the key of a collection of basic values updatable. The regression is covered at three levels: a unit row on the updater, a persist-and-reload spec for an owner with two collections (formerly a pending feature that pinned the defect), and the differential, which no longer needs to record the defect as a known divergence. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…mainGenerator A generated entity's association field is typed with the class generated for its target, because Hibernate resolves the target from the field type. generateAll therefore requires every referenced entity to be part of the call and builds the target types by name only, so cycles and self references need no ordering. The foreign key, fetching, cascade and missing-row handling are the binder's own decisions (ColumnBinder rules on a scratch column, CascadeBehaviorFetcher). The differential now generates whole connected groups and compares 158 to-one properties with the bound ManyToOne values, 52 of them also read back through Hibernate's annotation binder, with no mismatches. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The binder binds a valid one-to-one as a Hibernate OneToOne with no column that only names the property of the other side holding the foreign key, which is bound as a many-to-one. The generator states the inverse side as @OnetoOne(mappedBy) and keeps the foreign key side a @manytoone, because @OnetoOne with a join column would add a unique constraint the binder does not. The differential compares 22 inverse sides with the bound OneToOne values and reads them back through Hibernate's annotation binder with no mismatches. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
BLOCKER before merge: this branch carries a temporary CI commit. This PR is stacked, and to avoid starting a full CI matrix on every push or rebase of the stack it carries one temporary commit, Condition for merging this PR: that commit must be reverted on this branch first, and CI must then run and be green. The absence of CI checks on this PR is deliberate and does not mean the code was tested. The full build runs on #16576 and #16572, the PRs that target 10.0.x. |
…nless it shares a join table Ports the 8.0.x fix for #16665 to the Groovy conversion of CollectionKeyColumnUpdater by hand: the key is non-updatable only when another unidirectional to-many property of the owner uses the same collection table (issue 10811), so addTo on an owner with several hasMany to different classes is written to its join table instead of being discarded. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…/16568-merge-h7 # Conflicts: # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/CollectionKeyColumnUpdater.groovy
…oovy-grails-data-hibernate7
…ata-hibernate7' into nb/16568-merge-h7
…the Groovy conversion The three binder files the fix touches are Groovy here, so the changes are ported by hand into GrailsDomainBinder.groovy, GrailsPropertyBinder.groovy and HibernatePersistentProperty.groovy, and the new CompositeUserTypeBinder is added as Groovy. The specs and the docs merge as they are.
… since the last freeze The composite user type fixtures of #16695, the MultipleUnidirectionalHasManySpec entities of #16665, and the proxy, trait-initializer, CKCU and property-binder fixtures that arrived with earlier merges were not recorded, so ClassicOracleSpec failed on every native branch. Recorded with -Pgrails.test.refreezeClassicOracle=true; the two composite groups the classic binder cannot boot without a default mapping are recorded as unbootable.
…he Groovy conversion
… fixed collection-key feature Hibernate now derives a per-entity proxy base class (Entity$HibernateProxy), so a proxy's direct superclass is no longer the entity; the serialization spec asserts assignability instead, in both modes. The key of an entity collection stays updatable since 06d5ae4, so the pending feature of EntityCollectionKeyDefectSpec passes and is promoted to a regular feature.
The native binding now states what CompositeUserTypeBinder binds: a property mapped with a Hibernate CompositeUserType becomes an @Embedded field of the value class with @CompositeType and one @AttributeOverride per attribute of the user type's embeddable class, the attributes taken from its record components or its fields and readable properties in alphabetical order, the configured columns assigned by position and an attribute without one named <property>_<attribute>. Each column carries the property's nullability, uniqueness and constraints and its own column settings, so both bindings create the same schema; more columns than attributes is refused as the binder refuses it. A composite user type inside an embedded type is reported as unsupported. The differential harness compares the attribute columns of such a property with the recorded component, and the oracle records the two fixtures #16695 added. GeneratedDomainClassesCompositeUserTypeSpec boots the fixtures of CompositeUserTypeMappingSpec in both modes and compares the schemas, round-trips a value and queries by attribute.
…hether a key is updatable Port of 7138500 (#16686) to the Groovy conversion. CollectionKeyColumnUpdater asked every unidirectional to-many of the owner for its Hibernate collection; a to-many mapped with type: 'serializable' or with a user type is bound as a column and has none, and getCollection() throws a MappingException rather than returning null, so such an owner failed the session factory build. Filter the siblings with HibernateToManyProperty.isBoundAsCollection. CollectionKeyColumnUpdaterSpec and MultipleUnidirectionalHasManySpec carry the cases of #16686.
MuhAccount, MuhNote and CKCUColumnBoundOwner join the oracle; the former MuhItem group is regrouped under MuhAccount.
…acets subpackage The nineteen *Facets records and the two value types they are built from, CollectionKind and EmbeddedLeaf, move from domainbinding.jpa to domainbinding.jpa.facets, leaving the generator, the generated-class binder and the loaders in jpa as the classes that do something.
A package overview of domainbinding.jpa and a section of the hibernate-developer skill give the reading order of the binding: the switch, GeneratedDomainClassBinder.contribute, GrailsDomainGenerator and its facets, the two class loaders and GrailsIdGenerator, then the specs.
…oovy-grails-data-hibernate7
…oovy-grails-data-hibernate7 # Conflicts: # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateDatastore.groovy # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/CollectionBinder.groovy # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/binder/ManyToOneValuesBinder.groovy # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/secondpass/ToManyEntityMultiTenantFilterBinder.groovy # grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/multitenancy/MultiTenantEventListener.groovy
…/16568-merge-h7 # Conflicts: # .github/workflows/benchmark.yml # .github/workflows/build-health.yml # .github/workflows/codeanalysis.yml # .github/workflows/codeql.yml # .github/workflows/codestyle.yml # .github/workflows/coverage.yml # .github/workflows/end-to-end.yml # .github/workflows/gradle.yml # .github/workflows/groovy-snapshot-canary.yml # .github/workflows/rat.yml # .github/workflows/sitemesh2.yml # .github/workflows/validate-actions.yml # .github/workflows/vulnerability-scan.yml
🔎 No tests executed 🔎🏷️ Commit: 686239a Learn more about TestLens at testlens.app/docs. |
…y paths A move only, with no change to any file's content, so that the history of every class survives the conversion that follows: Git and GitHub connect a rename when the similarity is at least half, and a pure move is a whole match. The next commit converts the moved files.
The content of #16572 at 194cd43, on top of the move of the Java sources to their Groovy paths: the grails-data-hibernate7 core and dbmigration modules and their test fixtures converted to Groovy, with the semicolons kept under a suppression header as the stage-2 commits describe, plus the docs and build changes of that pull request. Idiomatic cleanup stays in the groovyism pull request that follows.
…commit): same tree, so this records the new history only
What
GORM for Hibernate 7 gains native domain binding: a
GrailsDomainGeneratorturns an already-built GORMPersistentEntityinto a JPA/Hibernate-annotated carrier class, and Hibernate's own annotation binder binds it. Sessions, queries and proxies keep using the real domain classes (the carrier's entity name, cache regions and class-loader lookups all resolve to the real class). In this PR it is opt-in (hibernate.generatedDomainClasses: true); the next PR in the stack makes it the default, and the last one deletes the hand-builtGrailsDomainBinder.Where to start reading
Native domain binding replaces GrailsDomainBinder building Hibernate's boot model by hand (about 5,800 lines that re-implement what Hibernate's annotation binder already does): GORM generates a JPA-annotated class for each domain class from its evaluated mapping and lets Hibernate's own annotation binder bind it. The application's real domain classes remain what Hibernate persists, loads, queries and proxies; the generated classes exist only while the mappings are bound.
The code lives in one package,
org.grails.orm.hibernate.cfg.domainbinding.jpa, with the value records in the subpackagejpa.facets. Reviewing it is easiest in the order the binding runs:HibernateConnectionSourceSettingsreadshibernate.generatedDomainClasses. When it is on,HibernateMappingContextConfigurationcreates aGeneratedDomainClassBinderand gives it toGrailsDomainBinder, which hands the entities to it instead of binding them itself. (From Hibernate 7: delete the classic domain binder #16570 on there is no switch:GormMappingContributoralways hands the entities to the generated-class binder.)GeneratedDomainClassBinder.contributeis called by Hibernate while it collects mappings. It asksGrailsDomainGenerator.generateAllfor an annotated class per entity and gives those classes to Hibernate's annotation binder. Its second half,getSessionFactoryBuilder, runs once the metadata is complete and points every bound entity at the real domain class, its property accessors and GORM's identifier generator.GrailsDomainGeneratoris where every mapping rule lives. It reads aGrailsHibernatePersistentEntity(properties plus the evaluatedmappingandconstraintsblocks) into the records ofjpa.facets, which state what to bind (columns, types, ids, associations, collections, inheritance, caches, tenancy, natural ids, indexes, unique keys), and then writes those facets as JPA and Hibernate annotations with ByteBuddy. A mapping it cannot describe is refused up front, by entity and property name, throughsupportsandunsupportedReason, before Hibernate boots. This is the large class; it will be split into per-concern collaborators once the classic binder is gone, so that its diff stays reviewable against the binder it replaces.GeneratedDomainClassLoaderresolves a domain class name to the generated class while Hibernate binds the mappings. Because a class loader answers a name the same way forever onceClass.forNamehas been asked, the switch to the real classes is made byGeneratedDomainClassLoaderService, which swaps to a second service whose loader resolves the same names to the application's classes.GrailsIdGeneratoris a marker annotation carrying the GORM id generator strategy and its parameters, since GORM's generators cannot be named through Hibernate's@IdGeneratorType.How it is verified. The specs under
grails.gorm.tests.generatedrun the same behaviour through both bindings. Two differential specs compare, for every scanned test domain class (about 1,170 entities and 3,400 properties), what the generator states against what the classic binder bound, both at the Hibernate boot-model level and at the DDL level. The classic side is frozen as recorded data undersrc/test/resources/classic-oracle, so the comparison survives the removal of the classic binder in #16570. The whole core suite also runs through the generated classes.The same reading order is in the
package-infoof thejpapackage and in the hibernate-developer skill.Why
The classic binder re-implements Hibernate's boot model by hand (~5,800 lines) and has been the source of a steady stream of mapping defects (seven of them fixed on
8.0.xduring this work: #16520, #16522, #16523, #16524, #16531, #16532, #16533, #16539). Letting Hibernate bind the entities removes that layer; the GORM mapping DSL stays the public surface.Scope
grails-data-hibernate7/core, packageorg.grails.orm.hibernate.cfg.domainbinding.jpa:GrailsDomainGenerator(ByteBuddy; every mapping option the classic binder honours, incl. composite identifiers via@IdClass, embedded types, all three inheritance strategies, multi-tenancy, natural ids, caches, custom and converting types, custom collection types),GeneratedDomainClassBinder(AdditionalMappingContributorplus an aligner that reproduces the classic binder's naming and DDL decisions where Hibernate's defaults differ),GeneratedDomainClassLoader[Service].ManyToManyOwnerValidator: a bidirectional many-to-many withbelongsToon neither side now fails at startup (MappingException) instead of silently never being written; on8.0.xthe same case logs a warning (Hibernate 7: warn when a many-to-many has no belongsTo on either side #16532).@Version, which Envers would skip; the optimistic-lock column is audited as before unless the application setsorg.hibernate.envers.do_not_audit_optimistic_locking_fielditself.grails.gorm.tests.generated, the whole core suite runnable through the generated classes (-Pgrails.test.generatedDomainClasses=true), and two differentials that compare native binding with the classic binder over every scanned test domain class (1,169 entities, 3,381 properties: 0 mismatches; DDL: 85 known differences, 0 unknown, 0 refused). The classic side of both differentials is frozen as recorded data (src/test/resources/classic-oracle/), so they survive the deletion of the classic binder.upgrading90x.adoc§1).Also in this PR, because the branch has to pass the repository's CodeNarc rules: the
grails-startup-progresssources that arrived from upstream in Java are converted to Groovy, and intentionally empty catch parameters are namedignoredin seven modules.Behaviour changes for applications (all documented in the upgrade notes)
UserCollectionTypeon ahasManyof entities takes effect (the classic binder ignored it); a shared embedded collection gets a table per owner; an embedded object as a part of a composite identifier is refused with a message pointing at the documented flattening workaround; a handful of schema differences listed in the manual (for examplecheck (idx >= 0)on list index columns).Verification
Every module of
grails-data-hibernate7in both modes at the tip of this PR: core 4,813 tests (classic default) and 4,523 (generated), dbmigration-core 40, dbmigration 110 + 93 CLI, plugin 39 + 2, spring-boot 2, all green; both differentials as above;codeStyleclean; docs built.Merge of the Java-to-Groovy review follow-up (#16572)
This branch now contains the reviewed #16572 (merged up the stack), including
9.0.xwith the upstream fixes #16533 (unique groups on enum and collection properties) and #16539 (nested composite foreign key column order).GrailsDomainGeneratorfollows the classic binder for both: it states the unique group of an enum property (UniqueKeyFacetsis nowrecord UniqueKeyFacets(name, columns); theboundflag is gone) and orders a nested composite foreign key's columns, and the columns they reference, by identifier property name.ddl-differential.txt,generator-differential.txt) is refrozen from the classic binder for the fixtures that came with9.0.x; 48 existing records changed, consistent with Hibernate 7: unique groups on enum and collection properties #16533/Hibernate 7: name nested composite foreign key columns in the referenced key's order #16539.9.0.xadded a GPars section as 1).codeStyleclean.