Spring Data MongoDB实现@DBRef级联存储

318 阅读3分钟

使用@DBRef时默认只会保存当前对象中的@DBRef对象,而不会保存嵌套对象中被@DBRef注解的类,例如:

//用户
@Document(collection = "Persons")
@Data
public class Person {
    @DBRef(lazy = true)
    private Book book;
}
//图书
@Document(collection = "Books")
@Data
public class Book {
    @DBRef(lazy = true)
    private Chapter chapter;
}
//章节
@Document(collection = "Chapters")
@Data
public class Chapter {...}

当使用上面的示例保存Person时,会报以下错误,原因是:被引用的对象的id属性为null

org.springframework.data.mapping.MappingException: Cannot create a reference to an object with a NULL id.

出现MappingException异常的原因:

  1. 被引用的对象在保存到数据库之前没有生成id。确保在保存被引用对象之前为其生成一个唯一的id。
  2. 被引用的对象在数据库中的id属性为null。在使用@DBRef进行查询时,被引用对象的id属性不能为空,否则无法创建引用。
  3. 引用关系配置错误。请确保在实体类之间的引用关系配置正确,包括正确使用@DBRef注解,以及引用对象的id属性和关联字段的映射关系正确。

AbstractMongoEventListener是什么?

AbstractMongoEventListener是Spring Data MongoDB框架提供的一个抽象类,用于监听MongoDB的事件并执行相应的操作。它提供了一组钩子方法,可以在MongoDB的不同事件发生时进行回调。

AbstractMongoEventListener的主要作用是允许开发人员在MongoDB的不同操作(如插入、更新、删除等)发生时执行自定义的逻辑。通过继承AbstractMongoEventListener并覆盖相应的方法,我们可以在特定操作之前、之后或在特定的操作上执行自定义操作。具体来说,AbstractMongoEventListener提供了以下一些方法:

  1. onBeforeConvert: 当将领域对象转换为持久化文档之前,调用此方法。
  2. onBeforeSave: 当将领域对象保存到数据库之前,调用此方法。
  3. onAfterSave: 在将领域对象保存到数据库之后,调用此方法。
  4. onAfterLoad: 在从数据库加载领域对象后,调用此方法。
  5. onBeforeDelete: 在删除领域对象之前,调用此方法。
  6. onAfterDelete: 在删除领域对象之后,调用此方法。

通过覆盖这些方法,可以实现一些额外的业务逻辑,如在保存对象之前进行数据验证、在对象保存之后执行一些后处理操作等。此外,AbstractMongoEventListener还可以扩展为自定义事件监听器,监听其他MongoDB事件(如索引创建、索引删除等)。

需要注意的是,为了使用AbstractMongoEventListener,我们需要将其注册为Spring的Bean,并将其与MongoTemplate或MongoRepository关联起来,以便框架在适当的时机调用监听器的方法。

自定义监听onBeforeConvert,实现级联存储

import com.sun.istack.internal.NotNull;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
​
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.List;
​
/**
 * mongoDB实现级联存储,支持List列表级联存储
 */
@Component
public class CascadingMongoEventListener extends AbstractMongoEventListener {
​
    @Resource
    private MongoOperations mongoOperations;
​
    @Override
    public void onBeforeConvert(BeforeConvertEvent event) {
        Object document = event.getSource();
        ReflectionUtils.doWithFields(document.getClass(), new CascadeCallback(document, mongoOperations));
    }
    private static class CascadeCallback implements ReflectionUtils.FieldCallback {
        private final Object source;
        private final MongoOperations mongoOperations;
        public CascadeCallback(Object source, MongoOperations mongoOperations) {
            this.source = source;
            this.mongoOperations = mongoOperations;
        }
        @Override
        public void doWith(@NotNull Field field) throws IllegalArgumentException, IllegalAccessException {
            ReflectionUtils.makeAccessible(field);
            if (!field.isAnnotationPresent(DBRef.class)) {
                return;
            }
            Object fieldValue = field.get(source);
            if(fieldValue == null) {
                return;
            }
            if(fieldValue instanceof List) {
                List<?> list = (List<?>) fieldValue;
                list.forEach(mongoOperations::save);
            } else {
                mongoOperations.save(fieldValue);
            }
        }
    }
}

使用

创建 PersonBookChapter

@Document(collection = "Persons")
@Data
public class Person {
    @JsonSerialize(using = ObjectIdSerializer.class)
    @JsonDeserialize(using = ObjectIdDeserializer.class)
    @Id
    private ObjectId id;
    private String name;
    @DBRef(lazy = true)
    private Book book;//支持List<Book>
}
​
@Document(collection = "Books")
@Data
public class Book {
​
    @JsonSerialize(using = ObjectIdSerializer.class)
    @JsonDeserialize(using = ObjectIdDeserializer.class)
    @Id
    private ObjectId id;
    private String name;
    private Double price;
    @DBRef(lazy = true)
    private Chapter chapter;//支持List<Chapter>
}
​
@Document(collection = "Chapters")
@Data
public class Chapter {
    @JsonSerialize(using = ObjectIdSerializer.class)
    @JsonDeserialize(using = ObjectIdDeserializer.class)
    @Id
    private ObjectId id;
    private String name;
    private String content;
}

验证

curl --location 'http://localhost:8080/person' \
--header 'Content-Type: application/json' \
--data '{
    "name":"小明",
    "book":{
            "name":"Go语言入门",
            "price":39.9,
            "chapter":{
                "name":"第一章",
                "content":"认识GO初级语法"
            }
    }
}'

总结

使用@DBRef进行级联存储的方式就是通过继承AbstractMongoEventListener并重写onBeforeConvert方法在保存文档之前先保存子对象,就可以实现级联存储,需要注意,使用当前配置会全局生效,如果想要自定义级联存储可以使用自定义注解实现,扫描指定注解后进行级联存储。