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

Kate

Administrator
Команда форума

Что нужно?​

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

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

  • Небольшой размер исполняемого файла.
  • Корректная обработка случаев, когда 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-библиотека.

 
Сверху