Вступление


Сама библиотека довольно таки зрелая, — первый релиз на гитхабе датируется аж 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

soci-9999.ebuild

Если вы обладатель 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
}

Пишем пул для соединений с базой данных


db_pool.hpp
#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


user_info.hpp
#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

Тестируем наш код


test.cxx
#ifndef test_cxx
#define test_cxx

#include "user_info.hpp"

// g++ -std=c++11 test.cxx -o test -lsoci_core -lsoci_postgresql -lsoci_mysql && ./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());

      // сформируем запрос создадим таблицу
      std::string query_str = "CREATE TABLE IF NOT EXISTS users(id";

      // нам нужно для каждого бэкенда, указать правильный тип авто-счётчика для поля id
      if (sql.get_backend_name() == "postgresql") query_str += " SERIAL ";
      else if (sql.get_backend_name() == "mysql") query_str += " INT AUTO_INCREMENT ";
      else query_str += " INT ";

      query_str += "NOT NULL PRIMARY KEY, birthday TIMESTAMP DEFAULT NOW(), firstname TEXT DEFAULT NULL, lastname TEXT DEFAULT NULL, friends TEXT DEFAULT NULL)";

      // выполняем запрос
      sql << query_str;

      // заполняем поля
      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};

      sql << "INSERT INTO users(birthday, firstname, lastname, friends) VALUES(:birthday, :firstname, :lastname, :friends)", soci::use(info);

      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)", soci::use(info);

      // индикатор для выборки, может быть: soci::i_ok, soci::i_null
      soci::indicator ind;

      // для MySQL получить id последней вставленной записи, для AUTO_INCREMENT:
      // sql.get_backend()->get_last_insert_id(sql, "users", reinterpret_cast<long&>(id));
      //
      // для PostgreSQL чтобы получить id последней записи, нужно сформировать запрос так:
      // sql << "INSERT INTO users(birthday, firstname, lastname, friends) VALUES(:birthday, :firstname, :lastname, :friends) RETURNING id", soci::use(info), soci::into(id, ind);

      // очищаем перед выборкой из БД
      info.clear();

      // делаем выборку нашей записи в очищенную структуру, по полю `lastname'
      sql << "SELECT * FROM users WHERE lastname = :label LIMIT 1", soci::use(std::string("Volin"), "label"), soci::into(info, ind);
      if (ind == soci::i_null) std::cout << "не удалось выбрать данные из БД ..." << std::endl;
      else 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-запросах.


Ссылки


SOCI на github
SOCI домашняя страница

Комментарии (15)


  1. Wilk
    10.09.2018 20:00

    Здравствуйте!

    Разве необходимо что-то делать с std::auto_ptr? Насколько я могу судить, std::auto_ptr используется только в случае, если не доступен std::unique_ptr, т.е. при сборке в режиме C++98/03. Или я что-то упускаю?

    P.S. Можно не открывать Visual Studio для сборки:

    cmake --build . --config Release


    (вместо Release можно использовать Debug, RelWithDegInfo, MinSizeRel или другой конфигурации сборки. Мелочь, конечно, но из терминала использовать удобнее получается.


    1. dmxvlx Автор
      10.09.2018 20:14

      Добрый вечер.

      Да вы правы, если пользовать ветку 4.*, но ссылку я дал на 3.2.3, а там такого нет ))
      Я сделаю правку в тексте, спасибо!


      1. Wilk
        10.09.2018 20:29

        Прошу простит невнимательность — пропустил ссылку на архив с исходными кодами, увидел только в конце ссылку на репозиторий.


  1. s-a-u-r-o-n
    10.09.2018 20:25

    Если хотите что-то более универсальное, используйте NanoDBC. NanoDBC использует ODBC или unixODBC в качестве backend, что позволяет использовать многочисленные драйверы ODBC в С++.


    1. dmxvlx Автор
      11.09.2018 04:06

      SOCI тоже поддерживает: ODBC with specific database driver, текст поправил, спасибо за замечание.


  1. Mingun
    11.09.2018 08:09

    Вообще, авторы библиотеки в периодически всплывающей активности на github-е рекомендуют использовать ветку 4.x (для нее нет релиза, нужно ставить прямо с гитхаба).


    Ну и я бы с опаской относился к этой библиотеке — она уже давно не развивается, а на гитхабе висит незакрытыми 86 задач. Так что если возникнут какие-то проблемы, вы будете с ними один на один.


    Да, и например, нормальной поддержки LOB-ов там нет, весь LOB читается за раз в строчку (как минимум в оракловском бекенде).


    1. dmxvlx Автор
      11.09.2018 08:29

      По поводу версии сделал замечание в тексте статьи — качать свежие сырцы.


      Незакрытые задачи по большей части всплывают из-за желания юзеров юзать из коробки
      очень специфичные вещи.


      А для LOB'ов юзайте стандартный API soci::blob:


      интерфейс soci::blob
      class SOCI_DECL blob
      {
      public:
          explicit blob(session & s);
          ~blob();
      
          std::size_t get_len();
      
          // offset is backend-specific
          std::size_t read(std::size_t offset, char * buf, std::size_t toRead);
      
          // offset starts from 0
          std::size_t read_from_start(char * buf, std::size_t toRead,
              std::size_t offset = 0);
      
          // offset is backend-specific
          std::size_t write(std::size_t offset, char const * buf,
              std::size_t toWrite);
      
          // offset starts from 0
          std::size_t write_from_start(const char * buf, std::size_t toWrite,
              std::size_t offset = 0);
      
          std::size_t append(char const * buf, std::size_t toWrite);
      
          void trim(std::size_t newLen);
      
          details::blob_backend * get_backend() { return backEnd_; }
      
      private:
          details::blob_backend * backEnd_;
      };


  1. demp
    11.09.2018 09:15
    +1

    Еще парочка похожих проектов примерно такого же состояния заброшенности:



    1. oYASo
      12.09.2018 00:30

      Ну я бы не был столь категоричен. В edba 143 коммита, последний был 2.5 года назад, cppdb дальше cppcms не забралась и, видимо, поэтому была заброшена.

      У soci 2450 коммитов, последний был пару месяцев назад, сам проект уже черти когда назад начал писаться. На самом деле, продукт зрелый.


      1. demp
        13.09.2018 00:24

        Да зрелый, более точное определение для проекта с датой последнего релиза 2015. Как и парочка упомянутых выше. Вот еще один зрелый проект для коллекции: https://github.com/pmed/sqlitepp :)


        По моему небольшому опыту, сама идея встраивания SQL-подобного кода в С++ программу в конечном итоге заканчивается спуском к деталям реализации конкретной БД и вызову конкретных функций коннектора этой БД.


        1. dmxvlx Автор
          13.09.2018 05:05

          По моему небольшому опыту, сама идея встраивания SQL-подобного кода в С++ программу в конечном итоге заканчивается спуском к деталям реализации конкретной БД и вызову конкретных функций коннектора этой БД.

          Ок. Мы вас слушаем, что нужно делать, что использовать, и почему нужно использовать именно то что вы предлагаете, а не какую-нибудь зрелую библиотеку, которая даёт гибкость в работе...


          Напишите статью "я против всяких ORM библиотек, я за чистый SQL", киньте ссылку, и мы будем в курсе раз и навсегда ))


          PS: советую перепрочитать статью, я её переработал, возможно в том виде в каком вы её читали, не дала вам ясных представлений...


          1. demp
            13.09.2018 09:24

            Делать и использовать нужно то, что подходит под требования проекта. На мой взгляд, edba выглядит лучше в качестве универсальной библиотеки. В ней, по крайней мере используется C++11 без ручного управления памятью и deprecated
            auto_ptr.


            Статей про плюсы/минусы ORM и без меня уже написано за последние лет 20 много, так что я, пожалуй, не буду увеличивать энтропию.


            Я глянул на код вашей статьи. В db_pool возможно двойное освобождение памяти — сырой указатель и отсутствие конструктора копирования гарантируют это.


            В user_info рукопашный код для перегона


            std::vector<int> friends; // айдишники друзей

            в строковое представление и назад как бы намекает, что реляционность вам не нужна.


            В test.cxx в строках


             // нам нужно для каждого бэкенда, указать правильный тип авто-счётчика для поля id
                  if (sql.get_backend_name() == "postgresql") query_str += " SERIAL ";
                  else if (sql.get_backend_name() == "mysql") query_str += " INT AUTO_INCREMENT ";

            вы показываете суть универсального доступа к БД — и вынуждены бороться с различиями диалектов SQL.


            1. dmxvlx Автор
              13.09.2018 10:31

              Делать и использовать нужно то, что подходит под требования проекта.

              Вот именно !


              А по всем остальным пунктам, касательно моего быдлокодерства (не заморочился с потоко-безопасностью, сделал хранение массива чисел в строке потому что в SOCI нет этой поддержки, борюсь с диалектами SQL) отвечу:


              всё что я посчитал нужным осветить в данной статье — я осветил,
              если вам не нравится как я подготовил этот tutorial — ну на вкус и цвет как говорится ))


  1. Nikita_velikiy
    11.09.2018 16:57
    +1

    Название библеотеки улыбнуло)


    1. dmxvlx Автор
      11.09.2018 16:58

      ЛОЛ...


      Никогда не замечал, всегда читал Соци ))


      Может поэтому она не популярна в РФ ?..