Пособие по Btrieve для
SQL-программистов.
Введение. Данный документ - краткая инструкция по
использованию в 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. Отключение
от 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. Может,
данный документ кому-то сэкономит время. Хотя лучше всего, если эта
информация вам не понадобится вовсе! vic@regentgroup.ru
|