MorphiaOrg / morphia

MongoDB object-document mapper in Java based on https://github.com/mongodb/mongo-java-driver
Apache License 2.0
1.65k stars 456 forks source link

Mapping does not work with generic types #1481

Closed Homer1991 closed 4 years ago

Homer1991 commented 4 years ago

Mapping does not work with generic types Using generic types in the @Entity does not work. The UUID gets correctly stored in MongoDB but when reading the Document is not correctly mapped to UUID.

To Reproduce Generic Entity:

@Entity
public class GenericEntity<T> {
    @Id
    protected T id;
    protected T test;

    public T getId() {
        return id;
    }

    public void setId(T id) {
        this.id = id;
    }

    public T getTest() {
        return test;
    }

    public void setTest(T test) {
        this.test = test;
    }
}

Specific Entity:

@Entity
public class SpecifiyEntity extends GenericEntity<UUID> {

}

Main-Class:

    public static void main(String[] args) {

        final Datastore datastore = Morphia.createDatastore(MongoClients.create(
                MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://127.0.0.1/"))
                        .uuidRepresentation(UuidRepresentation.STANDARD).build()),
                "morphia_example");

        datastore.getMapper().mapPackage("de.test");

        datastore.ensureIndexes();

        SpecifiyEntity beforeDB = new SpecifiyEntity();
        beforeDB.setId(UUID.randomUUID());
        beforeDB.setTest(UUID.randomUUID());
        datastore.save(beforeDB);

        SpecifiyEntity fromDB = datastore.find(SpecifiyEntity.class).filter(Filters.eq("_id", beforeDB.getId()))
                .iterator().next();

        System.out.println(beforeDB.getId());
        System.out.println(fromDB.getId());

        System.out.println(beforeDB.getTest());
        System.out.println(fromDB.getTest());
    }

Output:

6d7143cb-aa14-4d3f-96c0-fdda5e223ea6
org.bson.types.Binary@80d33fd8
685e8147-7a6f-40b1-a6a2-1da6b1f1bce9
org.bson.types.Binary@d18db8b1

Expected behavior Fields are mapped to the correct type.

Homer1991 commented 4 years ago

Maybe you can help with a workaround because the converters are deleted

Homer1991 commented 4 years ago

Interestingly after adding this:

        datastore.getMapper().addInterceptor(new EntityInterceptor() {
            @Override
            public void preLoad(Object ent, Document document, Mapper mapper) {
                System.out.println("test");
            }
        });

I get the following stack trace:

Exception in thread "main" java.lang.IllegalStateException: unknown type for bson mapping: class java.util.UUID
    at dev.morphia.mapping.codec.reader.DocumentReader.getBsonType(DocumentReader.java:347)
    at dev.morphia.mapping.codec.reader.ReaderState.getCurrentBsonType(ReaderState.java:97)
    at dev.morphia.mapping.codec.reader.DocumentReader.getCurrentBsonType(DocumentReader.java:43)
    at dev.morphia.mapping.codec.pojo.EntityDecoder.decodeModel(EntityDecoder.java:63)
    at dev.morphia.mapping.codec.pojo.EntityDecoder.decodeProperties(EntityDecoder.java:88)
    at dev.morphia.mapping.codec.pojo.EntityDecoder.decodeWithLifecycle(EntityDecoder.java:132)
    at dev.morphia.mapping.codec.pojo.EntityDecoder.decode(EntityDecoder.java:38)
    at dev.morphia.mapping.codec.pojo.MorphiaCodec.decode(MorphiaCodec.java:70)
    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:84)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:41)
    at org.bson.internal.LazyCodec.decode(LazyCodec.java:48)
    at org.bson.codecs.BsonDocumentCodec.readValue(BsonDocumentCodec.java:101)
    at com.mongodb.internal.operation.CommandResultDocumentCodec.readValue(CommandResultDocumentCodec.java:63)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:84)
    at org.bson.codecs.BsonDocumentCodec.decode(BsonDocumentCodec.java:41)
    at com.mongodb.internal.connection.ReplyMessage.<init>(ReplyMessage.java:51)
    at com.mongodb.internal.connection.InternalStreamConnection.getCommandResult(InternalStreamConnection.java:412)
    at com.mongodb.internal.connection.InternalStreamConnection.receiveCommandMessageResponse(InternalStreamConnection.java:308)
    at com.mongodb.internal.connection.InternalStreamConnection.sendAndReceive(InternalStreamConnection.java:258)
    at com.mongodb.internal.connection.UsageTrackingInternalConnection.sendAndReceive(UsageTrackingInternalConnection.java:99)
    at com.mongodb.internal.connection.DefaultConnectionPool$PooledConnection.sendAndReceive(DefaultConnectionPool.java:500)
    at com.mongodb.internal.connection.CommandProtocolImpl.execute(CommandProtocolImpl.java:71)
    at com.mongodb.internal.connection.DefaultServer$DefaultServerProtocolExecutor.execute(DefaultServer.java:224)
    at com.mongodb.internal.connection.DefaultServerConnection.executeProtocol(DefaultServerConnection.java:202)
    at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:118)
    at com.mongodb.internal.connection.DefaultServerConnection.command(DefaultServerConnection.java:110)
    at com.mongodb.internal.operation.CommandOperationHelper.executeCommand(CommandOperationHelper.java:343)
    at com.mongodb.internal.operation.CommandOperationHelper.executeCommand(CommandOperationHelper.java:334)
    at com.mongodb.internal.operation.CommandOperationHelper.executeCommandWithConnection(CommandOperationHelper.java:220)
    at com.mongodb.internal.operation.FindOperation$1.call(FindOperation.java:631)
    at com.mongodb.internal.operation.FindOperation$1.call(FindOperation.java:625)
    at com.mongodb.internal.operation.OperationHelper.withReadConnectionSource(OperationHelper.java:462)
    at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:625)
    at com.mongodb.internal.operation.FindOperation.execute(FindOperation.java:77)
    at com.mongodb.client.internal.MongoClientDelegate$DelegateOperationExecutor.execute(MongoClientDelegate.java:190)
    at com.mongodb.client.internal.MongoIterableImpl.execute(MongoIterableImpl.java:135)
    at com.mongodb.client.internal.MongoIterableImpl.iterator(MongoIterableImpl.java:92)
    at dev.morphia.query.MorphiaQuery.prepareCursor(MorphiaQuery.java:335)
    at dev.morphia.query.MorphiaQuery.iterator(MorphiaQuery.java:202)
    at dev.morphia.query.MorphiaQuery.iterator(MorphiaQuery.java:197)
    at de.code.common.morphia.test.Test.main(Test.java:54)

And in the debugger i can see that the Document now already has mapped it to UUID: image

But in a later step he again tries to map the UUID to BsonBinary

Homer1991 commented 4 years ago

I also tried adding the UUID Codec but this makes no difference:

        CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
                CodecRegistries.fromProviders(new UuidCodecProvider(UuidRepresentation.STANDARD)),
                MongoClientSettings.getDefaultCodecRegistry());

        final Datastore datastore = Morphia.createDatastore(
                MongoClients.create(MongoClientSettings.builder()
                        .applyConnectionString(new ConnectionString("mongodb://127.0.0.1/"))
                        .uuidRepresentation(UuidRepresentation.STANDARD).codecRegistry(codecRegistry).build()),
                "morphia_example");
evanchooly commented 4 years ago

UUIDs should work out of the box in 2.0 since Morphia just delegates to the driver's codecs. Check the guide for more information: https://mongodb.github.io/mongo-java-driver/4.1/upgrading/

stzanakis commented 4 years ago

I'm facing a similar issue where I have an enum field inside an entity. Maybe it's related to the above issue.. That enum has to decode/encode using a final field inside the enum and not the name of the enum. With @Converters was working but with the new way of codecs it does not, but perhaps I'm doing something wrong. What I did is I created a codec CustomEnumCodec by extending the EnumCodec and I have applied it in the following way.

final CodecRegistry codecRegistry = CodecRegistries
        .fromRegistries(MongoClientSettings.getDefaultCodecRegistry(),
            CodecRegistries.fromCodecs(new CustomEnumCodec()));
MongoClientSettings.builder().codecRegistry(codecRegistry);

Is that correct?

Homer1991 commented 4 years ago

It's not about handeling UUID. The problem is that the field is not recognized as type UUID:

For example here:

@Entity
public class GenericEntity<T> {
    @Id
    protected T id;
    protected T test;
    protected UUID test2;
}

The field test2 works as intended. The problem is the recognition of generic types.

Homer1991 commented 4 years ago

Also as i mentioned here the driver seems to do everything correctly the field comes back as UUID but morphia then can not handle it: https://github.com/MorphiaOrg/morphia/issues/1481#issuecomment-684782384

Homer1991 commented 4 years ago

@evanchooly do you have any idea what the problem is here?

evanchooly commented 4 years ago

I started digging in to this last night actually. I have beginnings of an idea but I'll have to dig in a bit deeper.

Homer1991 commented 4 years ago

Nice thanks for looking into this.

Skjivar commented 4 years ago

@evanchooly I'm not sure if this a valid solution, but i figured out that after reversing the hirarchy set in dev.morphia.mapping.codec.pojo.EntityModelBuilder.java:241 protected void configure() the mapping works as expected. I used the 2.0.x branch.

    protected void configure() {
        TypeData<?> parentClassTypeData = null;
        Set<Class<?>> classes = buildHierarchy(type);
        Map<String, TypeParameterMap> propertyTypeParameterMap = new HashMap<>();

        List<Class<?>> classesArray = new ArrayList<Class<?>>(classes);
        Collections.reverse(classesArray);

        List<Annotation> annotations = new ArrayList<>();
        for (Class<?> klass : classesArray) {
            List<String> genericTypeNames = processTypeNames(klass);

            annotations.addAll(List.of(klass.getDeclaredAnnotations()));

            processFields(klass, parentClassTypeData, genericTypeNames, propertyTypeParameterMap);

            parentClassTypeData = TypeData.newInstance(klass.getGenericSuperclass(), klass);
        }
        annotations(annotations);
    }

and my testcase in the dev.morphia.test.TestMapping looks like

    @Test
    public void testRecursiveGeneric() {
        getMapper().map(SpecificEntity.class);

        SpecificEntity beforeDb = new SpecificEntity();
        beforeDb.setId(UUID.randomUUID());
        beforeDb.setTest(UUID.randomUUID());
        beforeDb.setTest2(UUID.randomUUID());

        getDs().save(beforeDb);
        SpecificEntity fromDB = getDs().find(SpecificEntity.class).filter(Filters.eq("_id", beforeDb.getId()))
                .iterator().next();

        assertEquals(beforeDb.getId(), fromDB.getId());
        assertEquals(beforeDb.getTest(), fromDB.getTest());
        assertEquals(beforeDb.getTest2(), fromDB.getTest2());
    }

    @Entity
    public static class GenericEntity<T> {
        @Id
        protected T id;
        protected T test;
        protected UUID test2;

        public T getId() {
            return id;
        }

        public void setId(T id) {
            this.id = id;
        }

        public T getTest() {
            return test;
        }

        public void setTest(T test) {
            this.test = test;
        }

        public UUID getTest2() {
            return test2;
        }

        public void setTest2(UUID test2) {
            this.test2 = test2;
        }
    }

    @Entity
    public static class SpecificEntity extends GenericEntity<UUID> {

    }
evanchooly commented 4 years ago

That is, more or less, what I'd discovered last night, too, before I was too tired to finish testing. :) Nice work.