Почему при передаче числовых параметров из ТаблицыЗначений в MySQL через 1С++ записываются нули?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

При интеграции платформы 1С:Предприятие 7.7 с внешними базами данных MySQL через компоненту 1С++ и драйвер ODBC часто применяется метод пакетной загрузки ВыполнитьSQL_ИзТЗ. Однако разработчики нередко сталкиваются с ситуацией, когда текстовые поля переносятся корректно, а вместо чисел во временную таблицу базы данных записываются нули.

Разберем детально, почему возникает такая ситуация, как устроен механизм связывания параметров в ODBC и как правильно построить код для корректной передачи данных.

Причины сброса числовых данных

Проанализируем техническую сторону взаимодействия компоненты 1С++ и драйвера MySQL Connector/ODBC:

  1. Порядок вызова методов подготовки и описания параметров. Во внутренней архитектуре компоненты метод Подготовить() (вызов SQLPrepare) инициализирует дескриптор запроса и сбрасывает все ранее назначенные буферы параметров. Если вызвать ДобПараметр() до метода Подготовить(), созданная конфигурация параметров затирается. Драйвер пытается выполнить неявное автоопределение типов (через SQLDescribeParam), которое в драйверах MySQL работает некорректно, что и приводит к обнулению числовых полей.
  2. Несоответствие типов и размеров данных ODBC. Константа типа данных 4 соответствует типу SQL_INTEGER (стандартное 32-битное целое число размером 4 байта). Указание размера буфера 8 байт для 32-битного типа создает конфликт выделения памяти в драйвере ODBC. Для 8-байтных целых чисел в стандарте ODBC применяется тип SQL_BIGINT (значение -5).
  3. Использование Server-Side Prepared Statements. По умолчанию драйвер MySQL Connector/ODBC 5.x использует серверную подготовку выражений. Если сопоставление типов передано некорректно, бинарный протокол MySQL не может привести типы и подставляет значения по умолчанию (ноль).

Основное решение: исправление последовательности вызовов

Рассмотрим правильный порядок инициализации параметров запроса в объекте ODBCRecordSet. Метод Подготовить() должен вызываться строго до описания параметров через ДобПараметр().

Посмотрим на пример правильной загрузки данных из таблицы значений:


ТЗ = СоздатьОбъект("ТаблицаЗначений");
ТЗ.НоваяКолонка("id", "Число", 8);
ТЗ.НоваяСтрока();
ТЗ.id = 1;
ТЗ.НоваяСтрока();
ТЗ.id = 2;

RS.Выполнить("DROP TEMPORARY TABLE IF EXISTS tmp_table;");
RS.Выполнить("CREATE TEMPORARY TABLE tmp_table(id INT);");

// Сначала подготавливаем выражение:
RS.Подготовить("INSERT INTO tmp_table (id) VALUES(?);");

// И только потом связываем параметры:
// Для типа SQL_INTEGER (тип 4) стандартный размер составляет 4 байта
RS.ДобПараметр(1, 4, 4, 0);

// Выполняем пакетную вставку данных
RS.ВыполнитьSQL_ИзТЗ(ТЗ);

ИТ_Проверка = СоздатьОбъект("ИндексированнаяТаблица");
RS.ВыполнитьИнструкцию("SELECT * FROM tmp_table;", ИТ_Проверка);
ИТ_Проверка.ВыбратьСтроку();

Важные правила работы с методом ВыполнитьSQL_ИзТЗ

Чтобы избежать скрытых ошибок и утечек памяти при выгрузке десятков тысяч строк, необходимо учитывать специфику реализации пакетной вставки в компоненте 1С++:

  1. Позиционное сопоставление колонок. Метод ВыполнитьSQL_ИзТЗ связывает колонки таблицы значений со знаками вопроса ? строго по физическому порядку колонок в объекте ТаблицаЗначений, а не по их именам. Порядок колонок в ТЗ должен в точности дублировать порядок плейсхолдеров в SQL-запросе.
  2. Управление транзакциями. Пакетная вставка не открывает транзакцию автоматически. Если в процессе передачи 20 000 строк произойдет сбой, таблица останется заполненной наполовину. Всегда оборачивайте операцию в явную транзакцию: перед вставкой выполняйте RS.Выполнить("START TRANSACTION;");, а после успешного завершения — RS.Выполнить("COMMIT;");.
  3. Размер пакета. Для предотвращения переполнения сетевых буферов ODBC передавайте данные фиксированными порциями, задавая размер блока во втором параметре метода ВыполнитьSQL_ИзТЗ(ТЗ, 1000).
  4. Повторное использование дескриптора. Буфер памяти компоненты выделяется под размер первого переданного пакета. Не вызывайте метод повторно для наборов с меньшим количеством строк без повторной подготовки выражения через Подготовить() во избежание отправки «фантомных» строк из предыдущего вызова.

Альтернативный вариант: множественный INSERT (Bulk Insert)

Если специфика драйвера ODBC или нестандартные типы колонок MySQL вызывают трудности при связывании буферов, можно использовать прямую генерацию составного SQL-запроса пачками по 1000–2000 записей. Этот способ не требует настройки дескрипторов параметров и работает с высокой скоростью.

Разберем реализацию пакетного запроса:


RS.Выполнить("START TRANSACTION;");

СтрSQL = "INSERT INTO tmp_table (id) VALUES ";
Счетчик = 0;

ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
    Счетчик = Счетчик + 1;
    СтрSQL = СтрSQL + "(" + ТЗ.id + "),";
    
    Если Счетчик % 1000 = 0 Тогда
        // Убираем последнюю запятую и выполняем пакет
        СтрSQL = Лев(СтрSQL, СтрДлина(СтрSQL) - 1) + ";";
        RS.Выполнить(СтрSQL);
        СтрSQL = "INSERT INTO tmp_table (id) VALUES ";
    КонецЕсли;
КонецЦикла;

Если Счетчик % 1000 <> 0 Тогда
    СтрSQL = Лев(СтрSQL, СтрДлина(СтрSQL) - 1) + ";";
    RS.Выполнить(СтрSQL);
КонецЕсли;

RS.Выполнить("COMMIT;");

Альтернативный вариант: использование ADODB.Command

Для одиночных вставок или небольших объемов данных можно использовать стандартные объекты COM-соединения ADODB.Command с явным добавлением параметров через коллекцию Parameters, что полностью исключает путаницу с внутренними указателями буферов.

Посмотрим на пример работы через ADODB:


Db = СоздатьОбъект("ADODB.Command");
Db.ActiveConnection = ConnectionToSql;
Db.CommandType = 1; // adCmdText
Db.CommandText = "INSERT INTO tmp_table (id) VALUES (?)";

// Передаем значение в параметр
Db.Parameters.Item(0).Value = 5;
Db.Execute();

Тем не менее, для массивов от 10 000 строк и выше наиболее производительными решениями остаются корректно настроенный метод ВыполнитьSQL_ИзТЗ с предварительным вызовом Подготовить() либо множественный INSERT.

← На главную