Uploaded image for project: 'Java Driver'
  1. Java Driver
  2. JAVA-5470

Support decoding in DBRefCodec

    • Type: Icon: Improvement Improvement
    • Resolution: Declined
    • Priority: Icon: Unknown Unknown
    • None
    • Affects Version/s: None
    • Component/s: Codecs
    • None
    • Java Drivers
    • Hide

      1. What would you like to communicate to the user about this feature?
      2. Would you like the user to see examples of the syntax and/or executable code and its output?
      3. Which versions of the driver/connector does this apply to?

      Show
      1. What would you like to communicate to the user about this feature? 2. Would you like the user to see examples of the syntax and/or executable code and its output? 3. Which versions of the driver/connector does this apply to?

      Following the code https://github.com/mongodb/mongo-java-driver/blob/master/driver-core/src/main/com/mongodb/DBRefCodec.java#L66

      decoding a DbRef is not supported.

      In my case I am using it to with an aggregate pipeline using $lookup to load foreign collection and hydrate them.

      For example let's say we've got a Parent and a Child java pojos. 

      public class Parent {
          public static final String COLLECTION_NAMING = "Parent";
          ObjectId id;
          DBRef childDbRef;
          Child child;
      
          public Parent() {
          }
      
          public Parent(final ObjectId id) {
              this.id = Objects.requireNonNull(id);
          }
      
          public Parent(final ObjectId id, final Child child) {
              this.id = Objects.requireNonNull(id);
              this.child = Objects.requireNonNull(child);
              this.childDbRef = new DBRef(Child.COLLECTION_NAMING, child.id);
          }
      
          public ObjectId getId() {
              return id;
          }
      
          public void setId(ObjectId id) {
              this.id = id;
          }
      
          public Child getChild() {
              if (childDbRef != null && child == null) {
                  throw new IllegalStateException("Not loaded");
              }
              return child;
          }
      
          public DBRef getChildDbRef() {
              return childDbRef;
          }
      
          public void setChild(final DBRef dbRef) {
              assert Child.COLLECTION_NAMING.equals(dbRef.getCollectionName());
              this.childDbRef = dbRef;
          }
      
          public void setChild(Child child) {
              this.child = Objects.requireNonNull(child);
              this.childDbRef = new DBRef(Child.COLLECTION_NAMING, child.id);
          }
      }
      
      public class Child {
          public static final String COLLECTION_NAMING = "Child";
          ObjectId id;
          String name;
      
          public Child() {
          }
      
          public Child(final ObjectId id, final String name) {
              this.id = Objects.requireNonNull(id);
              this.name = Objects.requireNonNull(name);
          }
      
          public ObjectId getId() {
              return id;
          }
      
          public void setId(ObjectId id) {
              this.id = id;
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      }
      

      To ensure that the Child is hydrated I use this kind of request:

      @ApplicationScoped
      public class ParentRepository {
          private final MongoDatabase mongoDatabase;
      
          public ParentRepository(final MongoDatabase mongoDatabase) {
              this.mongoDatabase = Objects.requireNonNull(mongoDatabase);
          }
      
          public Parent findByParentId(final ObjectId objectId) {
              final String loadedChild = "child_loaded";
              // Warning aggregate order is important: filter must be defined first !!
              final Bson aggregateOnParentId = match(Filters.eq("_id", objectId));
              final Bson lookupPipeline = lookup(Child.COLLECTION_NAMING, "child.$id", "_id", loadedChild);
              // I've got only on document to link with. Using the unwind avoid an array in result and create an object instead
              // You can play with this sample https://mongoplayground.net/p/bFJHVUuKrjO
              final Bson unwindPipeline = unwind("$" + loadedChild, new UnwindOptions().preserveNullAndEmptyArrays(true));
              return mongoDatabase.getCollection(Parent.COLLECTION_NAMING, Parent.class)
                      .aggregate(List.of(aggregateOnParentId, lookupPipeline, unwindPipeline))
                      .first();
          }
      }
      

      With theses Codec

      public class ChildCodec implements CollectibleCodec<Child> {
          private final Codec<ObjectId> objectIdCodec;
      
          public ChildCodec() {
              this.objectIdCodec = MongoClientSettings.getDefaultCodecRegistry().get(ObjectId.class);
          }
      
          @Override
          public Child generateIdIfAbsentFromDocument(final Child child) {
              if (!documentHasId(child)) {
                  child.setId(new ObjectId());
              }
              return child;
          }
      
          @Override
          public boolean documentHasId(final Child child) {
              return child.getId() != null;
          }
      
          @Override
          public BsonValue getDocumentId(final Child child) {
              return new BsonObjectId(child.getId());
          }
      
          @Override
          public Child decode(final BsonReader reader, final DecoderContext decoderContext) {
              final Child child = new Child();
              reader.readStartDocument();
              while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                  final String fieldName = reader.readName();
                  if (fieldName.equals("_id")) {
                      child.setId(objectIdCodec.decode(reader, decoderContext));
                  } else if (fieldName.equals("name")) {
                      child.setName(reader.readString());
                  } else {
                      throw new IllegalStateException("Unknown fieldName " + fieldName);
                  }
              }
              reader.readEndDocument();
              return child;
          }
      
          @Override
          public void encode(final BsonWriter writer, final Child child, final EncoderContext encoderContext) {
              writer.writeStartDocument();
              writer.writeName("_id");
              objectIdCodec.encode(writer, child.getId(), encoderContext);
              writer.writeString("name", child.getName());
              writer.writeEndDocument();
          }
      
          @Override
          public Class<Child> getEncoderClass() {
              return Child.class;
          }
      }
      
      public class ParentCodec implements CollectibleCodec<Parent> {
          private final Codec<ObjectId> objectIdCodec;
          private final Codec<DBRef> dbRefCodec;
          private final Codec<Child> childCodec;
      
          public ParentCodec() {
              this.objectIdCodec = MongoClientSettings.getDefaultCodecRegistry().get(ObjectId.class);
              final CodecRegistry codecRegistry = CodecRegistries.fromCodecs(
                      new DecodableDBRefCodec(MongoClientSettings.getDefaultCodecRegistry()),
                      new ChildCodec());
              this.dbRefCodec = codecRegistry.get(DBRef.class);
              this.childCodec = codecRegistry.get(Child.class);
          }
      
          @Override
          public Parent generateIdIfAbsentFromDocument(final Parent parent) {
              if (!documentHasId(parent)) {
                  parent.setId(new ObjectId());
              }
              return parent;
          }
      
          @Override
          public boolean documentHasId(final Parent parent) {
              return parent.getId() != null;
          }
      
          @Override
          public BsonValue getDocumentId(final Parent parent) {
              return new BsonObjectId(parent.getId());
          }
      
          @Override
          public Parent decode(final BsonReader reader, final DecoderContext decoderContext) {
              final Parent parent = new Parent();
              reader.readStartDocument();
              while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                  final String fieldName = reader.readName();
                  if (fieldName.equals("_id")) {
                      parent.setId(objectIdCodec.decode(reader, decoderContext));
                  } else if (fieldName.equals("child")) {
                      if (reader.getCurrentBsonType().equals(BsonType.NULL)) {
                          reader.readNull();
                      } else {
                          parent.setChild(dbRefCodec.decode(reader, decoderContext));
                      }
                  } else if (fieldName.equals("child_loaded")) {
                      parent.setChild(childCodec.decode(reader, decoderContext));
                  } else {
                      throw new IllegalStateException("Unknown fieldName " + fieldName);
                  }
              }
              reader.readEndDocument();
              return parent;
          }
      
          @Override
          public void encode(final BsonWriter writer, final Parent parent, final EncoderContext encoderContext) {
              writer.writeStartDocument();
              writer.writeName("_id");
              objectIdCodec.encode(writer, parent.getId(), encoderContext);
              writer.writeName("child");
              if (parent.getChildDbRef() != null) {
                  dbRefCodec.encode(writer, parent.getChildDbRef(), encoderContext);
              } else {
                  writer.writeNull();
              }
              writer.writeEndDocument();
          }
      
          @Override
          public Class<Parent> getEncoderClass() {
              return Parent.class;
          }
      }
      

      However to do it I need to be able to decode the DbRef.

      To fix it I need to provide custom DbRefCodec this way:

      public class DecodableDBRefCodec extends DBRefCodec {
      
          public DecodableDBRefCodec(final CodecRegistry registry) {
              super(registry);
          }
      
          @Override
          public Class<DBRef> getEncoderClass() {
              return DBRef.class;
          }
      
          @Override
          public DBRef decode(final BsonReader reader, final DecoderContext decoderContext) {
              reader.readStartDocument();
              String databaseName = null;
              String ref = null;
              ObjectId id = null;
              while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                  final String dbRefFieldName = reader.readName();
                  if (dbRefFieldName.equals("$ref")) {
                      ref = reader.readString();
                  } else if (dbRefFieldName.equals("$id")) {
                      id = reader.readObjectId();
                  } else if (dbRefFieldName.equals("$db")) {
                      databaseName = reader.readString();
                  } else {
                      throw new IllegalStateException("Unknown fieldName " + dbRefFieldName);
                  }
              }
              reader.readEndDocument();
              assert ref != null;
              assert id != null;
              return new DBRef(databaseName, ref, id);
          }
      }
      

      Is it possible to update the DBRefCodec ?

      Regards,

      Damien

            Assignee:
            jeff.yemin@mongodb.com Jeffrey Yemin
            Reporter:
            damien.clementdhuart@gmail.com Damien Clément d'Huart
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: