Что нужно?
Определившись с требованиями получаем следующее.Желательно для бинарника:
- Небольшой размер исполняемого файла.
- Корректная обработка случаев, когда 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-библиотека.
Oracle Call Interface: как написать клиентское приложение на Си
Хабр, привет! Вообще-то я не настоящий сварщик программист, я системный администратор и администратор БД. Поэтому, когда несколько лет назад передо мной встала задача написать небольшое клиентское...
habr.com