I'm interested in using boost::serialization for some small classes
which need to be backed by a database.
I thought I'd try and take advantage of boost::serialization to manage
the basic marshaling of objects into/out of a binary format, and use
BerkeleyDB for database support.
Unfortunately, I looked around for some prior art, but couldn't find
any, so I'm posting here a small example of my own with the hope that
others may lend feedback on this basic approach, or else find use for
the ideas here in their own work.
The following code was built and (minimally) tested with gcc-3.4.6,
stlport-5.0.2, berkeleydb-4.4.20 (with stlport), boost-1.33.1 (with
gcc/stlport).
Thanks!
Andy
<code>
#include <string>
#include <iostream>
#include <sstream>
#include
#include
#include
using namespace std;
/**
* An overly simplistic representation of an every-day schmo.
*/
class Person
{
private:
unsigned int id_; // a horrible reduction
/**
* Inserts a textual representation of a Person into an output stream
(not
* intended for serialization).
* @param os an output stream
* @param p a Person to insert into the output stream
* @return the output stream
*/
friend ostream & operator<<(ostream & os, Person const & p)
{
return os << p.id_;
}
friend class boost::serialization::access;
friend class Database;
/**
* Serializes this Person into/out-of the given Archive, depending on
the type
* of the Archive.
* @param ar an input or output Archive
* @param version a class number
*/
template<typename Archive> void serialize(Archive & ar, unsigned int
const version)
{
ar & id_;
}
/**
* Serializes this Person into the given Dbt.
* @param[out] data the Dbt into which to serialize this Person
*/
void serialize(Dbt * data) const
{
// serialize this Person into binary archive
ostringstream oss(ios::out | ios::binary);
boost::archive::binary_oarchive oa(oss, boost::archive::no_header);
oa << (*this);
// copy serialized data (is there any way to avoid this?)
void * buf = malloc(sizeof(char) * oss.str().size());
memcpy(buf, oss.str().data(), oss.str().size());
// initialize Dbt*
memset(data, 0, sizeof(Dbt));
data->set_data(buf);
data->set_size(oss.str().size());
data->set_ulen(oss.str().size());
data->set_flags(DB_DBT_USERMEM);
}
/**
* Constructs a Person from the given Dbt key-data pair.
* @param[in] key a primary key containing a name (ignored for now)
* @param[in] data an element which references a serialized Person
*/
Person(Dbt const * key, Dbt const * data)
{
// construct input stream from data's void *
istringstream iss
(string(reinterpret_cast(data->get_data()),
data->get_size()),
ios::in | ios::binary);
boost::archive::binary_iarchive ia(iss, boost::archive::no_header);
ia >> (*this);
}
/**
* Extracts a secondary key (a Person's Id) from a given key-data
* (name-Person) pair.
* @param[in] secondary the secondary Db
* @param[in] key the primary key, containing a Person name
* @param[in] data the Person associated with the primary key
* @param[out] skey the secondary key to be generated
* @return zero on successful generation of secondary key, non-zero
on any
* error so BDB will ignore the possibly bogus secondary key.
*/
static int getSecondaryKey(Db * secondary, Dbt const * key, Dbt const
* data, Dbt * skey)
{
unsigned int * id = reinterpret_cast(malloc(sizeof(unsigned int)));
istringstream iss
(string(reinterpret_cast(data->get_data()),
data->get_size()),
ios::in | ios::binary);
boost::archive::binary_iarchive ia(iss, boost::archive::no_header);
ia >> (*id);
memset(skey, 0, sizeof(Dbt));
skey->set_data(id);
skey->set_size(sizeof(unsigned int));
skey->set_ulen(sizeof(unsigned int));
skey->set_flags(DB_DBT_APPMALLOC);
return 0;
}
public:
/**
* Constructs a Person with the specified Id.
*/
Person(unsigned int id = 0) : id_(id) {}
/**
* Destroys this Person
*/
~Person() {}
/**
* @return the Id of this Person
*/
unsigned int getId() { return id_; }
};
BOOST_CLASS_IMPLEMENTATION(Person,
boost::serialization::object_serializable)
BOOST_CLASS_TRACKING(Person, boost::serialization::track_never)
/**
* A helper class to manage a pair of BerkeleyDB instances (a primary
and an
* associated secondary database) used for storing Persons. The primary
db is
* keyed on a string name, while the secondary index is keyed on Person Id.
*/
class Database
{
private:
Db db_primary_;
Db db_secondary_;
/**
* Default ctor. we hide this.
*/
Database() : db_primary_(NULL, 0), db_secondary_(NULL, 0) {}
/**
* Closes Db instances in proper order. Called by dtor.
*/
void close() {
db_secondary_.close(0);
db_primary_.close(0);
}
public:
/**
* Constructs a Database using the given file to store both primary and
* secondary databases.
* @param[in] filename a pathname to a file in which to store data
*/
Database(string const & filename) :
db_primary_(NULL, 0),
db_secondary_(NULL, 0)
{
db_primary_.open
(0, // transaction id
filename.c_str(), // file name
"primary", // database name
DB_BTREE, // database type
DB_CREATE, // flags
0); // mode (see chmod(2))
db_secondary_.open
(0,
filename.c_str(),
"secondary",
DB_BTREE,
DB_CREATE,
0);
// associate the secondary with the primary
db_primary_.associate
(0, // transaction id
&db_secondary_, // secondary db
&Person::getSecondaryKey, // callback
DB_IMMUTABLE_KEY); // flags
}
/**
* Destroys this Database
*/
~Database() { close(); }
/**
* Stores a Person in this Database using the specified name as a
primary key.
* @param[in] name a string name to use as primary key for the given
Person
* @param[in] p the Person to insert into this Database
*/
void store(string const & name, Person const & p)
{
Dbt key, data;
key.set_data
(reinterpret_cast
(const_cast
(name.data())));
key.set_size(sizeof(char) * name.size());
p.serialize(&data);
db_primary_.put(0, &key, &data, 0);
}
/**
* Loads a Person from this Database using the specified name as a
primary key.
* @param[in] name the string name to use as primary key
* @return a newly constructed Person (caller is responsible for
deleting this
* Person)
*/
Person * load(string const & name)
{
Dbt key, data;
key.set_data
(reinterpret_cast
(const_cast
(name.data())));
key.set_size(sizeof(char) * name.size());
db_primary_.get(0, &key, &data, 0);
return new Person(&key, &data);
}
/**
* Loads a Person from this Database using the specified Id as a
secondary key.
* @param[in] id the Id to use as secondary key
* @return a newly constructed Person (caller is responsible for
deleting this
* Person)
*/
Person * load(unsigned int id)
{
Dbt key, pkey, data;
key.set_data(reinterpret_cast(&id));
key.set_size(sizeof(unsigned int));
db_secondary_.pget(0, &key, &pkey, &data, 0);
return new Person(&pkey, &data);
}
};
/**
* Performs a minimal test of the above Person database and serialization
* facilities.
*/
int main(int argc, char * args[])
{
cout << "Opening database" << endl;
Database db("person.db");
cout << "Creating person" << endl;
string name("Andy");
unsigned int id = 123456789;
Person p(id);
cout << "Storing person " << p << " using primary key \"" << name <<
"\"" << endl;
db.store(name, p);
cout << "Loading person " << p << " using secondary key \"" << id <<
"\"" << endl;
Person * p2 = db.load(id);
cout << "Loaded person " << *p2 << endl;
delete p2;
}
</code>