Easy Database Driven Development with Boost and SOCI

Posted by – October 21, 2013

Last weekend I started to learn more about C++, my first project is a contacts manager, so I googled a little about how to do some persistence on C++ with minimum code and then I found SOCI.

SOCI is a very small database library which gives us the illusion to be embedding SQL queries in C++ code as the project site says, in fact, this is the beauty behind the library, which makes your C++ code very clean and easy to maintain.

But there’s no way to be productive with C++ programming these days without Boost, this awesome library has several utilities classes which can speedup your development several times, this blog post will show a little more about how these two libraries can help you to develop a database driven console application.

Let’s start our project by defining all that we need for application startup, since this is a console application we will use boost::program_options to help us parse command line flags very easily, this application has two command line options “term” and “limit”, the first one is used to provide the search term used to perform a contact lookup, the other is used to limit the number of lookup entries returned.

Agenda.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/option.hpp>
#include <boost/program_options.hpp>
#include <iostream>

#include "Registry.h"

using namespace std;

namespace po = boost::program_options;

int main(int argc, char** argv) {

    po::options_description desc("Allowed options");
    desc.add_options()
        ("help", "produce help message")
        ("term", po::value<string>(), "search term")
        ("limit", po::value<int>()->default_value(10), "number of results")
        ;

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);  

    if (vm.count("term")) {
        string term = vm["term"].as<string>();

        int limit = 0;
        if (vm.count("limit")) {
            limit = vm["limit"].as<int>();
        }
        else {
            limit = 10;
        }

        Registry registry;

        registry.lookup(term, limit);
    }


    return 0;
}

Please note the usage of Registry class, this class is the heart of this application, it contains all the logic behind of contact lookup and output, we also make heavy use of boost here which help us to iterate over lookup result among other things as we can see below:

Registry.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <stdio.h>
#include <boost/format.hpp>
#include <boost/foreach.hpp>
#include <boost/preprocessor/repetition/repeat.hpp>
#include "Registry.h"
#include "Entry.h"

#define Fold(z, n, text)  text

#define STRREP(str, n) BOOST_PP_REPEAT(n, Fold, str)

using namespace std;
using namespace boost;

void Registry::lookup(string name, int limit) {

    printf("Searching for %s...\n", name.c_str());

    vector<Entry> entries = Entry::findByName(name);

    cout << format("%-40s %-20s\n") % "NAME" % "PHONE";
    cout << STRREP("-", 40) << " " << STRREP("-", 20) << "\n";

    BOOST_FOREACH( Entry entry, entries )
    {
       
        cout << format("%-40s %-20s\n") % entry.getName() % entry.getPhone();
    }

    printf("Done\n");
}

void Registry::newEntry(string name, string address, string telefone) {

    printf("Creating entry for %s...\n", name.c_str());

    Entry entry;

    entry.setName(name);
    entry.setAddress(address);
    entry.setPhone(telefone);

    entry.save();

    printf("Done\n");
}

There’s no way to get the Registry class working without call Entry class at some point, we do it when performing a registry lookup or saving data, the Entry class is the main piece behind our contacts manager solution, this class does its own persistence and have the login to load data from database.

The first step is make the class SOCI aware, you must create the class interface and then create a type_conversion specialization which will handle all database to C++ object mapping like show below:

Entry.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <soci/soci.h>
#include <soci/firebird/soci-firebird.h>

using namespace std;

class Entry {

    public:
        void setName(string);
        string getName();
        void setAddress(string);
        string getAddress();
        void setPhone(string);
        string getPhone();

        static vector<Entry> findByName(string);
        void save();

    private:
        string name;
        string address;
        string phone;
};

namespace soci
{
    template<>
    struct type_conversion<Entry>
    {
        typedef values base_type;

        static void from_base(values const & v, indicator /* ind */, Entry & e)
        {
            e.setName(v.get<string>("NAME"));
            e.setAddress(v.get<string>("ADDRESS"));
            e.setPhone(v.get<string>("PHONE"));
        }
   
        static void to_base(const Entry & e, values & v, indicator & ind)
        {
            Entry entry = const_cast<Entry &>(e);
            v.set("NAME", entry.getName());
            v.set("ADDRESS", entry.getAddress());
            v.set("PHONE", entry.getPhone());
            ind = i_ok;
        }
    };
}

Now we have to implement the Entry class, it contains few usual getters and setters, but with few additions, which allow this class to persist its state and return existing entries stored on database with help of SOCI obviously :D

Here’s how the class is implemented:

Entry.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#import "Entry.h"

using namespace soci;
using namespace std;

void Entry::setName(string name) {

    this->name = name;
}

string Entry::getName() {

    return this->name;
}

void Entry::setAddress(string address) {

    this->address = address;
}

string Entry::getAddress() {

    return this->address;
}

void Entry::setPhone(string phone) {

    this->phone = phone;
}

string Entry::getPhone() {

    return this->phone;
}

vector<Entry> Entry::findByName(string name) {

    Entry entry;

    vector<Entry> entries;

        //Starting a connection to database
    session sql(firebird, "service=/srv/firebird/registry.gdb user=SYSDBA password=1978@rpa");

        //Querying data using a prepared statement and place data into a Entry object
    statement st = (sql.prepare <<
                    "select NAME, ADDRESS, PHONE from ENTRY WHERE NAME like '%' || :NAME || '%'",
                    into(entry), use(name));
    st.execute();

        //Checking if we can fetch a row from resultset
    while (st.fetch())
    {
                //Pushing the object with mapped data into the entries vector
        entries.push_back(entry);
    }

    return entries;
}

void Entry::save() {

    session sql(firebird, "service=/srv/firebird/registry.gdb user=SYSDBA password=1978@rpa");

    sql << "insert into ENTRY (NAME, ADDRESS, PHONE) values(:NAME, :ADDRESS, :PHONE)", use(*this);

}

To build all this code I used cmake, with cmake I don’t have to take care of specific behind a build tool like make or wherever the developer uses to compile your code, the code below shows how we can build the application code above:

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 2.8)

FIND_PACKAGE( Boost 1.49 COMPONENTS program_options REQUIRED )
FIND_PACKAGE( Soci 3.2 COMPONENTS firebird REQUIRED )

INCLUDE_DIRECTORIES( ${Boost_INCLUDE_DIR} ${SOCI_INCLUDE_DIR} )

ADD_EXECUTABLE( agenda Agenda.cpp Registry.cpp Entry.cpp )

TARGET_LINK_LIBRARIES( agenda ${Boost_LIBRARIES} ${SOCI_LIBRARY} ${SOCI_firebird_PLUGIN} )

As you can see above, a basic cmake script consists of several very easy to understand pieces, the first of them is the FIND_PACKAGE command, we use this command to check for the existence of libraries installed on the system, in our project we need to check if Boost and SOCI are installed on system, once they are found, several variables like Boost_INCLUDE_DIR, Boost_LIBRARIES and SOCI variables are set and ready to be used inside the script.

Once the libraries are found, we can set a few build flags, one of them is the include directories, you can do this with INCLUDE_DIRECTORIES command, please note the usage of to variables within this command Boost_INCLUDE_DIR and SOCI_INCLUDE_DIR, they have been set by FIND_PACKAGE called previously, we also do the same during the call of TARGET_LINK_LIBRARIES, but this time we are passing the variables which are holding the paths to libraries binaries.

Now you can run cmake, please go to the directory where CMakeLists.txt is and type the following command:

cmake .

This will start cmake and will cause to generate the system Makefile, once the cmake is finished you can run make to build the project.

Share

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>