Знакомство с SOCI — C++ библиотекой доступа к базам данных
Вступление
Сама библиотека довольно таки зрелая, — первый релиз на гитхабе датируется аж 2004-ым годом. Я был удивлён когда Хабр в поисковике не выдал мне ни одной ссылки на статьи, в которых бы упоминалось об этой замечательной библиотеке.
Произносится как: сОцы, с ударением на первый слог.
SOCI поддерживает ORM, через специализацию soci::type_conversion.
Поддержка баз данных (БД) (бэкенды):
Я не стану переводить мануалы или приводить здесь код из примеров, а постараюсь адаптировать (с изменением структуры таблицы, и других упрощений) код из своего прошлого проекта, чтобы было нагляднее и интереснее.
Установка
Качаем сырцы из ветки master, распаковываем, и внутри директории выполняем команду:
В Windows
$ mkdir build && cd build && cmake -G»Visual Studio 15 2017 Win64” ../ && cmake —build. —config Release
или вместо последней команды, можно открыть получившийся проект в Visual Studio и собрать.
(о сборке при помощи cmake в командной строке подсказал Wilk)
В nix
$ mkdir build && cd build && cmake ../ && sudo make install
Если вы обладатель Gentoo Linux или Calculate Linux, и хотите иметь в системе самую свежую версию SOCI из официального репозитория на гитхабе, то можете сохранить данный файл установки в каталоге /usr/portage/dev-db/soci/
, перейти в него и выполнить команду:
# ebuild soci-9999.ebuild manifest && emerge -va =dev-db/soci-9999
# Copyright 1999-2018 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 EAPI=6 if [[ ${PV} == *9999 ]] ; then SCM="git-r3" EGIT_REPO_URI="https://github.com/SOCI/${PN}.git" fi CMAKE_MIN_VERSION=2.6.0 inherit cmake-utils ${SCM} DESCRIPTION="Makes the illusion of embedding SQL queries in the regular C++ code" HOMEPAGE="http://soci.sourceforge.net/" if [[ ${PV} == *9999 ]] ; then SRC_URI="" KEYWORDS="~amd64 ~x86" else SRC_URI="https://github.com/SOCI/${PN}/archive/${PV}.tar.gz -> ${P}.tar.gz" KEYWORDS="amd64 x86" fi LICENSE="Boost-1.0" SLOT="0" IUSE="boost doc +empty firebird mysql odbc oracle postgres sqlite static-libs test" RDEPEND=" firebird? ( dev-db/firebird ) mysql? ( virtual/mysql ) odbc? ( dev-db/unixODBC ) oracle? ( dev-db/oracle-instantclient-basic ) postgres? ( dev-db/postgresql:= ) sqlite? ( dev-db/sqlite:3 ) " DEPEND="${RDEPEND} boost? ( dev-libs/boost ) " src_configure() { local mycmakeargs=( -DWITH_BOOST=$(usex boost) -DSOCI_EMPTY=$(usex empty) -DWITH_FIREBIRD=$(usex firebird) -DWITH_MYSQL=$(usex mysql) -DWITH_ODBC=$(usex odbc) -DWITH_ORACLE=$(usex oracle) -DWITH_POSTGRESQL=$(usex postgres) -DWITH_SQLITE3=$(usex sqlite) -DSOCI_STATIC=$(usex static-libs) -DSOCI_TESTS=$(usex test) -DWITH_DB2=OFF ) #use MYCMAKEARGS if you want enable IBM DB2 support cmake-utils_src_configure } src_install() { use doc && local HTML_DOCS=( doc/. ) cmake-utils_src_install }
Пишем пул для соединений с базой данных
#ifndef db_pool_hpp #define db_pool_hpp // да простят меня пользователи НЕ GCC, но я не знаю как отключить // ворнинги для других компиляторов, о deprecated auto_ptr (если версия ниже 4) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" #include <soci/soci.h> #include <soci/connection-pool.h> #pragma GCC diagnostic pop #include <iostream> #include <string> class db_pool { soci::connection_pool* pool_; std::size_t pool_size_; public: db_pool():pool_(nullptr),pool_size_(0) {} ~db_pool() { close(); } soci::connection_pool* get_pool() { return pool_; } bool connect(const std::string& conn_str, std::size_t n = 5) { if (pool_ != nullptr) { close(); } int is_connected = 0; if (!(pool_ = new soci::connection_pool((pool_size_ = n)))) return false; try { soci::indicator ind; for (std::size_t _i = 0; _i < pool_size_; _i++) { soci::session& sql = pool_->at(_i); // для каждой сессии открываем соединение с БД sql.open(conn_str); // и проверяем простым запросом sql << "SELECT 1;", soci::into(is_connected, ind); if (!is_connected) break; else if (_i+1 < pool_size_) is_connected = 0; } } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } if (!is_connected) close(); return (pool_ != nullptr); } void close () { if (pool_ != nullptr) { try { for (std::size_t _i = 0; _i < pool_size_; _i++) { soci::session& sql = pool_->at(_i); sql.close(); } delete pool_; pool_ = nullptr; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } pool_size_ = 0; } } }; #endif
Определяем структуру таблицы в классе user_info
#ifndef user_info_hpp #define user_info_hpp #include "db_pool.hpp" #include <ctime> #include <vector> #include <regex> #include <numeric> #include <algorithm> #include <iomanip> // некоторые вспомогательные ф-ии для преобразования массивов в векторы и обратно template<typename T> static void extract_integers(const std::string& str, std::vector<T>& result ) { result.clear(); using re_iterator = std::regex_iterator<std::string::const_iterator>; using re_iterated = re_iterator::value_type; std::regex re("([\\+\\-]?\\d+)"); re_iterator rit(str.begin(), str.end(), re), rend; std::transform(rit, rend, std::back_inserter(result), [](const re_iterated& it){return std::stoi(it[1]); }); } template<typename T> static void split_integers(std::string& str, const std::vector<T>& arr) { str = "{"; if (arr.size()) { str += std::accumulate(arr.begin()+1, arr.end(), std::to_string(arr[0]), [](const std::string& a, T b){return a + ',' + std::to_string(b);}); } str += "}"; } // структура таблицы `users' class user_info { public: int id; // айди пользователя std::tm birthday; // день рождения std::string firstname, lastname; // имя и фамилия std::vector<int> friends; // айдишники друзей user_info():id(0),birthday(0),firstname(),lastname(),friends() {} void print() { std::cout.imbue(std::locale("ru_RU.utf8")); std::cout << "id: " << id << std::endl; std::cout << "birthday: " << std::put_time(&birthday, "%c %Z") << std::endl; std::cout << "firstname: " << firstname << std::endl; std::cout << "lastname: " << lastname << std::endl; std::string arr_str; split_integers(arr_str, friends); std::cout << "friends: " << arr_str << std::endl; } void clear() { id = 0; firstname = lastname = ""; friends.clear(); } user_info& operator=(const user_info& rhs) { if (this != &rhs) { id = rhs.id; birthday = rhs.birthday; firstname = rhs.firstname; lastname = rhs.lastname; friends = rhs.friends; } return *this; } }; // для работы со своими типами, в SOCI имеются конвертеры namespace soci { template<> struct type_conversion<user_info> { typedef values base_type; static void from_base(values const& v, indicator ind, user_info& p) { if (ind == i_null) return; try { p.id = v.get<int>("id", 0); p.birthday = v.get<std::tm>("birthday", {}); p.firstname = v.get<std::string>("firstname", {}); p.lastname = v.get<std::string>("lastname", {}); std::string arr_str = v.get<std::string>("friends", {}); extract_integers(arr_str, p.friends); } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } } static void to_base(const user_info& p, values& v, indicator& ind) { try { v.set("id", p.id); v.set("birthday", p.birthday); v.set("firstname", p.firstname); v.set("lastname", p.lastname); std::string arr_str; split_integers(arr_str, p.friends); v.set("friends", arr_str); ind = i_ok; return; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } ind = i_null; } }; } #endif
Тестируем наш код
#ifndef test_cxx #define test_cxx #include "user_info.hpp" // g++ -std=c++11 test.cxx -o test -lsoci_core -lsoci_postgresql && ./test int main() { db_pool db; /// \note замените "postgresql" на свой бэкенд, также измените имя БД и пользователя с паролем if (db.connect("postgresql://host='localhost' dbname='test' user='test' password='test'")) { try { soci::session sql(*db.get_pool()); // создаём таблицу если не существует sql << "CREATE TABLE IF NOT EXISTS users(id SERIAL PRIMARY KEY, birthday TIMESTAMP DEFAULT NOW(), firstname VARCHAR(64) DEFAULT NULL, lastname VARCHAR(64) DEFAULT NULL, friends INT[] DEFAULT NULL)"; // заполняем поля user_info info; std::time_t t = std::time(nullptr); info.birthday = *std::localtime(&t); info.firstname = "Dmitrij"; info.lastname = "Volin"; info.friends = {1,2,3,4,5,6,7,8,9}; int id = 0; // id новой записи (поле id пользователя) soci::indicator ind; // делаем запись в БД sql << "INSERT INTO users(birthday, firstname, lastname, friends) VALUES(:birthday, :firstname, :lastname, :friends) RETURNING id", soci::use(info), soci::into(id, ind); if (ind != soci::i_ok) std::cout << "не удалось записать данные в БД ..." << std::endl; t = std::time(nullptr); info.birthday = *std::localtime(&t); info.firstname = "Vasy"; info.lastname = "Pupkin"; info.friends = {11,22,33,44,55,66,77,88,99}; // делаем ещё одну запись в БД sql << "INSERT INTO users(birthday, firstname, lastname, friends) VALUES(:birthday, :firstname, :lastname, :friends) RETURNING id", soci::use(info), soci::into(id, ind); if (ind != soci::i_ok) std::cout << "не удалось записать данные в БД ..." << std::endl; std::cout << "id нового пользователя: " << id << std::endl; // очищаем перед выборкой из БД info.clear(); // делаем выборку нашей записи в очищенную структуру sql << "SELECT * FROM users WHERE id = :userid", soci::use(id, "userid"), soci::into(info, ind); if (ind == soci::i_null) std::cout << "не удалось выбрать данные из БД ..." << std::endl; // выводим в консоль полученные данные info.print(); std::cout << "++++++++++++++++++++++++++++++++++++++" << std::endl; // сейчас сделаем полную выборку soci::rowset<user_info> rs = (sql.prepare << "SELECT * FROM users"); for (auto it = rs.begin(); it != rs.end(); it++) { user_info & i = *it; i.print(); } // удаляем таблицу sql << "DROP TABLE IF EXISTS users"; } catch (std::exception const & e) { std::cerr << e.what() << std::endl; } } return 0; } #endif
Заключение
В этой статье мы расмотрели основные возможности библиотеки.
В следующей статье (если у читателей будет интерес), напишу о работе с типом BLOB — для хранения в БД файлов и картинок (в postgresql это поля типа OID), а также о транзакциях и prepared-запросах.
Ссылки

