Logo
blank Skip to main content

Simple ORM over SOCI Based on MySQL Backend

C++

SOCI is a free database access library thatโ€™s written in C++. The library itself is distributed via SourceForge and the documentation can be found both on the official website and SourceForge.

Initially designed as a C++ alternative to Oracle Call Interface, the scope of SOCI has greatly expanded over time. This library now serves as an easy way to access databases with C++ and supports different types of databases including MySQL, ODBC, Oracle, PostgreSQL, and SQLite3 with plans to add support for other popular database types as well.

As a company specializing in C++ development, we often use SOCI when we need to work with MySQL in C++, as it allows us to essentially create SQL queries within standard C++ code. In this article, weโ€™ll show you a practical example of how to use SOCI with MySQL in order to create a simple SOCI MySQL backend.

Creating a DbTableBase Class

One of the prerequisites to implementing a full-fledged application with MySQL and SOCI is having a class that contains Insert, Update, and Delete functions, among others. Letโ€™s name this class DBTableBase.

C++
template<typename RowType = soci::row>
class DBTableBase
{
    explicit DBTableBase(soci::session& session, const std::string& tableName)
        : m_session(session)
        , m_tableName(tableName)
{
}
   /* Other methods */
}

Our DBTableBase class must contain a member with the name of the:

C++
std::string m_tableName; 

It must also include the main interface for communicating with SOCI:

C++
soci::session& m_session;

Now we need to implement this template class for each table in the database that inherits the base class. In our example, it will look like this:

C++
class Students: public DBTableBase<Student>
{
     Students(soci::session&am; session):
        DBTableBase(session, g_studentsTableName)
    {
    }
   /* Other methods */
}

Related services

Outsource Software Development in C/C++

Creating a Table

As an example, letโ€™s create a small database called Students that will contain information on current students at a university. Letโ€™s add id, name, last_name, and faculty fields.

idnamelast namefaculty

In order to create the table, first we need to define the structure of the fields within it. The best way to do this is with BOOST_FUSION_DEFINE_STRUCT, which defines and adapts the structure as a model of Random Access Sequence.

C++
typedef boost::optional<int64_t> IdType;
BOOST_FUSION_DEFINE_STRUCT((db), Student,
  (db::IdType, id)
  (std::string, name)
  (std::string, last_name)
  (std::string, faculty)
)

The id field is set with the help of boost::optional. This is necessary to determine how this field is initialized. If the id field hasnโ€™t been initialized, it will contain the value boost::none. This function is very useful for fields that can contain a null value (keeping in mind that in MySQL, null and 0 are different).

Next, weโ€™ll define several constants:

C++
const char g_nameId[] = "id";
const char g_nameIdType[] = "BIGINT PRIMARY KEY AUTO_INCREMENT";
const char g_notNull[] = "NOT NULL";
const char g_studentsTableName[] = "Students_table";
const std::string g_nameType("VARCHAR(255)");

Then weโ€™ll create a Students object:

C++
Students students(session);

And then create the table:

C++
students.CreateTable();

Next, with the help of SOCI, we can construct a query to create the Students table:

C++
void Students::CreateTable() const
{
    m_session << BeginCreateTableQuery()
        << ", " << GetColName<1>() << " " << g_nameType << g_notNull
        << ", " << GetColName<2>() << " " << g_nameType << g_notNull
        << ", " << GetColName<3>() << " " << g_nameType << g_notNull
        << ", " << DeleteCascadeQuery(GetColName<3>(), "Faculty_table")
        << EndCreateTableQuery();
}

The method DBTableBase::BeginCreateTableQuery can be used as follows:

C++
std::string DBTableBase::BeginCreateTableQuery() const
{
    return std::string("CREATE TABLE" + m_tableName + " (" + g_nameId + " " + g_nameIdType;
}

The GetColName template function is defined like this:

C++
template<int col>
static constexpr std::string DBTableBase::GetColName()
{
    return boost::fusion::extension::struct_member_name<RowType, col>::call();
}

This construct will return the name of the specified column.

If you need to set a column for cascade deletion, you can use this:

C++
std::string DBTableBase::DeleteCascadeQuery(const std::string& colName, const std::string& tableName) const
{
    return std::string("CONSTRAINT fk_") + m_tableName + "_"+ colName + " FOREIGN KEY(" + colName +") REFERENCES " + tableName + "(" + g_nameId + ") ON DELETE CASCADE";
}

At the end of our query, we need to specify:

C++
std::string DBTableBase::EndCreateTableQuery() const
{
    return ");";
}

Weโ€™ve now finished creating the Students_table.

Read also:
4 techniques for Microsoft SQL database design and optimization

Insert

Next, we need to fill this table with data. Letโ€™s look at how we can insert a few lines into our Students table so it looks like this:

idnamelast_namefaculty
0WilliamTaylorFEIT
1MaryDaviesFEIT
2JackSmithFEIT

First, letโ€™s create three objects in db::Student:

C++
db::Student studentTaylor(db::IdType(), "William", "Taylor", "FEIT");
db::Student studentDavies(db::IdType(), "Mary", "Davies", "FEIT");
db::Student studentSmith(db::IdType(), "Jack", "Smith", "FEIT");

Then weโ€™ll call the Insert function to add them to DB:

C++
students.Insert(studentTaylor);
students.Insert(studentDavies);
students.Insert(studentSmith);

Hereโ€™s the implementation of the Insert function:

C++
int64_t DBTableBase::Insert(const RowType& row) const
{          
    std::stringstream query;
    query << "INSERT INTO " << m_tableName << " (";
    std::vector<std::string> colNames = GetColNames<RowType>();
    std::stringstream colsNames;
    std::stringstream colsValues;
    for (const auto& col : colNames)
    {
        if (!colsNames.str().empty())
        {
            colsNames << ", ";
            colsValues << ", ";
        }
        colsNames << col;
        colsValues << ":" << col;
    }
    query << colsNames.str() << ") VALUES (" << colsValues.str() << ")";
    DB_TRY
    {
        m_session <<query.str(), soci::use(row);
        int64_t id = 0;
        m_session.get_last_insert_id(m_tableName, id);
        return id;
    }
    DB_CATCH
    return 0;
}

Hereโ€™s the implementation of the GetColNames function:

C++
static std::vector<std::string> DBTableBase::GetColNames()
{
    std::vector<std::string> res;
    GetColNames<RowType>::Call(res);
    return res;
}

And hereโ€™s the implementation of the Call function:

C++
template<typename RowType, int index = boost::fusion::size(*(RowType*)nullptr) - 1>
struct GetColNames
{
    static void Call(std::vector<std::string>& val)
    {
        GetColNames<RowType, index - 1>::Call(val);
        std::string name = boost::fusion::extension::struct_member_name<RowType, index>::call();
        val.push_back(name);
    }
};

Related services

Custom Data Processing Solutions and Technologies

Update

Now letโ€™s consider a situation when weโ€™ve entered incorrect data into the table. For example, weโ€™ve entered the first name of a student by the last name Davies as Emily instead of Mary. To correct this mistake, we need to update the entry for studentDavies.

C++
studentDavies.name = "Emily";
students.Update(studentDavies);

Hereโ€™s the implementation of Update:

C++
bool DBTableBase::Update(const RowType& row) const
{
    std::stringstream query;
    query << "UPDATE " << m_tableName << " SET ";
    std::vector<std::string> colNames = GetColNames();
    for (const auto& col : colNames)
    {
        if (col != colNames.front())
        {
            query << ", ";
        }
        query << col << " = :" << col;
    }
    query << " WHERE " << g_nameId << " = " << row.id;
    m_session << query.str(), soci::use(row);
    return true;
}

After this operation, we can see the change in the table:

idnamelast_namefaculty
0WilliamTaylorFEIT
1EmilyDaviesFEIT
2JackSmithFEIT

Read also:
How To: Optimize SQL Queries (Tips and Techniques)

Delete

We can use the following command to delete studentsโ€™ information by id:

C++
students.Delete(0);
students.Delete(1);
students.Delete(2);

where:

C++
bool DBTableBase::Delete(int64_t id) const
{
    m_session << "DELETE FROM " << m_tableName << " WHERE " << g_nameId << " = :id", soci::use(id);
    return true;
}

After this operation, weโ€™ll have an empty table.

Transaction

If you have a series of operations that need to be performed together where an error in one operation must cause the cancellation of all previous operations, then you need to use soci::transaction:

C++
std::unique_ptr<soci::transaction> transaction(new soci::transaction(m_session));
try
{
    students.Insert(studentTaylor);
    students.Insert(studentDavies);
    students.Insert(studentSmith);
    transaction->commit();
}
catch(const std::exception& ex)
{
    std::cout << โ€œTransaction errorโ€ << ex.what() << std::endl;
    transaction->rollback();
}

If an error occurs when adding a student to the database, all insert operations will be cancelled.

Select

Letโ€™s consider a situation when we need to get the records for a student with id 2. In order to do this with SOCI, we need to use:

C++
soci::rowset<db::Student> student = students.SelectByID(2);

As a result, weโ€™ll get the following table:

idnamelast_namefaculty
2JackSmithFEIT

We can implement the SelectByID function as follows:

C++
soci::rowset<db::Student> DBTableBase::SelectByID(const int64_t id) const
{
    std::stringstream query;
    query << "SELECT id  FROM " << m_tableName << " WHERE id = " << id;
    return m_session.prepare << query.str();
}

Letโ€™s try to make a more complex query now โ€“ for example, letโ€™s say we need to select all students with the name William Taylor:

idnamelast_namefaculty
0WilliamWillsonFEIT
1WilliamTaylorFEIT
2TaylorDaviesFEIT
3JackTaylorFEIT
C++
typedef std::map<std::string, std::stringstream> KeyValuePairs;
KeyValuePairs value;
value["name"] << "William";
value["last_name"] << "Taylor";
soci::rowset<db::Student> rows = students.SelectByValues(value);

By executing this query, weโ€™ll get:

idnamelast_namefaculty
1WilliamTaylorFEIT

We can implement an easy way to get the desired string using several search parameters with the help of this function:

C++
soci::rowset<rowtype> DBTableBase::SelectByValues(const KeyValuePairs& pairs) const
{
    std::stringstream query;
    query << "SELECT * FROM " << m_tableName << " WHERE ";
    bool first = true;
    for (const auto& pair: pairs)
    {
        if (first)
        {
            first = false;
        }
        else
        {
            query << " AND ";
        }
        query <<  pair.first << " = \"" << pair.second.str() << "\"";
    }
    return m_session.prepare << query.str();
}

Conclusion

SOCI is one of the most popular and refined database access library examples out there. Our article shows only a few ways in which you can use SOCI to work with databases, but itโ€™s a nice illustration of the basic methodology. We hope that this information will prove useful and will encourage you to look more into SOCI in the future.

References

  1. SOCI – The C++ Database Access Library
  2. Boost Fusion Define Struct
  3. Boost.Optional
  4. MySQL

Have a question?

Ask our expert!

Tell us about your project

Send us a request for proposal! Weโ€™ll get back to you with details and estimations.

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us