[JAVA-416] Using an Array to create a DBObject breaks .equals and .hashcode Created: 16/Aug/11  Updated: 03/Apr/14  Resolved: 23/Jan/14

Status: Closed
Project: Java Driver
Component/s: API
Affects Version/s: 2.6.5
Fix Version/s: 2.12.0, 3.0.0

Type: Bug Priority: Minor - P4
Reporter: Brandon Hudgeons Assignee: Jeffrey Yemin
Resolution: Done Votes: 1
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

All Java


Issue Links:
Duplicate
is duplicated by JAVA-482 (DB/BSON)Object "equals" method is br... Closed
is duplicated by JAVA-1090 For embedded $regex, BasicBSONObject ... Closed
is duplicated by JAVA-1112 BasicBSONObject.equals is broken for ... Closed
Related
is related to JAVA-482 (DB/BSON)Object "equals" method is br... Closed
Backwards Compatibility: Fully Compatible

 Description   

Using a Java Array to store information in a DBObject breaks .equals and .hashcode:

scala> val one = MongoDBObject("anarray" -> Array(MongoDBObject("one" -> "oneval"),MongoDBObject("two"->"twoval")))
one: com.mongodb.casbah.commons.Imports.DBObject = { "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
 
scala> val two = MongoDBObject("anarray" -> Array(MongoDBObject("one" -> "oneval"),MongoDBObject("two"->"twoval")))
two: com.mongodb.casbah.commons.Imports.DBObject = { "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
 
scala> one == two
res16: Boolean = false

Using a List to store the equivalent object does not:

scala> val three = MongoDBObject("anarray" -> List(MongoDBObject("one" -> "oneval"),MongoDBObject("two"->"twoval")))
three: com.mongodb.casbah.commons.Imports.DBObject = { "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
 
scala> val four = MongoDBObject("anarray" -> List(MongoDBObject("one" -> "oneval"),MongoDBObject("two"->"twoval")))
four: com.mongodb.casbah.commons.Imports.DBObject = { "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
 
scala> three == four
res17: Boolean = true

Of course, the two objects are absolutely equal whether they are constructed with an Array or a List:

scala> mongoCollection += one
res18: com.mongodb.WriteResult = N/A
 
scala> mongoCollection += three
res19: com.mongodb.WriteResult = N/A
 
scala> mongoCollection.find().foreach(println)
{ "_id" : { "$oid" : "4e3723614206a091d3e32a4c"} , "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
{ "_id" : { "$oid" : "4e37236e4206a091d3e32a4d"} , "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}
 
scala> val recs = mongoCollection.find().toArray
recs: Array[com.mongodb.casbah.Imports.DBObject] = Array({ "_id" : { "$oid" : "4e3723614206a091d3e32a4c"} , "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]}, { "_id" : { "$oid" : "4e37236e4206a091d3e32a4d"} , "anarray" : [ { "one" : "oneval"} , { "two" : "twoval"}]})
 
scala> recs(0).get("anarray") == recs(1).get("anarray")
res24: Boolean = true

The fix should just be to store any collection that will end up as a BSON array internally as a java List.



 Comments   
Comment by Graham Thomson [ 27/Feb/14 ]

Worked fine for me!

Thanks,

Graham.

Comment by Jeffrey Yemin [ 27/Feb/14 ]

Hi there,

Interested parties can test the fix with 2.12.0-rc0, available either on github or Maven Central. Any takers?

Thanks,
Jeff

Comment by Jeffrey Yemin [ 23/Jan/14 ]

Fixed this in a fairly general way. Instead of trying to check equality correctly for every possible type, the equals method instead encodes both documents as BSON and checks the equality of the byte arrays.

The hashCode method, to fulfill its contract, takes the hash of the encoded BSON.

Comment by Githook User [ 23/Jan/14 ]

Author:

{u'username': u'jyemin', u'name': u'Jeff Yemin', u'email': u'jeff.yemin@10gen.com'}

Message: JAVA-416: Use serialized form of a document with lexicographically-sorted keys for equals and hashCode
Branch: 3.0.x
https://github.com/mongodb/mongo-java-driver/commit/759bb15140751f5f26ae5270799cb4eb79af13a3

Comment by Githook User [ 22/Jan/14 ]

Author:

{u'username': u'jyemin', u'name': u'Jeff Yemin', u'email': u'jeff.yemin@10gen.com'}

Message: JAVA-416: Use serialized form of a document with lexicographically-sorted keys for equals and hashCode
Branch: master
https://github.com/mongodb/mongo-java-driver/commit/8e972b271bf5fd270aee82cc98d0982c57784e7d

Comment by Oliver Gierke [ 23/Aug/11 ]

You're right. But currently arrays cannot be used at all if you rely on equals and hashCode.

Comment by Brandon Hudgeons [ 22/Aug/11 ]

Oliver, that would work if both dbObjects held arrays. (What you suggest still should be done, but it doesn't fix the specific problem we're talking about.)

We want

dbObject1.put("key", anArray);
dbObject2.put("key", aList);

dbObject1.equals(dbObject2); // iff anArray and aList hold the same items

Comment by Oliver Gierke [ 21/Aug/11 ]

For the Java side of things this can easily be fixed by using Arrays.equals(left, right). the equals(…) methods simply needs another if clause. The {hasCode()}} method should be adapted as well.

Comment by Brandon Hudgeons [ 19/Aug/11 ]

I'm rethinking this now ... maybe the best thing to do is to leave the Java driver as-is. If you use Java, you have to deal with some unfortunate type nonsense (like Int(1) != Long(1)). In Scala, there is all kinds of precedent for engineering == and ## to mask the underlying Java ugliness (like 1 == 1L). Probably best just to fix it there.

Comment by Brandon Hudgeons [ 18/Aug/11 ]

BasicDBObject descends from java.util.AbstractMap, so you'd expect

dbObject.put("key",x);
assert(x.equals(dbObject.get("key")));

This is, of course, inconsistent with the desire to have two dbObjects compare as equal if they would be represented by an equivalent document in mongodb but were created with different object types.

Two ways I see to address the issue:

1. make a well-documented exception to the above Map interface expectations when the value is a collection. Any time you put a collection value of any type, it will be stored as a DBList. This wouldn't break any explicit interface requirement (since BasicDBObject descends from LinkedHashMap<String,Object>), and I can't imagine that many, if any, clients need an explicit return type for collection values after they are stored and before they are saved/retrieved.
2. transform any collection value to a DBList for the purposes of .equals and .hashcode on the dbObject (maybe implement by lazily computing and caching the collection hashcode), but still return the original Object on .get.

If you did #2, the only difference between a user-created dbObject and a driver-created dbObject would be what you get back on a .get("collectionkey"). If you did #1, there'd be no difference (at least in this case).

I'd be happy to take a crack at either.

Comment by Brendan W. McAdams [ 16/Aug/11 ]

This may pose a more complex issue ...

Comparing two dbObjects. If it was created on the userspace side and not saved to MongoDB yet, the Array is likely to be represented as an Array primitive.

But once it has been sent to MongoDB and retrieved from there it will be represented as a DBList.

These two things should probably compare as equal.

What if they use a java.util.List or something else, as well?!

Comment by Brendan W. McAdams [ 16/Aug/11 ]

The bug was reported with Scala sample code originally but it is an issue for both Java and Scala users.

I'm going to create a scala side fix for SCALA-42, and backport a Java version as well against this bug.

Generated at Thu Feb 08 08:52:14 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.