Database Usage Example

In Database Example we created a class that opens and closes a database for us. We now make use of that class to load inventory data into two databases that we will use for our inventory system.

Again, remember that you can find the complete implementation for these functions in:

DB_INSTALL/examples_cxx/getting_started

where DB_INSTALL is the location where you placed your DB distribution.

Example 3.1 VENDOR Structure

We want to store data related to an inventory system. There are two types of information that we want to manage: inventory data and related vendor contact information. To manage this information, we could have created a structure for each type of data, but to illustrate storing mixed data without a structure we refrain from creating one for the inventory data.

We now show the definition of the VENDOR structure. Note that the VENDOR structure uses fixed-length fields. This is not necessary and in fact could represent a waste of resources if the number of vendors stored in our database scales to very large numbers. However, for simplicity we use fixed-length fields anyway, especially given that our sample data contains so few vendor records.

// File: gettingStartedCommon.hpp
#define MAXFIELD 20
typedef struct vendor {
    char name[MAXFIELD];             // Vendor name
    char street[MAXFIELD];           // Street name and number
    char city[MAXFIELD];             // City
    char state[3];                   // Two-digit US state code
    char zipcode[6];                 // US zipcode
    char phone_number[13];           // Vendor phone number
    char sales_rep[MAXFIELD];        // Name of sales representative
    char sales_rep_phone[MAXFIELD];  // Sales rep's phone number
} VENDOR;

Example 3.2 InventoryData Class

In order to manage our actual inventory data, we create a class that encapsulates the data that we want to store for each inventory record. Beyond simple data encapsulation, this class is also capable of marshaling the inventory data into a single contiguous buffer for the purposes of storing in that data in a DB database.

We also provide two constructors for this class. The default constructor simply initializes all our data members for us. A second constructor is also provided that is capable of populating our data members from a void *. This second constructor is not really needed until the next chapter where we show how to read data from the databases, but we include it here for the purpose of completeness anyway.

To simplify things a bit, we include the entire implementation for this class in gettingStartedCommon.hpp along with our VENDOR structure definition.

To begin, we create the public getter and setter methods that we use with our class' private members. We also show the implementation of the method that we use to initialize all our private members.

class InventoryData
{
public:
    inline void setPrice(double price) {price_ = price;}
    inline void setQuantity(long quantity) {quantity_ = quantity;}
    inline void setCategory(std::string &category) 
                    {category_ = category;}
    inline void setName(std::string &name) {name_ = name;}
    inline void setVendor(std::string &vendor) {vendor_ = vendor;}
    inline void setSKU(std::string &sku) {sku_ = sku;}

    inline double& getPrice() {return(price_);}
    inline long& getQuantity() {return(quantity_);}
    inline std::string& getCategory() {return(category_);}
    inline std::string& getName() {return(name_);}
    inline std::string& getVendor() {return(vendor_);}
    inline std::string& getSKU() {return(sku_);}

    // Initialize our data members
    void clear()
    {
        price_ = 0.0;
        quantity_ = 0;
        category_.clear();
        name_.clear();
        vendor_.clear();
        sku_.clear();
    } 

Next we implement our constructors. The default constructor simply calls the clear(). The second constructor takes a void * as an argument, which it then uses to initialize the data members. Note, again, that we will not actually use this second constructor in this chapter, but we show it here just to be complete anyway.

    // Default constructor
    InventoryData() { clear(); }

    // Constructor from a void *
    // For use with the data returned from a bdb get
    InventoryData(void *buffer)
    {
        char *buf = (char *)buffer;

        price_ = *((double *)buf);
        bufLen_ = sizeof(double);

        quantity_ = *((long *)(buf + bufLen_));
        bufLen_ += sizeof(long);

        name_ = buf + bufLen_;
        bufLen_ += name_.size() + 1;

        sku_ = buf + bufLen_;
        bufLen_ += sku_.size() + 1;

        category_ = buf + bufLen_;
        bufLen_ += category_.size() + 1;

        vendor_ = buf + bufLen_;
        bufLen_ += vendor_.size() + 1;
    } 

Next we provide a couple of methods for returning the class' buffer and the size of the buffer. These are used for actually storing the class' data in a DB database.

    // Marshalls this classes data members into a single
    // contiguous memory location for the purpose of storing
    // the data in a database.
    char *
    getBuffer()
    {
        // Zero out the buffer
        memset(databuf_, 0, 500);
        // Now pack the data into a single contiguous memory location
        // for storage.
        bufLen_ = 0;
        int dataLen = 0;

        dataLen = sizeof(double);
        memcpy(databuf_, &price_, dataLen);
        bufLen_ += dataLen;

        dataLen = sizeof(long);
        memcpy(databuf_ + bufLen_, &quantity_, dataLen);
        bufLen_ += dataLen;

        packString(databuf_, name_);
        packString(databuf_, sku_);
        packString(databuf_, category_);
        packString(databuf_, vendor_);

        return (databuf_);
    }

    // Returns the size of the buffer. Used for storing
    // the buffer in a database.
    inline int getBufferSize() { return (bufLen_); } 

Our last public method is a utility method that we use to get the class to show itself.

     // Utility function used to show the contents of this class
    void
    show() {
        std::cout << "\nName:         " << name_ << std::endl;
        std::cout << "    SKU:        " << sku_ << std::endl;
        std::cout << "    Price:      " << price_ << std::endl;
        std::cout << "    Quantity:   " << quantity_ << std::endl;
        std::cout << "    Category:   " << category_ << std::endl;
        std::cout << "    Vendor:     " << vendor_ << std::endl;
    } 

Finally, we provide a private method that is used to help us pack data into our buffer, and we declare our private data members.

private:

    // Utility function that appends a char * to the end of
    // the buffer.
    void
    packString(char *buffer, std::string &theString)
    {
        int string_size = theString.size() + 1;
        memcpy(buffer+bufLen_, theString.c_str(), string_size);
        bufLen_ += string_size;
    }

    // Data members
    std::string category_, name_, vendor_, sku_;
    double price_;
    long quantity_;
    int bufLen_;
    char databuf_[500];
}; 

Example 3.3 example_database_load

Our initial sample application loads database information from several flat files. To save space, we won't show all the details of this example program. However, as always you can find the complete implementation for this program here:

DB_INSTALL/examples_cxx/getting_started

where DB_INSTALL is the location where you placed your DB distribution.

We begin with the normal include directives and forward declarations:

// File: example_database_load.cpp
#include <iostream>
#include <fstream>
#include <cstdlib>

#include "MyDb.hpp"
#include "gettingStartedCommon.hpp" 

// Forward declarations
void loadVendorDB(MyDb&, std::string&);
void loadInventoryDB(MyDb&, std::string&);

Next we begin our main() function with the variable declarations and command line parsing that is normal for most command line applications:

// Loads the contents of vendors.txt and inventory.txt into
// Berkeley DB databases. 
int
main(int argc, char *argv[])
{
    // Initialize the path to the database files
    std::string basename("./");
    std::string databaseHome("./");

    // Database names
    std::string vDbName("vendordb.db");
    std::string iDbName("inventorydb.db");

    // Parse the command line arguments here and determine 
    // the location of the flat text files containing the 
    // inventory data here. This step is omitted for clarity.

    //  Identify the full name for our input files, which should
    //  also include some path information.
    std::string inventoryFile = basename + "inventory.txt";
    std::string vendorFile = basename + "vendors.txt";

    try
    {
        // Open all databases.
        MyDb inventoryDB(databaseHome, iDbName);
        MyDb vendorDB(databaseHome, vDbName);

        // Load the vendor database
        loadVendorDB(vendorDB, vendorFile);

        // Load the inventory database
        loadInventoryDB(inventoryDB, inventoryFile);
    } catch(DbException &e) {
        std::cerr << "Error loading databases. " << std::endl;
        std::cerr << e.what() << std::endl;
        return(e.get_errno());
    } catch(std::exception &e) {
        std::cerr << "Error loading databases. " << std::endl;
        std::cerr << e.what() << std::endl;
        return(-1);
    }

    return(0);
} // End main 

Note that we do not explicitly close our databases here. This is because the databases are encapsulated in MyDb class objects, and those objects are on the stack. When they go out of scope, their destructors will cause the database close to occur.

Notice that there is not a lot to this function because we have pushed off all the database activity to other places.

Next we show the implementation of loadVendorDB(). We load this data by scanning (line by line) the contents of the vendors.txt file into a VENDOR structure. Once we have a line scanned into the structure, we can store that structure into our vendors database.

Note that we use the vendor's name as the key here. In doing so, we assume that the vendor's name is unique in our database. If it was not, we would either have to select a different key, or architect our application such that it could cope with multiple vendor records with the same name.

// Loads the contents of the vendors.txt file into a database
void
loadVendorDB(MyDb &vendorDB, std::string &vendorFile)
{
    std::ifstream inFile(vendorFile.c_str(), std::ios::in);
    if ( !inFile )
    {
        std::cerr << "Could not open file '" << vendorFile
                  << "'. Giving up." << std::endl;
        throw std::exception();
    }

    VENDOR my_vendor;
    while (!inFile.eof())
    {
        std::string stringBuf;
        std::getline(inFile, stringBuf);
        memset(&my_vendor, 0, sizeof(VENDOR));

        // Scan the line into the structure.
        // Convenient, but not particularly safe.
        // In a real program, there would be a lot more
        // defensive code here.
        sscanf(stringBuf.c_str(),
          "%20[^#]#%20[^#]#%20[^#]#%3[^#]#%6[^#]#%13[^#]#%20[^#]#%20[^\n]",
          my_vendor.name, my_vendor.street,
          my_vendor.city, my_vendor.state,
          my_vendor.zipcode, my_vendor.phone_number,
          my_vendor.sales_rep, my_vendor.sales_rep_phone);

        Dbt key(my_vendor.name, strlen(my_vendor.name) + 1);
        Dbt data(&my_vendor, sizeof(VENDOR));

        vendorDB.getDb().put(NULL, &key, &data, 0);
    }
    inFile.close();
} 

Finally, we need to write the loadInventoryDB() function. To load the inventory information, we read in each line of the inventory.txt file, obtain each field from it, then we load this data into an InventoryData instance.

To help us obtain the various fields from each line of input, we also create a simple helper function that locates the position of the first a field delimiter (a pound (#) sign) from a line of input.

Note that we could have simply decided to store our inventory data in a structure very much like the VENDOR structure that we use above. However, by storing this data in the InventoryData class, which identifies the size of the data that it contains, we can use the smallest amount of space possible for the data that we are storing. The result is that our cache can be smaller than it might otherwise be and our database will take less space on disk than if we used a structure with fixed-length fields.

For a trivial dataset such as what we use for these examples, these resource savings are negligible. But if we were storing hundreds of millions of records, then the cost savings may become significant.

// Used to locate the first pound sign (a field delimiter)
// in the input string.
int
getNextPound(std::string &theString, std::string &substring)
{
    int pos = theString.find("#");
    substring.assign(theString, 0, pos);
    theString.assign(theString, pos + 1, theString.size());
    return (pos);
}

// Loads the contents of the inventory.txt file into a database
void
loadInventoryDB(MyDb &inventoryDB, std::string &inventoryFile)
{
    InventoryData inventoryData;
    std::string substring;
    int nextPound;

    std::ifstream inFile(inventoryFile.c_str(), std::ios::in);
    if (!inFile)
    {
        std::cerr << "Could not open file '" << inventoryFile
                  << "'. Giving up." << std::endl;
        throw std::exception();
    }

    while (!inFile.eof())
    {
        inventoryData.clear();
        std::string stringBuf;
        std::getline(inFile, stringBuf);

        // Now parse the line
        if (!stringBuf.empty())
        {
            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setName(substring);

            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setSKU(substring);

            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setPrice(strtod(substring.c_str(), 0));

            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setQuantity(strtol(substring.c_str(), 0, 10));

            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setCategory(substring);

            nextPound = getNextPound(stringBuf, substring);
            inventoryData.setVendor(substring);

            void *buff = (void *)inventoryData.getSKU().c_str();
            int size = inventoryData.getSKU().size()+1;
            Dbt key(buff, size);

            buff = inventoryData.getBuffer();
            size = inventoryData.getBufferSize();
            Dbt data(buff, size);

            inventoryDB.getDb().put(NULL, &key, &data, 0);
        }
    }
    inFile.close();
} 

In the next chapter we provide an example that shows how to read the inventory and vendor databases.