Simple ODM Framework
The uma::bson API added a simple callback based Object-Document Mapping (ODM) framework in version 2.2. This makes it possible for client applications/libraries to create domain models that use the various uma::bson::Value instances to store data and inherit from the abstract uma::bson::Object class. Serialisation to/from BSON is provided by the API through the callback methods.
Serialisation to BSON involves retrieving a list of field names that are to be serialised. The framework iterates over the list of field names and invokes the getValue method to retrieve the value to be serialised. The de-serialisation process invokes the setValue method on the instance to populate the fields with data from the BSON data stream.
Callback Methods
The following three pure virtual methods must be implemented by the model object to enable automatic BSON serialisation support. An optional virtual method must be implemented by model objects that store arrays of other objects.
const Object::FieldNames getFieldNames() const
Return a
std::vector<std::string>
that lists the fields in the instance that are to be serialised. Note that the order of fields specified is important when comparing different BSON data streams. This method is used primarily during the BSON serialisation process. The equality operator for uma::bson::Object also uses this method.Value& getValue( const std::string& name )
This method must return the value for the field with the specified name in the object. Note that the API at present requires a reference to the Value, thus forcing clients to model internal fields as uma::bson::Value instances. This is purely for performance reasons, and may be modified in a future version to return an object, which would enable modelling fields as raw datatypes. This method is used by the BSON serialisation process as it iterates over the list of field names, and retrieves the value that is to be serialised.
Note that this method is also used during the de-serialisation process when populating Object data types. The de-serialiser fetches the reference to the current (usually default constructed) Object instance from the destination model instance and then populates it with the data from the BSON stream. This is done to ensure that the Object instance being populated is of the same type as the type used in the destination model class.
void setValue( const std::string& name, const uma::bson::Value& value )
This method is used to set the value of a field in the model object by name. This method is the primary driver for the BSON de-serialisation process. The BSON elements parsed from the data stream trigger invocation of this method with appropriate name and value pairs.
As mentioned in the notes about the
getValue
method, for Object type fields, the references to the existing instances are fetched from the destination model object and then populated. This method is still invoked with the updated object, so the implementation ideally would check for same memory location before assigning.Note
Starting with version 2.3 of the API, this method is no longer pure virtual. A default implementation is provided which should handle most needs. The internal BSON de-serialiser fetches the reference to the current object/array fields using the getValue method and populates them. Hence, there should be no need to update the already updated field instance. The method is still virtual to allow any specific customisations, or for cases where client code implements its its own serialisation. The unit test suite has been updated to test the proper behaviour of the default implementation.
Object* getObjectForArray( const std::string& name )
This optional method should be implemented by all model objects that store other objects in an uma::bson::Array. Note that this is not required if the array stores simple types (Integer, Date, String), and is only required when the values stored in the array are other objects. The de-serialisation process will invoke this method, populate and set in the array instance that is being constructed with the BSON data. Note that the Object instance returned must ideally be a freshly created instance on the heap. The uma::bson::Element instance that stores this object instance takes ownership of the memory and hence client code must not delete these instances.
Note that the default implementation for this method throws an exception. Classes that include array fields that store other objects must implement this method, or an exception will be raised at run time. Proper unit testing should catch this issue, and lead to this method being implemented where appropriate.
Sample Model Class
The following sample model class (taken from the unit test suite) illustrates the concepts behind the ODM framework. The ManufacturingObject class contains two fields - name and model. The public interface for the class encapsulates the uma::bson::String fields and presents clients a std::string based interface.
Header
The class contains two string fields which store the name and model identifier. The public interface for the class allows users to get/set the stored value as std::string instances. The callback methods are made private to ensure that the serialisation implementation is hidden from callers. This in general is the recommended pattern for modelling your domain objects using the ODM framework.
#ifndef SAMPLE_VALUE_MANUFACTUREROBJECT_H #define SAMPLE_VALUE_MANUFACTUREROBJECT_H #include <uma/bson/Object.h> #include <uma/bson/String.h> namespace sample { namespace value { class ManufacturerObject : public uma::bson::Object { public: ManufacturerObject() {} const std::string& getName() const { return name.getValue(); } void setName( const std::string& n ) { name.setValue( n ); } const std::string& getModel() const { return model.getValue(); } void setModel( const std::string& m ) { model.setValue( m ); } private: const FieldNames getFieldNames() const; uma::bson::Value& getValue( const std::string& name ); void setValue( const std::string& name, const uma::bson::Value& value ); private: uma::bson::String name; uma::bson::String model; }; } // namespace value } // namespace sample #endif // SAMPLE_VALUE_MANUFACTUREROBJECT_H
Implementation
The getFieldNames method creates a new vector and populates it with the field names. Since this information is unlikely to change at runtime, we may modify the method declaration to make it return a reference to a vector. This will allow a static vector to be maintained and returned to avoid duplicating this information all the time.
Both the getValue and setValue methods throw an exception if an invalid element name is specified. This is generally a good practice and will help find any issues while unit testing the serialisation process.
Both the getValue and setValue methods throw an exception if an invalid element name is specified. This is generally a good practice and will help find any issues while unit testing the serialisation process.
#include "ManufacturerObject.h" using namespace sample::value; using namespace uma::bson; using std::string; const Object::FieldNames ManufacturerObject::getFieldNames() const { FieldNames names; names.push_back( "name" ); names.push_back( "model" ); return names; } uma::bson::Value& ManufacturerObject::getValue( const string& field ) { if ( field == "name" ) return name; if ( field == "model" ) return model; throw Poco::InvalidArgumentException( "No field with name: " + field + " in sample::value::ManufacturerObject" ); } void ManufacturerObject::setValue( const string& field, const uma::bson::Value& value ) { const uma::bson::String& str = dynamic_cast<const uma::bson::String&>( value ); if ( field == "name" ) { name = str; return; } if ( field == "model" ) { model = str; return; } throw Poco::InvalidArgumentException( "No field with name: " + field + " in sample::value::ManufacturerObject" ); }
Client Code
sample::value::ManufacturerObject manufacturer; manufacturer.setName( “BlackBerry” ); manufacturer.setModel( “Q10” ); // a little later in your code std::stringstream ss; manufacturer.toBson( ss ); // serialise as BSON data stream. // Another part of your code sample::value::ManufacturerObject mobj; mobj.populate( ss ); // de-serialise data from BSON stream if ( manufacturer != mobj ) throw “Serialisation failed”;
That is all there is to serialisation to and from BSON. The laborious part of the ODM framework is in implementing the getValue and setValue methods. Users may also find the requirement of modelling all their data as instances of uma::bson::Value too restricting as well. In a future release we may move towards a MetaObject structure that stores the name of the field and two functions pointers for the accessor and mutator for the field. This may lead to more elegant code, however it is unlikely that the tedium of adding additional code to support BSON serialisation is unlikely to be alleviated to any great degree.
We may also decide to use Qt MetaObject support or CERN Reflex framework to provide true reflection features and avoid the use of callback methods or function pointers all together and just serialise fields that are defined as instances of uma::bson::Value in a class.
We may also decide to use Qt MetaObject support or CERN Reflex framework to provide true reflection features and avoid the use of callback methods or function pointers all together and just serialise fields that are defined as instances of uma::bson::Value in a class.