Working with Databases in C++: An Introduction
Using the SOCI library and MySQL database server
Introduction
This article will serve as an introduction to working with databases in C++. After reading this article, you will have accumulated enough knowledge to start writing C++ applications that interact with your favourite database backends.
The full pipeline of developing an application will be explored — from installing necessary libraries to compiling our final application. More specifically, this article will detail how to:
- Download and Install the SOCI library.
This section will describe how to download SOCI and install its libraries and header files on your system. - Set up a database environment on a MySQL server.
In order to safely access the MySQL server from our C++ application, we will create a user and assign that user a role. This role will describe what operations the user can perform and which databases are accessible on the server. - Write a simple C++ SOCI application.
This section will detail how to use the SOCI API to insert and retrieve data from the database. - Compile and run our C++ application.
This section will demonstrate how to compile and run our C++ application from the command line using the GCC compiler.
What is SOCI?
SOCI stands for Simple Open (Database) Call Interface and is a database access library written in C++. Several database backends have been developed for SOCI, meaning that you can use one simple API provided by SOCI to work with a range of databases.
For example, it’s entirely possible to write a single C++ application with SOCI that interacts with several database backends, such as MySQL, PostgreSQL and and SQLite 3. Without SOCI, you would need to learn and implement the individual C++ APIs provided by each particular backend your application requires, which would be time-consuming and tedious.
SOCI supports popular open source database backends including MySQL, PostgreSQL, SQLite 3 and Firebird, as well as database servers used commercially such as Oracle.
Prerequisites
All software adopted in this article is open-source and freely available to download and install on your system. To complete the steps demonstrated in this article, make sure you have the following software already installed on your system:
- C++ Compiler
Such as GCC, LLVM/Clang or Microsoft Visual C++. - CMake 2.8+
Used for building the SOCI API and libraries. Download CMake or install thecmake
package using your OS’s native package manager. - MySQL Server and MySQL Shared Libraries
MySQL can be installed via your operating system’s package manager (Linux) or via the MySQL Community Server download page. Shared libraries are also included in a standard MySQL installation. MySQL Version 8.0.18 is used throughout this article.
Installing Library Dependencies
This section will detail the installation of SOCI on the Mac OS X and Linux operating systems (Unix style systems). Instructions on how to install the Boost libraries will also be provided as SOCI uses the Boost.Date_Time
library.
For readers who are using Windows, please refer to the boost download page where you can download and install Boost via prebuilt Windows binaries. Also refer to the SOCI Installation page for specific instructions on running CMake on Windows and installing SOCI to your system.
Installing Boost
Luckily, the Boost libraries can be installed via a single command using your operating system’s native package manager. The following commands will install the Boost libraries and corresponding header files on your system. In addition to Mac OS X, installation commands are provided for major Linux distributions — so please choose the appropriate installation command for your operating system:
- Mac OS X:
sudo brew install boost
- Arch:
sudo pacman -S boost
- Debian and Ubuntu:
sudo apt-get install libboost-all-dev
- CentOS and Fedora:
sudo dnf install boost-devel
The default install location for the Boost libraries is the /usr/local/lib
directory. The header files are installed in the /usr/local/include/boost
directory. You can confirm the install locations by using the ls
command in the terminal:
Installing SOCI
The process to install SOCI is also quite straightforward, but involves a few more steps. We are going to download the SOCI Git repository, generate a native build environment using CMake, produce the SOCI libraries and header files, and finally install (copy) them to appropriate locations on our system.
That might sound like a lot of steps to break down and understand, so let’s start by executing the following two commands in a terminal. The cd
command firstly navigates you to your home directory. The git clone
command then downloads the SOCI Git repository to this directory:
cd ~
git clone git://github.com/SOCI/soci.git
This repository contains everything we need to install the SOCI libraries and header files — it contains CMake build files, documentation, tests, and the actual C++ source code. As of this writing, the latest stable version of SOCI will be downloaded (SOCI version 4.0).
Moving forward, the next task is to generate a native build environment using CMake. A native build environment contains scripts that a particular operating system uses in order to compile source code, create libraries and build executables. On a Unix-style operating system (such as Mac OS X and Linux), a Makefile will be produced by CMake. We then invoke the Unix make
program to read the Makefile, which in turn will compile and generate SOCI’s headers and shared libraries.
CMake can be described as a simple interpreter that reads a script and subsequently generates build files for a particular platform in a compiler-independent way.
Next, we create a directory named build
in the soci
repository that will contain the native build environment produced by CMake:
cd soci # Enter the soci repository
mkdir build # Create a new build directory
cd build # Enter the build directory
It is perfectly fine to name the build
directory anything you like, but we will stick to convention in this case. Moving forward, we execute three more commands that will actually install SOCI to our system:
# Generate native build files
cmake -G "Unix Makefiles" ..# Generate SOCI libraries and headers
make# Copy libraries and headers to filesystem
make install
Let’s take a look at these different commands and examine how they work:
cmake -G "Unix Makefiles" ..
Thecmake
command is invoked to generate our native build environment. Two important arguments are passed here. The-G
option specifies a build system generator. In this case, the"Unix Makefiles"
generator is selected in order for CMake to produce a Makefile. Lastly,..
points to the root directory of thesoci
repository which also includes theCMakeLists.txt
file —cmake
requires this file to generate the build files.make
A program that will read theMakefile
in thebuild
directory and generate the shared libraries, source files, and binaries that make up the SOCI library.make install
This command will copy the generated shared libraries and include files to appropriate locations on your filesystem.
SOCI Shared libraries will be copied to /usr/local/lib
, while header files will be copied to /usr/local/include/soci
. Near the end of this article, we will configure the C++ compiler to search through these directories in order to resolve our application’s header and library dependencies.
Let’s confirm the install locations by using the ls
command in the terminal:
At this point, SOCI should be installed on your system and ready to use! The next section will detail how we go about setting up a database environment that our application will interact with during runtime.
Preparing A MySQL Database
In this section, we will walk through setting up our database environment. More specifically, we will be working with a MySQL database server that will serve as the database backend to our C++ application.
Before starting on the C++ application, we need to complete a few tasks on the database server. For this demonstration, we will:
- Create a new database called
soci_db
. - Create a table called
users
that will enable us to insert and retrieve some data from our C++ application. - Create a role called
role_soci_dev
and assign it privileges appropriate for developers working on our application. - Create a user called
soci_dev1@localhost
that is able to connect to MySQL from the C++ application and work with thesoci_db
database. - Grant the
role_soci_dev
role to thesoci_dev1
user, and set it as the default role.
The following gist is a MySQL script that sets up our database environment:
If you are already familiar with the SQL statements used in the script and do not feel like further explanation is necessary, feel free to download the file and execute the script from your MySQL server.
Once logged into MySQL as the root user, run MySQL’s source
command to execute the script:
source /path/to/script.sql
- Replace
/path/to/
to the location that the downloaded script resides on your filesystem.
If you have executed the script and understand all the SQL commands, feel free to proceed to the next section in this article (Developing a SOCI C++ Application).
The following subsections will walk you through each statement contained in the script above, and provide additional explanation of what the statement does.
Creating a New Database and Table
We will firstly log in to our MySQL server as the root
user. Let’s open up a terminal and run the following command:
mysql -u root -p
Enter password: <your root password>
- The
-u
flag requires a username that represents the user we are logging in as. Because we would like administrative privileges on our MySQL server, we shall log in as theroot
user. - The
-p
flag is necessary if the user you are logging in as requires a password. Go ahead and enter yourroot
password when prompted, followed by the Enter key.
Let’s proceed to create a new database and use it via the following two statements:
CREATE DATABASE IF NOT EXISTS soci_db;
USE soci_db;
- We create a database called
soci_db
with theCREATE DATABASE
statement. TheIF NOT EXISTS
clause makes sure that the database name is unique on the server. - We then execute the
USE
statement in order to enter thesoci_db
database container so we can proceed to create objects within it.
For the purposes of our simple proof-of-concept application, we will create a table called users
and store some rudimentary data — a first name, last name, email address, and a boolean representing if the user is active or not. The CREATE TABLE
statement is invoked to create the users
table:
CREATE TABLE users (
id INT AUTO_INCREMENT,
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
active BOOL DEFAULT TRUE,
PRIMARY KEY(id)
);
Notice how we make an effort to adhere to robust database table design principles by adding a primary key and a few constraints to our table’s columns:
- The
id
column serves as the primary key, which means that all records in theusers
table can be uniquely identified with theid
value. Theid
column of the first row inserted into the table will contain one. TheAUTO_INCREMENT
property will also incrementid
by one for each new record. - The
NOT NULL
constraint is applied to thefirst_name
,last_name
andemail
columns. These columns must therefore always contain a value, and not contain aNULL
value. TheUNIQUE
constraint is also applied to theemail
column, which ensures that all email address values stored in the table are different. - Lastly, the
DEFAULT
constraint is applied to theactive
column. MySQL will automatically provide aTRUE
value if nothing is provided for theactive
column when inserting a new user.
To verify that our table has been created successfully, lets describe it with the DESC
statement:
DESC users;
It’s good practice to study the output of the DESC
statement and confirm that the table has the correct column names, data types, and constraints set up as intend before moving forward.
Creating a Role
Our next task is to create a role that permits operations relevant only to the soci_db
database. This role will then be granted to a user on our database server.
Later on when we develop a simple C++ application, we will use the SOCI API to perform just two tasks on our database backend — inserting and retrieving users. With this in mind, the connecting user should not have access outside the soci_db
database, or be able to perform tasks that a superuser would typically do.
To accomplish this, we will create a developer role that can be granted to developers working on the soci_db
database. In other words, database users who have been granted this role will be able to execute many SQL statements within the soci_db
database in order to develop the C++ application effectively.
Let’s go ahead and create a new role called role_soci_dev
with the CREATE ROLE
statement:
CREATE ROLE role_soci_dev;
Role names will be saved along with user names in the mysql.user
system table. To easily query roles on our database server, we have prefixed the role name with role_
.
Next, we will grant privileges to our newly created role. These privileges will dictate what statements a user will be able to perform on the database server.
In our case, we would like to give a developer a high degree of freedom inside the soci_db
database — such as creating, deleting and updating database objects, as well as inserting data. Luckily, there is a privilege within MySQL called ALL
that will satisfy this requirement.
Let’s run the GRANT
statement to add the necessary privileges to the role_soc_dev
role:
GRANT ALL ON soci_db.* TO role_soci_dev;
- The above statement can be read as: Give the
role_soci_dev
roleALL
privileges for all tables when using thesoci_db
database. - The asterisk (*) in
soci_db.*
represents all tables in thesoci_db
database. To grant these privileges to just a single table, replace the asterisk with a valid table name.
We can verify that the ALL
privilege has been granted to our role by executing the SHOW GRANTS
statement:
SHOW GRANTS FOR role_soci_dev;
The output to this statement should include:
GRANT USAGE ON *.* TO `role_soci_dev`@`%`
TheUSAGE
privilege will only permit users to be able to log in to the MySQL server. In other words, this privilege carries the meaning of “no privileges”, and is granted to every newly created user and role.GRANT ALL PRIVILEGES ON `soci_db`.* TO `role_soci_dev`@`%`
This output verifies that theALL
privilege on thesoci_db
database has been granted to our role successfully.
We are moving forward nicely! We have now created a role on our MySQL server that can be granted to any user that we create. In other words, if we hire five developers to work on our application, we can create five users on our MySQL server and give them the role we just created via a single statement.
Creating a User
Let’s go ahead and create a new user on the MySQL server called soci_dev1
. This user will log in to the MySQL server from our C++ application. The CREATE USER
statement creates a new user:
CREATE USER soci_dev1@localhost IDENTIFIED BY ‘Secure123’;
- The
soci_dev1
user connects to the MySQL server fromlocalhost
. In other words, this user is only able to connect to MySQL from the localhost machine, and not remotely. - The
IDENTIFIED BY
clause allows us to specify a password that will be required every time the user logs in to the MySQL server. Feel free to use a more secure password on your end!
If you now go ahead and log in to MySQL as the soci_dev1
user, you won’t be able to access any databases, or execute any statements for that matter. As mentioned previously, the USAGE
privilege is the only privilege granted to the user at this point.
To enable the user to perform operations on the server, we need to execute just a few more statements. Lets start with using the GRANT
statement to give the user the role_soci_dev
role we created a moment ago:
GRANT role_soci_dev TO soci_dev1@localhost;
Note that we must specify both the username (soci_dev1
) and hostname (localhost
) when referencing our user.
Let’s confirm that the role is applied to the user by executing the SHOW GRANTS
statement:
SHOW GRANTS FOR soci_dev1@localhost USING role_soci_dev;
The USING
clause allows us to view the privileges associated with the role_soci_dev
role. If you were to execute this statement without the USING
clause, the applied role name will only be displayed without its associated privileges — go ahead and give it a try.
Now, if we proceed to log in to MySQL with the soci_dev1
user, we still won’t be able to actually do anything! The role_soci_dev
role will remain inactive until we tell MySQL what role to activate for the user after they have logged in. Therefore, we must set a default role that will be active when the user logs in.
To accomplish this, lets use the following SET DEFAULT ROLE
statement:
SET DEFAULT ROLE ALL TO soci_dev1@localhost;
Now, every role that has been granted to our user will be active, by default, when they log in to MySQL.
Verifying the Database Environment
Let’s now confirm everything is working by logging out as the root
user, and logging in as the soci_dev1
user:
/* exit mysql as the root user */
exit# back in the terminal, log in to MySQL as the soci_dev1 user
mysql -u soci_dev1 -p
Enter password: <Secure123>
Notice how we specify soci_dev1
after the -u
flag to log in as our new user. Note that it is not necessary to append the the hostname (@localhost
) to the user in this case. Also remember to input the password you gave the soci_dev1
user that was specified in the CREATE USER
statement.
Before proceeding to write the C++ application, let’s confirm that we have set up our database environment correctly. More specifically, we would like to confirm that:
- The
soci_dev1
user has the correct privileges. - We can access and work with the
soci_db
database. - We cannot access any other databases on the server.
Firstly, let’s confirm that we are logged in as the user we intend to be by running the following statement:
SELECT user();
soci_dev1@localhost
should be returned after executing this statement.
You will sometimes see the current_user()
and current_user
(without parenthesis) functions used instead of user()
.
Secondly, let’s verify the privileges we have by running the SHOW GRANTS
statement:
SHOW GRANTS;
Notice that we have used the short form of the SHOW GRANTS
statement to display our own privileges.
Thirdly, we need to confirm that we only have access to the soci_db
database:
SHOW DATABASES;
The only database name displayed in this list should be soci_db
. As this user does not have the necessary privileges to access any other database, no other database names are displayed, including the MySQL system databases.
Lastly, lets enter the soci_db
database and show the users
table by running the following statements:
USE soci_db;
SHOW TABLES;
Feel free to run additional statements to verify other aspects of our database environment as the soci_dev1
user. It might be a good idea to examine the users
table with the DESC
statement, or to even start playing with data by using the INSERT
, SELECT
, UPDATE
and DELETE
statements.
With a database environment set up, let’s proceed to writing our C++ application with SOCI.
Developing a C++ Application Using SOCI
This section will walk through writing a simple C++ application using the SOCI library. The application will perform the following tasks:
- Connect to the
soci_db
database on our MySQL backend as thesoci_dev1
user. - Insert a new user into the
users
table. The application will display prompts allowing you to input values for the new user’s first name, last name, and email address. - Retrieve and display all records from the
users
table. For every row that is retrieved from the database, the value inside each column will be output to the terminal.
The following gist contains the entire C++source code for this demonstration. Feel free to type out the code in your own hello.cpp
source file while trying to understand how the application works.
Alternatively, you can download the source file and proceed to the Compiling and Running Our Application section to run the program. After running the program, you may wish to return to this section to type out the code yourself and study the explanations that follow:
The following sub-sections will break down the application’s C++ code and provide an explanation on what is being performed.
Header Files and Namespaces
We start by including all the header files our application requires. In this case, our program needs to interface with the soci.h
header file, as well as the header file relevant to the database backend we are adopting.
As our database backend is MySQL, the application must include the soci-mysql.h
header file. If your solution has adopted an Oracle backend, you would be required to include the soci-oracle.h
header file instead, and so on.
Additionally, we include three standard library header files in order to work with exceptions (exception
), terminal input and output (iostream
), and strings (string
):
#include <soci/soci.h>
#include <soci/mysql/soci-mysql.h>
#include <exception>
#include <iostream>
#include <string>
It is considered good practice to firstly include external library header files, followed by header files local to your project, and finally standard library header files. Moreover, header files listed in alphabetical order adds another level of organisation to your includes section.
Moving forward, we bring the soci
namespace and some C++ standard library items into scope:
using namespace soci;using std::cin;
using std::cout;
using std::endl;
using std::string;
Everything in SOCI is declared in the soci
namespace, so we expose the entire soci
namespace to our application. From now on, any item we reference that is declared in the soci
namespace will not require the soci::
prefix, which will improve the readability of our code.
Furthermore, we individually bring some standard library items into scope that will be used for handling data input and output to the terminal.
Exception Handling
The main
function starts by defining a try ... catch
block to handle errors appropriately:
int main() { try {
// ...
}
catch(const std::exception& e) {
std::cerr << "Error: " << e.what() << endl;
}
}
It is important to implement some exception handling in our program as SOCI will throw an exception if an error occurs when it interacts with the backend database.
All database related errors will result in an exception being thrown — such as when the SOCI library is unable to make a successful connection to the backend database, is unable to retrieve data, and so on.
If an exception is thrown during runtime, it will propagate to the main
function and be handled in the catch
block. In our case, we call the exception::what()
method to attempt to describe the error that occurred before the application terminates. The error message will be displayed in the terminal.
Connecting to the MySQL Database
Moving forward, our application immediately attempts to connect to our MySQL database backend. In order to do this, we create a soci::session
object stored in a variable called sql
, and pass it some information:
// Connect to MySQL database
session sql(mysql, "db=soci_db user=soci_dev1 password=Secure123");
The soci:session
object encapsulates the database connection and other backend related details. Therefore, we must pass it the following items:
mysql
: The MySQL backend factory object that enables interactions with a MySQL backend. This object is declared insidesoci-mysql.h
."db=soci_db user=soci_dev1 password=Secure123"
: The generic connection string that specifies which database to use, and what user to log in as.
The generic connection string passed to the soci::session
constructor includes:
db=soci_db
: It will connect to a database on our server calledsoci_db
.user=soci_dev1
: It will log in to our server as thesoci_dev1
user.password=Secure123
: The passwordSecure123
is used for thesoci_dev1
user.
Remember to input the correct information that reflects your database environment — use the correct database name, user name, and password you created earlier in this article!
Getting Data
After the soci::session
object is created, we are connected to the database backend and are ready to interact with it in some way.
Let’s take a look at inserting a new user record into the users
table using SOCI. Furthermore, it would be nice if we could manually type in values for the new user’s first name, last name, and email columns.
To accomplish this, we start by creating three empty string objects that will store the user’s first name, last name, and email information. These string objects are then passed into a utility function called get_data
in order to populate the string
objects with values. We also pass a prompt string as the first parameter to this function:
// Get data for new user
string first_name, last_name, email; get_data("> Enter first name: ", first_name);
get_data("> Enter last name: ", last_name);
get_data("> Enter email address: ", email);
The get_data
function is defined directly below our using
statements:
template<typename T>
void get_data(const string prompt, T& value) {
cout << prompt;
cin >> value;
}
This function simply displays a prompt in the terminal, and allows us to enter a value that will be used to initialise the object T
. Let’s take a closer look at the implementation:
template<typename T>
The functionget_data
is in fact a template function. In the first line we declare a type calledT
, and use it as a placeholder for the function’s second parameter.void get_data(const string prompt, T& value)
TheT
placeholder serves as the type for the second parameter, calledvalue
. It is also passed by reference — denoted by the&
symbol. The first parameter is of typeconst string
, and will serve as a simpleprompt
.cout << prompt;
The prompt is output to the terminal, and should instruct the user of what type of data to enter.cin >> value;
Lastly,T
is initialised with some data usingcin
.
The get_data
template function is a useful utility that can be reused throughout the application. It prevents us from having to re-type the same code in multiple places to do the same task of initialising an item with some custom data.
Inserting Data
The application now proceeds to insert a new user in the users
table with the string data we have collected via the keyboard. We call another utility function called insert_user
to accomplish this:
// Insert a new row into users table
insert_user(sql, first_name, last_name, email);
insert_user
is another utility function we have written ourselves that enables us to quickly add many users to the database with one function call. It is defined near the top of our source code:
void insert_user(session& sql,
const string& first_name,
const string& last_name,
const string& email) { // Insert data into users table
sql << "INSERT INTO users(first_name, last_name, email)"
"VALUES(:fn, :ln, :e)",
use(first_name, "fn"),
use(last_name, "ln"),
use(email, "e"); cout << "> Successfully inserted user." << endl << endl;
}
Firstly, we must provide four items to the insert_user
function:
session& sql
: A reference to our session object, which contains our database connection. It is required to execute SQL statements.const string&
parameters: Are references to string objects representing the user’sfirst_name
,last_name
andemail
values that we would like to insert into the database.
Inside the function, we use the session object to immediately execute an INSERT INTO
SQL statement to add a new user to the database:
// Insert data into users table
sql << "INSERT INTO users(first_name, last_name, email)"
"VALUES(:fn, :ln, :e)",
use(first_name, "fn"),
use(last_name, "ln"),
use(email, "e");
Notice that the SQL statement we would like to execute is a string enclosed in double quotes, and comes after the insertion operator (<<
). We realise from this statement that the insertion operator has been overloaded within the soci::session
object to provide an easy mechanism for executing SQL statements. With SOCI, we also do not need to terminate the SQL statement with a semi colon.
In order to attach data to the SQL string, SOCI provides mechanisms to bind local buffers for input and output data. To use this data binding feature, we firstly specify placeholders within the SQL statement string in the form of :placeholder_name
. For example, we have specified :fn
, :ln
, and :e
placeholder names in the statement above, which are essentially “holes” that the first_name
, last_name
, and email
strings will fill. The placeholders represent holes for input variables.
A comma follows the SQL statement string, and then the soci::use
expression is called three times (also separated by commas) to bind our local variables to the SQL string. We can bind variables in two ways:
- Binding by position: Where
soci::use
expressions are used in order of the “holes” in the statement. - Binding by name: Where the placeholder name is also passed to
soci::use
to clearly associate the local variable with the given placeholder.
Binding by name is adopted in our implementation because we also pass the placeholder name along with the variable it will be associated with. For example, the first_name
string will be slotted into the :fn
placeholder, and so on.
SOCI assumes local variables provided as soci::use
elements live as long as it takes to execute the whole statement. Therefore, named variables should be used to ensure object lifetime is sufficient.
The function lastly outputs a message to the terminal, informing the application’s user that the SQL statement executed successfully:
cout << "> Successfully inserted user." << endl << endl;
Retrieving Data
In addition to inserting data, we would also like SOCI to retrieve data from the database. Therefore, this section will describe how we can retrieve all rows from the users
table and display each row’s data to the terminal.
Another utility function named display_users
is used that handles the task of displaying all records in the users
table. The soci::session
object containing the database connection is also passed by reference into this function:
// Retrieve all rows from users table and output data
display_users(sql);
The display_users
function is defined just above the main
function in our source file:
void display_users(session& sql) { // Retrieve all rows from users table
rowset<row> rs = (sql.prepare << "SELECT * FROM users"); // Iterate through the result set
for (rowset<row>::const_iterator it = rs.begin();
it != rs.end(); ++it) { const row& r = *it;
std::cout << "ID: " << r.get<int>(0) << endl
<< "First Name: " << r.get<string>(1) << endl
<< "Last Name: " << r.get<string>(2) << endl
<< "Email: " << r.get<string>(3) << endl
<< "Active: " << r.get<int>(4) << endl << endl;
}
}
Let’s firstly take a look at how we can retrieve row data from MySQL:
rowset<row> rs = (sql.prepare << "SELECT * FROM users");
The soci::rowset<T>
container class provides a means of executing queries and accessing results dynamically using an STL-like interface. In the line above, we are retrieving rows from the users
table and storing them in a soci::row
object. In other words, the query results are bound to soci::row
elements, which are accessible through the soci::rowset<T>
interface.
We call the soci::session::prepare
method to not only execute the SQL statement, but also set up a PREPARED
statement on the MySQL server. As a result, the SELECT
statement will be cached on the database server and will not need to be re-parsed by MySQL in future calls — meaning it can execute immediately, potentially speeding up data retrieval.
With the rows fetched from the database backend and returned to our application, we then enter a for
loop and iterate through the result set:
// Iterate through the result set
for (rowset<row>::const_iterator it = rs.begin();
it != rs.end(); ++it) {
// ...
}
Similar to iterating through a STL container such as a vector
or list
, we instantiate a constant input iterator that points to the first soci::row
in the soci::rowset<T>
result set with a call to rs.begin()
. The loop will iterate on each row until it reaches the last row. The loop ends when the iterator reaches the last item in the soci::rowset<T>
. The last soci::row
is checked by calling rs.end()
.
Our for
loop implementation is displayed in the following code snippet:
const row& r = *it;
std::cout << "ID: " << r.get<int>(0) << endl
<< "First Name: " << r.get<string>(1) << endl
<< "Last Name: " << r.get<string>(2) << endl
<< "Email: " << r.get<string>(3) << endl
<< "Active: " << r.get<int>(4) << endl << endl;
Inside the for
loop we create a variable called r
that stores a constant reference to the soci::row
that the iterator is currently pointing at in memory. From here we can use this variable to access the value of each column of the row, and output its value to the terminal.
The soci::row::get
method is called multiple times within the cout
statement. Let’s firstly examine its syntax:
row.get<column_data_type>(column_index)
column_data_type
: Represents the data type of the column we would like to retrieve.column_index
: The column index number, where the first (far-left) column of the table has an index value of0
, the next column having an index value of1
, and so on.
The data type expressed in column_data_type
must match up with the actual data in the database column. For example:
row.get<int>(0) // Returns user's id as an int
row.get<string>(1) // Returns user’s first name as a string
If an incorrect type T
is requested when calling row::get<T>()
, an exception of type std::bad_cast
is thrown.
With that, our application is complete. I hope it gives you some ideas of how you can develop simple applications with SOCI, and that it also encourages you to check out the official SOCI documentation to learn more about the workings of the API.
Let’s move forward and talk about compiling and running our application!
Compiling and Running Our Application
This section will walk through the process of compiling and running our application. The instructions presented in this section will primarily focus on UNIX environments — such as the Mac OS X and Linux operating systems.
For readers who are using Windows, feel free to read through the following instructions and try to mirror the process using your IDE’s compiler settings. As soon as I’m in possession of a Windows system, this section will be updated with specific instructions for Windows OS!
UNIX Environments (Mac OS X, Linux, UNIX)
We shall use the G++ compiler (also referred to as the GCC compiler) to compile our C++ application. Additionally, we will do the entire compilation process inside a terminal.
To check if your system has the g++
compiler installed, run the following command inside a terminal:
which g++
The path to the g++
binary should output to the terminal after running this command — a typical location is /usr/bin/g++
. If nothing is returned, either:
- Mac OS X: Download the Xcode command line tools.
- Linux: Download and install the appropriate gcc package using your Linux distribution’s native package manager.
Moving forward, we now need to configure some search paths that the g++
compiler will consult to locate necessary header files and shared libraries on our filesystem that are required by our application. These search paths will be set via environment variables.
The following command exports the CPATH
environment variable. The compiler will use the search paths set in this variable to find our application’s header files:
export CPATH="/usr/local/include:/usr/local/mysql/include"
/usr/local/mysql/include
is not a standard file location that compilers will check by default. Therefore, we specify it here somysql.h
can be found.
The next command exports the LIBRARY_PATH
environment variable. The compiler will use the search paths set in this variable to locate necessary shared libraries during the linking phase:
export LIBRARY_PATH="/usr/local/lib:/usr/local/mysql/lib"
/usr/local/mysql/lib
is also a custom location that the compiler doesn’t check by default. Since thelibmysqlclient.so
shared library is located here, we must export its location.
Additionally, we export the LD_LIBRARY_PATH
environment variable, which is consulted when the actual program is executed (after it has been compiled and linked) to search for directories containing shared libraries:
export LD_LIBRARY_PATH="/usr/local/lib:/usr/local/mysql/lib"
Use the printenv
command to verify your environment. Call printenv
by itself to list all of your environment variables, or printenv VARIABLE_NAME
to display the value of a single environment variable.
With the search paths set up we are now ready to compile the application using the g++
command. After navigating to the directory containing your hello.cpp
source file, execute the following command:
g++ -std=c++11 hello.cpp -o hello -lsoci_core -lsoci_mysql -ldl
-lmysqlclient
The following list breaks down each argument we provide the g++
command:
-std=c++11
: We will request to use the features of the C++ 11 standard to build our target.hello.cpp
: The single source file representing our program.-o hello
: An executable file calledhello
will be produced as output.-lsoci_core -lsoci_mysql -ldl
: Shared libraries required to interface with the SOCI API.-lmysqlclient
: The official MySQL shared library.
Providing everything went smoothly during the compiling and linking phases, we can now run our final executable from the terminal:
./hello
In Summary
We have covered a lot of ground in this article and explored the various stages that need considering if you are a developer of database software using C++.
We looked at installing our adopted database access library — SOCI, as well as the Boost libraries. We also set up a database environment with MySQL, which involved creating various database objects and a user with developer privileges. From there, we wrote a simple C++ application to demonstrate some of the SOCI API. Our application connects to a database backend, and is able to interact with it to insert and retrieve data. Lastly, we looked at compiling our application using the GCC compiler which involved exporting necessary environment variables, enabling the compiler to resolve the application’s header and library dependencies.
To continue learning about SOCI and implementing more sophisticated database applications, I certainly recommend reading the official SOCI documentation, including the User Guide section, which breaks down each feature of the library whilst also providing example code.
I have also be published an article that builds directly from this one and walks through developing a more sophisticated application with SOCI and MySQL:
In addition, you may wish to try the following challenges to build on and solidify the knowledge you have gained in this article:
- Use SOCI to connect to another backend database, such as PostgreSQL or Firebird.
- Write some unit tests with a testing library, such as Boost.Test.
- Expand your database environment by creating more tables with certain relationships, and then query them with the SOCI API.
Keep in mind that you will need to include the correct SOCI header file, and link to the correct shared library depending on your adopted database backend.