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)
正如我在上次编辑中所述,问题在于自定义序列化/反序列化和 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。该错误在我的代码中。
| 归档时间: |
|
| 查看次数: |
1181 次 |
| 最近记录: |