Работа с внешними файлами DBF через встроенный объект XBase в платформе 1С:Предприятие 7.7 — классическая задача, с которой часто сталкиваются разработчики при интеграции систем. Однако этот механизм таит в себе неочевидные нюансы. Один из наиболее распространенных сбоев — внезапное появление пустой строки в файле DBF, после чего запись любых последующих данных полностью прекращается, хотя в отладчике все свойства объекта выглядят заполненными.
В этой статье мы подробно разберем, почему возникает пустая запись, как работают индексы CDX в связке с таблицами DBF, почему нельзя использовать метод Сжать() внутри транзакций или циклов, и как правильно организовать отказоустойчивую запись данных.
Давайте проанализируем, что происходит на уровне СУБД (движка Codebase), когда мы выполняем стандартный цикл добавления записи. Типичный проблемный код выглядит следующим образом:
Xb = СоздатьОбъект("XBase");
Xb.ОткрытьФайл(Путь + "File.dbf", Путь + "File.cdx", 0);
NAME = Данные.NAME;
ID = Данные.ID;
Xb.ТекущийИндекс("IDXID");
Xb.Ключ.ID = ID;
Если ЗначениеЗаполнено(Данные.NOM) = 1 Тогда
Если Xb.НайтиПоКлючу(0) = 0 Тогда
Xb.Добавить();
Xb.ID = ID;
Xb.NAME = NAME;
КонецЕсли;
Xb.IDPOSTNOM = Строка(Данные.NOM);
Иначе
Если Xb.НайтиПоКлючу(0) = 1 Тогда
Xb.Удалить();
КонецЕсли;
Xb.Сжать();
КонецЕсли;
Xb.Записать();
Xb.ЗакрытьФайл();
Разберем по шагам внутреннюю логику работы этого алгоритма:
Добавить() физически выделяет под запись новое место (строку) в файле DBF. На этом этапе все поля новой записи инициализируются значениями по умолчанию: пустые строки, нули и пустые даты.Xb.ID, Xb.NAME и другие. Эти данные пока находятся в оперативной памяти (в буфере записи).Записать() платформа пытается зафиксировать данные из буфера на диск и одновременно обновить все связанные индексы в файле CDX.ТекущийИндекс()) происходит ошибка — например, нарушается уникальность поля — процесс записи аварийно прерывается на уровне базы данных.В результате этого прерывания заполненный буфер сбрасывается, но физически созданная методом Добавить() строка в DBF-файле уже создана и остается пустой! Более того, индексный файл CDX рассинхронизируется с таблицей DBF, из-за чего последующие операции поиска и добавления начинают работать некорректно или полностью блокируются.
Обратите внимание на создание индексов. Допустим, при создании файла был определен индекс с признаком уникальности:
БД.ДобавитьИндекс("IDXIDPOSTNOM", "IDPOSTNOM", 1, 0, "");
Третий параметр, равный 1, указывает системе, что значение поля IDPOSTNOM должно быть строго уникальным во всей таблице.
Частая ошибка разработчиков заключается в предположении, что если перед записью установлен другой текущий индекс (например, Xb.ТекущийИндекс("IDXID")), то проверки по остальным индексам выполняться не будут. Это не так. При вызове Записать() СУБД обновляет все зарегистрированные в файле CDX индексы. Если вы попытаетесь записать дублирующееся значение в поле IDPOSTNOM, система вернет ошибку уникальности, запись прервется, и в файле появится пустая строка.
Решение проблемы на уровне структуры файлов: если поле не обязано быть уникальным, измените признак уникальности на 0 при создании индекса:
БД.ДобавитьИндекс("IDXIDPOSTNOM", "IDPOSTNOM", 0, 0, "");
Посмотрим на ветку Иначе в исходном коде:
Если Xb.НайтиПоКлючу(0) = 1 Тогда
Xb.Удалить();
КонецЕсли;
Xb.Сжать();
Метод Сжать() (в англоязычной версии — Pack) выполняет физическое удаление записей, помеченных на удаление с помощью метода Удалить(). Для этого движок XBase вынужден перестроить всю таблицу: создать временный файл, скопировать туда только актуальные записи, удалить старый файл, переименовать новый и полностью перестроить все связанные индексы.
Вызов Сжать() внутри итерационного цикла обработки записей приводит к критическим последствиям:
Как сделать правильно: помечайте записи на удаление методом Удалить() внутри цикла обработки, а метод Сжать() вызывайте строго один раз в самом конце работы алгоритма, перед закрытием файла.
// ... Код цикла, где выполняются операции удаления Xb.Удалить() ...
// Выносим сжатие за пределы основного цикла обработки:
Если Xb.Модифицирован() = 1 Тогда
Xb.Записать();
КонецЕсли;
Xb.Сжать(); // Физически удаляем помеченные записи один раз для всего файла
Xb.ЗакрытьФайл();
Объект XBase в 1С 7.7 при возникновении ошибок записи часто ведет себя «молча» — программа не падает с критической ошибкой выполнения, а просто молча портит файл DBF. Чтобы избежать этого, необходимо обязательно анализировать состояние выполнения после вызова Записать() с помощью метода КодОшибки().
Рассмотрим пример правильного написания отказоустойчивого кода записи данных с обработкой ошибок:
Xb.Добавить();
Xb.ID = ID;
Xb.NAME = NAME;
Xb.IDPOSTNOM = Строка(Данные.NOM);
Xb.Записать();
// Проверяем, успешно ли прошла запись в DBF/CDX
Если Xb.КодОшибки() < 0 Тогда
КодОш = Xb.КодОшибки();
// Отменяем транзакцию записи в буфере, чтобы не плодить пустые строки
Xb.Отменить();
Если КодОш = -340 Тогда
Сообщить("Ошибка: Нарушение уникальности индекса! Запись отклонена.", "!");
Иначе
Сообщить("Системная ошибка работы с DBF. Код ошибки: " + КодОш, "!");
КонецЕсли;
КонецЕсли;
Метод Отменить() в случае сбоя очищает буфер записи и предотвращает появление той самой "пустой строки" в таблице, сохраняя целостность структуры данных.
Если бизнес-логика вашей задачи жестко требует уникальности поля IDPOSTNOM, то простое отключение уникальности в индексе — это плохой обходной путь, который приведет к дублированию данных. В таком случае необходимо проверять уникальность значения программно перед выполнением добавления.
Посмотрим на пример реализации такой проверки:
// Нам нужно добавить новую запись, но поле IDPOSTNOM должно быть уникальным
НовоеЗначениеIDPOSTNOM = Строка(Данные.NOM);
// 1. Переключаемся на индекс по проверяемому полю
Xb.ТекущийИндекс("IDXIDPOSTNOM");
Xb.Ключ.IDPOSTNOM = НовоеЗначениеIDPOSTNOM;
// 2. Ищем, нет ли уже такого значения в файле
Если Xb.НайтиПоКлючу(0) = 1 Тогда
// Запись с таким ключом уже существует! Предотвращаем дублирование
Сообщить("Запись со значением IDPOSTNOM = " + НовоеЗначениеIDPOSTNOM + " уже существует! Пропускаем.", "W");
Иначе
// Записи нет, можем безопасно добавлять
Xb.ТекущийИндекс("IDXID"); // Возвращаем рабочий индекс поиска по ID
Xb.Добавить();
Xb.ID = ID;
Xb.NAME = NAME;
Xb.IDPOSTNOM = НовоеЗначениеIDPOSTNOM;
Xb.Записать();
КонецЕсли;
При таком подходе вы гарантируете целостность базы данных и полностью исключаете возникновение скрытых ошибок СУБД Codebase, обеспечивая стабильную и быструю работу вашей обработки обмена данными.