When I was trying to avoid Morphia, but walking toward it, I discovered implementing the POJOs as DBObjectss. This worked to a point, exactly one level (Account) but my encoding failed to reach down to the Address arrays embedded within.
(Note: Ultimately, it might be better to use Morphia which DOES handle all of this stuff. See Transcription of Notes on Morphia.)
This code is missing MongoSetup.java which is still found in version 1. I also removed the introducing paragraphs and sample Mongo document. These are still in the first example here.
Note that Account is still "experimental" in that it's not actually used in MongoComplexArrays outside of method readByType() where I was putting it through its paces. It worked very well after I implemented all the requirements of interface DBObject (except for the Address arrays not being able to benefit from analogous attention to those same requirements).
This is the meat of our example including the CRUD methods.
package experiment.mongo; import java.util.ArrayList; import java.util.Iterator; import org.bson.types.ObjectId; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; /** * The point of this code is to explore creating and maintaining arrays in Mongo documents. */ public class MongoComplexArrays { private static MongoSetup mongo = new MongoSetup( "funstuff" ); private static DBCollection collection = null; private static void cleanup() { // refresh the collection each time... collection = mongo.getCollection( "accounts" ); collection.drop(); collection = mongo.getCollection( "accounts" ); } /** Illustrates how to create a document with an array inside. In a proper service, this * would just create (add) a new address and the method would take a parameter indicating * which user and another to communicate the new address. * * @return the oid of Jack's new user account for playing around with. */ private static ObjectId setup() { /* Let's create a new object with an array of addresses inside. Something like: * { * "name" : "Jack", * "addresses" : * [ * { "type" : 1, "street" : "123 My Street", "city" : "Bedford Falls", "state" : "NJ" }, * { "type" : 2, "street" : "456 My Street", "city" : "Bedford Falls", "state" : "NJ" } * ] * } */ BasicDBObject account = new BasicDBObject(); account.put( "name", "Jack" ); Address address1 = new Address( Address.HOME_ADDRESS, "123 My Street", "Bedford Falls", "NJ" ); Address address2 = new Address( Address.BILLING_ADDRESS, "456 My Street", "Bedford Falls", "NJ" ); address1.generateId(); address2.generateId(); DBObject dbo; ArrayList< DBObject > array = new ArrayList< DBObject >(); dbo = address1.bsonFromPojo(); array.add( dbo ); dbo = address2.bsonFromPojo(); array.add( dbo ); account.put( "addresses", array ); collection.insert( account ); System.out.println( "At this point, open the Mongo console and type: " ); System.out.println( " > use funstuff" ); System.out.println( " > db.accounts.find().pretty();" ); // let's find this document and record Jack's user account oid... DBCursor cursor = collection.find(); while( cursor.hasNext() ) account = ( BasicDBObject ) cursor.next(); return ( ObjectId ) account.get( "_id" ); } /** Illustrates how to create a new address inside the document now set up. * * @param accountoid identifies the user whose address vector is to be added to. * @param address the new address to add. */ private static void create( ObjectId accountoid, Address address ) { BasicDBObject match = new BasicDBObject(); match.put( "_id", accountoid ); BasicDBObject addressSpec = new BasicDBObject(); addressSpec.put( "_id", address.getId() ); addressSpec.put( "type", address.getType() ); addressSpec.put( "street", address.getStreet() ); addressSpec.put( "city", address.getCity() ); addressSpec.put( "state", address.getState() ); BasicDBObject update = new BasicDBObject(); update.put( "$push", new BasicDBObject( "addresses", addressSpec ) ); collection.update( match, update ); } /** Illustrates how to read elements of the embedded array out. We want to save the * type-2 address' oid for later update and delete in the example, also the account * oid too for we'd have that in a usual application setting. * * @param accountoid identifies the user whose address vector is to be read. * @param type the sort of address to find in the vector; in a real service there * could be more than one of these, but for this exercise, we're just * going to assume only one. * @return the address read or null. */ private static Address readByType( ObjectId accountoid, int type ) { BasicDBObject match = new BasicDBObject(); match.put( "_id", accountoid ); DBCursor cursor = collection.find( match ); try { DBObject d = collection.findOne( accountoid ); d.hashCode(); collection.setObjectClass( Account.class ); Account a = ( Account ) collection.findOne( accountoid ); @SuppressWarnings( "unchecked" ) ArrayList< DBObject > dbos = ( ArrayList< DBObject > ) a.get( "addresses" ); for( DBObject db : dbos ) { /* This DOES step through each address as one expects. Unfortunately, there * is no way (?) to get it to be Address instead of DBObject. */ db.hashCode(); } collection.setObjectClass( null ); a.hashCode(); } catch( IllegalArgumentException e ) { System.out.println( e.getMessage() ); } BasicDBObject account = null; while( cursor.hasNext() ) account = ( BasicDBObject ) cursor.next(); System.out.println( ( String ) account.get( "name" ) + ":" ); BasicDBList addresses = ( BasicDBList ) account.get( "addresses" ); for( Iterator< Object > it = addresses.iterator(); it.hasNext(); ) { BasicDBObject dbo = ( BasicDBObject ) it.next(); Address address = new Address(); address.makePojoFromBson( dbo ); System.out.println( " [" + address.getId() + "] " + address.getStreet() + ", " + address.getCity() + ", " + address.getState() + " (" + address.getType() + ")" ); // we're going to return when we find the first one with this type... if( address.getType() == type ) return address; } return null; } /** Illustrates how to update an element in the embedded array. This method morphs us * toward a more proper service implementation (of update). * * db.accounts.update( { "_id" : "$saveAccountOid" }, { $set : { "addresses" : { "_id" : "$saveAddressOid" } } } ) * * @param accountoid identifies the user whose address vector is to be changed. * @param address the address to change assuming its _id corresponds with an existing one for the user. */ private static void update( ObjectId accountoid, Address address ) { // start by pulling the address to be updated from the array... delete( accountoid, address.getId() ); // now re-add the address as updated... create( accountoid, address ); } /** * This is a more intelligent version of update(). The first half of the update * operation is to locate the user account (by oid) and the address (also by oid). * The second half is to update all the data fields requiring it. This way, the * address entity passed need only specify those fields that are to change. * * @param accountoid identifies the user whose address vector is to be changed. * @param address the address to change assuming its _id corresponds with an existing one for the user. */ private static void update2( ObjectId accountoid, Address address ) { BasicDBObject match = new BasicDBObject(); match.put( "_id", accountoid ); match.put( "addresses._id", address.getId() ); BasicDBObject addressSpec = new BasicDBObject(); Integer type = address.getType(); String temp; if( type > 0 && type < 6 ) addressSpec.put( "addresses.$.type", type ); if( ( temp = address.getStreet() ) != null ) addressSpec.put( "addresses.$.street", temp ); if( ( temp = address.getCity() ) != null ) addressSpec.put( "addresses.$.city", temp ); if( ( temp = address.getState() ) != null ) addressSpec.put( "addresses.$.state", temp ); BasicDBObject update = new BasicDBObject(); update.put( "$set", addressSpec ); collection.update( match, update ); } /** Illustrates how to remove an element from the embedded array. The element is * an address as indicated by the oid passed. At this console, this would be: * * db.accounts.update( { "_id" : "$saveAccountOid" }, { $pull : { "addresses" : { "_id" : "$saveAddressOid" } } } ) * * There is an account specification, the first argument, that uniquely describes * Jack's account. This is followed by the specification of the address we wish to * delete from the array of addresses in that account. * * If we were going to delete Jack, the user, this would all be simpler and we'd * call collection.remove(). This example is only on how to remove an element from * an embedded array (remember?). * * This method, like update() above, is closer to a real service implementation. * * @param accountoid identifies the user whose address vector is to be changed. * @param addressoid the address to be deleted from the vector. */ private static void delete( ObjectId accountoid, ObjectId addressoid ) { BasicDBObject match = new BasicDBObject(); match.put( "_id", accountoid ); BasicDBObject addressSpec = new BasicDBObject(); addressSpec.put( "_id", addressoid ); BasicDBObject update = new BasicDBObject(); update.put( "$pull", new BasicDBObject( "addresses", addressSpec ) ); collection.update( match, update ); } private static void seeChange( String where ) { DBCursor cursor = collection.find(); DBObject account = null; while( cursor.hasNext() ) account = cursor.next(); System.out.println( where ); Gson gson = new GsonBuilder().setPrettyPrinting().create(); String json = gson.toJson( account ); System.out.println( json ); } public static void main( String[] args ) { ObjectId saveAccountOid = null; Address newAddress = new Address( Address.SHIPPING_ADDRESS, "789 My Street", "Bedford Falls", "NJ" ); Address billingAddress = null; Address changedAddress = new Address( Address.BILLING_ADDRESS, "1852 Exeter Avenue", "Bedford Falls", "NJ" ); newAddress.generateId(); System.out.println( "CLEANUP -----------------------------------------" ); cleanup(); System.out.println( "SETUP -------------------------------------------" ); saveAccountOid = setup(); System.out.println( "CREATE ------------------------------------------" ); create( saveAccountOid, newAddress ); seeChange( "After creating new address..." ); System.out.println( "READBYTYPE --------------------------------------" ); billingAddress = readByType( saveAccountOid, Address.BILLING_ADDRESS ); changedAddress.setId( billingAddress.getId() ); System.out.println( "UPDATE ------------------------------------------" ); update( saveAccountOid, changedAddress ); seeChange( "After updating billing address..." ); Address changedAddress2 = new Address( Address.ALTERNATE_ADDRESS, "2222 Bulldog Boulevard", null, null ); changedAddress2.setId( billingAddress.getId() ); System.out.println( "UPDATE2 -----------------------------------------" ); update2( saveAccountOid, changedAddress2 ); seeChange( "After updating billing to alternate address (second method)..." ); System.out.println( "DELETE ------------------------------------------" ); delete( saveAccountOid, billingAddress.getId() ); seeChange( "After deleting billing address..." ); } }
After implementing Account (see below), I came back in here and completed the full DBObject exercise too. It didn't magically produce the desired effect: the addresses in any object of type Account remain an array of DBObjects, only halfway to my goal. This is better than nothing, however.
Ultimately, I received the advice from the [email protected] that my options were limited to a) adopt Morphia, b) patch the MongoDB Java driver manually according to some mention somewhere in Jira or c) handle serialization/deserialization manually.
At this point, I'm out of time to screw around and will probably opt for (c) or, maybe soon, (a).
package experiment.mongo; import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.bson.BSONObject; import org.bson.types.ObjectId; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** * This is a POJO used in conjunction with the MongoComplexArrays example. */ public class Address implements DBObject, Serializable { private ObjectId _id; private int type; private String street; private String city; private String state; // address types: public static final int HOME_ADDRESS = 1; public static final int BILLING_ADDRESS = 2; public static final int SHIPPING_ADDRESS = 3; public static final int ALTERNATE_ADDRESS = 5; public Address() {} public Address( int type, String street, String city, String state ) { this.type = type; this.street = street; this.city = city; this.state = state; } public ObjectId getId() { return this._id; } public void setId( ObjectId _id ) { this._id = _id; } public void generateId() { if( this._id == null ) this._id = new ObjectId(); } public int getType() { return this.type; } public void setType( int type ) { this.type = type; } public String getStreet() { return this.street; } public void setStreet( String street ) { this.street = street; } public String getCity() { return this.city; } public void setCity( String city ) { this.city = city; } public String getState() { return this.state; } public void setState( String state ) { this.state = state; } public DBObject bsonFromPojo() { BasicDBObject document = new BasicDBObject(); document.put( "_id", this._id ); document.put( "type", this.type ); document.put( "street", this.street ); document.put( "city", this.city ); document.put( "state", this.state ); return document; } public void makePojoFromBson( DBObject bson ) { BasicDBObject b = ( BasicDBObject ) bson; this._id = ( ObjectId ) b.get( "_id" ); this.type = ( Integer ) b.get( "type" ); this.street = ( String ) b.get( "street" ); this.city = ( String ) b.get( "city" ); this.state = ( String ) b.get( "state" ); } /* Implementation of DBObject--essential for Mongo be able to return a properly formed * object of type Account from DBCollection.findOne(), etc. Embedded in an Account, this * stuff is never in fact called. */ @Override public boolean containsField( String field ) { return( field.equals( "_id" ) || field.equals( "type" ) || field.equals( "street" ) || field.equals( "city" ) || field.equals( "state" ) ); } @Override @Deprecated public boolean containsKey( String key ) { return containsField( key ); } @Override public Object get( String field ) { if( field.equals( "_id" ) ) return this._id; if( field.equals( "type" ) ) return this.type; if( field.equals( "street" ) ) return this.street; if( field.equals( "city" ) ) return this.city; if( field.equals( "state" ) ) return this.state; return null; } @Override public Set< String > keySet() { Set< String > set = new HashSet< String >(); set.add( "_id" ); set.add( "type" ); set.add( "street" ); set.add( "city" ); set.add( "state" ); return set; } @Override public Object put( String field, Object object ) { if( field.equals( "_id" ) ) { this._id = ( ObjectId ) object; return object; } if( field.equals( "type" ) ) { this.type = ( Integer ) object; return object; } if( field.equals( "street" ) ) { this.street = ( String ) object; return object; } if( field.equals( "city" ) ) { this.city = ( String ) object; return object; } if( field.equals( "state" ) ) { this.state = ( String ) object; return object; } return null; } @Override public void putAll( BSONObject bson ) { for( String key : bson.keySet() ) put( key, bson.get( key ) ); } @SuppressWarnings( "unchecked" ) @Override public void putAll( @SuppressWarnings( "rawtypes" ) Map map ) { for( Map.Entry< String, Object > entry : ( Set< Map.Entry< String, Object > > ) map.entrySet() ) put( entry.getKey().toString(), entry.getValue() ); } @SuppressWarnings( "rawtypes" ) @Override public Map toMap() { Map< String, Object > map = new HashMap< String, Object >(); if( this._id != null ) map.put( "_id", this._id ); if( this.type != 0 ) map.put( "type", this.type ); if( this.street != null ) map.put( "street", this.street ); if( this.city != null ) map.put( "city", this.city ); if( this.state != null ) map.put( "state", this.state ); return map; } @Override public Object removeField( String field ) { throw new RuntimeException("Unsupported method."); } @Override public boolean isPartialObject() { return false; } @Override public void markAsPartialObject() { throw new RuntimeException("Method not implemented."); } }
I created this POJO, which wasn't formally established in the first version, as an exercise to learn how to implement a DBObject.
package experiment.mongo; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.bson.BSONObject; import org.bson.types.ObjectId; import experiment.mongo.Address; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** * This is a POJO used in conjunction with the MongoComplexArrays example. */ public class Account implements DBObject { private ObjectId _id; private String name; private ArrayList< Address > addresses; public Account() {} public Account( String name ) { this.name = name; } public ObjectId getId() { return this._id; } public void setId( ObjectId _id ) { this._id = _id; } public void generateId() { if( this._id == null ) this._id = new ObjectId(); } public String getName() { return this.name; } public void setName( String name ) {this.name = name; } public ArrayList< Address > getAddresses() { return this.addresses; } public void addAddress( Address address ) { this.addresses.add( address ); } /** * Create a Mongo document from this POJO, but let the caller handle inserting * or using it in whatever way he wishes. * * @return a ready DBObject. */ public DBObject bsonFromPojo() { BasicDBObject document = new BasicDBObject(); document.put( "_id", this._id ); document.put( "name", this.name ); if( this.addresses.size() > 0 ) { DBObject dbo; ArrayList< DBObject > array = new ArrayList< DBObject >(); for( Address address : this.addresses ) { dbo = address.bsonFromPojo(); array.add( dbo ); } document.put( "addresses", array ); } return document; } /** * Reset values in this POJO from the specified Mongo document. "this" is usually * a new, but blank POJO. * * @param bson to supply new values. */ public void makePojoFromBson( DBObject bson ) { BasicDBObject b = ( BasicDBObject ) bson; this._id = b.getObjectId( "_id" ); this.name = b.getString ( "name" ); //this.addresses = // DBCollection.setInternalClass(); } /* Implementation of DBObject--essential for Mongo be able to return a properly formed * object of type Account from DBCollection.findOne(), etc. * * For one implementation, see * https://github.com/dadastream/lib-mongomapper/blob/master/src/main/java/net/karmafiles/ff/core/tool/dbutil/converter/dbobject/DBObjectProxy.java * * Also, check out * https://github.com/jakubholynet/blog/tree/master/miniprojects/generic-pojo-mappers */ @Override public boolean containsField( String field ) { return( field.equals( "_id" ) || field.equals( "name" ) || field.equals( "addresses" ) ); } @Override @Deprecated public boolean containsKey( String key ) { return containsField( key ); } @Override public Object get( String field ) { // We could keep a Map in sync with the POJO, then just do return map.get( field ). // Then, for the map() method, just return that map. For putAll( Map ), we would // just use Map.putAll(). These are optimizations. See // https://github.com/dadastream/lib-mongomapper/blob/master/src/main/java/net/karmafiles/ff/core/tool/dbutil/converter/dbobject/DBObjectMapProxy.java if( field.equals( "_id" ) ) return this._id; if( field.equals( "name" ) ) return this.name; // not sure why Mongo is looking for these, but I saw it doing this in the debugger... // if( field.equals( "$err" ) ) return "You were looking for '$err'?"; // if( field.equals( "err" ) ) return "You were looking for 'err'?"; // if( field.equals( "errmsg" ) ) return "You were looking for 'errmsg'?"; // if( field.equals( "$code" ) ) return "You were looking for '$code'?"; // if( field.equals( "code" ) ) return "You were looking for 'code'?"; // if( field.equals( "assertionCode" ) ) return "You were looking for 'assertionCode'?"; // TODO what to return here? This just isn't right! if( field.equals( "addresses" ) ) return this.addresses; return null; } /** * Return a list of keys (field names as would be keys in the map). */ @Override public Set< String > keySet() { Set< String > set = new HashSet< String >(); set.add( "_id" ); set.add( "name" ); set.add( "addresses" ); // TODO: add stuff like this? set.add( "addresses._id" ); set.add( "addresses.type" ); set.add( "addresses.street" ); set.add( "addresses.city" ); set.add( "addresses.state" ); return set; } @SuppressWarnings( "unchecked" ) @Override public Object put( String field, Object object ) { if( field.equals( "_id" ) ) { this._id = ( ObjectId ) object; return object; } if( field.equals( "name" ) ) { this.name = ( String ) object; return object; } // just for get() above, this is where stuff goes south... if( field.equals( "addresses" ) ) { this.addresses = ( ArrayList< Address > ) object; return object; } return null; } /** * This is my makePojoFromBson() method. */ @Override public void putAll( BSONObject bson ) { for( String key : bson.keySet() ) put( key, bson.get( key ) ); // BasicDBObject b = ( BasicDBObject ) bson; // // this._id = b.getObjectId( "_id" ); // this.name = b.getString ( "name" ); } @SuppressWarnings( "unchecked" ) @Override public void putAll( @SuppressWarnings( "rawtypes" ) Map map ) { for( Map.Entry< String, Object > entry : ( Set< Map.Entry< String, Object > > ) map.entrySet() ) put( entry.getKey().toString(), entry.getValue() ); // Map< String, Object > m = ( Map< String, Object > ) map; // // if( m.containsKey( "_id" ) ) // this._id = ( ObjectId ) m.get( "_id" ); // if( m.containsKey( "name" ) ) // this.name = ( String ) m.get( "name" ); // // // TODO: put map values to as many addresses as are in the map? // if( m.containsKey( "addresses" ) ) // this.addresses = ( ArrayList< Address > ) m.get( "addresses" ); } @SuppressWarnings( "rawtypes" ) @Override public Map toMap() { Map< String, Object > map = new HashMap< String, Object >(); if( this._id != null ) map.put( "_id", this._id ); if( this.name != null ) map.put( "name", this.name ); if( this.addresses != null ) { // nope, this is silliness too: what's a poor boy to do? for( Address address : this.addresses ) { ObjectId _id = address.getId(); if( _id != null ) map.put( "addresses._id", address.getId() ); Integer type = address.getType(); if( type != null ) map.put( "addresses.type", address.getType() ); String temp; temp = address.getStreet(); if( temp != null ) map.put( "addresses.street", address.getStreet() ); temp = address.getCity(); if( temp != null ) map.put( "addresses.city", address.getCity() ); temp = address.getState(); if( temp != null ) map.put( "addresses.state", address.getState() ); } map.put( "addresses", this.addresses ); } return map; } @Override public Object removeField( String field ) { throw new RuntimeException("Unsupported method."); } @Override public boolean isPartialObject() { return false; } @Override public void markAsPartialObject() { throw new RuntimeException("Method not implemented."); } }
This is from a discussion I had with Achille of 10gen.
The trip from POJO to MongoDB was badly done (in italics in the original mail). It did not retain the fieldnames, identity and type. Here it is again (just that bit):
if( getIdtypes().size() > 0 ) { List< BasicDBObject > list = new ArrayList< BasicDBObject >(); for( IdentityType idt : getIdtypes() ) { BasicDBObject idtype = new BasicDBObject(); idtype.put( "identity", idt.getIdentity() ); idtype.put( "type", idt.getType() ); list.add( idtype ); } document.put( "idtypes", list ); }
That is, a complex object array.
public class IdentityType { private String identity; private String type; public IdentityType() { } public IdentityType( String identity, String type ) { this.identity = identity; this.type = type; } public String getIdentity() { return identity; } public void setIdentity( String identity ) { this.identity = identity; } public String getType() { return type; } public void setType( String type ) { this.type = type; } public String toString() { StringBuilder sb = new StringBuilder(); sb.append( "{\n" ); sb.append( " identity: " + this.identity + "\n" ); sb.append( " type: " + this.type + "\n" ); sb.append( "\n}" ); return sb.toString(); } }
This is the relevant POJO code:
private List< IdentityType > idtypes = new ArrayList< IdentityType >(); public List< IdentityType > getIdtypes() { return this.idtypes; } public void setIdtypes( List< IdentityType > types ) { this.idtypes = types; } public void addIdtype( IdentityType type ) { this.idtypes.add( type ); }
Here's the trip from POJO to MongoDB document:
public DBObject getBsonFromPojo() { BasicDBObject document = new BasicDBObject(); ... if( getIdtypes().size() > 0 ) { List< BasicDBObject > list = new ArrayList< BasicDBObject >(); for( IdentityType idtype : getIdtypes() ) list.add( new BasicDBObject( idtype.getIdentity(), idtype.getType() ) ); document.put( "idtypes", list ); } ... return document; }
It's an easier trip back from MongoDB to POJO:
public void makePojoFromBson( DBObject bson ) { BasicDBObject b = ( BasicDBObject ) bson; ... setIdtypes( ( List< IdentityType > ) b.get( "idtypes" ) ); }