spring数据mongodb两次调用save导致duplicate key异常

spring data mongodb calling save twice leads to duplicate key exception

我尝试使用 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);
    }

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

为什么没有保存,而是再次尝试插入?

相关:

Spring Data MongoRepository save causing Duplicate Key error


编辑:版本:

mongodb 4, spring 引导 2.3.3.RELEASE


编辑更多详细信息:

存储库:

public interface FooRepository extends MongoRepository<Foo, String> 

实体:

@Document
public class Foo {

    @Id
    private String id;
    private FooState state;


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

    
 ...

}

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;
        }
    }
}

编辑:预期行为

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

Save the object to the collection for the entity type of the object to save. This will perform an insert if the object is not already present, that is an 'upsert'.


编辑 - 新见解:

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

我用另一个实体创建了另一个类似的测试并且成功了。

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

@CompoundIndex(unique = true, name = "fooId_name", def = "{'fooId': 1, 'name': 1}")

更新:我想我发现了问题。由于我在转换器 (Document.parse()) 中使用自定义 serialization/deserialization,因此 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);
            }
        }
    }



}

所有 MongoDB 驱动程序都包含在客户端生成 ID 的功能。如果您只是为了获取 id 而保存,请研究如何使用 client-side id 生成并完全删除第一个保存。

如我上次编辑所述,问题出在自定义 serialization/deserialization 和 mongo 文档转换上。这导致 id 为空,因此执行了插入而不是更新插入。

以下代码是我自定义转换器映射对象 ID 的实现:

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);
        }
    }


}

因此得出结论:如果 id 不为空,则两次后续保存应该(并且将会)肯定会导致更新插入。该错误在我的代码中。

我相信您在尝试第二次保存而不从数据库中获取时遇到了这个问题。您正在更改保存返回的对象,而不是保存到数据库中的对象。尝试使用 findById 之类的方法检索现有的 foo,然后执行后续步骤并保存它