-
Type: Improvement
-
Resolution: Declined
-
Priority: Unknown
-
None
-
Affects Version/s: None
-
Component/s: Codecs
-
None
-
Java Drivers
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