Введение. Данный документ - краткая инструкция по
использованию в DELPHI прямых обращений к ядру CУБД Btrieve, написанная для
людей с SQL-мышлением, которым судьба-индейка подсунула свинью в виде
производственной необходимости временно (или, не дай Создатель, постоянно)
использовать это чудо Pervasive-вской мысли. Мне самому пришлось долго и больно
перестраиваться с незамутненных SQL-понятий работы с данными на суровую логику
программирования более низкого уровня. Данная шпаргалка призвана облегчить этот
процесс, и содержит параллели между основными SQL-командами
(select/insert/update/delete) и вызовами функции BTRVID, описанной по принципу
"черного ящика". Сразу скажу, что не являюсь специалистом по Btrievу
(т.е. могу путаться в Btrievовских терминах), большая часть информации получена
эмпирическим путем, все примеры использовались в жизни, отлажены и работают.
Отдельно замечу следующее. Вообще говоря, в природе
существует Pervasive ODBC, его заменитель Titan Btrieve, и, возможно, кое-что
еще, что позволяет обращаться к Btrieve-данным через SQL-запросы. Однако
во-первых, это дело работает на порядок медленнее, а во-вторых, в определенных
ситуациях (например в моей - мне пришлось организовывать перекачку данных из
Informix SQL-сервера в программу "Парус", использующую Btrieve. С
Informixом, сами понимаете, никаких проблем не было) из-за специфического
формата данных, определяемого конкретным программным продуктом, использование
ODBC порой оказывается в принципе невозможным. Например, его (ODBC) приводят в
смятение нулевые байты в текстовых полях. И последнее. По правильному, для
работы с Btrievом нужно устанавливать Btrievовский Engine, пытаться добиться от
него стабильной работы, поганить реестр и все такое. (Может, конечно, это со
мной что-то не так, но мне пришлось через это пройти. Хотя на моей работе
тратить дни на то, чтобы заставить продукт проявить заявленные возможности
считается непозволительной роскошью). Вообще говоря, можно ограничиться набором
DLL-библиотек из Pervasive.SQL 2000 Workgroup Engine, которые достаточно
переписать в директорию с приложением, или в директорию, прописанную в путях,
чтобы можно было работать с Btrievом. Ну - удачи!
Необходимое окружение. Прежде всего в директорию,
содержащую исходники Delphi-приложения, нужно поместить вышеупомянутые DLL и
файлы Btrapi32.pas и BtrConst.pas из Pervasive SDK 2000 и в uses прописать
BtrConst и BtrApi32. Если кому интересно назначение этих файлов, может
посмотреть сам - там все достаточно прозрачно. Затем в type нужно прописать
следующие два типа: CLIENT_ID = packed record networkandnode :
array[1..12] of char; applicationID : array[1..3] of
char; threadID : smallint; end;
VERSION_STRUCT = packed
record version : smallint; revision : smallint; MKDEId :
char; end;
а в var следующие переменные:
client :
CLIENT_ID; {вручную не переинициализировать!} versionBuffer : array[1..3] of
VERSION_STRUCT; {вручную не переинициализировать!} status : smallint;
{вручную не переинициализировать!} posBlock : string[128]; {вручную не
переинициализировать!} dataBuffer : array[0..255] of char; keyBuf :
string[255]; keyNum : smallint; dataLen : word;
Физический смысл большинства из этих переменных мне ясен
смутно, но он и не важен. Будем считать их частью черного ящика. Переменные,
помеченные комментарием {ВРУЧНУЮ НЕ ПЕРЕИНИЦИАЛИЗИРОВАТЬ!} являются системными,
Btrieve сам их как-то заполняет и потом с ними работает. Остальные же несут
разную смысловую нагрузку в зависимости от действия.
Подключение к Btrieve-БД (SQL: Database & Connect в
одном флаконе). Чтобы производить любые операции с данными Btrieve, нужно в
первую очередь подконнектиться к базе. Чтобы подключиться, нужно выполнить
следующий фрагмент кода: fillchar(keyBuf, sizeof(keyBuf), #0); keybuf
:= '<Полный путь к любому из *.btr файлов в директории, где лежит БД>' +
#0; fillchar(client.networkAndNode, sizeof(client.networkAndNode),
#0); client.applicationID := 'MT' + #0; {так надо} сlient.threadID := 50;
{так надо} fillchar(versionBuffer, sizeof(versionBuffer), #0); dataLen :=
sizeof(versionBuffer); status := BTRVID(B_VERSION, {системная
константа} posBlock, {системная} versionBuffer,
{системная} dataLen, {см.выше} keyBuf[1],
{см.выше} 0, client); {системная}
if status = B_NO_ERROR значит подконнектилось успешно, иначе -
по каким-то причинам не получилось. Причины (из известных) могут быть следующие
- приложение не нашло необходимые DLL-ки, неправильно указан путь в keybuf,
кривой btr-файл. Подробно ошибки описаны в Pervasive SDK 2000 хэлпах. Отключение от Btrieve-БД (SQL:
Disconnect & Close database). dataLenHead := 0; status := BTRVID(
B_STOP, {системная константа} posBlock, {системная} DataBuffer,
{см.выше} dataLen, {см.выше} keyBuf[1], {см.выше - предыдущий
раздел} 0, {} client ); {системная}
Желательно в конце
работы это делать. Во избежание.
Открытие таблицы(SQL: Аналог отсутствует). Для
выполнения любой операции с таблицей (select/insert/update/delete) необходимо
сначала ее открыть. Следующий фрагмент открывает одну отдельно взятую таблицу:
fillchar(keyBuf, sizeof(keyBuf), #0); keybuf := '<Полный путь к
*.btr файлу, где лежит таблица>' + #0; fillchar(dataBuffer,
sizeof(dataBuffer), #0); dataLen := 0; status := BTRVID(B_OPEN, {системная
константа} PosBlock, {системная} dataBuffer,
{см.выше} dataLen, {см.выше} keyBuf[1],
{см.выше} 0, client) {системная}
Если status != B_NO_ERROR, значит, что-то не сложилось. Или файл кривой, или
путь неправильный, или с ним уже кто-то работает, или таблица уже открыта, или
см. ошибки. Вообще говоря, можно открывать сразу несколько таблиц, и
одновременно с ними работать. Но в этом случае для корректной работы нужно для
каждой таблицы завести свой набор переменных
posBlock, dataBuffer, keyBuf, keyNum, dataLen
Наверное,
можно даже применить массивы.
Закрытие таблицы(SQL: Аналог отсутствует). Все то же
самое, как в открытии таблицы. Единственное отличие - B_CLOSE вместо B_OPEN. Для
вящей корректности после окончания работ с таблицей нужно ее закрыть.
Добавление записи(SQL: INSERT). Для работы с конкретной
таблицей необходимо: 1) Чтобы на эту таблицу было наложено заклинание
эксклюзивного доступа... (Sorry, пиво...) Продолжаю на трезвую голову. Так вот:
для работы с таблицей на самом деле необходимо следующее- 1) Должна
быть описана структура таблицы. Например (таблица хозяйственных операций
"Паруса"):
type OPERHEAD_STRUCT = packed record ISN :
integer; opDate : array[0..8] of char; agnFrom : array[0..15] of
char; agnTo : array[0..15] of char; docType : array[0..5] of
char; docNumb : array[0..12] of char; docDate : array[0..8] of
char; basType : array[0..5] of char; basNumb : array[0..12] of
char; basDate : array[0..8] of char; {см. ссылку в тексте} resSum :
double; spMark : array[0..5] of char; opCont : array[0..80] of
char; invSign : smallint; subAgn : array[0..15] of char; appName :
array[0..8] of char; appIsn : longint; resMSum : double; {см. ссылку в
тексте} sysNumb : array[0..3] of char; regNumb : longint; opMngCnt :
array[0..80] of char; end;
var OperheadRecord :
OPERHEAD_STRUCT;
Важно! Для корректной работы должен быть соблюден размер полей
в байтах. Через Database Explorer можно посмотреть структуру Btrieve-таблицы, с
которой нужно работать. Там мы увидим, что поле resMSum, например, типа FLOAT и
размером 8 байт. Этим размером и свойствами в Delphi обладает тип double. Поля
типа CHAR в Btrieve имеют две характеристики - логическую длину и физическую (на
байт больше). Это также видно в DBE. Поле basDate, например, логически длиной 9
байт, а физически - 10. Нужно исходить из логической длины, и поля типа CHAR
описывать как начинающиеся с нуля массивы of char. Поле basDate в этом случае
представляется как массив 0..8, т.е. 9 байт.
2) Данная запись должна быть проинициализирована. Вообще
Btrieve все аналоги SQL-операций может выполнять только с записью целиком.
Поэтому перед каждым "INSERT"ом или "UPDATE"ом нужно, чтобы
все поля записи были заполнены нужным образом. Специфика заполнения
такова: а) Текстовые поля: StrPCopy(OperheadRecord.agnto, AsString со
значением + #0); { собственно присвоение значения
} CharToOem(OperheadRecord.agnto,OperheadRecord.agnto); { конвертация
- если необходимо. CharToOem всего лишь одна из набора } {
конвертационных функций из MustDie SDK }
б) Числовые
поля: OperheadRecord.resmsum := AsFloat со значением;
Здесь
главное - совместимость типов. Достаточно легко определяется.
в)
Автоинкременты: fillchar(OperheadRecord.isn , SizeOf(OperheadRecord.isn),
#0); Независимо от типа. Просто заполняем их нулевыми байтами, Btrieve сам
присвоит нужное значене.
3) Собственно INSERT : dataLen :=
sizeof(OPERHEAD_STRUCT); status := BTRVID(B_INSERT, {системная
константа} PosBlock, {системная} OperheadRecord, {собственно
запись - см.выше} dataLen, {см.выше} keyBuf, {путь к таблице -
см.выше} -1, client) ; {системная}
Статус, понятно,
должен оказаться равен B_NO_ERROR.
Позиционирование/Поиск(SQL: SELECT & FETCH в одном
флаконе). Я для работы обошелся всего тремя опциями - B_GET_EQUAL,
B_GET_FIRST, B_GET_NEXT. Вообще их там больше. Но поскольку даже в совокупности
они все равно не заменят всю прелесть SQL, лучше потратить силы не на изучение
всяких нюансов сомнительной ценности, а на то, чтобы избежать использования
Btrieve в принципе :-). Для начала - общая информация. B_GET_EQUAL предназначен
для выборки одной записи из таблицы. Если результатом запроса будет выборка из
более чем одной записи, он вернет первую и на этом успокоится. B_GET_FIRST
инициализирует выборку из более чем одной записи, и возвращает первую из выборки
(скажу сразу - сортировка мне была не нужна, насчет нее ничего не знаю.). Если
дальше мы будем выполнять B_GET_NEXT, то за каждое выполнение будем получать по
одной следующей записи из выборки. Причем если упрется в конец файла, то выдаст
status с кодом 9. В противном случае похоже, что просто пойдет дальше по файлу,
несмотря на то, что начиная с какой-то записи все последующие уже не будут
отвечать условиям выборки. Скажу сразу, что здесь я не уверен до конца в своих
знаниях, могу ошибаться. Короче, у меня в программе в WHILE с GET_NEXTами стоит
проверка сразу двух условий - чтобы status был B_NO_ERROR и чтобы полученные
значения условиям выборки отвечали. Хотя бы одно не выполнилось - из WHILE
выхожу. Пока работает. И последнее. Все эти GETы могут искать только по полям,
описанным в индексах таблицы. Свое мнение об этом деликатно опускаю. Теперь -
собственно описание синтаксиса. Для того, чтобы выполнить B_GET_EQUAL, нужно:
а) Описать структуру индексов. Например, в уже упомянутой таблице OPERHEAD
есть следующие индексы:
type OPERHEAD_INDEX7 = packed
record appIsn : longint; appName : array[0..8] of
char; end; OPERHEAD_INDEX0 = packed record ISN :
integer; end;
var OperHeadIndex0 : OPERHEAD_INDEX0; {индекс номер
0} OperHeadIndex7 : OPERHEAD_INDEX7; {индекс номер 7}
Информацию
об индексаx можно посмотреть в том же DBE.
б) Далее -
код:
OperheadIndex0.isn := значение AsInteger; {присвоение параметров
поиска} fillchar(OperheadRecord , SizeOf(OperheadRecord), #0); dataLen :=
sizeof(OPERHEAD_STRUCT); status := BTRVID(B_GET_EQUAL, {системная
константа} PosBlock, {системная} OperheadRecord, {сюда будет
возвращен результат поиска} dataLen, {см.выше} OperheadIndex0,
{см.выше} 0, {номер индеска - см.выше} client);
{системная}
Если status <> B_NO_ERROR and <>
B_KEY_VALUE_NOT_FOUND - значит, чего-то нашлось.
Использование других GETов осуществляется по этой же схеме.
Единственная разница - вместо B_GET_EQUAL нужно поставить B_GET_FIRST или
B_GET_NEXT. Важно! B_GET_EQUAL может использовать только уникальные индексы!
B_GET_FIRST и B_GET_NEXT - любые.
Обновление записи(SQL: UPDATE). Самое главное - здесь
рассмотрен вариант обновления одной записи. У меня сложилось впечатление, что
вообще в Btrieve можно обновлять и удалять только по одной записи, но поскольку
у меня не хватило терпения изучить HELP до конца, наверняка утверждать не могу.
Итак: Непосредственно перед действием обновления у нас должен быть выполнен
B_GET_EQUAL, который спозиционирует указатель в таблице именно на ту запись,
которую мы будем апдейтить. ( У меня есть подозрение, что информация о
позиционировании хранится в PosBlockе. Впрочем, какая разница?) Далее мы должны
полностью заполнить запись таблицы (в нашем случае - OperheadRecord). Вообще
говоря, поскольку только что выполнился B_GET_EQUAL, OperheadRecord у нас уже
заполнен. Мы можем просто переприсвоить те поля, какие хотим изменить. Специфику
заполнения см. в разделе, посвященном INSERTу. Далее - код: dataLen :=
SizeOf(OPERHEAD_STRUCT); status := BTRVID(B_UPDATE, {системная
константа} posBlock, {системная} OperheadRecord,
{см.выше} dataLen, {см.выше} keyBuf, {путь к таблице -
см.выше} -1, client) {системная}
Status, сами
понимаете, должен оказаться B_NO_ERROR. Если все в порядке.
Удаление записи(SQL: DELETE). Использование практически
совпадает с B_UPDATE. В смысле, сначала должен быть B_GET_EQUAL и все такое.
Фрагмент кода немного отличается - B_DELETE вместо B_UPDATE, и DataBuffer вместо
OperheadRecord - какой смысл передавать данные, если запись сейчас будет
удалена? fillchar(dataBuffer, sizeof(dataBuffer), #0); dataLen :=
SizeOf(OPERHEAD_STRUCT); Status := BTRVID(B_DELETE, {системная
константа} PosBlock, {системная} DataBuffer,
{см.выше} dataLen, {см.выше} KeyBuf, {путь к таблице -
см.выше} -1, client) {системная}
Про status уже и не
говорю.
Заключение. Вот, собственно и все, что мне известно. В
принципе, в разрозненном виде это все есть и в Pervasive SDK 2000 Help, и в
Инете, но мне, например, понадобилось две недели, чтобы вычленить именно эту
информацию, абстрагироваться от ненужной, и c прискорбием убедиться в
несостоятельности обычного Pervasive ODBC. Может, данный документ кому-то
сэкономит время. Хотя лучше всего, если эта информация вам не понадобится вовсе!
Литература по Btrieve
|