spring data mongodb调用save两次导致重复键异常

Man*_*hek 6 java spring mongodb spring-data-mongodb spring-boot

我尝试使用 spring 数据 mongodb 存储库保存一个实体。我有一个级联保存的 EventListener。

问题是,我需要保存一个实体以获取其内部 id 并执行进一步的状态更改并在之后保存该实体。

 @Test
    void testUpdate() {
        FooDto fooDto = getResource("/json/foo.json", new TypeReference<FooDto>() {
        });
        Foo foo = fooMapper.fromDTO(fooDto);
        foo = fooService.save(foo);
        log.info("Saved foo: " + foo);
        foo.setState(FooState.Bar);
        foo = fooService.save(foo);
        log.info("Updated foo: " + foo);
    }
Run Code Online (Sandbox Code Playgroud)

我有一个关于 foo 子集合的索引。它不会更新子项,但会尝试将它们插入两次,从而导致 org.springframework.dao.DuplicateKeyException。

为什么它不保存而是尝试再次插入?

有关的:

Spring Data MongoRepository 保存导致重复键错误


编辑:版本:

mongodb 4、spring boot 2.3.3.RELEASE


编辑更多细节:

存储库:

public interface FooRepository extends MongoRepository<Foo, String> 
Run Code Online (Sandbox Code Playgroud)

实体:

@Document
public class Foo {

    @Id
    private String id;
    private FooState state;


    @DBRef
    @Cascade
    private Collection<Bar> bars = new ArrayList<>();

    
 ...

}
Run Code Online (Sandbox Code Playgroud)

CascadeMongoEventListener:

//from https://mflash.dev/blog/2019/07/08/persisting-documents-with-mongorepository/#unit-tests-for-the-accountrepository
public class CascadeMongoEventListener extends AbstractMongoEventListener<Object> {

    private @Autowired
    MongoOperations mongoOperations;

    public @Override void onBeforeConvert(final BeforeConvertEvent<Object> event) {
        final Object source = event.getSource();
        ReflectionUtils
                .doWithFields(source.getClass(), new CascadeSaveCallback(source, mongoOperations));
    }


    private static class CascadeSaveCallback implements ReflectionUtils.FieldCallback {

        private final Object source;
        private final MongoOperations mongoOperations;

        public CascadeSaveCallback(Object source, MongoOperations mongoOperations) {
            this.source = source;
            this.mongoOperations = mongoOperations;
        }

        public @Override void doWith(final Field field)
                throws IllegalArgumentException, IllegalAccessException {
            ReflectionUtils.makeAccessible(field);

            if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(Cascade.class)) {
                final Object fieldValue = field.get(source);

                if (Objects.nonNull(fieldValue)) {
                    final var callback = new IdentifierCallback();
                    final CascadeType cascadeType = field.getAnnotation(Cascade.class).value();

                    if (cascadeType.equals(CascadeType.PERSIST) || cascadeType.equals(CascadeType.ALL)) {
                        if (fieldValue instanceof Collection<?>) {
                            ((Collection<?>) fieldValue).forEach(mongoOperations::save);
                        } else {
                            ReflectionUtils.doWithFields(fieldValue.getClass(), callback);
                            mongoOperations.save(fieldValue);
                        }
                    }
                }
            }
        }
    }


    private static class IdentifierCallback implements ReflectionUtils.FieldCallback {

        private boolean idFound;

        public @Override void doWith(final Field field) throws IllegalArgumentException {
            ReflectionUtils.makeAccessible(field);

            if (field.isAnnotationPresent(Id.class)) {
                idFound = true;
            }
        }

        public boolean isIdFound() {
            return idFound;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:预期行为

来自 org.springframework.data.mongodb.core.MongoOperations#save(T) 中的文档:

将对象保存到要保存的对象的实体类型的集合中。如果对象不存在,这将执行插入,即“更新插入”。


编辑 - 新见解:

它可能与 Bar 子集合上的索引有关。(DbRef 和 Cascade 导致从 EventListener 调用 mongoOperations::save)

我用另一个实体创建了另一个类似的测试并且它起作用了。

子“Bar”实体的索引(在父“Foo”实体中作为集合保存):

@CompoundIndex(unique = true, name = "fooId_name", def = "{'fooId': 1, 'name': 1}")
Run Code Online (Sandbox Code Playgroud)

更新:我想我发现了问题。由于我在我的转换器 (Document.parse()) 中使用自定义序列化/反序列化,因此 id 字段未正确映射。这导致 id 为空,因此这会导致插入而不是更新。

如果我正确解决了这个问题,我会写一个答案。

public class MongoResultConversion {

    @Component
    @ReadingConverter
    public static class ToResultConverter implements Converter<Document, Bar> {

        private final ObjectMapper mapper;

        @Autowired
        public ToResultConverter(ObjectMapper mapper) {
            this.mapper = mapper;
        }

        public MeasureResult convert(Document source) {
            String json = toJson(source);
            try {
                return mapper.readValue(json, new TypeReference<Bar>() {
                });
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }


        protected String toJson(Document source) {
            return source.toJson();
        }

    }



    @Component
    @WritingConverter
    public static class ToDocumentConverter implements Converter<Bar, Document> {

        private final ObjectMapper mapper;

        @Autowired
        public ToDocumentConverter(ObjectMapper mapper) {
            this.mapper = mapper;
        }

        public Document convert(Bar source) {

            String json = toJson(source);
            return Document.parse(json);

        }

        protected String toJson(Bar source) {
            try {
                return mapper.writeValueAsString(source);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
    }



}
Run Code Online (Sandbox Code Playgroud)

Man*_*hek 5

正如我在上次编辑中所述,问题在于自定义序列化/反序列化和 mongo 文档转换。这导致 id 为空,因此完成了插入而不是 upsert。

以下代码是我用于映射 objectid 的自定义转换器的实现:

public class MongoBarConversion {

    @Component
    @ReadingConverter
    public static class ToBarConverter implements Converter<Document, Bar> {

        private final ObjectMapper mapper;

        @Autowired
        public ToBarConverter(ObjectMapper mapper) {
            this.mapper = mapper;
        }

        public Bar convert(Document source) {
            JsonNode json = toJson(source);
            setObjectId(source, json);
            return mapper.convertValue(json, new TypeReference<Bar>() {
            });
        }

        protected void setObjectId(Document source, JsonNode jsonNode) {
            ObjectNode modifiableObject = (ObjectNode) jsonNode;
            String objectId = getObjectId(source);
            modifiableObject.put(ID_FIELD, objectId);
        }

        protected String getObjectId(Document source) {
            String objectIdLiteral = null;
            ObjectId objectId = source.getObjectId("_id");
            if (objectId != null) {
                objectIdLiteral = objectId.toString();
            }
            return objectIdLiteral;
        }


        protected JsonNode toJson(Document source) {
            JsonNode node = null;
            try {
                String json = source.toJson();
                node = mapper.readValue(json, JsonNode.class);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            return node;
        }

    }


    @Component
    @WritingConverter
    public static class ToDocumentConverter implements Converter<Bar, Document> {

        private final ObjectMapper mapper;

        @Autowired
        public ToDocumentConverter(ObjectMapper mapper) {
            this.mapper = mapper;
        }

        public Document convert(Bar source) {
            try {
                JsonNode jsonNode = toJson(source);
                setObjectId(source, jsonNode);
                String json = mapper.writeValueAsString(jsonNode);
                return Document.parse(json);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }

        protected void setObjectId(Bar source, JsonNode jsonNode) throws JsonProcessingException {
            ObjectNode modifiableObject = (ObjectNode) jsonNode;
            JsonNode objectIdJson = getObjectId(source);
            modifiableObject.set("_id", objectIdJson);
            modifiableObject.remove(ID_FIELD);
        }

        protected JsonNode getObjectId(Bar source) throws JsonProcessingException {
            ObjectNode _id = null;
            String id = source.getId();
            if (id != null) {
                _id = JsonNodeFactory.instance.objectNode();
                _id.put("$oid", id);
            }
            return _id;
        }

        protected JsonNode toJson(Bar source) {
            return mapper.convertValue(source, JsonNode.class);
        }
    }


}
Run Code Online (Sandbox Code Playgroud)

所以总结一下:如果 id 不为空,那么两次后续保存应该(并且将会)肯定会导致一个 upsert。该错误在我的代码中。