From ae407e3023a800d46865b200daf6c916efb181e6 Mon Sep 17 00:00:00 2001 From: evanchooly Date: Wed, 21 Oct 2026 20:55:06 -0400 Subject: [PATCH] Precompute serialization flags; leave @LoadOnly out of the encode order (plan item 3.6) MorphiaPropertySerialization read three config values and scanned the property's annotations for every value it checked. The config is immutable and the annotations are fixed once a property is mapped, so it now works out storeNulls, storeEmpties and whether the property is never written (@LoadOnly, or a final field with ignoreFinals) when it's built. The codec plan's encode order also leaves out @LoadOnly properties, which no encoder writes, and EntityEncoder writes the document's start and end directly instead of through a capturing lambda. --- .../codec/MorphiaPropertySerialization.java | 41 +++++---------- .../morphia/mapping/codec/pojo/CodecPlan.java | 7 ++- .../mapping/codec/pojo/EntityEncoder.java | 25 ++++------ .../codec/pojo/TestCodecPlanEncodeOrder.java | 50 +++++++++++++++++++ design/mapping-game-plan.md | 2 +- 5 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 core/src/test/java/dev/morphia/mapping/codec/pojo/TestCodecPlanEncodeOrder.java diff --git a/core/src/main/java/dev/morphia/mapping/codec/MorphiaPropertySerialization.java b/core/src/main/java/dev/morphia/mapping/codec/MorphiaPropertySerialization.java index 225fa3f27a0..83e7e3fdf68 100644 --- a/core/src/main/java/dev/morphia/mapping/codec/MorphiaPropertySerialization.java +++ b/core/src/main/java/dev/morphia/mapping/codec/MorphiaPropertySerialization.java @@ -1,8 +1,6 @@ package dev.morphia.mapping.codec; -import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.List; import java.util.Map; import com.mongodb.lang.Nullable; @@ -17,44 +15,31 @@ * Determines if a property should be serialized or not */ public class MorphiaPropertySerialization implements PropertySerialization { - private final List annotations; - private final MorphiaConfig config; - - private final PropertyModel property; + // all fixed once the property is mapped, so worked out here rather than on every value + private final boolean storeNulls; + private final boolean storeEmpties; + private final boolean neverSerialized; /** * @param config the configuration to use * @param property the property in question */ public MorphiaPropertySerialization(MorphiaConfig config, PropertyModel property) { - this.config = config; - annotations = property.getAnnotations(); - this.property = property; + storeNulls = config.storeNulls(); + storeEmpties = config.storeEmpties(); + neverSerialized = property.hasAnnotation(LoadOnly.class) || config.ignoreFinals() && property.isFinal(); } @Override public boolean shouldSerialize(@Nullable Object value) { - if (!config.storeNulls() && value == null) { - return false; - } - if (config.ignoreFinals() && property.isFinal()) { + if (neverSerialized) { return false; } - if (!config.storeEmpties()) { - if (value instanceof Map && ((Map) value).isEmpty() - || value instanceof Collection && ((Collection) value).isEmpty()) { - return false; - } - } - return isNotLoadOnly(); - } - - private boolean isNotLoadOnly() { - for (int i = 0; i < annotations.size(); i++) { - if (annotations.get(i).annotationType().equals(LoadOnly.class)) { - return false; - } + if (value == null) { + return storeNulls; } - return true; + return storeEmpties + || !(value instanceof Map && ((Map) value).isEmpty() + || value instanceof Collection && ((Collection) value).isEmpty()); } } diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java index cd69976ba94..37ac7351653 100644 --- a/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java +++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/CodecPlan.java @@ -6,6 +6,7 @@ import com.mongodb.lang.Nullable; +import dev.morphia.annotations.LoadOnly; import dev.morphia.annotations.internal.MorphiaInternal; import dev.morphia.mapping.codec.EnumCodec; import dev.morphia.mapping.codec.MorphiaDateCodec; @@ -84,8 +85,9 @@ public final class CodecPlan { directBsonTypes[slot] = codecs[slot] != null ? DIRECT_BSON_TYPES.getOrDefault(codecs[slot].getClass(), 0) : 0; } PropertyModel idProperty = model.getIdProperty(); + // the id is written first, separately, and @LoadOnly properties are never written by any encoder encodeOrder = Arrays.stream(properties) - .filter(property -> property != idProperty) + .filter(property -> property != idProperty && !property.hasAnnotation(LoadOnly.class)) .toArray(PropertyModel[]::new); } @@ -128,7 +130,8 @@ private static int mask(BsonType... types) { } /** - * @return the properties to encode after the id, in order. Callers must not modify the array. + * @return the properties to encode after the id, in order, without {@code @LoadOnly} properties. Callers must not + * modify the array. */ PropertyModel[] encodeOrder() { return encodeOrder; diff --git a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java index 4dc0e65f832..17c5cc29a31 100644 --- a/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java +++ b/core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java @@ -15,8 +15,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static dev.morphia.mapping.codec.CodecHelper.document; - /** * @param the entity type * @hidden @@ -42,19 +40,16 @@ public void encode(BsonWriter writer, T value, EncoderContext encoderContext) { if (areEquivalentTypes(value.getClass(), model.getType())) { LOG.debug("Encoding document using codec for {}", morphiaCodec.getEntityModel().getType().getName()); - document(writer, () -> { - - PropertyModel idModel = model.getIdProperty(); - encodeIdProperty(writer, value, encoderContext, idModel); - - if (model.useDiscriminator()) { - encodeDiscriminator(writer, model); - } - - for (PropertyModel propertyModel : morphiaCodec.getPlan().encodeOrder()) { - encodeProperty(writer, propertyModel, value, encoderContext); - } - }); + // written out rather than through CodecHelper.document, whose lambda would capture these locals per encode + writer.writeStartDocument(); + encodeIdProperty(writer, value, encoderContext, model.getIdProperty()); + if (model.useDiscriminator()) { + encodeDiscriminator(writer, model); + } + for (PropertyModel propertyModel : morphiaCodec.getPlan().encodeOrder()) { + encodeProperty(writer, propertyModel, value, encoderContext); + } + writer.writeEndDocument(); } else { morphiaCodec.codecForSubtype((Class) value.getClass()) .encode(writer, value, encoderContext); diff --git a/core/src/test/java/dev/morphia/mapping/codec/pojo/TestCodecPlanEncodeOrder.java b/core/src/test/java/dev/morphia/mapping/codec/pojo/TestCodecPlanEncodeOrder.java new file mode 100644 index 00000000000..53ca01d920d --- /dev/null +++ b/core/src/test/java/dev/morphia/mapping/codec/pojo/TestCodecPlanEncodeOrder.java @@ -0,0 +1,50 @@ +package dev.morphia.mapping.codec.pojo; + +import java.util.Arrays; +import java.util.List; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +import dev.morphia.MorphiaDatastore; +import dev.morphia.annotations.LoadOnly; +import dev.morphia.config.ManualMorphiaConfig; +import dev.morphia.mapping.DiscriminatorFunction; +import dev.morphia.mapping.MapperType; +import dev.morphia.test.mapping.codec.harness.models.HarnessRenames; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** + * The plan's encode order holds every property an encoder may write, in model order: everything but the id, which is + * written first on its own, and {@code @LoadOnly} properties, which no encoder writes. + */ +public class TestCodecPlanEncodeOrder { + @ParameterizedTest + @EnumSource(MapperType.class) + void leavesOutTheIdAndLoadOnlyProperties(MapperType mapper) { + // nothing here talks to a server + try (MongoClient client = MongoClients.create("mongodb://localhost:1")) { + MorphiaDatastore datastore = new MorphiaDatastore(client, ManualMorphiaConfig.configure() + .database("codec_plan_encode_order") + .mapper(mapper) + .packages(List.of(HarnessRenames.class.getPackageName())) + .discriminator(DiscriminatorFunction.className()) + .applyIndexes(false) + .applyCaps(false) + .applyDocumentValidations(false)); + CodecPlan plan = ((MorphiaCodec) datastore.getCodecRegistry().get(HarnessRenames.class)).getPlan(); + EntityModel model = plan.model(); + + List expected = model.getProperties().stream() + .filter(property -> property != model.getIdProperty() && !property.hasAnnotation(LoadOnly.class)) + .toList(); + Assertions.assertEquals(expected, Arrays.asList(plan.encodeOrder())); + Assertions.assertTrue(Arrays.stream(plan.encodeOrder()).noneMatch(property -> property.getName().equals("legacy")), + mapper + ": the @LoadOnly property is left out"); + Assertions.assertEquals(model.getProperties().size() - 2, plan.encodeOrder().length, mapper.toString()); + } + } +} diff --git a/design/mapping-game-plan.md b/design/mapping-game-plan.md index 35ad6dd911c..ceb1e5ae37f 100644 --- a/design/mapping-game-plan.md +++ b/design/mapping-game-plan.md @@ -234,7 +234,7 @@ JMH numbers on both mappers. - Cache `ArrayCodec`'s component codec. - Add a per-codec subtype-codec cache for encode. -### 3.6 Precompute serialization flags and the encode order (#6, B11/A7) +### ~~3.6 Precompute serialization flags and the encode order (#6, B11/A7)~~ - Give `MorphiaPropertySerialization` final booleans. - Put `encodeOrder` in the plan, without the id property and without properties that are always skipped. - Use `==` instead of `equals(idModel)`. The deep `equals` is already gone after 2.7.