背景

在使用 MongoTemplate 进行 upsert 操作时,发现mongodb表中,同一个字段出现了两种命名,如下:

mongoTemplate查询并仅返回MongoDB文档中的某个字段 mongotemplate返回指定字段_字段名

说明:upsert方法中的参数,为 mongodb 中自带Update对象,且该对象的字段使用的是实体类的字段名,实体类如下:

@Document(collection = ElementDO.COLLECTION_NAME)
public class ElementDO {
 
    public static final String COLLECTION_NAME = "app_ids_element";
 
    @Id
    private String id;
 
    /**
     * 元素ID
     */
    @Field("element_id")
    @Indexed(unique = true, background = true)
    private String elementId;
 
    /**
     * 平台类型
     */
    @Field("platform_type")
    private PlatformTypeEnum platformType;
 
    /**
     * 探针ID
     */
    @Field("agent_id")
    private String agentId;
 
    /**
     * 元素详情
     */
    private ElementBase detail;
 
}

 

 

问题分析

出现如上问题,第一反应就是实体类字段的@Field注解中别名未生效,那为啥没有生效呢?带着这个疑问,又衍生了几个问题:

  1. 之前使用 save(ElementDO element)时,别名是怎么生效的?
  2. upsert 在什么情况下,别名才会生效?

 

源码分析

save方法

1、save方法入口:

protected <T> T doSave(String collectionName, T objectToSave, MongoWriter<T> writer) {
    objectToSave = maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName)).getSource();
    objectToSave = maybeCallBeforeConvert(objectToSave, collectionName);
 
    // 通过objectToSave对象类型,得到对应的AdaptibleEntity子类
   AdaptibleEntity<T> entity = operations.forEntity(objectToSave, mongoConverter.getConversionService());
    entity.assertUpdateableIdIfNotSet();
 
    // objectToSave对象转Document对象
    MappedDocument mapped = entity.toMappedDocument(writer);
    Document dbDoc = mapped.getDocument();
 
    maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName));
    objectToSave = maybeCallBeforeSave(objectToSave, dbDoc, collectionName);
 
    // 保存,返回主键ID
    Object id = saveDocument(collectionName, dbDoc, objectToSave.getClass());
 
    // 向入参对象中回写主键ID
    T saved = populateIdIfNecessary(objectToSave, id);
    maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName));
 
    return maybeCallAfterSave(saved, dbDoc, collectionName);
}

2、继续深入:

@Override
public MappedDocument toMappedDocument(MongoWriter<? super T> writer) {
    T bean = propertyAccessor.getBean();
 
    Document document = new Document();
     
    // 通过MappingMongoConverter将javabean转为Document
    writer.write(bean, document);
 
    if (document.containsKey(ID_FIELD) && document.get(ID_FIELD) == null) {
        document.remove(ID_FIELD);
    }
 
    return MappedDocument.of(document);
}

说明:Document为 Map 和 Bson 的子类,也是由一个个的 key: value 组成

3、继续深入:

protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation<?> typeHint) {
    if (null == obj) {
        return;
    }
 
    Class<?> entityType = obj.getClass();
    Optional<Class<?>> customTarget = conversions.getCustomWriteTarget(entityType, Document.class);
 
    if (customTarget.isPresent()) {
        Document result = doConvert(obj, Document.class);
        BsonUtils.addAllToMap(bson, result);
        return;
    }
 
    // 如果为Map对象:遍历map中的每个key,如果有value为javabean,递归走到该方法
    if (Map.class.isAssignableFrom(entityType)) {
        writeMapInternal((Map<Object, Object>) obj, bson, ClassTypeInformation.MAP);
        return;
    }
 
    // 如果为Collection对象:遍历集合中的每个元素,如果有元素为javabean,递归走到本方法
    if (Collection.class.isAssignableFrom(entityType)) {
        writeCollectionInternal((Collection<?>) obj, ClassTypeInformation.LIST, (Collection<?>) bson);
        return;
    }
 
    // javabean转为可持久化的entity对象
    MongoPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(entityType);
 
    // 将entity转为bson对象:如果对象中有嵌套对象,也会递归下面的writeInternal方法
    writeInternal(obj, bson, entity);
    addCustomTypeKeyIfNecessary(typeHint, obj, bson);
}

说明:writeInternal方法:当_id的值为空时,会从bson中删除该key

4、继续深入上一步的转化,最终会走到:

public void doWithProperties(PropertyHandler<P> handler) {
 
    Assert.notNull(handler, "PropertyHandler must not be null!");
 
    // 循环对实体类中的所有字段进行处理
    for (P property : persistentPropertiesCache) {
        handler.doWithPersistentProperty(property);
    }
}

说明:persistentPropertiesCache 中的原始数据,是通过 BeanUtils.getPropertyDescriptors(classType) 得到的

5、继续深入:

private void assertUniqueness(MongoPersistentProperty property) {
    // 获取保存到mongodb中的字段名
    String fieldName = property.getFieldName();
    MongoPersistentProperty existingProperty = properties.get(fieldName);
 
    if (existingProperty != null) {
        throw new MappingException(
                String.format(AMBIGUOUS_FIELD_MAPPING, property.toString(), existingProperty.toString(), fieldName));
    }
 
    properties.put(fieldName, property);
}

6、继续深入,看到获取字段名的方法:

public String getFieldName() {
    // 主键ID
    if (isIdProperty()) {
        if (getOwner().getIdProperty() == null) {
            return ID_FIELD_NAME;
        }
 
        if (getOwner().isIdProperty(this)) {
            return ID_FIELD_NAME;
        }
    }
 
    // 有@Field注解的字段:取注解中的别名
    if (hasExplicitFieldName()) {
        return getAnnotatedFieldName();
    }
 
    String fieldName = fieldNamingStrategy.getFieldName(this);
 
    if (!StringUtils.hasText(fieldName)) {
        throw new MappingException(String.format("Invalid (null or empty) field name returned for property %s by %s!",
                this, fieldNamingStrategy.getClass()));
    }
 
    return fieldName;
}

 

 

update方法

update方法的入参,一般都是Update对象,该对象内部有一个Map对象,所以数据结构类似于Map。

1、更新入口:

protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update,
            @Nullable Class<?> entityClass, boolean upsert, boolean multi) {
 
    MongoPersistentEntity<?> entity = entityClass == null ? null : getPersistentEntity(entityClass);
 
    UpdateContext updateContext = multi ? queryOperations.updateContext(update, query, upsert)
            : queryOperations.updateSingleContext(update, query, upsert);
    updateContext.increaseVersionForUpdateIfNecessary(entity);
 
    // 获取查询条件的Document对象
    Document queryObj = updateContext.getMappedQuery(entity);
    UpdateOptions opts = updateContext.getUpdateOptions(entityClass);
 
    // 获取更新对象的Document对象
    Document updateObj = updateContext.getMappedUpdate(entity);
    MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.UPDATE, collectionName, entityClass,
            updateObj, queryObj);
             
    // ...
}

2、循环Update对象中的每个key,然后再计算对应的持久化字段名

public Document getMappedObject(Bson query, @Nullable MongoPersistentEntity<?> entity) {
    Document result = new Document();
    for (String key : BsonUtils.asMap(query).keySet()) {
        Field field = createPropertyField(entity, key, mappingContext);
 
        if (field.getProperty() != null && field.getProperty().isUnwrapped()) {
 
            Object theNestedObject = BsonUtils.get(query, key);
            Document mappedValue = (Document) getMappedValue(field, theNestedObject);
            if (!StringUtils.hasText(field.getMappedKey())) {
                result.putAll(mappedValue);
            } else {
                result.put(field.getMappedKey(), mappedValue);
            }
        } else {
            // 循环每个key,计算可持久化的key
            Entry<String, Object> entry = getMappedObjectForField(field, BsonUtils.get(query, key));
            result.put(entry.getKey(), entry.getValue());
        }
    }
 
    return result;
}

3、继续深入:

protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity<?> entity) {
    if(entity != null && entity.isUnwrapped()) {
        return converter.convertToMongoType(source, entity);
    }
 
    return converter.convertToMongoType(source,
            entity == null ? ClassTypeInformation.OBJECT : getTypeHintForEntity(source, entity));
}

4、继续深入:

public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation<?> typeInformation) {
    // 如果是基础类型,直接使用key作为可持久化字段名
    if (conversions.isSimpleType(obj.getClass())) {
        Class<?> conversionTargetType;
        if (typeInformation != null && conversions.isSimpleType(typeInformation.getType())) {
            conversionTargetType = typeInformation.getType();
        } else {
            conversionTargetType = Object.class;
        }
        return getPotentiallyConvertedSimpleWrite(obj, conversionTargetType);
    }
 
    if (obj instanceof Document) {
        Document newValueDocument = new Document();
        for (String vk : ((Document) obj).keySet()) {
            Object o = ((Document) obj).get(vk);
            newValueDocument.put(vk, convertToMongoType(o, typeInformation));
        }
        return newValueDocument;
    }
 
    if (obj instanceof Map) {
        Document result = new Document();
        for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) obj).entrySet()) {
            result.put(entry.getKey().toString(), convertToMongoType(entry.getValue(), typeInformation));
        }
        return result;
    }
 
    // 如果是javabean
    Document newDocument = new Document();
    this.write(obj, newDocument);
 
    return !obj.getClass().equals(typeInformation.getType()) ? newDocument : removeTypeInfo(newDocument, true);
}

说明:如果key的类型为 javabean,则和上面 save 方法第3步之后的流程一样了

 

upsert方法

upsert方法和update方法的入口是同一个,之后的流程也是差不多的,这里就不再重复分析~

 

结论

save方法:

  • 入参为一个javabean时:
  • 如果字段上有@Field注解,则使用注解中的别名作为mongo表字段名
  • 如果字段上没有@Field注解,则直接使用属性名
  • 入参为一个Map对象时:
  • 如果key的类型为基础类型,则直接使用key作为作为mongo表字段名,
  • 如果key为为嵌套对象,那做法跟javabean是一样的

update/upsert方法:

  • Update对象类似于Map结构,所以做法跟上面的save(Map)类似

 

其他知识点

save和insert区别

save和insert方法的区别就是:

  • save 方法:先判断文档是否存在(根据文档的_id字段进行匹配),存在则执行更新操作,否则执行插入操作。
  • insert 方法:只执行插入操作,如果文档已经存在(根据文档的_id字段进行匹配),会抛出 DuplicateKeyException 异常。

两个方法的源码分析:

1、save 方法:

protected Object saveDocument(String collectionName, Document dbDoc, Class<?> entityClass) {
    return execute(collectionName, collection -> {
        MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass,
                dbDoc, null);
        WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
 
        MappedDocument mapped = MappedDocument.of(dbDoc);
 
        MongoCollection<Document> collectionToUse = writeConcernToUse == null //
                ? collection //
                : collection.withWriteConcern(writeConcernToUse);
 
        if (!mapped.hasId()) {
            // _id不存在则会执行新增操作
            collectionToUse.insertOne(dbDoc);
        } else {
            MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
            UpdateContext updateContext = queryOperations.replaceSingleContext(mapped, true);
            Document replacement = updateContext.getMappedUpdate(entity);
 
            Document filter = updateContext.getMappedQuery(entity);
            // ...
             
            collectionToUse.replaceOne(filter, replacement, new ReplaceOptions().upsert(true));
        }
        return mapped.getId();
    });
}

说明:在执行 saveDocument 方法前,在 writeInternal 方法中,如果发现有 _id 字段,但是值为空,会从 document 中剔除_id字段。

insert 方法:

protected Object insertDocument(String collectionName, Document document, Class<?> entityClass) {
    return execute(collectionName, collection -> {
        MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass,
                document, null);
        WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
 
        if (writeConcernToUse == null) {
            collection.insertOne(document);
        } else {
            collection.withWriteConcern(writeConcernToUse).insertOne(document);
        }
 
        return operations.forEntity(document).getId();
    });
}

 

 

回写主键ID

调用save方法时,成功保存后,会将主键ID回写到入参对象中

1、在执行完saveDocument或者insertDocument后,会返回一个主键ID,然后立马执行下面的方法:

protected <T> T populateIdIfNecessary(T savedObject, Object id) {
 
    return operations.forEntity(savedObject, mongoConverter.getConversionService()) //
            .populateIdIfNecessary(id);
}

2、最终又会执行:

public T populateIdIfNecessary(@Nullable Object id) {
 
    if (id == null) {
        return propertyAccessor.getBean();
    }
 
    MongoPersistentProperty idProperty = entity.getIdProperty();
    if (idProperty == null) {
        return propertyAccessor.getBean();
    }
 
    if (identifierAccessor.getIdentifier() != null) {
        return propertyAccessor.getBean();
    }
 
    // 将id回写到入参对象中
    propertyAccessor.setProperty(idProperty, id);
    return propertyAccessor.getBean();
}

 

批量保存

批量保存时,如果出现唯一键索引重复,是否继续执行取决于 ordered 字段(对应 MongoTemplate 中的 BulkOperations):

  • ordered:true:默认设置。插入多个文档时,会按插入顺序逐个处理文档,并在遇到的第一个错误时停止插入,已经成功插入的会保留到集合中
  • ordered:false:插入多个文档时,发生错误也会继续处理剩下的文档,不会中断

mongoTemplate查询并仅返回MongoDB文档中的某个字段 mongotemplate返回指定字段_java_02

mongoTemplate查询并仅返回MongoDB文档中的某个字段 mongotemplate返回指定字段_字段_03

db.runCommand({
  insert: "app_ids_element",
  "$db": "tenant_e95dd4a454e6c86412ca",
  "ordered": false,
  "documents": [{
        "element_id": "xwj1",
        "agent_id": "a1",
        "detail": {
            "decode_cmd": "abc1",
            "_class": "com.qt4.ids.dal.model.element.ElementProcess"
        },
        "_class": "com.qt4.ids.dal.model.element.ElementDO"
    }, {
        "element_id": "xwj2",
        "agent_id": "a2",
        "_class": "com.qt4.ids.dal.model.element.ElementDO"
    }, {
        "element_id": "xwj3",
        "agent_id": "a3",
        "_class": "com.qt4.ids.dal.model.element.ElementDO"
    }]
})

批量插入命令示例

app_ids_element 表中的 element_id为唯一索引,当表中存在 xwj1 的数据时,执行以上命令还是会成功插入 xwj2 和 xwj3。执行结果如下:

mongoTemplate查询并仅返回MongoDB文档中的某个字段 mongotemplate返回指定字段_字段_04

 

说明:通过 errmsg 可以看出插入失败的原因。如果全部插入成功,是没有 writeErrors 的。

 

知识改变世界