Знакомство с SOCI — C++ библиотекой доступа к базам данных

в 16:28, , рубрики: c++, firebird, mysql, oracle, postgresql, sqlite, базы данных, Программирование

Вступление

Сама библиотека довольно таки зрелая, — первый релиз на гитхабе
датируется аж 2004 годом. Я был удивлён когда хабр в поиске
не выдал мне ни одной ссылки на статьи, в которых бы упоминалось
об этой замечательной библиотеке.

SOCI поддерживает ORM, через специализацию type_conversion.

В SOCI имеются бэкенды для:

  • Firebird
  • MySQL
  • Oracle
  • PostgreSQL
  • SQLite

Я не стану переводить мануалы или приводить здесь код из примеров,
а постараюсь адаптировать (с изменением структуры таблицы, и других упрощений)
код из своего прошлого проекта, чтобы было наглядней и интересней.

Установка

Качаем релиз, распаковываем, и внутри директории выполняем команду:

В Windows

$ mkdir build && cd build && cmake -G"Visual Studio 15 2017 Win64” ..

открываем получившийся проект в Visual Studio и собираем.

В nix

$ mkdir build && cd build && cmake… && sudo make install

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

#ifndef db_pool_hpp
#define db_pool_hpp

// да простят меня пользователи НЕ GCC, но я не знаю как отключить
// ворнинги для других компиляторов, о deprecated auto_ptr
#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>
/// note замените "postgresql" на "mysql" или "sqlite3" - для вашего бэкенда
#include <soci/postgresql/soci-postgresql.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(); }
    bool ret = false;
    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);
  re_iterator rend;
  std::transform(rit, rend, std::back_inserter(result), [](const re_iterated& it){return std::stoul(it[1]); });
}

template<typename T>
static void split_integers(std::string& str, const std::vector<T>& arr) {
  str.clear();
  str = "{";
  if (arr.size()) {
    str += std::accumulate(arr.begin()+1, arr.end(), std::to_string(arr[0]),
                           [](const std::string& a, int 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; tm = {0}; firstname = lastname = ""; friends.clear(); }

};

// для работы со своими типами, в 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);
        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(6) without time zone DEFAULT now(), firstname text DEFAULT NULL, lastname text DEFAULT NULL, friends integer[] 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);

      std::cout << "id нового пользователя: " << id << std::endl;

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

      // делаем выборку нашей записи в очищенную структуру
      sql << "SELECT * FROM users WHERE id=:userid;", soci::use(id, "userid"), soci::into(info, ind);

      // выводим в консоль полученные данные
      info.print();

      // удаляем таблицу
      sql << "DROP TABLE IF EXISTS users;";

    } catch (std::exception const & e) { std::cerr << e.what() << std::endl; }
  }

  return 0;
}

#endif

Заключение

В этой статье мы расмотрели малую часть возможностей библиотеки.

В следующей статье (если у читателей будет интерес), могу написать
о работе с полями типа BLOB — для хранения файлов и картинок.
А также о транзакциях и prepared-запросах.

Ссылка на проект

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

Автор: dmxvlx

Источник

Поделиться

* - обязательные к заполнению поля