Spring Data Mongo Reactive预加载嵌套@DbRef抛出MappingException问题
MongoDB关联查询类型转换问题排查
问题背景
我有三个MongoDB集合:User、UserPlot和Plot。User集合通过@DBRef关联UserPlot列表,UserPlot集合通过@DBRef关联Plot。需求是查询所有User时,预加载其关联的UserPlot,同时预加载UserPlot关联的Plot。在UserRepository中使用@Aggregation编写findAllIncludePlots方法后,调用时抛出org.springframework.data.mapping.MappingException,提示无法将ArrayList类型转换为Plot类型。
相关代码与数据
实体类代码
@Data @NoArgsConstructor @Document(collection = "users") public class User { @Id private String id; @Indexed(name = "ui_email", unique = true) private String email; private String password; @Indexed(name = "ui_first_name") private String firstName; @Indexed(name = "ui_last_name") private String lastName; @DBRef private List<UserPlot> plots; } @Data @Document(collection = "users_have_plots") public class UserPlot { @Id private String id; private String userId; @DBRef private Plot plot; } @Data @Document(collection = "plots") @NoArgsConstructor public class Plot { @Id private String id; private Integer x; private Integer y; }
Repository代码
@Repository public interface UserRepository extends ReactiveMongoRepository<User, String> { @Aggregation(pipeline = { "{$lookup:{from:'users_have_plots',let:{plots:'$plots'},pipeline:[{$match:{$expr:{$in:['$_id',{$ifNull:['$$plots.$id',[]]}]}}},{$lookup:{from:'plots',localField:'plot.$id',foreignField:'_id',as:'plot'}}],as:'plots'}}" }) Flux<User> findAllIncludePlots(); }
启动类代码
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class) @EnableReactiveMongoRepositories public class FarmApplication { public static void main(String[] args) { SpringApplication.run(FarmApplication.class, args); } @Bean CommandLineRunner t(UserRepository userRepository) { return a -> { userRepository.findAllIncludePlots().map(User::getPlots).doOnNext(System.out::println).subscribe(); }; } }
错误堆栈信息
org.springframework.data.mapping.MappingException: Cannot convert [Document{{_id=6426b069ca83da130cdb7f70, x=0, y=0, _class=com.vilya.farm.domain.model.Plot}}] of type class java.util.ArrayList into an instance of class com.vilya.farm.domain.model.Plot; Implement a custom Converter<class java.util.ArrayList, class com.vilya.farm.domain.model.Plot> and register it with the CustomConversions; Parent object was: UserPlot(id=6426b073ca83da130cdb7f71, userId=6412c76956d4170a7de34d92, plot=null) -> User(id=6412c76956d4170a7de34d92, email=abc@abc.com, password=$2a$10$s9VgOYd.fOKZF66TnAsjWemiCYkA7aG45NJpuSNgbVxpcIGF7fWqu, firstName=f, lastName=l, plots=null) -> null Caused by: org.springframework.data.mapping.MappingException: Cannot convert [Document{{_id=6426b069ca83da130cdb7f70, x=0, y=0, _class=com.vilya.farm.domain.model.Plot}}] of type class java.util.ArrayList into an instance of class com.vilya.farm.domain.model.Plot; Implement a custom Converter<class java.util.ArrayList, class com.vilya.farm.domain.model.Plot> and register it with the CustomConversions; Parent object was: UserPlot(id=6426b073ca83da130cdb7f71, userId=6412c76956d4170a7de34d92, plot=null) -> User(id=6412c76956d4170a7de34d92, email=abc@abc.com, password=$2a$10$s9VgOYd.fOKZF66TnAsjWemiCYkA7aG45NJpuSNgbVxpcIGF7fWqu, firstName=f, lastName=l, plots=null) -> null at org.springframework.data.mongodb.core.convert.MappingMongoConverter$DefaultConversionContext.convert(MappingMongoConverter.java:2292) ~[spring-data-mongodb-4.0.0.jar:4.0.0] ...
MongoDB数据示例
vilya_be> db.plots.find({}) [ { _id: ObjectId("6426b069ca83da130cdb7f70"), x: 0, y: 0, _class: 'com.vilya.farm.domain.model.Plot' } ] vilya_be> db.users_have_plots.find({}) [ { _id: ObjectId("6426b073ca83da130cdb7f71"), userId: '6412c76956d4170a7de34d92', plot: DBRef("plots", ObjectId("6426b069ca83da130cdb7f70")), _class: 'com.vilya.farm.domain.model.UserPlot' } ] vilya_be> db.users.find({}) [ { _id: ObjectId("6412c76956d4170a7de34d92"), email: 'abc@abc.com', password: '$2a$10$s9VgOYd.fOKZF66TnAsjWemiCYkA7aG45NJpuSNgbVxpcIGF7fWqu', firstName: 'f', lastName: 'l', plots: [ DBRef("users_have_plots", ObjectId("6426b073ca83da130cdb7f71")) ], _class: 'com.vilya.farm.domain.model.User' }, { _id: ObjectId("6414667360e4ba4481052627"), email: 'abc1@abc.com', password: '$2a$10$OP52phZ61l2JX2e2TQOu9ubYFBYcPeqEZ92ox2Nyyp5e.MEZk7GhS', firstName: 'f', lastName: 'l', _class: 'com.vilya.farm.domain.model.User' } ]
MongoDB版本:6.0.2-focal(Docker Desktop运行)
问题分析与解决
错误原因
嵌套的$lookup执行后,plot字段被赋值为一个ArrayList(因为$lookup的as参数默认返回数组),但实体类UserPlot中的plot是单个Plot对象,Spring Data MongoDB无法将数组转换为单个对象,因此抛出类型转换异常。
修正方案
修改@Aggregation的管道,在查询Plot后,将数组形式的plot转换为单个对象。可以使用$arrayElemAt取出数组的第一个元素,适配实体类的字段类型。
修正后的Repository代码:
@Repository public interface UserRepository extends ReactiveMongoRepository<User, String> { @Aggregation(pipeline = { "{$lookup:{from:'users_have_plots',let:{plots:'$plots'},pipeline:[", "{$match:{$expr:{$in:['$_id',{$ifNull:['$$plots.$id',[]]}]}}},", "{$lookup:{from:'plots',localField:'plot.$id',foreignField:'_id',as:'plot'}}," , "{$addFields:{plot:{$arrayElemAt:['$plot',0]}}}" , // 将数组转成单个对象 "],as:'plots'}}" }) Flux<User> findAllIncludePlots(); }
额外说明
$arrayElemAt:['$plot',0]会提取plot数组的第一个元素,符合UserPlot中单个Plot对象的映射要求。- 如果存在UserPlot没有关联Plot的情况,
$arrayElemAt会返回null,与实体类中的plot字段允许空值的定义匹配,不会引发额外异常。
内容的提问来源于stack exchange,提问作者SoT
相关产品推荐
相关产品推荐

