C: libmysqlclient – примеры работы с MySQL API

Автор: | 29/01/2019

Давно не писал на С (последний раз – почти год тому, см. What is: Linux namespaces, примеры PID и Network namespaces).

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

Ниже приводятся примеры работы с сервером MySQL/MariaDB на С, используя API из библиотеки libmysqlclient.

Примеры взяты из поста MySQL C API programming tutorial с небольшими изменениями и отсебятинкой.

Получившиеся примеры доступны в репозитории тут>>>.

Устаналиваем на Ubuntu/Debian с помощью apt:

[simterm]

$ sudo apt install default-libmysqlclient-dev

[/simterm]

Первый пример

Напишем первый пример, который выполнит подключение к серверу MySQL, и выведет его версию:

#include <my_global.h>
#include <mysql.h>

int main(int argc, char **argv) {
  printf("MySQL client version: %s\n", mysql_get_client_info());
  exit(0);
}

Собираем его:

[simterm]

$ gcc get_version.c -o get_version `mysql_config --cflags --libs`

[/simterm]

Запускаем:

[simterm]

$ ./get_version 
MySQL client version: 10.1.37-MariaDB

[/simterm]

Убедимся, что это правда:

[simterm]

$ mysql --version
mysql  Ver 15.1 Distrib 10.1.37-MariaDB, for debian-linux-gnu (x86_64) using readline 5.2

[/simterm]

Кратко рассмотрим сам код:

  • #include <my_global.h> и #include <mysql.h>: включаем заголовочные файлы для работы с MySQL, которые расположены в каталоге /usr/include/mysql/, путь к которому передадим через вызов mysql_config (см. ниже). В частности – /usr/include/mysql/mysql.h содежит описание mysql_get_client_info()
  • затем вызываем mysql_get_client_info(), которая возвращает версию используемой библиотеки

gcc и mysql_config

При компиляции выше мы использовали вызов mysql_config, что бы получить пути к используемым библиотекам и флагам сборки MySQL-клиента.

Можно вызвать mysql_config напрямую, что бы посмотреть эти данные:

[simterm]

$ mysql_config
Usage: /usr/bin/mysql_config [OPTIONS]
Options:
        --cflags         [-I/usr/include/mysql ]
        --include        [-I/usr/include/mysql]
        --libs           [-L/usr/lib/x86_64-linux-gnu  -lmariadbclient -lpthread -lz -lm -ldl]
        --libs_r         [-L/usr/lib/x86_64-linux-gnu  -lmariadbclient -lpthread -lz -lm -ldl]
        --plugindir      [/usr/lib/x86_64-linux-gnu/mariadb18/plugin]
        --socket         [/var/run/mysqld/mysqld.sock]
        --port           [0]
        --version        [10.1.37]
        --libmysqld-libs [-L/usr/lib/x86_64-linux-gnu  -lmysqld -lpthread -lz -lm -ldl -lcrypt -laio]
        --variable=VAR   VAR is one of:
                pkgincludedir [/usr/include/mysql]
                pkglibdir     [/usr/lib/x86_64-linux-gnu]
                plugindir     [/usr/lib/x86_64-linux-gnu/mariadb18/plugin]

[/simterm]

Или отобразить только неоходимые данные, что мы и делаем при вызове gcc:

[simterm]

$ mysql_config --cflags --libs
-I/usr/include/mysql 
-L/usr/lib/x86_64-linux-gnu  -lmariadbclient -lpthread -lz -lm -ldl

[/simterm]

Создание базы данных

В следующем примере – выполним подключение к серверу и запрос на создание новой базы данных.

Тут нам потребуются данные доступа:

  • имя хоста – cdb-example.setevoy.org.ua (через CNAME направлено на AWS RDS инстанс с MariaDB)
  • имя пользователя – setevoy
  • пароль пользователя

Подключаемся к серверу БД, создаём пользоваеля:

[simterm]

MariaDB [(none)]> grant all privileges on `%`.* to 'setevoy'@'%' identified by 'p@ssw0rd';
Query OK, 0 rows affected (0.06 sec)

[/simterm]

Приступаем к коду:

#include <my_global.h>
#include <mysql.h>

int main(int argc, char **argv) {

  MYSQL *con = mysql_init(NULL);

  if (con == NULL) {
      fprintf(stderr, "%s\n", mysql_error(con));
      exit(1);
  }

  if (mysql_real_connect(con, "cdb-example.setevoy.org.ua", "setevoy", "p@ssw0rd",
          NULL, 0, NULL, 0) == NULL) {
      fprintf(stderr, "%s\n", mysql_error(con));
      mysql_close(con);
      exit(1);
  }

  if (mysql_query(con, "CREATE DATABASE testdb")) {
      fprintf(stderr, "%s\n", mysql_error(con));
      mysql_close(con);
      exit(1);
  }

  mysql_close(con);
  exit(0);

}

Собираем его:

[simterm]

$ gcc create_db.c -o create_db `mysql_config --cflags --libs`

[/simterm]

Запускаем:

[simterm]

$ ./create_db

[/simterm]

Проверяем:

[simterm]

$ mysql -h cdb-example.setevoy.org.ua -u setevoy -pp@ssw0rd -e "show databases"
+--------------------+
| Database           |
+--------------------+
| information_schema |
| innodb             |
| mysql              |
| performance_schema |
| testdb             |
| tmp                |
+--------------------+

[/simterm]

testdb появилась, всё работает.

Процесс выполнения программы:

  • MYSQL *con = mysql_init(NULL) – с помощью mysql_init() инициализируем создание новой сессии для создания подключения
  • if (con == NULL) – проверяем его состояние, если сессия не создана – выходим с ошибкой, используя exit(1);
  • mysql_real_connect(con, "cdb-example.setevoy.org.ua", "setevoy", "p@ssw0rd") – используя mysql_real_connect() создаём подключение, используя созданную сессию (con), и передавая имя хоста, пользователя и пароль, и тут же проверяем его состояние с помощью if mysql_real_connect() == NULL
  • mysql_query(con, "CREATE DATABASE testdb") – с помощью mysql_query() выполняем запрос на создание базы данных
  • mysql_close() – закрываем сессию, созданную с помощью mysql_init()

Создание и наполнение таблицы

Теперь у нас есть пользователь, и база.

Следующим шагом – создадим в этой базе таблицу, и запишем в неё данные:

#include <my_global.h>
#include <mysql.h>

void finish_with_error(MYSQL *con) {
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv) {

  MYSQL *con = mysql_init(NULL);

  if (con == NULL) {
      fprintf(stderr, "%s\n", mysql_error(con));
      exit(1);
  }

  if (mysql_real_connect(con, "cdb-example.setevoy.org.ua", "setevoy", "p@ssw0rd",
          "testdb", 0, NULL, 0) == NULL) {
      finish_with_error(con);
  }
 
  if (mysql_query(con, "DROP TABLE IF EXISTS ExampleTable")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "CREATE TABLE ExampleTable(Id INT, TextCol TEXT, IntCol INT)")) {
      finish_with_error(con);
  }

  if (mysql_query(con, "INSERT INTO ExampleTable VALUES(1, 'TextValue', IntValue)")) {
      finish_with_error(con);
  }

  mysql_close(con);
  exit(0);

}

Тут, что бы не писать один и тот же код при каждом выполнении запросов к MySQL, мы сначала определяем единую функцию, которую будем использовать для проверки успешности подключения:

...
void finish_with_error(MYSQL *con) {
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}
...

Затем подключаемся к базе, удаляем таблицу, если она есть (mysql_query(con, "DROP TABLE IF EXISTS ExampleTable")), создаём новую таблицу с тремя колонками (mysql_query(con, "CREATE TABLE ExampleTable(Id INT, TextCol TEXT, IntCol INT)"), и вносим тестовые данные (mysql_query(con, "INSERT INTO ExampleTable VALUES(1, 'TextValue', 12345)")).

Собираем:

[simterm]

$ gcc create_table.c -o create_table `mysql_config --cflags --libs`

[/simterm]

Запускаем:

[simterm]

$ ./create_table

[/simterm]

Проверяем:

[simterm]

MariaDB [testdb]> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| ExampleTable     |
+------------------+

MariaDB [testdb]> desc ExampleTable;
+---------+---------+------+-----+---------+-------+
| Field   | Type    | Null | Key | Default | Extra |
+---------+---------+------+-----+---------+-------+
| Id      | int(11) | YES  |     | NULL    |       |
| TextCol | text    | YES  |     | NULL    |       |
| IntCol  | int(11) | YES  |     | NULL    |       |
+---------+---------+------+-----+---------+-------+

MariaDB [testdb]> select * from ExampleTable;
+------+-----------+--------+
| Id   | TextCol   | IntCol |
+------+-----------+--------+
|    1 | TextValue |  12345 |
+------+-----------+--------+

[/simterm]

Получение данных из базы

Теперь, когда данные в базе есть – попробуем их оттуда получить.

Для этого мы выполним:

  1. создание подключения
  2. выполнение запроса
  3. получение результата

Код может выглядить так:

#include <my_global.h>
#include <mysql.h>

#define DB_HOST "cdb-example.setevoy.org.ua"
#define DB_USER "setevoy"
#define DP_PASS "p@ssw0rd"
#define DB_NAME "testdb"
#define DB_TABLE "ExampleTable"

void finish_with_error(MYSQL *con) {

  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main(int argc, char **argv) {

  MYSQL *con = mysql_init(NULL);

  if (con == NULL) {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
  }

  if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, 0) == NULL) {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT * FROM " DB_TABLE)) {
      finish_with_error(con);
  }
  
  MYSQL_RES *result = mysql_store_result(con);
  
  if (result == NULL) {
      finish_with_error(con);
  }

  int num_fields = mysql_num_fields(result);

  MYSQL_ROW row;
  
  while ((row = mysql_fetch_row(result))) { 

      for(int i = 0; i < num_fields; i++) { 
          printf("%s ", row[i] ? row[i] : "NULL"); 
      } 
          printf("\n"); 
  }
  
  mysql_free_result(result);
  mysql_close(con);
  
  exit(0);
}

Первое отличие – тут мы выносим параметры для подключения к серверу в макросы препроцессора, создавая такие себе const variables:

...
#define DB_HOST "cdb-example.setevoy.org.ua"
#define DB_USER "setevoy"
#define DP_PASS "p@ssw0rd"
#define DB_NAME "testdb"
#define DB_TABLE "ExampleTable"
...

И затем используем их при вызове функций:

...
  if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, 0) == NULL) {
      finish_with_error(con);
  }

  if (mysql_query(con, "SELECT * FROM " DB_TABLE)) {
      finish_with_error(con);
  }
...

Не очень уверен, насколько кошерно задавать значения через макрос (хотя пишут, что это нормальный подход), можно использовать другой вариант – с помощью const:

...
const char * db_host = "mysqlhost.com";
...

Затем с помощью mysql_store_result() мы сохраняем результат запроса в структуре MYSQL_RES:

...
  MYSQL_RES *result = mysql_store_result(con);
  
  if (result == NULL) {
      finish_with_error(con);
  }
...

Потом подсчитываем кол-во колонок в таблице, используя mysql_num_fields():

...
int num_fields = mysql_num_fields(result);
...

А затем в цикле while проверяем каждую строку, и выводим её на экран:

...
  MYSQL_ROW row;
  
  while ((row = mysql_fetch_row(result))) { 

      for(int i = 0; i < num_fields; i++) { 
          printf("%s ", row[i] ? row[i] : "NULL"); 
      } 
          printf("\n"); 
  }
...

В строке row[i] ? row[i] : "NULL" проверяем условие: если row[i] имеет значение – то выводим его с помощью модификатора %s (string) для printf(), а если пусто – то пропускаем.

Последним шагом – удаляем данные и закрываем соединение:

...
  mysql_free_result(result);
  mysql_close(con);
...

Собираем, как в примерах выше, запускаем:

[simterm]

$ ./get_data 
1 TextValue 12345 

[/simterm]

Получение ID

Ещё один пример работы с библиотекой MySQL – получение ID последней добавленной в таблицу записи.

Для этого можно использовать функцию mysql_insert_id(), которая работает, если колонка Id создана с AUTO_INCREMENT.

Используем такой код:

#include <my_global.h>
#include <mysql.h>

#define DB_HOST "cdb-example.setevoy.org.ua"
#define DB_USER "setevoy"
#define DP_PASS "p@ssw0rd"
#define DB_NAME "testdb"
#define DB_TABLE "ExampleTable"

void finish_with_error(MYSQL *con) {
    
    fprintf(stderr, "%s\n", mysql_error(con));
    mysql_close(con);
    exit(1);
}

void mysqlexec(MYSQL *con, char *query) {

    printf("Running query: %s\n", query);

    if (mysql_query(con, query)) {
      finish_with_error(con);
    }

}

int main() {

    MYSQL *con = mysql_init(NULL);

    if (con == NULL) {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
    }

    if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, 0) == NULL) {
      finish_with_error(con);
    }

    if (mysql_query(con, "DROP TABLE IF EXISTS " DB_TABLE)) {
      finish_with_error(con);
    }

    char *buffer = malloc(1024);
    sprintf(buffer, "CREATE TABLE %s(Id INT PRIMARY KEY AUTO_INCREMENT, TextCol TEXT, IntCol INT)", DB_TABLE);

    if (mysql_query(con, buffer)) {
        finish_with_error(con);
    }

    char *textArray[] = {"a", "b", "c"};
    int intArray[] = {1, 2, 3};

    int n;
    // count intArray[] lengh
    // example taken from the https://www.sanfoundry.com/c-program-number-elements-array/
    n = sizeof(intArray)/sizeof(int);

    int i;
    for (i=0; i<n; i++) {
        // best to check needed size for maloc() using sizeof()
        sprintf(buffer, "INSERT INTO %s VALUES(NULL, '%s', '%d')" , DB_TABLE, textArray[i], intArray[i]);
        mysqlexec(con, buffer);
    }

    int id = mysql_insert_id(con);

    printf("The last inserted row id is: %d\n", id);

    mysql_close(con);
    exit(0);
}

В оригинале для добавления записей использовался немного другой подход:

...
  if (mysql_query(con, "INSERT INTO Writers(Name) VALUES('Leo Tolstoy')"))
  {    
      finish_with_error(con);    
  }
  
  if (mysql_query(con, "INSERT INTO Writers(Name) VALUES('Jack London')"))
  {    
      finish_with_error(con);
  }
  
  if (mysql_query(con, "INSERT INTO Writers(Name) VALUES('Honore de Balzac')"))
  {    
      finish_with_error(con);
  }
...

Но я решил немого его “улучшить”, и в данном случае используется два массива, в которых мы храним данные:

...
    char *textArray[] = {"a", "b", "c"};
    int intArray[] = {1, 2, 3};
...

А затем в цикле перебираются их значения, и с помощью sprintf() формируется строка с запросом в переменной buffer:

...
    int i;
    for (i=0; i<n; i++) {
        // best to check needed size for maloc() using sizeof()
        sprintf(buffer, "INSERT INTO %s VALUES(NULL, '%s', '%d')" , DB_TABLE, textArray[i], intArray[i]);
...

Которая далее передаётся функции mysqlexec(), где и выполняется:

...
void mysqlexec(MYSQL *con, char *query) {

    printf("Running query: %s\n", query);

    if (mysql_query(con, query)) {
      finish_with_error(con);
    }

}
...

Собираем, и запускаем:

[simterm]

$ ./get_id 
Running query: INSERT INTO ExampleTable VALUES(NULL, 'a', '1')
Running query: INSERT INTO ExampleTable VALUES(NULL, 'b', '2')
Running query: INSERT INTO ExampleTable VALUES(NULL, 'c', '3')
The last inserted row id is: 3

[/simterm]

The last inserted row id is: 3 – искомое значение.

Проверяем данные в базе:

[simterm]

MariaDB [testdb]> select * from ExampleTable;
+----+---------+--------+
| Id | TextCol | IntCol |
+----+---------+--------+
|  1 | a       |      1 |
|  2 | b       |      2 |
|  3 | c       |      3 |
+----+---------+--------+

[/simterm]

Получение заголовков колонок

Следующий пример – получение заголовков колонок из нашей таблицы:

[simterm]

MariaDB [testdb]> desc ExampleTable;
+---------+---------+------+-----+---------+----------------+
| Field   | Type    | Null | Key | Default | Extra          |
+---------+---------+------+-----+---------+----------------+
| Id      | int(11) | NO   | PRI | NULL    | auto_increment |
| TextCol | text    | YES  |     | NULL    |                |
| IntCol  | int(11) | YES  |     | NULL    |                |
+---------+---------+------+-----+---------+----------------+

[/simterm]

Используем такой код:

#include <my_global.h>
#include <mysql.h>

#define DB_HOST "cdb-example.setevoy.org.ua"
#define DB_USER "setevoy"
#define DP_PASS "p@ssw0rd"
#define DB_NAME "testdb"
#define DB_TABLE "ExampleTable"
#define TEST "TEST"

void finish_with_error(MYSQL *con) {
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main() {

    MYSQL *con = mysql_init(NULL);

    if (con == NULL) {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
    }

    if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, 0) == NULL) {
      finish_with_error(con);
    }

    char *buffer = malloc(1024);
    sprintf(buffer, "SELECT * FROM %s LIMIT 3", DB_TABLE);

    if (mysql_query(con, buffer)) {
      finish_with_error(con);
    }

    MYSQL_RES *result = mysql_store_result(con);

    if (result == NULL) {
      finish_with_error(con);
    }  

    int num_fields = mysql_num_fields(result);

    MYSQL_ROW row;
    MYSQL_FIELD *field;

    while ((row = mysql_fetch_row(result))) { 
      for(int i = 0; i < num_fields; i++) { 
          if (i == 0) {              
             while(field = mysql_fetch_field(result)) {
                printf("%s ", field->name);
             }
             printf("\n");           
          }
          printf("%s\t", row[i] ? row[i] : "NULL"); 
      }
    }

    printf("\n");

    mysql_free_result(result);
    mysql_close(con);

    exit(0);
}

Тут в MYSQL_ROW row и MYSQL_FIELD *field – создаём две переменные для структур MYSQL_ROW и MYSQL_FIELD. MYSQL_FIELD содержит информацию о полях – имя, тип, размер, а MYSQL_ROW – данные (см. C API Data Structures).

Далее – в цикле получаем значения строк с помощью row = mysql_fetch_row(result), и затем во втором цикле – поле name из структуры MYSQL_FIELD:

...
    while ((row = mysql_fetch_row(result))) { 
      for(int i = 0; i < num_fields; i++) { 
          if (i == 0) {              
             while(field = mysql_fetch_field(result)) {
                printf("%s ", field->name);
             }
             printf("\n");           
          }
          printf("%s\t", row[i] ? row[i] : "NULL"); 
      }
    }
...

Тут в строке  printf("%s ", field->name) мы получаем значение поля name структуры filed.

Другой вариант получения данных – с помощью такой формы (см. Pointer to a Structure in C):

...
printf("%s ", (*field).name);
...

Собираем, запускаем:

[simterm]

$ ./get_headers 
Id TextCol IntCol 
1       a       1
2       b       2
3       c       3

[/simterm]

Множественные запросы

И последний пример – вызов нескольких SQL-операторов в одном запросе.

Для этого добавим флаг CLIENT_MULTI_STATEMENTS при создании соединения:

...
    if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, CLIENT_MULTI_STATEMENTS) == NULL) {
      finish_with_error(con);
    }
...

Полностью код может быть таким:

#include <my_global.h>
#include <mysql.h>

#define DB_HOST "cdb-example.setevoy.org.ua"
#define DB_USER "setevoy"
#define DP_PASS "p@ssw0rd"
#define DB_NAME "testdb"
#define DB_TABLE "ExampleTable"
#define TEST "TEST"

void finish_with_error(MYSQL *con) {
  fprintf(stderr, "%s\n", mysql_error(con));
  mysql_close(con);
  exit(1);
}

int main() {

    MYSQL *con = mysql_init(NULL);

    if (con == NULL) {
      fprintf(stderr, "mysql_init() failed\n");
      exit(1);
    }

    if (mysql_real_connect(con, DB_HOST, DB_USER, DP_PASS,
          DB_NAME, 0, NULL, CLIENT_MULTI_STATEMENTS) == NULL) {
      finish_with_error(con);
    }

    char *buffer = malloc(1024);
    sprintf(buffer, "SELECT TextCol FROM %1$s WHERE Id=1; SELECT TextCol FROM %1$s WHERE Id=2; SELECT TextCol FROM %1$s WHERE Id=3", DB_TABLE);

    if (mysql_query(con, buffer)) {
        finish_with_error(con);
    }

    int status = 0;

    do {

        MYSQL_RES *result = mysql_store_result(con);
    
        if (result == NULL) {
            finish_with_error(con);
        }
    
        MYSQL_ROW row = mysql_fetch_row(result);
    
        printf("%s\n", row[0]);
    
        mysql_free_result(result);
    
        status = mysql_next_result(con);
    
        if (status > 0) {
            finish_with_error(con);
        }
    
      } while (status == 0);

    mysql_close(con);
    exit(0);
}

В целом тут всё аналогично примерам выше, но строку запроса мы формируем немного иначе – используя одну переменную с именем таблицы, и несколько плейсхолдеров (placeholder, не знаю адекватный перевод):

...
    sprintf(buffer, "SELECT TextCol FROM %1$s WHERE Id=1; SELECT TextCol FROM %1$s WHERE Id=2; SELECT TextCol FROM %1$s WHERE Id=3", DB_TABLE);
...

В %1$s мы подставляем значение из DB_TABLE и формируем строку с тремя SELECT.

Собираем, проверяем:

[simterm]

$ ./multi 
a
b
c

[/simterm]

В целом на этом всё.

Ссылки по теме