Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Misleading error message when trying to lookup discriminator of entity not mapped yet #2637

Closed
h-arlt opened this issue Nov 2, 2023 · 11 comments
Labels
Milestone

Comments

@h-arlt
Copy link

h-arlt commented Nov 2, 2023

Describe the bug
Attempts to load an entity from datastore that has at least one property/field of unmapped entity type fails with an error saying

Decoding errored with: Two entities have been mapped using the same discriminator value

After doing some debugging I noticed that the entity in question is not mapped at all but the mapper's discriminator class cache contains an entry for the entity's type which in turn triggers the error stated above.
I'm wondering whether the check in DiscriminatorLookup#addModel is usefull as long as there are multiple ways to fill the cache.

Mapping the entity type in question explicitly before fetching the entity from DB "fixes" the issue.

To Reproduce
Run the test case below.

Expected behavior
Test either passes without any error or meaningful error message about unmapped entity type.

Please complete the following information:

  • Server Version: 7.0.2
  • Driver Version: 4.11.0
  • Morphia Version: 2.4.7-SNAPSHOT

Additional context
Sample test case to reproduce the issue.

package dev.morphia.test;

import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;

import org.testng.annotations.Test;

import dev.morphia.annotations.Entity;

import dev.morphia.test.models.TestEntity;

public class TestDiscriminatorLookup extends TestBase
{

    @Test
    public void testLookup()
    {
        withConfig(buildConfig(SomeEntity.class), () -> {
            final SomeEntity entity = new SomeEntity();
            entity.setShape(new Shape.Square());

            getDs().save(entity);
        });

        final SomeEntity entity = getDs().find(SomeEntity.class).first();
        assertNotNull(entity);
        assertTrue(Shape.Square.class.isInstance(entity.getShape()));
    }

    @Entity
    public static class SomeEntity extends TestEntity
    {
        private Shape shape;

        public Shape getShape()
        {
            return shape;
        }

        public void setShape(Shape shape)
        {
            this.shape = shape;
        }
    }

    @Entity
    public static abstract class Shape
    {
        public static class Square extends Shape
        {
            public double side;
        }
    }
}
@h-arlt h-arlt added the bug label Nov 2, 2023
@evanchooly
Copy link
Member

Do you want this release or not? 😄 I have a feeling I know the problem. I'll try to get that cleaned up tonight.

@evanchooly evanchooly added this to the 2.4.7 milestone Nov 2, 2023
@evanchooly
Copy link
Member

I just ran your test case on 2.4.x and it worked just fine...

@evanchooly
Copy link
Member

What's the full error message you get with that?

@h-arlt
Copy link
Author

h-arlt commented Nov 3, 2023

I just ran your test case on 2.4.x and it worked just fine...

That's quite odd. Just checked with latest revision on 2.4.x branch and test fails with error:

[ERROR] dev.morphia.test.TestDiscriminatorLookup.testLookup -- Time elapsed: 0.683 s <<< FAILURE!
org.bson.codecs.configuration.CodecConfigurationException: Failed to decode 'Shape'. Decoding errored with: Two entities have been mapped using the same discriminator value (dev.morphia.test.TestDiscriminatorLookup$Shape$Square):  dev.morphia.test.TestDiscriminatorLookup$Shape$Square and dev.morphia.test.TestDiscriminatorLookup$Shape$Square
	at dev.morphia.mapping.codec.pojo.EntityDecoder.getCodecFromDocument(EntityDecoder.java:111)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.decode(EntityDecoder.java:51)
	at dev.morphia.mapping.codec.pojo.MorphiaCodec.decode(MorphiaCodec.java:76)
	at org.bson.codecs.DecoderContext.decodeWithChildContext(DecoderContext.java:96)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.decodeModel(EntityDecoder.java:68)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.decodeProperties(EntityDecoder.java:89)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.decode(EntityDecoder.java:48)
	at dev.morphia.mapping.codec.pojo.MorphiaCodec.decode(MorphiaCodec.java:76)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.decode(EntityDecoder.java:53)
	at dev.morphia.mapping.codec.pojo.MorphiaCodec.decode(MorphiaCodec.java:76)
	at com.mongodb.internal.operation.CommandResultArrayCodec.decode(CommandResultArrayCodec.java:52)
	at com.mongodb.internal.operation.CommandResultDocumentCodec.readValue(CommandResultDocumentCodec.java:60)
	at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:87)
	at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:42)
	at org.bson.internal.LazyCodec.decode(LazyCodec.java:53)
	at org.bson.codecs.BsonDocumentCodec.readValue(BsonDocumentCodec.java:104)
	at com.mongodb.internal.operation.CommandResultDocumentCodec.readValue(CommandResultDocumentCodec.java:63)
	at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:87)
	at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:42)
	at com.mongodb.internal.connection.ReplyMessage.<init>(ReplyMessage.java:48)
	at com.mongodb.internal.connection.InternalStreamConnection.getCommandResult(InternalStreamConnection.java:567)
	at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:461)
	at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:372)
	at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:114)
	at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:765)
	at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:76)
	at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:209)
	at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:115)
	at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:83)
	at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:74)
	at com.mongodb.internal.connection.DefaultServer$OperationCountTrackingConnection.command(DefaultServer.java:299)
	at com.mongodb.internal.operation.SyncOperationHelper.createReadCommandAndExecute(SyncOperationHelper.java:273)
	at com.mongodb.internal.operation.FindOperation.lambda$execute$1(FindOperation.java:325)
	at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$0(SyncOperationHelper.java:127)
	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:152)
	at com.mongodb.internal.operation.SyncOperationHelper.lambda$withSourceAndConnection$1(SyncOperationHelper.java:126)
	at com.mongodb.internal.operation.SyncOperationHelper.withSuppliedResource(SyncOperationHelper.java:152)
	at com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection(SyncOperationHelper.java:125)
	at com.mongodb.internal.operation.FindOperation.lambda$execute$2(FindOperation.java:322)
	at com.mongodb.internal.operation.SyncOperationHelper.lambda$decorateReadWithRetries$12(SyncOperationHelper.java:292)
	at com.mongodb.internal.async.function.RetryingSyncSupplier.get(RetryingSyncSupplier.java:67)
	at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:333)
	at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:73)
	at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:153)
	at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:130)
	at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:90)
	at dev.morphia.query.MorphiaQuery.prepareCursor(MorphiaQuery.java:372)
	at dev.morphia.query.MorphiaQuery.iterator(MorphiaQuery.java:228)
	at dev.morphia.query.MorphiaQuery.first(MorphiaQuery.java:200)
	at dev.morphia.query.MorphiaQuery.first(MorphiaQuery.java:195)
	at dev.morphia.test.TestDiscriminatorLookup.testLookup(TestDiscriminatorLookup.java:22)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at org.testng.internal.invokers.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:139)
	at org.testng.internal.invokers.TestInvoker.invokeMethod(TestInvoker.java:664)
	at org.testng.internal.invokers.TestInvoker.invokeTestMethod(TestInvoker.java:227)
	at org.testng.internal.invokers.MethodRunner.runInSequence(MethodRunner.java:50)
	at org.testng.internal.invokers.TestInvoker$MethodInvocationAgent.invoke(TestInvoker.java:957)
	at org.testng.internal.invokers.TestInvoker.invokeTestMethods(TestInvoker.java:200)
	at org.testng.internal.invokers.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:148)
	at org.testng.internal.invokers.TestMethodWorker.run(TestMethodWorker.java:128)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at org.testng.TestRunner.privateRun(TestRunner.java:848)
	at org.testng.TestRunner.run(TestRunner.java:621)
	at org.testng.SuiteRunner.runTest(SuiteRunner.java:443)
	at org.testng.SuiteRunner.runSequentially(SuiteRunner.java:437)
	at org.testng.SuiteRunner.privateRun(SuiteRunner.java:397)
	at org.testng.SuiteRunner.run(SuiteRunner.java:336)
	at org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
	at org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:95)
	at org.testng.TestNG.runSuitesSequentially(TestNG.java:1280)
	at org.testng.TestNG.runSuitesLocally(TestNG.java:1200)
	at org.testng.TestNG.runSuites(TestNG.java:1114)
	at org.testng.TestNG.run(TestNG.java:1082)
	at org.apache.maven.surefire.testng.TestNGExecutor.run(TestNGExecutor.java:155)
	at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.executeSingleClass(TestNGDirectoryTestSuite.java:102)
	at org.apache.maven.surefire.testng.TestNGDirectoryTestSuite.execute(TestNGDirectoryTestSuite.java:91)
	at org.apache.maven.surefire.testng.TestNGProvider.invoke(TestNGProvider.java:137)
	at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)
	at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
	at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)
	at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)
Caused by: dev.morphia.mapping.MappingException: Two entities have been mapped using the same discriminator value (dev.morphia.test.TestDiscriminatorLookup$Shape$Square):  dev.morphia.test.TestDiscriminatorLookup$Shape$Square and dev.morphia.test.TestDiscriminatorLookup$Shape$Square
	at dev.morphia.mapping.DiscriminatorLookup.addModel(DiscriminatorLookup.java:65)
	at dev.morphia.mapping.Mapper.documentNewModel(Mapper.java:588)
	at dev.morphia.mapping.Mapper.register(Mapper.java:573)
	at dev.morphia.mapping.Mapper.register(Mapper.java:568)
	at dev.morphia.mapping.Mapper.getEntityModel(Mapper.java:269)
	at dev.morphia.mapping.codec.MorphiaCodecProvider.get(MorphiaCodecProvider.java:74)
	at org.bson.codecs.configuration.CodecProvider.get(CodecProvider.java:70)
	at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
	at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:70)
	at org.bson.internal.ChildCodecRegistry.get(ChildCodecRegistry.java:92)
	at org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider(ProvidersCodecRegistry.java:95)
	at org.bson.internal.ProvidersCodecRegistry.lambda$get$0(ProvidersCodecRegistry.java:82)
	at java.base/java.util.Optional.orElseGet(Optional.java:364)
	at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:80)
	at org.bson.internal.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:50)
	at dev.morphia.mapping.codec.pojo.EntityDecoder.getCodecFromDocument(EntityDecoder.java:105)
	... 83 more

@evanchooly
Copy link
Member

OK. I think I've found the difference. I'd put it off in its own package which isolated it from mapping. This test introduces a duplicate mapping and that's failing but the message is poorly formed. I'll clean that up.

@h-arlt
Copy link
Author

h-arlt commented Nov 3, 2023

Not sure if I got you right. What exactly do you mean by "duplicate mapping"?
The test makes use of the withConfig closure to be able to save some data to DB without having its type mapped in the original datastore's mapper.

Use-case is as follows: Image you have some data stored in DB yet and your application starts up without explicitly mapping the entity types that were used to save the data in first place. If you try to load the data from DB, the discriminator lookup will fill its cache before the entity model is known to the mapper. When the mapper then tries build the model, it fails with the error message mentioned before.

@evanchooly
Copy link
Member

Yeah, i'm digging in now and it's weirder than I thought. There are technically two Mappers at play in your test. Neither should know about the other but we seem to be getting a double mapping but maybe the two Mappers are a red herring...

@h-arlt
Copy link
Author

h-arlt commented Nov 3, 2023

IMO the issue is the discriminator lookup, or more specifically, its cache that is filled in two ways: when adding an entity model and when doing a lookup for a given discriminator String value. In case the latter happens before the model is added (in EntityDecoder#decodeProperties), the error comes up.

@evanchooly
Copy link
Member

I found that place you mentioned about adding the lookup early. I think that went in to support mapping external types, maybe? That class is old old so some of that is lost to history. At any rate, it's all cleaned up.

evanchooly added a commit that referenced this issue Nov 6, 2023
@evanchooly
Copy link
Member

jreleaser was being fussy but 2.4.7 is up on central.

@h-arlt
Copy link
Author

h-arlt commented Nov 6, 2023

Fix works like a charm 👍

evanchooly added a commit that referenced this issue Nov 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants