Sources
Delphi Russian Knowledge Base
DRKB - база знаний по Дельфи в рунете, составленная Виталием Невзоровым

Использованию в Delphi прямых обращений к ядру Btrieve

01.01.2007
vic@regentgroup.ru

Введение.

Данный документ - краткая инструкция по использованию в 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).

Для работы с конкретной таблицей необходимо: Чтобы на эту таблицу было наложено заклинание эксклюзивного доступа... (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, нужно:

  1. Описать структуру индексов. Например, в уже упомянутой таблице 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.

  2. Далее - код:

    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. Может, данный документ кому-то сэкономит время. Хотя лучше всего, если эта информация вам не понадобится вовсе!

Previous page:
ПРИЛОЖЕНИЕ G: РАСШИРЕННЫЕ ТИПЫ КЛЮЧЕЙ
Top:
DRKB
Next page:
ObjectHaven