Хабр, привет! Вообще-то я не настоящий сварщик программист, я системный администратор и администратор БД. Поэтому, когда несколько лет назад передо мной встала задача написать небольшое клиентское приложение для автоматизации рутинных процедур, я потратил немало времени, чтобы разобраться в этом вопросе. Если вдруг у вас примерно такая же ситуация, надеюсь, эта статья сэкономит ваше время и силы.

Что нужно?

Определившись с требованиями получаем следующее.

Желательно для бинарника:

  • Небольшой размер исполняемого файла.

  • Корректная обработка случаев, когда OCI-библиотека недоступна

Необходимо для бинарника:

  • Работать утилита должна на любом компьютере с ОС Windows выше Windows 7 или Windows Server 2008 без установки каких-либо фреймворков и Runtime Environment. Должен быть установлен только какой-то из продуктов Oracle, включающий OCI-библиотеку.

Очень желательно для исходников:

  • Чтобы готовый проект можно было без проблем скомпилировать на другом компьютере без установки каких-либо библиотек. Так как у нас есть похожий legacy проект, которую лет 20 назад писал давно ушедший сотрудник. Его рабочую станцию мы храним в виде образа ВМ, так как подготовить окружение на новой рабочей станции - задача крайне нетривиальная.

Какую технологию использовать?

Oracle предлагает следующие варианты:

  • Pro*C/C++ - Предкомпилятор Oracle. Инструмент программирования, который позволяет встраивать операторы SQL в хост-программу высокого уровня. Предварительный компилятор принимает основную программу в качестве входных данных, преобразует встроенные инструкции SQL в стандартные вызовы библиотеки Oracle во время выполнения и создает исходную программу, которую вы можете скомпилировать, связать и выполнить. Разобраться с этим у меня не получилось, поэтому ничего к этому описанию я добавить не могу

  • C++ Call Interface (OCCI) - API, который предоставляет приложениям C++ доступ к данным в базе данных Oracle. OCCI позволяет программистам на C++ использовать весь спектр операций с базами данных Oracle, включая обработку инструкций SQL и манипулирование объектами. Так как я С++ в реальных проектах не использовал, этот вариант мне явно не подходил

  • Oracle Call Interface (OCI) - API, который позволяет создавать приложения, использующие вызовы функций для доступа к базе данных Oracle и управления всеми этапами выполнения инструкций SQL. OCI поддерживает типы данных, соглашения о вызовах, синтаксис и семантику C и C++. Он предоставляет библиотеку стандартных функций доступа к базе данных и поиска в виде библиотеки динамической среды выполнения (библиотеки OCI), которая может быть связана в приложении во время выполнения. Также в описании указано, что все оракловые утилиты (типа sqlplus, exp, imp и прочие) написаны именно с использованием OCI. Что же еще нужно?

  • Oracle Database Programming Interface for C (ODPI-C) - это C-библиотека с открытым исходным кодом, которая упрощает использование общих функций интерфейса вызовов Oracle (OCI) для драйверов баз данных Oracle и пользовательских приложений. DPIC находится поверх OCI и требует клиентских библиотек Oracle. Проект лежит на Гитхабе. Про этот вариант я прочитал только когда писал эту статью. Но, посмотрев примеры программ, я нисколько не пожалел, что выбрал OCI. Может, это вопрос привычки и опыта, но простой эта библиотека мне не показалась.

Пишем простое приложение

Подключение библиотеки

Для того, чтобы использовать OCI функции в своей программе, нужно подключить библиотеку и получить адреса функций:

Подключение библиотеки и получений адресов функций
//подкллючаем библиотеку в Windows
hOCIDll = LoadLibraryW(L"oci.dll");
//или в Линукс
ocimodule = dlopen("libclntsh.so", RTLD_LAZY);

//Определяем тип для указателя функции
typedef sword(*pOCIEnvCreate)(OCIEnv **hOraEnvp,
	ub4           mode,
	const void    *ctxp,
	const void    *(*malocfp)
	(void  *ctxp,
		size_t size),
	const void    *(*ralocfp)
	(void  *ctxp,
		void  *memptr,
		size_t newsize),
	const void(*mfreefp)
	(void  *ctxp,
		void  *memptr),
	size_t         xtramemsz,
	void           **usrmempp);

//выделяем переменную для адреса функции
pOCIEnvCreate OCIEnvCreate;

//Получаем адрес функции на Windows
OCIEnvCreate = (pOCIEnvCreate)GetProcAddress(hOCIDll,
		"OCIEnvCreate");
// Или на Linux
OCIEnvCreate = (pOCIEnvCreate)dlsym(ocilib,"OCIEnvCreate");

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

Определения функций можно найти либо на https://docs.oracle.com в Call Interface Programmer's Guide для вашей версии библиотеки либо в заголовочном файле %ORACLE_HOME%\OCI\include\ociap.h

Инициализация окружения

Далее нам нужно инициализировать структуры OCIEnv (хендл окружения) и OCIError (хендл для обработки ошибок).

Инициализация окружения
OCIEnv *hOraEnv = NULL;
OCIError *hOraErr = NULL;

	
OCIEnvCreate((OCIEnv **)&hOraEnv,
		(ub4)OCI_DEFAULT | OCI_OBJECT,
		(const void  *)0,
		(const void  * (*)(void  *, size_t))0,
		(const void  * (*)(void  *, void  *, size_t))0,
		(const void(*)(void  *, void  *))0,
		(size_t)0, (void  **)0));

	
OCIHandleAlloc((const void *)hOraEnv,
		(void **)&hOraErr,
		OCI_HTYPE_ERROR,
		(size_t)0,
		(void **)0));

Создание сессии

Для каждой сессии нам нужно инициализировать хендл OCISvcCtx, именно он используется для выполнения sql-выражений. Я, руководствуясь демонстрационными примерами от Oracle, создавал сессии так:

Установка сессии
OCIServer *hOraServer = NULL;
OCISvcCtx *hOraSvcCtxOCI = NULL;
OCISession *hOraSession = NULL;


//Аллоцируем хендл OCIServer
OCIHandleAlloc((const void *)hOraEnv, (void **)&hOraServer,
        OCI_HTYPE_SERVER, (size_t)0, (dvoid **)0);

char *dbconnectstring = "servername:1521/ORCL";
//Подключаемся к серверу
OCIServerAttach(hOraServer, hOraErr, (const OraText *)dbconnectstring, 
                (sb4)strlen(dbconnectstring), (ub4)OCI_DEFAULT);

//Аллоцируем хендл сервисного контекста
OCIHandleAlloc((const void *)hOraEnv, (void **)&hOraSvcCtx,
        OCI_HTYPE_SVCCTX, (size_t)0, (dvoid **)0);

//Помещаем хендл сервера в сервисного контекста
OCIAttrSet((void *)hOraSvcCtx, OCI_HTYPE_SVCCTX, (void *)hOraServer, 
           (ub4)0, OCI_ATTR_SERVER, (OCIError *)hOraErr));
    
    
//Аллоцируем хендл для сессии
OCIHandleAlloc((const void *)hOraEnv, (void **)&hOraSession, 
               (ub4)OCI_HTYPE_SESSION, (size_t)0, (void **)0);

char * username = "SCOTT";    
//Помещаем имя пользователя в хендл сессии
OCIAttrSet((void *)hOraSession, (ub4)OCI_HTYPE_SESSION, 
           (void *)username, (ub4)strlen(username), 
           (ub4)OCI_ATTR_USERNAME, hOraErr);

char *password = "tiger";
//Помещаем пароль в хендл сессии    
OCIAttrSet((void *)hOraSession, (ub4)OCI_HTYPE_SESSION, 
           (void *)password, (ub4)strlen(password), 
           (ub4)OCI_ATTR_PASSWORD, hOraErr);

//флаг для указания, является ли пользователь sysdba
bool assysdba = 1;
//Начинаем сессию
OCISessionBegin(hOraSvcCtx, hOraErr, 
                hOraSession, OCI_CRED_RDBMS, 
                (ub4)(OCI_DEFAULT | (assysdba ? OCI_SYSDBA : 0)));
    
//Помещаем сессию в сервисный контекст
OCIAttrSet((void *)hOraSvcCtx, (ub4)OCI_HTYPE_SVCCTX, 
           (void *)hOraSession, (ub4)0, (ub4)OCI_ATTR_SESSION, 
           hOraErr);

Уже в процессе написания статьи, я увидел, что для этих же целей можно использовать функцию OCILogon2. Судя по всему, это намного проще и удобнее. А главное, нужно меньше писать кода. Но мы уже оставим все, как есть.

Вставляем данные

Пример функции для загрузки данных в базу:

Выполняем Insert
char * insert_statement = "INSERT INTO simple_table\
	(id, textfield)\
	VALUES\
	(:id, :string)";

	sword status;

	int id;
	char stringBuffer[100];

OCIStmt *hOraPlsqlStatement = NULL;
//Аллоцируем хендл для sql-выражения
OCIHandleAlloc((const void *)hOraEnv, 
               (void **)&hOraPlsqlStatement, OCI_HTYPE_STMT, 
               (size_t)0, (void **)0);

//Подготавливаем его
OCIStmtPrepare(hOraPlsqlStatement, hOraErr, 
               (const OraText *)insert_statement, 
               (ub4)strlen(insert_statement), 
               (ub4)OCI_NTV_SYNTAX, (ub4)OCI_DEFAULT);

//Биндим наши переменные id и stringBuffer в sql-выражение
OCIBind  *bnd1p = NULL;
OCIBind  *bnd2p = NULL;
OCIBindByName(hOraPlsqlStatement, &bnd1p, 
              hOraErr, (text *)":id", -1, (void *)&id, 
              (sb4)sizeof(id), SQLT_INT, (void *)0, 
              (ub2 *)0, (ub2 *)0, (ub4)0, (ub4 *)0, OCI_DEFAULT);
OCIBindByName(hOraPlsqlStatement, &bnd2p, 
              hOraErr, (text *)":string", -1, 
              (void *)stringBuffer, (sb4)(sizeof(stringBuffer)), 
              SQLT_STR, (void *)0, (ub2 *)0, (ub2 *)0, 
              (ub4)0, (ub4 *)0, OCI_DEFAULT);
	
//вставляем данные в цикле
	for (id = 1; id < 10; id++)
	{
		sprintf(stringBuffer, "This is the %d string", id);
		
		status = OCIStmtExecute(hOraSvcCtx, hOraPlsqlStatement, hOraErr, (ub4)1, (ub4)0, (CONST OCISnapshot *) NULL, (OCISnapshot *)NULL, OCI_DEFAULT);
		if (status != OCI_SUCCESS && status != OCI_SUCCESS_WITH_INFO)
		{
			checkerr(hOraErr, status);
			OCIHandleFree(hOraPlsqlStatement, OCI_HTYPE_STMT);
			return FALSE;
		}
	}

//освобождаем хендл
OCIHandleFree(hOraPlsqlStatement, OCI_HTYPE_STMT);

В целях увеличения быстродействия и экономии ресурсов базы в случае множественных инсертов нужно вначале подготовить выражение с заполнителями типа :id и :string, связать их с соответствующими переменными, и затем изменять значение переменных и выполнять выражение.

Получаем данные

Код для получения данных из базы:

Получаем данные
char * select_statement = "select id, textfield from simple_table order by id";

sword status;

int id;
char stringBuffer[100];

OCIStmt *hOraPlsqlStatement = NULL;
OCIHandleAlloc((const void *)hOraEnv, 
               (void **)&hOraPlsqlStatement, OCI_HTYPE_STMT, 
               (size_t)0, (void **)0);


OCIStmtPrepare(hOraPlsqlStatement, hOraErr, 
               (const OraText *)select_statement, 
               (ub4)strlen(select_statement), 
               (ub4)OCI_NTV_SYNTAX, (ub4)OCI_DEFAULT);


OCIDefine *OraIdDefine = NULL;
//Привязываем переменную id к первому полю в запросе
OCIDefineByPos(hOraPlsqlStatement, &OraIdDefine, 
               hOraErr, 1, (void *)&id, (sword)sizeof(id), 
               SQLT_INT, (void *)0, (ub2 *)0, (ub2 *)0, 
               OCI_DEFAULT);


OCIDefine *OraStringDefine = NULL;
//Привязываем stringBuffer ко второму полю в запросе
OCIDefineByPos(hOraPlsqlStatement, &OraStringDefine, 
               hOraErr, 2, (void *)stringBuffer, 
               (sword)sizeof(stringBuffer), 
               SQLT_STR, (void *)0, (ub2 *)0, 
               (ub2 *)0, OCI_DEFAULT);

//Выполняем запрос, не получая никаких данных
status = OCIStmtExecute(hOraSvcCtx, hOraPlsqlStatement, hOraErr, (ub4)0, (ub4)0, (CONST OCISnapshot *) NULL, (OCISnapshot *)NULL, OCI_DEFAULT);
if (status != OCI_SUCCESS && status != OCI_SUCCESS_WITH_INFO)
{
		checkerr(hOraErr, status);
		OCIHandleFree(hOraPlsqlStatement, OCI_HTYPE_STMT);
		return FALSE;
}

printf("id | textfield\n");
//Получаем данные в цикле
while ((status = OCIStmtFetch2(hOraPlsqlStatement, hOraErr, 1, OCI_DEFAULT, 0, OCI_DEFAULT)) == OCI_SUCCESS || status == OCI_SUCCESS_WITH_INFO)
	{
		printf("%d | %s\n", id, stringBuffer);
	}

OCIHandleFree(hOraPlsqlStatement, OCI_HTYPE_STMT);
	

При получении данных поступаем аналогично, привязываем переменные с запрошенными полями в селекте, выполняем выражение и затем в цикле одну за другой получаем строки

Закрываем сессию

После нужных операции нужно закрыть сессию:

Закрываем сессию
OCISessionEnd(hOraSvcCtx, hOraErr, hOraSession, OCI_DEFAULT);

OCIHandleFree(hOraSession, OCI_HTYPE_SESSION);

OCIHandleFree(hOraSvcCtx, OCI_HTYPE_SVCCTX);

OCIServerDetach(hOraServer, hOraErr, (ub4)OCI_DEFAULT);

OCIHandleFree(hOraServer, OCI_HTYPE_SERVER);

Закрываем окружение

После того, как взаимодействие с базой больше не нужно, закрываем окружение:

Закрываем окружение
OCIHandleFree(hOraErr, OCI_HTYPE_ERROR);

OCIHandleFree(hOraEnv, OCI_HTYPE_ENV);

OCITerminate(OCI_DEFAULT);

Итоги работы

Вот ссылки на проекты на гитхабе для Windows(Visual Studio) и Linux(NetBeans)

О файлах в проекте

Заголовочные файлы oci.h, ocidem.h, ocidfn.h, ocikpr.h, oratypes.h, orl.h взяты из каталога %ORACLE_HOME%\OCI\include (Может, я их немного модифицировал, чтобы не было неразрешенных зависимостей, но этого я уже точно не помню)
ocipfndfn.h - этот файл с определениями типов указателей на функции составлен мной, тут те OCI функции, которые я использовал в своих проектах.
OraFunction.c - Основной файл проекта, тут определения функций для работы с БД.
OraFunction.h - заголовочный файл с объявлениями функции.
ParseCmdLine.c - функция для парсинга командной строки, для получения логина, пароля и строки подключения к базе. Параметры передаются в утилиту стандартным для Оракловых утилит форматом login/pass@dbconnect.
main.c - функция main.

Что утилита делает

Утилита подключается к базе данных, создает таблицу simple_table, заполняет ее данными, получает из нее данные и удаляет таблицу.

Как скомпилировать

Как и было запланировано с самого начала, для компиляции утилиты не требуется никаких дополнительных файлов и библиотек. Для работы, само собой, требуется установленный Oracle Client либо Instant Oracle Cllient, главное, чтобы была доступна OCI-библиотека.

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


  1. VGoudkov
    05.11.2021 15:03
    +3

    По заголовку в принципе было понятно, что будут страдания, но что настолько - я, честно говоря, не ожидал. Ладно бы статься была из 1995, тогда это вот всё, да. Но в 2021? Как research проект - может быть. Как решение реальной задачи - КМК overhead 146% по времени разработки и поддержки. Тем более, что в целевых системах - только свежая винда, ограничений по аппаратным ресурсам явно нет. Я не специалист в .Net, но вроде там какая-та версия библиотек прямо вместе с ОС ставится, т.е. собираете небольшой .exe на C# и ага, задача решена. Вариантов выстрелить в ногу в разы меньше будет. Можно вообще сразу посмотреть в сторону GraalVM / Java, там нативный образ из коробки будет и не только под винду, и даже OCI не потребуется - в JDBC уже есть всё, что нужно.


    1. yuryleb
      05.11.2021 15:50
      -1

      Не совсем, все-таки потребуется установка провайдера Oracle для dotNet или Java, есть даже "тонкие" провайдеры без зависимостей от клиента Oracle, но там к базе надо будет соединяется с полным текстом дескриптора (см. содержимое файла sqlnet.ora). Но в остальном да, с C# всё намного проще и быстрее.


      1. VGoudkov
        05.11.2021 16:00
        +1

        Для Java не потребуется. Потребуется ojdbc-что-нибудь.jar для сборки, но он потом будет в образе лежать, который fat jar. Полный текст дескриптора нужен как рыбе зонтик, достаточно ip или доменного имени сервера, порта и SID. Если немного заморочиться, эти данные можно в коде программы добыть прямо из sqlnet.ora на целевой машине, благо известно, где его искать. Там много чего становится сразу можно :)


    1. johnny_the_cat Автор
      07.11.2021 03:48

      Ряд требований я специально опустил, чтобы не загромождать статью лишней информацией, но ситуация была такова:
      1) Утилитой должна быть удобна для использования не админами и не разработчиками - то есть у нее должен быть графический интерфейс.
      2) Оракл обычно ассоциируется с кровавым энтерпрайзом - безумные бюджеты, терабайты оперативной памяти и прочее. У нас ситуация в этом смысле нестандартная: клиенты - бюджетные организации, где все еще встречаются сервера с 4 ГБ оперативки, на котором расположен и сервер БД и сервер приложений. Скромность в потреблении ресурсов имела большое значение.
      3) Есть в нашей стране уголки, где проблемы с интернетом. Либо очень маленькая скорость, либо нестабильный линк, либо и то и другое. Размер исполняемого файла был немаловажным требованием.
      4) В У очень многих наших клиентов нет штатных админов и получить доступ на сервер для работ - тот еще квест. (скриншот ТимВьювера в вордовском документе - наше все)
      Отсюда и вытекают ограничения, которые были поставлены еще на этапе проектирования.
      - .Net не желателен, так как требует перезагрузки. Ну и скачать его тоже не везде будет просто.
      - Java - очень хороший вариант, так как у нас есть штатные разработчики и мне вообще не пришлось бы писать это)) Но минусы тут - у некоторых клиентов скачка 100 МБ для JRE - это час-другой. Плюс 100-200 МБ оперативки, которую утилитка сразу себе потребует могут быть непозволительной роскошью))

      Поэтому и пришлось писать именно так. Пара лет использования показали, что усилия были не напрасны


  1. bormotov
    05.11.2021 15:55

    Работать утилита должна на любом компьютере с ОС Windows выше Windows 7 или Windows Server 2008 без установки каких-либо фреймворков и Runtime Environment. Должен быть установлен только какой-то из продуктов Oracle, включающий OCI-библиотеку.

    Кажется, сейчас разобраться с каким-либо популярным в текущий момент "упаковщиком всего нужного в один бинарник" для "вашего любимого инструмента разработки" проще и выгоднее (по затрате усилий), чем писать клиентские приложения для OracleDB на C/C++
    И не могу промолчать уходя немного в оффтопик: довелось длительное время (лет 10, наверное) внедрять и сопровождать продукт, у которого в качестве базы данных Oracle DB, и на фоне вот того опыта, слова "должен быть какой-то из продуктов Oracle, включающий OCI", у меня вызывают нервный смех. Прекратил всего этого касаться я когда еще не перешли целиком на 12c, а только присматривались. У версий 10 и 11, с вот этим все очень-очень-очень всё непросто, и может быть множество разных неожиданных фокусов даже если у сервера базы и клиента разные patch level. Другими словами, я не верю, что в реальной жизни можно полностью исключить какую-либо установку чего-либо из продуктов Oracle на компьютер с клиентским приложением. В тоже время, могу легко представить, что есть четки процедуры развертывания/обновления клиентских мест, но тогда известен набор "что должно быть точно", и опять-же, гораздо эффективнее, расширить этот набор, добавив требуемые библиотеки, а утилиты разрабатывать используя удобные инструменты.


    1. johnny_the_cat Автор
      07.11.2021 03:53

      Я выше ответил на вопрос, зачем были эти страдания)

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

      Плюс всегда можно поставлять эту утилиту с нужной версией Oracle Instant Client - тогда никаких установок не нужно


  1. sshikov
    05.11.2021 16:11

    >другими словами, я не верю, что в реальной жизни можно полностью исключить какую-либо установку чего-либо из продуктов Oracle на компьютер с клиентским приложением.
    Ну JDBC например не требует установки. Вам возможно потребуется Java, чтобы приложение запустить, но она тоже не требует установки, и можно быть принесена с собой. В теории потребуется обновлять иногда драйвер, так как скажем драйвер от 12 версии не будет уметь что-то делать из того, что появилось в 19-й — но это уже зависит от функций приложения, от того, какую версию СУБД оно ожидает.


  1. Mikihiso
    05.11.2021 17:22

    Самое неприятное с работой с ораклом из сишного кода с Pro C насколько я помню, что нужно явно перечислять все поля и иметь сишную структурку данных, которая эти поля отражает, что доставляет чутка дискомфорта если например нужно сделать select * ... из таблички с парой сотен полей :)


    1. johnny_the_cat Автор
      07.11.2021 03:56

      Звучит круто))


  1. gochaorg
    05.11.2021 20:36
    +1

    Спасибо за статью, будет полезна.

    Хотелось бы услышать по подробнее по саму задачу. Возможно это можно решить все проще, например так:

    Ставь oracle клиент вообще не обязательно, можно просто скачать oracle instant client - он занимает около 30 мб и не требует и установки, в комплекте и oci, и sqlplus.exe. а при желание наверно можно уменьшить и до 10 мб.

    С другой стороны имея sqlplus.exe, то C++ особо не нужен, достаточно будет написать скрипт на pl/sql который будет исполняться на клиенте (посредством sqlplus) и работать с oracle db.

    Если нужны специфичные от ос функции, у вас всегда есть под рукой powershell, либо wsh+wmi

    Если подумать то все это должно быть не больше 20..30 мб и все это замечательно можно упаковать zip/rar/7z sfx


    1. johnny_the_cat Автор
      07.11.2021 04:05
      +1

      Ставь oracle клиент вообще не обязательно, можно просто скачать oracle instant client

      Да, использование InstantClient - удобный вариант. Просто в нашем случае на компьютерах, на которых предполагается использование утилиты уже стоит либо СУБД, либо установленный клиент.

      С другой стороны имея sqlplus.exe, то C++ особо не нужен, достаточно будет написать скрипт на pl/sql который будет исполняться на клиенте (посредством sqlplus) и работать с oracle db.

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

      Если нужны специфичные от ос функции, у вас всегда есть под рукой powershell, либо wsh+wmi

      Так же было важно не использовать никакие другие ресурсы, кроме взаимодействие в базой через подключение, чтобы не было разницы, работаешь ты на сервере или на удаленной машине. То есть никаких ОС-зависимых вещей в ней нет.

      Я здесь описал некоторую специфику. Несколько лет использования показали, что в нашем случае этот вариант был оправдан. Но рекомендовать именно его я, конечно, не стану