You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 12:55:02