hk2166 opened a new pull request, #16781:
URL: https://github.com/apache/grails-core/pull/16781

   ## Description
   
   Fixes the 8.0.0 data binding slowdown reported in #16558. On 8.0.x every 
bind onto a domain instance (`new Book(map)`, `bindData`, `obj.properties = 
params`) built its list of `bindable: false` properties from scratch:
   
   1. `DataBindingUtils.getConstrainedProperties` asked the instance for 
`constraintsMap` and `getConstraintsMap()`. A domain class has neither, so it 
threw and caught a missing property exception and a missing method exception. 
It then evaluated the class's `constraints` closure again through 
`ValidationSupport`.
   2. `BindingIncludeLists.bindableConstraintValue` then called 
`getMetaConstraintValue` and read `metaConstraints` on each constrained 
property. `ValidationSupport` and `Validateable` wrap each property in a 
`ConstrainedDelegate`, which has neither, so it threw and caught two more 
exceptions for every property before it reached the wrapped one. Command 
objects pay this part too, which answers the question about commands on the 
issue.
   
   ### Changes
   
   There is no new cache:
   
   - **Domain instances and proxies:** for an instance, or a proxy, of a mapped 
entity, binding reads the constraints from the entity's validator 
(`mappingContext.getEntityValidator(entity)`). The mapping context evaluates 
them once, when it creates the validator, and keeps them for as long as it 
holds the entity. With GORM for Hibernate or MongoDB, 
`grailsApplication.mappingContext` is the datastore's mapping context, so this 
is the validator `validate()` uses. Proxies are resolved through the mapping 
context's proxy handler, as `bindingType` already does.
   - **Binder lookup:** `GrailsWebDataBinder` passes its own 
`GrailsApplication`, so the lookup doesn't resolve the application again on 
every bind.
   - **No exceptions:** the accessors are looked up through the metaclass 
(`hasProperty`, `getMetaMethod`, `getStaticMetaMethod`) before they are called, 
so a missing one no longer throws. The lookup order is unchanged.
   - **Everything else:** an object that isn't a mapped entity, or whose 
validator doesn't expose its constraints, takes the existing path.
   
   ### Before and after
   
   These are µs per call. Each figure is the median of the last 3 of 5 rounds; 
each round ran 5,000 warm-up calls, then timed 20,000 calls.
   
   | | 8.0.x | This PR |
   |---|---:|---:|
   | Domain: `new Book(a: 1.0, b: 1.0, c: 1.0, d: 1.0)` | 76.1 | 12.4 |
   | Domain: `DataBindingUtils.bindObjectToInstance` | 82.6 | 13.2 |
   | Command (`Validateable`): `DataBindingUtils.bindObjectToInstance` | 32.2 | 
14.3 |
   | Domain: `new Book()` + 4 setters (no binding, for reference) | 2.8 | 3.2 |
   
   How these were measured:
   - A temporary Spock spec in `grails-test-suite-persistence`, run with 
`DataTest` (the in-memory datastore, not Hibernate), on JDK 21 on an Apple 
Silicon Mac. The spec is not part of this PR.
   - The domain class and the command each have 24 constrained properties, 2 of 
them `bindable: false`, and the bind sets 4 `BigDecimal` properties.
   - The "after" times include the entity lookup that was a concern on the 
issue: a proxy check, a lookup of the entity by class name and a lookup of its 
validator.
   - The absolute numbers are lower than in the report, which was measured in a 
`bootRun` app with Hibernate. The improvement from 8.0.x is the point of the 
comparison.
   - The report also mentions 
`GrailsWebDataBinder.getNestedBindingIncludeList`, which runs for each bound 
property. This PR doesn't change it.
   
   ### Tests
   
   The tests use the public binding APIs: the binder, `DataBindingUtils`, the 
map constructor and `bindData`.
   
   - `GrailsWebDataBinderSpec` (`grails-test-suite-persistence`):
     - A domain class whose `constraints` closure counts how often it runs is 
bound through the binder, `DataBindingUtils` and the map constructor. The 
`bindable: false` property is not bound, and the closure does not run again. On 
8.0.x this test fails, because the closure runs on every bind.
     - A domain subclass doesn't bind the properties that it or its superclass 
constrain `bindable: false`.
   - `DomainProxyBindingSpec` (`grails-test-suite-uber`): a proxy of a domain 
class doesn't bind a `bindable: false` property, through `bindData` or 
`DataBindingUtils`.
   - `DataBindingUtilsSpec` (`grails-web-databinding`):
     - An instance of a mapped entity, and a proxy of one, are bound with the 
constraints their validator holds. Both tests fail on 8.0.x.
     - An entity whose validator doesn't expose its constraints falls back to 
the constraints its class declares.
     - A constrained property that wraps another is read through the one it 
wraps.
   
   Verification:
   - All tests in `grails-web-databinding` (68), 
`grails-test-suite-persistence` (112), `grails-test-suite-web` (444, 1 skipped) 
and `grails-test-suite-uber` (626, 4 skipped) pass. The skips also occur on 
8.0.x.
   - `./gradlew cleanViolationReports aggregateViolations --continue` reports 
no Checkstyle, CodeNarc, PMD or repository convention violations. SpotBugs is 
disabled.
   - I didn't run the repository-wide `clean` or `:grails-test-report:check`, 
which would run every module's tests.
   
   Fixes #16558
   
   Generative AI tooling (Claude Code) was used to research the cause, write 
the change and its tests, and run the benchmark. I reviewed the change.
   
   ## Contributor Checklist
   
   Please review the following checklist before submitting your pull request. 
Pull requests that do not meet these requirements may be closed without review.
   
   ### Issue and Scope
   
   - [x] This PR is linked to an existing issue that has been **acknowledged or 
approved** by the project team. If no approved issue exists, please give 
background on why this change is necessary.  Tickets are preferred for release 
change log history.
   - [x] This PR addresses the **complete scope** of the linked issue. Partial 
implementations or unfinished work should not be submitted for review. *(The 
constraints are no longer evaluated on each bind, which is what the issue 
reports. The smaller cost the report attributes to 
`getNestedBindingIncludeList` is unchanged.)*
   - [x] This PR contains a **single, focused change**. Unrelated changes 
should be submitted as separate pull requests.
   - [x] This PR targets the **correct branch** for the type of change:
       - **Patch release branches** (e.g., `7.0.x`): Bug fixes only. No new 
features or API changes.
       - **Minor release branches** (e.g., `7.1.x`): New features are welcome, 
but breaking existing APIs must be avoided.
       - **Major release branches** (e.g., `8.0.x`): Reserved for major 
changes. Breaking API changes are permitted.
   
   ### Code Quality
   
   - [x] I have **added or updated tests** that cover the changes introduced in 
this PR. All code contributions are expected to include appropriate test 
coverage.
   - [ ] I have verified that all existing tests pass by running `./gradlew 
build --rerun-tasks`. *(Not the full build; all tests in the four affected 
modules pass, see above.)*
   - [x] My code follows the project's **code style** guidelines. I have run 
`./gradlew codeStyle` and resolved any violations. See [Code 
Style](../CONTRIBUTING.md#code-style) for details.
   - [x] This PR does **not** include mass reformatting, style-only changes, or 
large-scale refactoring unless it was **explicitly approved** in the linked 
issue. Unsolicited reformatting will not be accepted.
   - [x] If generative AI tooling was used in preparing this contribution, a 
quality model was used to ensure contributions are **consistent with the 
project's quality standards**.
   
   ### Licensing and Attribution
   
   - [x] All contributed code is provided under the [Apache License 
2.0](https://www.apache.org/licenses/LICENSE-2.0), and new source files include 
the appropriate **Apache license header**.
   - [ ] I have the necessary rights to submit this contribution and confirm it 
is my own original work (see [Legal 
Notice](../CONTRIBUTING.md#i-want-to-contribute)).
   - [x] If generative AI tooling was used in preparing this contribution, I 
have followed the [Apache Software Foundation's policy on generative 
tooling](https://www.apache.org/legal/generative-tooling.html) and have 
properly attributed its use.
   
   ### Documentation
   
   - [x] If this PR introduces user-facing changes, I have included or updated 
the relevant documentation. *(No user-facing change: binding behaves as before, 
only faster.)*
   - [x] If this PR adds a new feature, I have updated the **What's New** 
section of the Grails Guide.
   - [x] If this PR introduces breaking changes or changes that require user 
action during an upgrade, I have updated the **Upgrade Notes** for the 
corresponding version in the Grails Guide.
   - [x] The PR description clearly explains **what** was changed and **why**.
   
   ---
   
   > **First-time contributors:** Please read our [Contributing 
Guide](../CONTRIBUTING.md) before submitting.
   > Pull requests that appear to be auto-generated, incomplete, or unrelated 
to an approved issue may be
   > closed to help maintainers focus on reviewed and planned work. We 
appreciate your understanding.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to