Context
As many of you may know I have a library that allows C++ objects to be converted into JSON/YAML/BSON automatically with a single declaration (see previous code reviews).
I am now (trying) using this to connect and send data to Mongo without the developer having to write any specific code to convert their C++ objects into BSON.
I also have a wrapper around a Socket that makes it behave like a std::iostream
so it can simply be used with operator<<
and operator>>
. This Socket
wrapper, when used in my Service Library (Nissa
), allows thread (Co-Routine) switching when a read/write would block, thus making it very efficient.
Usage Pattern
I want to insert objects into Mongo like this:
struct Person { std::string name; int age; }; ThorsAnvil_MakeTraits(Person, name, age); int main() { MongoConnection connection(DB, USER, PASSWORD); // Creates a connection // It wraps a Socket stream described // above but also sends all the // handshaking and authentication required // by Mongo. // But you can think of it like a stream. Person person{"Loki", 12}; using ThorsAnvil::DB::Mongo::make_CmdDB_Insert; // This line below is what I am interested in. // I will show you more usage below and how I though about implementing it // with the code. Hoping for connection << make_CmdDB_Insert(DB, "Collection", QueryOptions{}, person); CmdDB_Reply reply; connection >> reply; }
Usage of Insert
I am using Insert
as an example here. But there are several commands each with their own unique options. So I want to get some feedback on this idea before I go an implement everything for all the commands.
The Insert has the following extra optional parameters: Full Details
ordered
Default: True
Inserts are ordered.
Method:unordered()
=> turns this offwriteConcern
An object that defines how the object is written.
Method:setWriteConcern(int w = 1, bool j = false, std::time_t wtimeout = 0)
bypassDocumentValidation
Default: false
Validates the object being written to the connection.
Method:byPass()
=> turn off validation (i.e. set param to true)comment
A string that will be written to different places in Mongo.
Method:setComment(std::string&& c)
add a comment
Normally the default values are fine, but I do want the user to be able to modify them if required. The thing is that if they are not explicitly set, then I don't want to send the value over the wire (as there is a cost to convert them and write them to the socket). So I thought I could set it up like this:
If I try putting all the options in the constructor then it will lead to a proliferation of constructor methods. Which seems hard. So I want to try using option methods that return a reference to the object so they can be chained; like this:
connection << make_CmdDB_Insert(DB, "Collection", QueryOptions{}, person) .unordered() .setComment("Hi there"); // etc Just add a call for each option you want to set. // You should be able to use any combination of options // in any order. // // The reason to do this is to avoid an explosion in constructors // with different parameters in different orders. // // With Only 4 options there are 8 different constructors needed. // With other commands (like Find) the number of options is 16 // Which would lead to thousands of constructors.
Implementation
Since this is code review. Here is my implementation.
Note: There is an underlying layer that is not provided here (for brevity). I will be positing all the code for a full review when it is done.
CmdDB_Insert.h#ifndef THORSANVIL_DB_MONGO_CMD_DB_INSERT_H #define THORSANVIL_DB_MONGO_CMD_DB_INSERT_H // https://docs.mongodb.com/manual/reference/command/insert/#dbcmd.insert #include "CmdDB.h" namespace ThorsAnvil::DB::Mongo { template<typename Document> struct Insert { public: Insert(std::string const& collection, Document const& doc); void unordered(); void byPass(); void setWrieConcern(int w = 1, bool j = false, std::time_t wtimeout = 0); void setComment(std::string&& c); private: // Allow Serialization code access to private members. friend class ThorsAnvil::Serialize::Traits<Insert>; friend class ThorsAnvil::Serialize::Filter<Insert>; // What fields will not be serialized. // Serialized if not in the filter or in the filter and value is true. std::map<std::string, bool> filter = {{"ordered", false}, {"writeConcern", false}, {"bypassDocumentValidation", false}, {"comment", false}}; // Members that will be sent on the wire in BSON to mongo server. std::string insert; std::vector<Document> documents; bool ordered = true; WriteConcern writeConcern; bool bypassDocumentValidation = false; std::string comment; }; // Wrapper around the CmdDB_Query object. // This inherits from Op_Query: // This handles the low level OP_QUERY object that contains the header and query flags. // // The CmdDB_Query // This handles writing to the underlying connection object. // Insert is the command it is writing (as defined above) // Document: Is the user defined object that we are writing. template<typename Document> using CmdDB_Insert = CmdDB_Query<Insert<Document>>; template<typename Document> CmdDB_Insert<Document> make_CmdDB_Insert(std::string const& db, std::string const& collection, QueryOptions&& options, Document const& doc) { using Document = typename std::iterator_traits<I>::value_type; return CmdDB_Insert<Document>(db, collection, std::move(options), doc); } } // Tells the serializing code to filter the member based on the member filter. ThorsAnvil_Template_MakeFilter(1, ThorsAnvil::DB::Mongo::Insert, filter); // Tells the serialization code what to send across the wire in BSON. ThorsAnvil_Template_MakeTrait(1, ThorsAnvil::DB::Mongo::Insert, insert, documents, ordered, writeConcern, bypassDocumentValidation, comment); #include "CmdDB_Insert.tpp" #endif
CmdDB_Insert.tpp #ifndef THORSANVIL_DB_MONGO_CMD_DB_INSERT_TPP #define THORSANVIL_DB_MONGO_CMD_DB_INSERT_TPP #ifndef THORSANVIL_DB_MONGO_CMD_DB_INSERT_H #error "This should only be included from CmdDB_Insert.h" #endif namespace ThorsAnvil::DB::Mongo { template<typename Document> Insert<Document>::Insert(std::string const& collection, Document const& doc) : insert(collection) , documents(1, doc) {} template<typename Document> void Insert<Document>::unordered() { ordered = false; filter["ordered"] = true; } template<typename Document> void Insert<Document>::byPass() { bypassDocumentValidation = false; filter["bypassDocumentValidation"] = true; } template<typename Document> void Insert<Document>::setWrieConcern(int w, bool j, std::time_t wtimeout) { writeConcern = WriteConcern{w, j, wtimeout}; filter["writeConcern"] = true; } template<typename Document> void Insert<Document>::setComment(std::string&& c) { comment = c; filter["comment"] = true; } } #endif
CmdDB.h #ifndef THORSANVIL_DB_MONGO_CMD_DB_H #define THORSANVIL_DB_MONGO_CMD_DB_H // https://docs.mongodb.com/manual/reference/command/nav-crud/ #include "Op_Query.h" #include "Op_Reply.h" #include "ThorSerialize/SerUtil.h" namespace ThorsAnvil::DB::Mongo { enum class ReadConcernLevel {local, available, majority, linearizable}; struct Collation { std::string locale; bool caseLevel; std::string caseFirst; int strength; bool numericOrdering; std::string alternate; std::string maxVariable; bool backwards; }; struct WriteErrors { std::size_t index; int code; std::string errmsg; }; struct WriteConcernError { int code; std::string errmsg; }; struct CmdReply { double ok; std::size_t n; std::string errmsg; std::string codeName; int code; std::vector<WriteErrors> writeErrors; }; class CmdDB_Reply: public Op_Reply<CmdReply> { public: bool replyCount() const; virtual bool isOk() const override; virtual std::string getHRErrorMessage() const override; protected: friend std::ostream& operator<<(std::ostream& stream, HumanReadable<CmdDB_Reply> const& reply); std::ostream& printHR(std::ostream& stream) const {return Op_Reply::printHR(stream);} }; struct WriteConcern { WriteConcern(int w = 1, bool j = false, std::time_t wtimeout = 0); int w; bool j; std::time_t wtimeout; }; template<typename Action> class CmdDB_Query: public Op_Query<Action> { public: template<typename... Args> CmdDB_Query(std::string const& db, std::string const& collection, QueryOptions&& options, Args&&... args); // Insert CmdDB_Query& byPass(); // Options // All these options are implemented for this object // in the CmdDB.tpp file. // // But if you make a call to a method that is not implemented // in your action object then you will get a compile time error. // i.e. If you used .addHint("A hint") on an Insert Action // this will fail to compile as this will call .addHint() // on the actio but this is not implements on Insert // but is implemented on Find document. // Insert & Delete CmdDB_Query& unordered(); CmdDB_Query& setWrieConcern(int w = 1, bool j = false, std::time_t wtimeout = 0); // Insert & Find CmdDB_Query& setComment(std::string&& c); // Find CmdDB_Query& addFileds(std::initializer_list<std::string> const& fieldNames); CmdDB_Query& addHint(std::string&& hint); CmdDB_Query& setSkip(std::size_t val); CmdDB_Query& setLimit(std::size_t val); CmdDB_Query& setBatchSize(std::size_t val); CmdDB_Query& singleBatch(); CmdDB_Query& setMaxTimeout(std::size_t val); CmdDB_Query& addReadConcern(ReadConcernLevel val); CmdDB_Query& addMax(std::string const& field, int val); CmdDB_Query& addMin(std::string const& field, int val); CmdDB_Query& justKeys(); CmdDB_Query& showId(); CmdDB_Query& tailableCursor(); CmdDB_Query& tailedCursorAwait(); CmdDB_Query& setNoCursorTimeout(); CmdDB_Query& setAllowPartialResults(); CmdDB_Query& useDisk(); friend std::ostream& operator<<(std::ostream& stream, HumanReadable<CmdDB_Query> const& data); friend std::ostream& operator<<(std::ostream& stream, CmdDB_Query const& data) {return data.print(stream);} }; } ThorsAnvil_MakeEnum(ThorsAnvil::DB::Mongo::ReadConcernLevel, local, available, majority, linearizable); ThorsAnvil_MakeTrait(ThorsAnvil::DB::Mongo::Collation, locale, caseLevel, strength, numericOrdering, alternate, maxVariable, backwards); ThorsAnvil_MakeTrait(ThorsAnvil::DB::Mongo::WriteConcern, w, j, wtimeout); ThorsAnvil_MakeTrait(ThorsAnvil::DB::Mongo::WriteErrors, index, code, errmsg); ThorsAnvil_MakeTrait(ThorsAnvil::DB::Mongo::WriteConcernError, code, errmsg); ThorsAnvil_MakeTrait(ThorsAnvil::DB::Mongo::CmdReply, ok, n, writeErrors, writeConcernError, errmsg, codeName, code); #include "CmdDB.tpp" #endif
CmdDB.tpp #ifndef THORSANVIL_DB_MONGO_CMD_DB_TPP #define THORSANVIL_DB_MONGO_CMD_DB_TPP #ifndef THORSANVIL_DB_MONGO_CMD_DB_H #error "This should only be included from CmdDB.h" #endif namespace ThorsAnvil::DB::Mongo { template<typename Action> template<typename... Args> CmdDB_Query<Action>::CmdDB_Query(std::string const& db, std::string const& collection, QueryOptions&& options, Args&&... args) : Op_Query<Action>(db + ".$cmd", std::forward<QueryOptions>(options), 1, 0, collection, std::forward<Args>(args)...) {} template<typename Action> CmdDB_Query<Action>& CmdDB_Query<Action>::unordered() { this->getQuery().unordered(); return *this; } template<typename Action> CmdDB_Query<Action>& CmdDB_Query<Action>::byPass() { this->getQuery().byPass(); return *this; } template<typename Action> CmdDB_Query<Action>& CmdDB_Query<Action>::setComment(std::string&& val) { this->getQuery().setComment(std::forward<std::string>(val)); return *this; } // etc. // These all look like // 1: Call getQuery() which gets the `Insert` object. // 2: Call the setComment() or appropriate option method on action object. // 3: return *this to allow chaining. } #endif
If you are interested:
- ThorsSerializer : Serialization of C++ objects without writting code
- ThorsCrypto : Crypto to Authenticate
- ThorsDBSQLMongo : DB Accesses
- ThorsNisse : Server to provide non blocking reads/writes