При интеграции платформы 1С:Предприятие 7.7 с внешними базами данных MySQL через компоненту 1С++ и драйвер ODBC часто применяется метод пакетной загрузки ВыполнитьSQL_ИзТЗ. Однако разработчики нередко сталкиваются с ситуацией, когда текстовые поля переносятся корректно, а вместо чисел во временную таблицу базы данных записываются нули.
Разберем детально, почему возникает такая ситуация, как устроен механизм связывания параметров в ODBC и как правильно построить код для корректной передачи данных.
Проанализируем техническую сторону взаимодействия компоненты 1С++ и драйвера MySQL Connector/ODBC:
Подготовить() (вызов SQLPrepare) инициализирует дескриптор запроса и сбрасывает все ранее назначенные буферы параметров. Если вызвать ДобПараметр() до метода Подготовить(), созданная конфигурация параметров затирается. Драйвер пытается выполнить неявное автоопределение типов (через SQLDescribeParam), которое в драйверах MySQL работает некорректно, что и приводит к обнулению числовых полей.4 соответствует типу SQL_INTEGER (стандартное 32-битное целое число размером 4 байта). Указание размера буфера 8 байт для 32-битного типа создает конфликт выделения памяти в драйвере ODBC. Для 8-байтных целых чисел в стандарте ODBC применяется тип SQL_BIGINT (значение -5).Рассмотрим правильный порядок инициализации параметров запроса в объекте 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;", ИТ_Проверка);
ИТ_Проверка.ВыбратьСтроку();
Чтобы избежать скрытых ошибок и утечек памяти при выгрузке десятков тысяч строк, необходимо учитывать специфику реализации пакетной вставки в компоненте 1С++:
ВыполнитьSQL_ИзТЗ связывает колонки таблицы значений со знаками вопроса ? строго по физическому порядку колонок в объекте ТаблицаЗначений, а не по их именам. Порядок колонок в ТЗ должен в точности дублировать порядок плейсхолдеров в SQL-запросе.RS.Выполнить("START TRANSACTION;");, а после успешного завершения — RS.Выполнить("COMMIT;");.ВыполнитьSQL_ИзТЗ(ТЗ, 1000).Подготовить() во избежание отправки «фантомных» строк из предыдущего вызова.Если специфика драйвера 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;");
Для одиночных вставок или небольших объемов данных можно использовать стандартные объекты 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.