Давно не писал на С (последний раз – почти год тому, см. 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]
Получение данных из базы
Теперь, когда данные в базе есть – попробуем их оттуда получить.
Для этого мы выполним:
- создание подключения
- выполнение запроса
- получение результата
Код может выглядить так:
#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]
В целом на этом всё.