Search code examples
c++mongodbqtmongo-cxx-driver

move copy and/or assign a mongodb cxx cursor


I'm writing a series of classes that wrap around the mongodb-cxx classes in order to conform them to the storage API of the rest of my application, which is written in Qt.

I don't need the full function set of Mongo, basically just CRUD: create, read, update, delete, and find, perhaps with a few more advanced parameters passed in (read/write concerns, etc).

I had wanted to create a wrapper class around mongocxx::cursor such that the results of a find() could be iterated "later", in the data format used by the storage API. By "later" I mean that the (wrapped) cursor is returned to an abstraction class that can iterate over the results and validate data against a schema, or entries can be skipped, or results can be sent in groups (asynchronously) to an end client, etc.

For instance, if I have: (this is abbreviated a little for the example)

using namespace mongocxx;

class QMongoClient
{
    static instance _instance;

    QMongoClient(QString url) : _client( uri( qPrintable(url) ) );  
    client _client;
    QMongoDatabase operator[](QString d);
};

class QMongoDatabase
{
    QMongoDatabase(database db) : _database(db);
    database _database;
    QMongoCollection operator [](QString c);
};

class QMongoCollection
{
    QMongoCollection(collection c) : _collection(c);
    collection _collection;
    //create... same as insert()
    //read... same as find_one()
    //update... same as upsert() or update()
    //delete...
    //find... a query will be provided
    QMongoCursor findAll() { cursor c = _collection.find({}); return QMongoCursor(c); };
};

class QMongoCursor
{
    QMongoCursor(cursor c) : _cursor(c);
    //copy/move/assign constructors needed, probably

    cursor _cursor;

    QString data(); //return document contents as JSON string, for instance

    //begin() <-- iterator
    //end() <-- iterator
};

I had hoped this method would work, and it does, up until the cursor, which throws a compiler error:

error: call to implicitly-deleted copy constructor of 'mongocxx::v_noabi::cursor'

The idea is to do something like:

QMongoClient client("mongodb://url...");
QMongoDatabase db = client["somedbname"];
QMongoCollection coll = db["somecollection"];
QMongoCursor c = coll.findAll();

//or more abbreviated:
QMongoCursor c = client["somedbname"]["somecollection"].findAll();

//iterate over c here as needed, or send it to another abstraction layer...

I think this is happening because mongocxx::cursor objects are not meant to be passed around, and when they fall out of scope they are destroyed.

For instance when QMongoCursor::findAll() finishes, the returned cursor is meant to be destroyed, and can't be copied in the QMongoCursor constructor.

It seems very inefficient for the findAll() function to retrieve all documents at that moment and return them (potentially unknown size of result set). I'd like to be able to retrieve results 10 at a time, for instance, and send them over the network to the end client in groups asynchronously.

So the question is/are:

  • how do I keep a cursor alive, if it's being destroyed when it falls out of scope and has no copy/move constructor
  • or how can I create a pointer to a cursor and know what its lifespan is and when to delete it
  • is there another method for this that I'm missing? I've been looking in the documentation but not finding it.

I think I'm missing some c++ syntax or mechanism, like move assign or something like that.

Any ideas?


Solution

  • Well, mongocxx::cursor objects are moveable, but you haven't asked for it to be moved, you are asking for a copy. Try:

    QMongoCursor(cursor c) : _cursor(std::move(c));
    

    You probably want to do that for your QDatabase and QCollection constructors too.

    One thing you should think about too is that if you are building a higher level toolkit, you need to pay close attention to the lifetime rules relating client, database, collection, and cursor objects. You may find that building a shared_ptr like abstraction to ensure proper lifetimes are enforced by the type system is advantageous for your usec-case.

    We didn't make the driver work that way because we didn't want to force that decision on people. Instead, we felt that it could and should be delegated to a higher level abstraction, probably much like the one you are building.