最近项目使用mongo作为持久层
遇到问题:
项目中使用的主键(包括内嵌文档)都是ObjectId类型.以前实体使用String类型.
在测试的时候使用spring-data-mongo 发现数据 都能通过主键查询得到结果.
可以在上线后 发现,mongo内嵌文档通过spring-data-mongo主键查询不出来的.
数据结构类似:
{
"_id" : ObjectId("571867bde4b0855e73cf72a8"),
"isDel" : "0",
"delTime" : NumberLong(0),
"lModTime" : NumberLong(0),
"uid" : "8af0b0e4bc671857fe7aaa59i",
"isEnCV" : "0",
"cvName" : "xxxx",
"verifyTime" : NumberLong(0),
"views" : NumberLong(0),
"downloads" : NumberLong(0),
"cnName" : "xxxx",
"gender" : "xxx",
"birthday" : NumberLong(606585600),
"degreeId" : "5",
"degreeName" : "xxxx",
"marry" : "M",
"nationality" : "中国",
"email" : "xxxx",
"education" : [
{
"_id" : ObjectId("571867b7e4b0855e73cf729f"),
"degreeId" : "5",
"degreeName" : "本科",
"college" : "xxx",
"major" : "xxxx",
"start" : NumberLong(1188576000),
"end" : NumberLong(1309449600),
"description" : ""
}
]
}
查询语句:
主键查询: (这样是能查询到的)
(cvid = 571867bde4b0855e73cf72a8)
Query.query(Criteria.where(
"_id"
).is(Cvid))
内嵌文档查询: (这样式是查询不到的)
(eduid = 571867b7e4b0855e73cf729f)
Query.query(Criteria.where(
"education._id"
).is(eduid)
)
于是 好奇心驱使下开始排查问题。发现spring-data-mongo对于主键查询做了优化判断.如果字段名称
contains 匹配 _id 如果匹配上了 底层的会对入参进行默认的ObjectId转换.
而内嵌文档的查询主键是 xx._id ,所以判断是不包含的,所以没有进行ObjectId转换.
最终解决方案:
在主键查询时自己去显示调用ObjectId的判断.使查询语句能识别ObjectId类型或者String类型
Query.query(Criteria.where("education._id").is(getObjectValue(eduid)))
import org.bson.types.ObjectId;
public Object getObjectValue(String value)
{
return ObjectId.isValid(value) ? new ObjectId(value) : value;
}
由此我们跟踪下 spring-data-mongo怎么处理的?
spring-data-mongodb-1.3.2
package org.springframework.data.mongodb.core.convert.QueryMapper;
public class QueryMapper {
private static final List<String> DEFAULT_ID_NAMES = Arrays.asList("id", "_id");
public DBObject getMappedObject(DBObject query, MongoPersistentEntity<?> entity) {
if (isNestedKeyword(query)) {
return getMappedKeyword(new Keyword(query), entity);
}
DBObject result = new BasicDBObject();
for (String key : query.keySet()) {
// TODO: remove one once QueryMapper can work with Query instances directly
if (Query.isRestrictedTypeKey(key)) {
@SuppressWarnings("unchecked")
Set<Class<?>> restrictedTypes = (Set<Class<?>>) query.get(key);
this.converter.getTypeMapper().writeTypeRestrictions(result, restrictedTypes);
continue;
}
if (isKeyword(key)) {
result.putAll(getMappedKeyword(new Keyword(query, key), entity));
continue;
}
Field field = entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
Object rawValue = query.get(key);
String newKey = field.getMappedKey();
if (isNestedKeyword(rawValue) && !field.isIdField()) {
Keyword keyword = new Keyword((DBObject) rawValue);
result.put(newKey, getMappedKeyword(field, keyword));
} else {
result.put(newKey, getMappedValue(field, rawValue));
}
}
return result;
}
private Object getMappedValue(Field documentField, Object value) {
if (documentField.isIdField()) {
if (value instanceof DBObject) {
DBObject valueDbo = (DBObject) value;
if (valueDbo.containsField("$in") || valueDbo.containsField("$nin")) {
String inKey = valueDbo.containsField("$in") ? "$in" : "$nin";
List<Object> ids = new ArrayList<Object>();
for (Object id : (Iterable<?>) valueDbo.get(inKey)) {
ids.add(convertId(id));
}
valueDbo.put(inKey, ids.toArray(new Object[ids.size()]));
} else if (valueDbo.containsField("$ne")) {
valueDbo.put("$ne", convertId(valueDbo.get("$ne")));
} else {
return getMappedObject((DBObject) value, null);
}
return valueDbo;
} else {
return convertId(value);
}
}
if (isNestedKeyword(value)) {
return getMappedKeyword(new Keyword((DBObject) value), null);
}
if (documentField.isAssociation()) {
return convertAssociation(value, documentField.getProperty());
}
return convertSimpleOrDBObject(value, documentField.getPropertyEntity());
}
public Object convertId(Object id) {
try {
return conversionService.convert(id, ObjectId.class);
} catch (ConversionException e) {
// Ignore
}
return delegateConvertToMongoType(id, null);
}
private static class MetadataBackedField extends Field {
private final MongoPersistentEntity<?> entity;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoPersistentProperty property;
/**
* Creates a new {@link MetadataBackedField} with the given name, {@link MongoPersistentEntity} and
* {@link MappingContext}.
*
* @param name must not be {@literal null} or empty.
* @param entity must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public MetadataBackedField(String name, MongoPersistentEntity<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
//to do.......
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#with(java.lang.String)
*/
@Override
public MetadataBackedField with(String name) {
//to do.......
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isIdKey()
*/
@Override
public boolean isIdField() {
MongoPersistentProperty idProperty = entity.getIdProperty();
if (idProperty != null) {
return idProperty.getName().equals(name) || idProperty.getFieldName().equals(name);
}
return DEFAULT_ID_NAMES.contains(name);
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getProperty()
*/
@Override
public MongoPersistentProperty getProperty() {
return property;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getEntity()
*/
@Override
public MongoPersistentEntity<?> getPropertyEntity() {
//to do.......
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#isAssociation()
*/
@Override
public boolean isAssociation() {
//to do.......;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getTargetKey()
*/
@Override
public String getMappedKey() {
//to do.......
}
private PersistentPropertyPath<MongoPersistentProperty> getPath(String name) {
//to do.......
}
}
}
—————————–ObjectId 校验规则 ———————————-
package org.bson.types;
public class ObjectId implements Comparable<ObjectId> , java.io.Serializable {
public static boolean isValid( String s ){
if ( s == null )
return false;
final int len = s.length();
if ( len != 24 )
return false;
for ( int i=0; i<len; i++ ){
char c = s.charAt( i );
if ( c >= '0' && c <= '9' )
continue;
if ( c >= 'a' && c <= 'f' )
continue;
if ( c >= 'A' && c <= 'F' )
continue;
return false;
}
return true;
}
}