Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I properly extend Mongo's Document class?

Tags:

java

mongodb

I've created a class User that extends Document. User just has some simple constructors and getters/setters around some strings and ints. However, when I try to insert the User class into Mongo I get the following error:

Exception in thread "main" org.bson.codecs.configuration.CodecConfigurationException: Can't find a codec for class com.foo.User.
    at org.bson.codecs.configuration.CodecCache.getOrThrow(CodecCache.java:46)
    at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:63)
    at org.bson.codecs.configuration.ProvidersCodecRegistry.get(ProvidersCodecRegistry.java:37)
    at org.bson.BsonDocumentWrapper.asBsonDocument(BsonDocumentWrapper.java:62)
    at com.mongodb.MongoCollectionImpl.documentToBsonDocument(MongoCollectionImpl.java:507)
    at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:292)
    at com.mongodb.MongoCollectionImpl.insertMany(MongoCollectionImpl.java:282)
    at com.foo.bar.main(bar.java:27)

Sounds like I need to work with some Mongo Codecs stuff, but I'm not familiar with it and some quick googling returns some results that seem pretty advanced.

How do I properly write my User class for use in Mongo? Here is my class for reference:

public class User extends Document {
    User(String user, List<Document > history, boolean isActive, String location){
        this.append("_id", user)
                .append("history", history)
                .append("isActive", isActive)
                .append("location", location);
    }

    public List<Document > getHistory(){
        return this.get("history", ArrayList.class);
    }

    public void addToHistory(Document event){
       List<Document> history = this.getHistory();
        history.add(event);
        this.append("history", history);
    }

    public boolean hasMet(User otherUser){
        List<String> usersIveMet = this.getUsersMet(),
                     usersTheyMet = otherUser.getUsersMet();
        return !Collections.disjoint(usersIveMet, usersTheyMet);
    }

    public List<String> getUsersMet() {
        List<Document> usersHistory = this.getHistory();
        List<String> usersMet = usersHistory.stream()
                .map(doc -> Arrays.asList(doc.getString("user1"), doc.getString("user1")))
                .filter(u -> !u.equals(this.getUser()))
                .flatMap(u -> u.stream())
                .collect(Collectors.toList());
        return usersMet;
    }

    public String getUser(){
        return this.getString("_id");
    }
}
like image 759
David says Reinstate Monica Avatar asked May 25 '26 08:05

David says Reinstate Monica


2 Answers

Since you are trying to create new object (even if you extend from Document), Mongo has no way to recognize it and therefore you need to provide encoding/decoding in order to let Mongo to know about your object (at least I cannot see other way than this..).

I played with your User class a bit and get it work. So, here is how I defined a User class:

public class User {

    private List<Document> history;
    private String id;
    private Boolean isActive;
    private String location;

    // Getters and setters. Omitted for brevity..
}

Then you need provide encoding/decoding logic to your User class:

public class UserCodec implements Codec<User> {

    private CodecRegistry codecRegistry;

    public UserCodec(CodecRegistry codecRegistry) {
        this.codecRegistry = codecRegistry;
    }

    @Override
    public User decode(BsonReader reader, DecoderContext decoderContext) {
        reader.readStartDocument();
        String id = reader.readString("id");
        Boolean isActive = reader.readBoolean("isActive");
        String location = reader.readString("location");

        Codec<Document> historyCodec = codecRegistry.get(Document.class);
        List<Document> history = new ArrayList<>();
        reader.readStartArray();
        while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
            history.add(historyCodec.decode(reader, decoderContext));
        }
        reader.readEndArray();
        reader.readEndDocument();

        User user = new User();
        user.setId(id);
        user.setIsActive(isActive);
        user.setLocation(location);
        user.setHistory(history);
        return user;
    }

    @Override
    public void encode(BsonWriter writer, User user, EncoderContext encoderContext) {
        writer.writeStartDocument();
        writer.writeName("id");
        writer.writeString(user.getId());
        writer.writeName("isActive");
        writer.writeBoolean(user.getIsActive());
        writer.writeName("location");
        writer.writeString(user.getLocation());

        writer.writeStartArray("history");
        for (Document document : user.getHistory()) {
            Codec<Document> documentCodec = codecRegistry.get(Document.class);
            encoderContext.encodeWithChildContext(documentCodec, writer, document);
        }
        writer.writeEndArray();
        writer.writeEndDocument();
    }

    @Override
    public Class<User> getEncoderClass() {
        return User.class;
    }
}

Then you need a codec provided for type checking before starting serialization/deserialization.

public class UserCodecProvider implements CodecProvider {

    @Override
    @SuppressWarnings("unchecked")
    public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
        if (clazz == User.class) {
            return (Codec<T>) new UserCodec(registry);
        }
        return null;
    }
}

And finally, you need to register your provider to your MongoClient, that's all.

public class MongoDb {

    private MongoDatabase db;

    public MongoDb() {
        CodecRegistry codecRegistry = CodecRegistries.fromRegistries(
                CodecRegistries.fromProviders(new UserCodecProvider()),
                MongoClient.getDefaultCodecRegistry());
        MongoClientOptions options = MongoClientOptions.builder()
                .codecRegistry(codecRegistry).build();
        MongoClient mongoClient = new MongoClient(new ServerAddress(), options);
        db = mongoClient.getDatabase("test");
    }

    public void addUser(User user) {
        MongoCollection<User> collection = db.getCollection("user").withDocumentClass(User.class);
        collection.insertOne(user);
    }

    public static void main(String[] args) {
        MongoDb mongoDb = new MongoDb();

        Document history1 = new Document();
        history1.append("field1", "value1");
        history1.append("field2", "value2");
        history1.append("field3", "value3");

        List<Document> history = new ArrayList<>();
        history.add(history1);

        User user = new User();
        user.setId("someId1");
        user.setIsActive(true);
        user.setLocation("someLocation");
        user.setHistory(history);
        mongoDb.addUser(user);
    }
}
like image 164
eruslmu Avatar answered May 27 '26 22:05

eruslmu


A bit late but just stumbled across the issue and was also somewhat disappointed by the work involved in the proposed solutions so far. Especially since it requires tons of custom code for every single Document extending class you wish to persist and might also exhibit sub-optimal performance noticeable in large data sets.

Instead I figured one might piggyback off DocumentCodec like so (Mongo 3.x):

public class MyDocumentCodec<T extends Document> implements CollectibleCodec<T> {

private DocumentCodec _documentCodec;
private Class<T> _class;
private Constructor<T> _constructor;

public MyDocumentCodec(Class<T> class_) {
    try {
        _documentCodec = new DocumentCodec();
        _class = class_;
        _constructor = class_.getConstructor(Document.class);
    } catch (Exception ex) {
        throw new MCException(ex);
    }
}

@Override
public void encode(BsonWriter writer, T value, EncoderContext encoderContext) {
    _documentCodec.encode(writer, value, encoderContext);
}

@Override
public Class<T> getEncoderClass() {
    return _class;
}

@Override
public T decode(BsonReader reader, DecoderContext decoderContext) {
    try {
        Document document = _documentCodec.decode(reader, decoderContext);
        T result = _constructor.newInstance(document);
        return result;
    } catch (Exception ex) {
        throw new MCException(ex);
    }
}

@Override
public T generateIdIfAbsentFromDocument(T document) {
    if (!documentHasId(document)) {
        Document doc = _documentCodec.generateIdIfAbsentFromDocument(document);
        document.put("_id", doc.get("_id"));
    }
    return document;
}

@Override
public boolean documentHasId(T document) {
    return _documentCodec.documentHasId(document);
}

@Override
public BsonValue getDocumentId(T document) {
    return _documentCodec.getDocumentId(document);
}
}

This is then registered along the lines of

MyDocumentCodec<MyClass> myCodec = new MyDocumentCodec<>(MyClass.class);
CodecRegistry codecRegistry = CodecRegistries.fromRegistries(MongoClient.getDefaultCodecRegistry(),
                CodecRegistries.fromCodecs(myCodec));
MongoClientOptions options = MongoClientOptions.builder().codecRegistry(codecRegistry).build();
MongoClient dbClient = new MongoClient(new ServerAddress(_dbServer, _dbPort), options);

Switching to this approach along with bulking up some operations (which probably has a large effect) I just managed to run an operation that previously took several hours to 30 mins. The decode method can probably be improved but my main concern was inserts for now.

Hope this helps someone. Please let me know if you see issues with this approach.

Thanks.

like image 29
beluga Avatar answered May 27 '26 20:05

beluga



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!