Spring 和 MongoDB:以更扁平的方式保存值对象

Spring and MongoDB: Saving Value Objects in a more flat way

对于我正在设计和开发的新 Spring 应用程序,出于多种技术原因,我们使用 MongoDB 作为持久层。这是我尝试实施一些 DDD 原则的第一个项目,包括值对象。我试图找到保存 ValueObject 的最佳方法,它实际上只是一个字符串。使用 Lombok 的 @Value,我的 Spring REST 控制器愉快地将值解析为 RestController 端的 ValueObject。但是当保存值时,它会以结构化的方式保存在 MongoDB 端。

例如

我的旁白:

@Value
public class PersonKey {
    private String value;
}

我将存储在 MongoDB 中的文档:

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

MongoDB 中保存的内容:

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

我真正想要的是:

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

当然有最少的额外样板代码.. :-)

看来您唯一的选择是使用 AbstractMongoEventListeneronAfterConvert 方法在转换后修改 DBObject。不幸的是,无法轻松更改文档中单个字段的转换。保存整个文档时使用自定义转换器,而不是单个字段。您也不能使用 getter 方法来替换字段访问("The fields of an object are used to convert to and from fields in the document. Public JavaBean properties are not used." 来自 http://docs.spring.io/spring-data/data-mongo/docs/1.4.2.RELEASE/reference/html/mapping-chapter.html)。因此,实现您想要的唯一方法是通过 mongodb 事件。但是,您可以在事件处理程序中使用反射来检查字段是否使用 @Value 注释进行注释,因此可以以更通用的方式转换它。如果存在 @Value 注释,只需将 DBObject 中的注释替换为它的值 属性.

为此,您需要扩展 AbstractMongoEventListener。您可以在此处查看带有 onBeforeSave 事件处理程序的示例:

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

更新: 正如@maartinus 在评论中注意到的那样,使用反射来搜索 @Value 对象将不起作用,因为它在运行时不可用(保留设置为 SOURCE)。因此,您需要使用单一方法 value() 引入您自己的注释或接口(例如 ValueObject),这将 return 对象的值。

巧合的是,我正在处理完全相同的问题。您实际上可以使用 CustomConverters 执行此操作。也许不完全像你想要的那样,但方式非常相似。我一直无法克服的唯一问题是您不能在运行时动态选择转换器。这是由于在行号 182 上实施了 CustomConversions#registerConversion

这个例子展示了我目前正在做的事情。我使用的是 GenericConverter,但您也可以回退到常规 Converters。此示例不检查 Field 注释。相反,它使用一个名为 SingleValue 的接口。我所有代表单个值的值对象都实现了这个接口。所以不需要在 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);
            }
        }
    }
}

这是我能找到的最优雅的方式。仍在寻找自动注册所有类型的方法。这可以通过注释和类路径扫描来实现。

请注意,此实现假设了一些事情

  1. 你有一个接口 SingleValue 和方法 getValue
  2. 你的每个 'single-valued' 值 类 都有一个带有内部类型表示的单个参数的构造函数(例如,UUID 的字符串)

编辑:我发布了错误的示例代码。已更新