Pull to refresh

Oracle Call Interface: как написать клиентское приложение на Си

Reading time8 min
Views4.8K

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

Что нужно?

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

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

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

  • Корректная обработка случаев, когда 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-библиотека.

Tags:
Hubs:
Total votes 3: ↑2 and ↓1+1
Comments11

Articles