Давно не писал на С (последний раз — почти год тому, см. 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() == NULLmysql_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]
В целом на этом всё.