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.