PDA

View Full Version : Spring Data Document M2: Bug with @DBRef



DevChris
Apr 19th, 2011, 10:36 AM
@DBRef works with Lists but not with properties.

With:
@DBRef
private List<Address> addresses;
Loading and saving works!

With:
@DBRef
private Address address;
Only saving works (correct data at mongoDB).
Loading throws this exception:

Exception in thread "main" org.springframework.core.convert.ConverterNotFound Exception: No converter found capable of converting from 'com.mongodb.DBRef' to 'persontest.Address'





@Document
public class User {
@Id
private ObjectId id;

@Indexed
private Integer ssn;

@DBRef
private List<Address> addresses;

@DBRef
private Address address;
...
}




@Document
public class Address {

@Id
private ObjectId id;

private String street;
...
}




User user = new User();
user.setSsn(111);

Address address = new Address();
address.setStreet("Mainstreet");
mongoOps.insert(address);

user.setAddress(address);

mongoOps.insert(user);

log.info(mongoOps.findOne(new Query(Criteria.where("ssn").is(111)), User.class));

Jon Brisbin
Apr 19th, 2011, 02:25 PM
Yikes! :(

I pushed a fix for this and created a test case specifically for non-list, referenced properties.

If you use a snapshot build (after the nightly runs and creates one, that is :)), you should get a fix for that.

Sorry for the bug. It was a two-line fix so should have been caught earlier.

aachpach
Jul 5th, 2011, 04:51 PM
org.springframework.core.convert.ConverterNotFound Exception: No converter found capable of converting from 'com.mongodb.DBRef' to 'com.test.Person'
I am using spring-data-mongodb version 1.0.0.M2. Please advise.

aachpach
Jul 5th, 2011, 05:51 PM
Hi a quick correction, I updated to 1.0.0.M3 and i still have the same issue. Can you please tell me on which version did your fix get in?

mcorey
Sep 28th, 2011, 09:50 PM
I have a case where this is continuing to fail in 1.0.0M4 -- the option to use a List is also not working for me, however. Is it expected that this has been fixed? Is there bug to track it?

M

mcorey
Sep 28th, 2011, 11:29 PM
This is a total hack, but it does seem to work until a real fix can make it into the Spring code base. The issue is most definitely the storage of the reference, not the loading -- the value on the '$id' field is stored as "3a7blahblahblah" instead of ObjectId("3a7blahblahblah"). So the solution is to use a Mongo event listener to make this happen before the save occurs:


@Component
public class MyDocReferenceFixer extends AbstractMongoEventListener<MyDoc> {

@Override
public void onBeforeSave(MyDoc source, DBObject dbo) {
super.onBeforeSave(source, dbo);

Object o = dbo.get("myReferenceProperty");
DBRef dbref = (DBRef)o;

dbo.put("myReferenceProperty", new DBRef(dbref.getDB(), dbref.getRef(), new ObjectId((String)dbref.getId())));
}

}

Pretty simple -- biggest downfall here (aside from it being a total hack) is that you have to do it for every Document type that has a DBRef, which will get irritating after a while. It does appear to work so far, though -- after putting this in place, my DBRef objects are eagerly loaded when using the MongoTemplate object, and when using a repository.

M

johannz
Sep 29th, 2011, 01:50 AM
VGuna and I (johannz) discussed this on September 9 in http://forum.springsource.org/showthread.php?114421-Lists-with-DBRef-have-size-1-but-get%280%29-returns-null

I created a bug for it: https://jira.springsource.org/browse/DATADOC-275 - DBRef fields and collections are returning nulls. (https://jira.springsource.org/browse/DATADOC-275) In that bug I researched the cause, and have a proposed fix.

Copied from the bug:
Analysis

I think the problem is in the MappingMongoConverter, and how it is storing DBRefs. Looking at the code for the createDBRef method, it is not doing any type conversion of the ID that it retrieves from the target object, when it should be.

From the createDBRef method, exception handling removed:



Object id = null;
BeanWrapper<MongoPersistentEntity<Object>, Object> wrapper = BeanWrapper.create(target, conversionService);
id = wrapper.getProperty(idProperty, Object.class, useFieldAccessOnly);

String collection = dbref.collection();
if ("".equals(collection)) {
collection = targetEntity.getCollection();
}

String dbname = dbref.db();
DB db = StringUtils.hasText(dbname) ? mongoDbFactory.getDb(dbname) : mongoDbFactory.getDb();
return new DBRef(db, collection, id);

Suggested fix

I suspect the line "id = wrapper.getProperty(idProperty, Object.class, useFieldAccessOnly);" should be replaced by this code, copied from writeInternal:


Object idObj = null;
Class<?>[] targetClasses = new Class<?>[] { ObjectId.class, String.class, Object.class };
for (Class<?> targetClass : targetClasses) {
try {
idObj = wrapper.getProperty(idProperty, targetClass, useFieldAccessOnly);
if (null != idObj) {
break;
}
} catch (ConversionException ignored) {
} catch (IllegalAccessException e) {
throw new MappingException(e.getMessage(), e);
} catch (InvocationTargetException e) {
throw new MappingException(e.getMessage(), e);
}
}

if (null != idObj) {
dbo.put("_id", idObj);
} else {
if (!VALID_ID_TYPES.contains(idProperty.getType())) {
throw new MappingException("Invalid data type " + idProperty.getType().getName()
+ " for Id property. Should be one of " + VALID_ID_TYPES);
}
}

mcorey
Sep 29th, 2011, 06:46 AM
I did see that defect after my initial post, thanks -- it was one of the pieces of evidence I used to find the workaround. Unfortunately, it doesn't do me any good until the next version of spring-data rolls out with that code (I tried the latest snapshot, which didn't have any effect). I think I certainly was able to prove that the theory that the $id value is being stored incorrectly is true, at least in my instance, and my work around enables me to move forward for now.

M