spring-data-mongodb从2.x升级至3.x版本,aggregate group操作,分组字段出现_id前缀的解决方式

背景

  由于Spring boot安全漏洞,须将项目中Spring boot升级至2.3.4版本,2.3.4版本集成了spring-data-mongodb 3.x版本,项目中原spring-data-mongodb 2.x版本被替换成spring-data-mongodb 3.x版本,须进行兼容性适配。
  spring-data-mongodb从2.x升级至3.x版本部分api变化见官网项目链接

问题描述

  适配过程中发现,项目中存在大量aggregate group操作,例如:

TypedAggregation<A> aggregation = Aggregation.newAggregation(A.class,Aggregation.match(Criteria.and("q").in(...)),Aggregation.group("w", "e").sum("totalcount").as("SumTotal").sum("coveragecount").as("SumCoverage"));
AggregationResults<BasicDBObject> aggregate = mongoTemplateProvince.aggregate(aggregation, BasicDBObject.class);
List<BasicDBObject> mappedResults = aggregate.getMappedResults();

  获取group结果返回给前端页面。
  spring-data-mongodb 2.x版本中,mappedResults结果会对group的属性进行处理,去除结果集group属性之前的_id前缀,例如上述代码中,返回结果集字段中是"w",“e"而非”_id.w","_id.e"
spring-data-mongodb 3.x版本中则不会进行处理,直接返回"_id.w","_id.e"

排查过程

  查看源码发现在MongoTemplate#doAggregate(Aggregation aggregation, String collectionName, Class outputType,AggregationOperationContext context)方法中,结果集返回时有做Callback处理。
spring-data-mongodb 2.x版本中使用UnwrapAndReadDocumentCallback内部类,代码如下:

class UnwrapAndReadDocumentCallback<T> extends ReadDocumentCallback<T> {

    public UnwrapAndReadDocumentCallback(EntityReader<? super T, Bson> reader, Class<T> type, String collectionName) {
        super(reader, type, collectionName);
    }

    @Override
    public T doWith(@Nullable Document object) {

        if (object == null) {
            return null;
        }

        Object idField = object.get(Fields.UNDERSCORE_ID);

        if (!(idField instanceof Document)) {
            return super.doWith(object);
        }

        Document toMap = new Document();
        Document nested = (Document) idField;
        toMap.putAll(nested);

        for (String key : object.keySet()) {
            if (!Fields.UNDERSCORE_ID.equals(key)) {
                toMap.put(key, object.get(key));
            }
        }

        return super.doWith(toMap);
    }
}

  其中对_id为Document类的实例对象进行处理,将_id对象中的属性拷贝到一个新Document中,从而去除结果集中_id前缀。
spring-data-mongodb 3.x版本中使用ReadDocumentCallback内部类,代码如下:

private class ReadDocumentCallback<T> implements DocumentCallback<T> {

		private final @NonNull EntityReader<? super T, Bson> reader;
		private final @NonNull Class<T> type;
		private final String collectionName;

		@Nullable
		public T doWith(@Nullable Document document) {

			T source = null;

			if (document != null) {
				maybeEmitEvent(new AfterLoadEvent<>(document, type, collectionName));
				source = reader.read(type, document);
			}

			if (source != null) {
				maybeEmitEvent(new AfterConvertEvent<>(document, source, collectionName));
				source = maybeCallAfterConvert(source, document, collectionName);
			}

			return source;
		}
	}

  基于Spring Application Event方式对返回结果进行处理,继续查看AfterConvertEvent源码:

public class AfterLoadEvent<T> extends MongoMappingEvent<Document> {

	private static final long serialVersionUID = 1L;
	private final Class<T> type;

	/**
	 * Creates a new {@link AfterLoadEvent} for the given {@link Document}, type and collectionName.
	 *
	 * @param document must not be {@literal null}.
	 * @param type must not be {@literal null}.
	 * @param collectionName must not be {@literal null}.
	 * @since 1.8
	 */
	public AfterLoadEvent(Document document, Class<T> type, String collectionName) {

		super(document, document, collectionName);

		Assert.notNull(type, "Type must not be null!");
		this.type = type;
	}

	/**
	 * Returns the type for which the {@link AfterLoadEvent} shall be invoked for.
	 *
	 * @return never {@literal null}.
	 */
	public Class<T> getType() {
		return type;
	}
}

  其中调用父类构造方法,父类构造方法源码如下

public MongoMappingEvent(T source, @Nullable Document document, @Nullable String collectionName) {

		super(source);
		this.document = document;
		this.collectionName = collectionName;
	}

  可看出直接进行对象赋值,未对结果集进行特殊处理——返回结果集中group属性包含_id前缀。

解决方法

  综上,发现问题出现原因,由于项目中存在大量aggregate group查询逻辑,全部更改工作量太大。考虑兼容性适配。
  因为spring-data-mongodb 3.x版本ReadDocumentCallback类中使用Application Event方式对结果集进行处理,考虑注入一个自定义的Bean,重写其AfterLoadEvent方法,在方法中提取出_id对象的属性放入结果对象中,保证原业务处理代码逻辑不变。自定义Application Event如下,继承AfterConvertCallback类,重写onAfterConvert方法:

@Component
public class CzxMongoAfterConvertCallback implements AfterConvertCallback {

    @Override
    public Object onAfterConvert(Object entity, Document document, String collection) {
        if(entity == null) {
            return null;
        }
        if (entity instanceof BasicDBObject) {
            BasicDBObject bs = (BasicDBObject) entity;
            Map id = MapUtils.getMap(document, Fields.UNDERSCORE_ID);
            if (MapUtil.isNotEmpty(id) && id.size() > 1) {
                bs.putAll(id);
                return bs;
            }
        } else if (entity instanceof Document) {
            Map id = MapUtils.getMap(document, Fields.UNDERSCORE_ID);
            if (MapUtil.isNotEmpty(id) && id.size() > 1) {
                document.putAll(id);
                return document;
            }
        }
        return entity;
    }
}

onAfterConvert方法中提取_id的属性赋值到新的实体对象中。
  Spring boot在启动时会自动加载ApplicationContext中的CzxMongoAfterConvertCallback Bean覆盖原AfterLoadEvent实例。至此spring-data-mongodb从2.x升级至3.x版本,aggregate group操作,分组字段出现_id前缀的兼容性问题得到解决。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值