Today , I met with a problem that I have a field whose type is a class code-generated Protobuf. If I used Morphia to automatically map this field to an embedded Mongo document, morphia will map each of its member field by reflection which was not wanted by me. So here came the requirement that I want to manually map that field. For example I have a annotated class :
@Entity(value ="Resources", noClassnameStored = true) public class ResourceDO implements IResource { @Id private String systemId; ... @Embedded Rule rule; ... }
I wanted to manually encode (to BSON Object) and decode (from BSON Object) the "rule" field. And the class Rule is of a recursive form :
public class Rule { String id ; Rule rule; public String getId() { return id; } public void setId(String id) { this.id = id; } public Rule getRule() { return rule; } public void setRule(Rule rule) { this.rule = rule; } }
I found that Morphia.getMapper().getConverters() can get all the default converters provided by Morphia. And the DefaultConverter also provide a addConverter() method , so I realized the following converter:
public class RuleConverter extends TypeConverter { public RuleConverter(){ super(Rule.class); } @Override public Object decode(Class targetClass, Object fromDBObject, MappedField optionalExtraInfo) throws MappingException { if (fromDBObject == null) return null; BSONObject bson = (BSONObject) fromDBObject; Rule root = new Rule(); String id = (String)bson.get("id"); root.setId(id); Rule parent = root; while ( bson.get("rule")!= null ){ bson = (BSONObject)bson.get("rule"); Rule rule = new Rule(); id = (String)bson.get("id"); rule.setId(id); parent.setRule(rule); parent = rule; } return root; } @Override public boolean isSupported(Class<?> c, MappedField optionalExtraInfo) { return oneOf(c, supportTypes); } @Override public Object encode(Object value, MappedField optionalExtraInfo) { Rule rule = (Rule) value; BSONObject root = new BasicBSONObject(); String id = rule.getId(); root.put("id", id); BSONObject parent = start; while ( rule.getRule() != null ){ rule = rule.getRule(); BSONObject bson = new BasicBSONObject(); id = rule.getId(); bson.put("id", id); parent.put("rule", bson); parent = bson; } return root; } }
However , I found , when I inserted the resource , it works well, for example :
{ _id : XXXXXXX , ...., rule : { id : "1", rule : { id : "2" } } }
But when I loaded the resource, the fromObject passed to my decode method is :
{ id : "2" }
When I looked at the source codes of Morphia, it was because that Mapper.readMappedField() will invoke EmbeddedMapper.fromObject() to read my filed. And EmbeddedMapper will strip the other fileds of the BSON Object by invoking :
Object dbVal = mf.getDbObjectValue(dbObject);
And then the BSON Object will become.
{ id : "1", rule : { id : "2" } }
Then it will invoke DefaultConverters.fromDBObject() which will again invoke :
Object dbVal = mf.getDbObjectValue(dbObject);
to strip the outer layer of the BSON Object , so the BSON Object passed to the decode method of my converter became :
{ id : "2" }
So, based on the following codes of Mapper.readMappedField() :
private void readMappedField(DBObject dbObject, MappedField mf, Object entity, EntityCache cache) { if (mf.hasAnnotation(Property.class) || mf.hasAnnotation(Serialized.class) || mf.isTypeMongoCompatible() || converters.hasSimpleValueConverter(mf)) opts.valueMapper.fromDBObject(dbObject, mf, entity, cache, this); else if (mf.hasAnnotation(Embedded.class)) opts.embeddedMapper.fromDBObject(dbObject, mf, entity, cache, this); else if (mf.hasAnnotation(Reference.class)) opts.referenceMapper.fromDBObject(dbObject, mf, entity, cache, this); else { opts.defaultMapper.fromDBObject(dbObject, mf, entity, cache, this); } }
I add a tag @Property(value="rule") to my "rule" field, thus make Mapper to invoke ValueMapper.fromDBObject() which work around the problem.