Repository navigation
Skip Conversions when the value already fits (plan item 3.7) - #4412
Merged
Merged
Conversation
PropertyModel.setValue and ConstructorCreator now pass a value straight through when it is already an instance of the target type, with primitives boxed, instead of calling Conversions.convert. Conversions.convert looks its map up with get rather than computeIfAbsent, so it no longer inserts an empty map for every new value class. The decoder's fallback path no longer converts before handing the value to the creator, which converts it again.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Removing fallback property-type conversion breaks schema-drift refreshes and constructor mappings that require an intermediate conversion.
1 open issue
What changed in this PR
This PR implements plan item 3.7 to reduce conversion overhead when Morphia decodes values that already fit their target types.
Changes:
- Adds boxed-type fast paths for property assignment and constructor arguments.
- Avoids caching empty conversion maps and removes decoder-side fallback conversion.
- Adds conversion tests and marks the plan item complete.
| File | Description |
|---|---|
design/mapping-game-plan.md |
Marks item 3.7 complete. |
core/src/test/java/dev/morphia/mapping/codec/TestConversions.java |
Tests boxing, registered conversions, pass-through values, and null handling. |
core/src/main/java/dev/morphia/mapping/internal/ConstructorCreator.java |
Caches boxed parameter types and skips unnecessary conversions. |
core/src/main/java/dev/morphia/mapping/codec/pojo/PropertyModel.java |
Adds a boxed-type assignment fast path. |
core/src/main/java/dev/morphia/mapping/codec/pojo/EntityDecoder.java |
Delegates fallback conversion to instance creators. |
core/src/main/java/dev/morphia/mapping/codec/Conversions.java |
Adds a boxing helper and avoids empty-map insertion. |
Review effort: Balanced
The refresh codec's creator sets values without converting them, and a constructor parameter can need the property's type as an intermediate step. The creator's fast path skips the second conversion once the value fits.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Plan item 3.7 (review B8).
The problem
Every decoded property goes through
Conversions.convert, even when the value already has the right type. The early return,fromType.equals(target), misses the common cases: anIntegerfor anintfield, anArrayListfor aList, aHashMapfor aMap. Each miss then rancomputeIfAbsent(fromType, …), which also inserted an emptyConcurrentHashMapfor every new value class. Then.get(target)came back empty and the value was returned unchanged.When a stored value doesn't match the codec (schema drift), the decoder's fallback path converted the value, and the creator converted it again even though it already fit.
Change
PropertyModel.setValueandConstructorCreator.setnow set the value directly when it's already an instance of the target type, with primitives treated as their wrappers. Only values that need converting reachConversions.convert.getinstead ofcomputeIfAbsentinConversions.convert. An unregistered source type is no longer added to the map.EntityDecoderstill converts the fallback value to the property's type, and the creator's fast path now skips the second conversion because the value fits. (The first version of this PR dropped the decoder's conversion instead; see the review thread for why that was wrong.)Conversions.boxedType(Class), a small@MorphiaInternalhelper (MethodType.methodType(t).wrap()), shared by both call sites.Tests
TestConversions: boxing; registered conversions still apply ("42"→int,42.0→long,"c"→Character); values without a conversion pass through;null→0for primitives.TestDriftedValues: a drifted value reaching a constructor parameter whose type differs from the property's (Integer→Stringproperty →Characterparameter) still converts through the property type.-Dmorphia.mapper=critterand with reflection: 1498 + 5 tests, 0 failures.JMH
Expect a small decode-only gain. The review estimated ~10–20 ns per primitive- or interface-typed property, with SIMPLE having 4 such properties.
decode, master (f41533b8735) and this branch alternating, three rounds eachReading it:
computeIfAbsentinserted were per value class, not per value, so after warm-up they never showed up in B/op.Full
CodecBenchmark+MixedCodecBenchmark, one run each (master reflection run was disturbed)The first master reflection run was noisy: decode error bars reached ±2,264 ns, which inflates the reflection "gains" to 20–35%. That's why the decode rerun above exists. The critter run and all encode rows are clean; encode code is untouched.
reflection
critter
Decisions to review
PropertyModelandConstructorCreator.Plan, not onCodecPlanslots as the plan item said. The conversion happens insetValueand the creator, which never see the codec plan. The boxed type depends only on the property's declared type, not on the datastore, so it fits the shared model (it's computed lazily on first use and isn't copied).ArrayList→Listconversion never runs because anArrayListalready fits aListproperty. No default conversion is like that; the review flagged the same risk. A user who needs to rewrite a value of the right type should use a codec.refresh()of an entity with a drifted field fails onmastertoo.refreshdecodes throughDocumentReader, whosereadStringthrowsClassCastExceptionon a stored integer instead ofBsonInvalidOperationException, so the fallback is never reached. It belongs in its own PR.