4

For a new Spring application I'm designing and developing, we're using MongoDB as the persistence layer for a number of technical reasons. This is the first project where I'm trying to implement some DDD principles, including Value Objects. I'm trying to find the best way to save a ValueObject which is in fact simply a String. Using Lombok's @Value, my Spring REST Controller happily parses a value into a ValueObject on the RestController side. But then when saving the value, it gets saved in a structured way on the MongoDB side.

For example

My VO:

@Value
public class PersonKey {
    private String value;
}

The document I'll be storing in MongoDB:

@Document
public class PersonDocument {
    private PersonKey personKey;
    private Name name;
    ...
}

What gets saved in MongoDB:

{.. "personKey": {"value": "faeeaf2"} ...}

What I actually want:

{.. "personKey": "faeeaf2" ..}

Of course with minimal extra boilerplate code.. :-)

Kristof
  • 1,684
  • 2
  • 23
  • 49
  • In `PHP` I've done a converter class that get called for every `insert`, `update` and `find` mongodb operation. That converter is recursive and uses reflection. – Constantin Galbenu Apr 07 '17 at 09:44
  • That's PHP though, a dynamic language. I'd daresay it's different for Java as a static language :-) – Kristof Apr 07 '17 at 09:49
  • That's why I haven't provided an answer. Maybe it helps you. – Constantin Galbenu Apr 07 '17 at 09:51
  • It is possible to add custom converters which help do the translation back and from the MongoDB layer, as explained in http://docs.spring.io/spring-data/data-mongo/docs/1.4.2.RELEASE/reference/html/mapping-chapter.html which might be a solution, but is not dynamic enough for my liking – Kristof Apr 07 '17 at 09:56

2 Answers2

1

It seems your only option is to use AbstractMongoEventListener with onAfterConvert method to modify the DBObject after conversion. Unforunately, it's not possible to easily change the conversion of single field in the document. Custom converters are used when saving entire document, not single fields. You also cannot use getter methods to replace field access ("The fields of an object are used to convert to and from fields in the document. Public JavaBean properties are not used." from http://docs.spring.io/spring-data/data-mongo/docs/1.4.2.RELEASE/reference/html/mapping-chapter.html). So, the only way to achieve what you want is through mongodb events. However, you can use reflection in the event handler to check if field is annotated with @Value annotation, so it's possible to convert it in more generic way. If @Value annotation is present, simply replace it in DBObject with it's value property.

To achieve this, you need to extend the AbstractMongoEventListener. You can see the example here with onBeforeSave event handler:

https://github.com/ttrelle/spring-data-examples/blob/master/springdata-mongodb/src/main/java/mongodb/OrderBeforeSaveListener.java

Update: As @maartinus noticed in the comments, using reflection to search for @Value objects will not work, because it's not available in the runtime (retention set to SOURCE). Therefore, you need to introduce your own annotation or interface (e.g. ValueObject) with single method value() that will return the value of the object.

Mike Wojtyna
  • 751
  • 5
  • 10
0

Coincidentally I am working on exactly the same problem. And you can actually do this with CustomConverters. Maybe not exactly like you want but in a very similar way. The only problem I have been unable to overcome is that you can't dynamically choose Converters at runtime. This is due to implementation of CustomConversions#registerConversion on line number 182.

This example shows what I am currently working on. I am using a GenericConverter but you can also fallback to regular Converters. This example doesn't check on Field annotations. Rather it uses an interface called SingleValue. All of my Value Objects that represent a single value implement this interface. So no need for an annotation on the Field.

@Configuration
@Slf4j
public class MongoDbConfiguration {

    @Bean
    @Primary
    public CustomConversions mongoCustomConversions() throws Exception {
        final List converterList = new ArrayList<>();
        converterList.add(new SingleValueConverter());
        return new CustomConversions(converterList);
    }

    private static class SingleValueConverter implements GenericConverter {

        @Override
        public Set<ConvertiblePair> getConvertibleTypes() {
            return new HashSet<>(Arrays.asList(new ConvertiblePair[]{
                    new ConvertiblePair(UUIDEntityReference.class, String.class),
                    new ConvertiblePair(String.class, FundingProcessId.class)
                    // put here all of your type conversions (two way!)
            }));
        }

        @Override
        public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            try {
                // first check if the instance of this type is an instance of our 'SingleValue' interface
                if (source instanceof SingleValue) {
                    return ((SingleValue) source).getValue();
                }

                final Class<?> objectType = targetType.getType();
                final Constructor<?> constructor = objectType.getConstructor(sourceType.getType());
                return constructor.newInstance(source);
            } catch (ReflectiveOperationException e) {
                throw new RuntimeException("Could not convert unexpected type " +
                        sourceType.getObjectType().getName() + " to " + targetType.getObjectType().getName(), e);
            }
        }
    }
}

This is the most elegant way I could find. Still looking for way to automate the registration of all types. This may be possible with annotations and classpath scanning.

Note that this implementation assumes a couple of things

  1. You have an interface SingleValue with a method getValue
  2. Each of your 'single-valued' Value classes have a constructor with a single argument of the internal type representation (e.g. String for a UUID)

EDIT: I posted the wrong example code. Updated it

sdegroot
  • 237
  • 3
  • 11