Интеграция решений на платформе 1С с интернет-магазинами и внешними сайтами на базе СУБД MySQL — классическая задача автоматизации торговой деятельности. Однако разработчики часто сталкиваются с критической проблемой: при попытке напрямую записать данные в удаленную базу MySQL через стандартный механизм COM-соединения и драйвер ODBC скорость импорта падает до неприемлемых значений. Например, выгрузка каталога из 13 500 товаров может занимать 15–20 минут и более.
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем слабые места типовых решений и рассмотрим несколько эффективных альтернативных подходов, которые позволяют ускорить импорт в десятки раз.
Давайте выясним, почему прямое подключение из 1С к удаленной базе MySQL работает так медленно. Главный виновник здесь — не сам драйвер ODBC и не COM-технология как таковая, а архитектурная ошибка в реализации цикла записи. Рассмотрим типичный фрагмент кода, который часто используют разработчики:
Пока Выборка.Следующий() Цикл
// ... подготовка данных ...
Connection.Execute("INSERT INTO tested SET name = '" + Наименование + "', price = '" + Цена + "'");
КонецЦикла;
При таком подходе на каждую из 13 500 строк номенклатуры программа выполняет отдельное обращение к серверу БД. Если сервер MySQL находится на удаленном хостинге (в интернете), то на каждый единичный запрос накладывается сетевая задержка (RTT — Round Trip Time). Даже при отличной связи и задержке в 20 миллисекунд только на сетевое ожидание для 13 500 запросов уйдет:
13 500 * 0.02 сек = 270 секунд (4.5 минуты) чистого ожидания сети!
Добавьте к этому накладные расходы на парсинг каждого SQL-запроса сервером MySQL, транзакционные издержки (сброс логов на диск при каждой операции) и накладные расходы самого COM-соединения. В итоге мы получаем крайне низкую производительность. Ниже мы детально разберем пять способов решения этой проблемы.
Если архитектура решения требует прямого подключения к MySQL, мы можем кардинально сократить количество сетевых запросов, объединив множество одиночных инструкций INSERT в один пакетный запрос. СУБД MySQL поддерживает синтаксис множественной вставки: INSERT INTO table (col1, col2) VALUES (val1, val2), (val3, val4), ...;.
Рассмотрим, как реализовать пакетную запись порциями по 50–100 элементов. Это позволит сократить количество обращений к базе данных с 13 500 до 135–270 запросов.
Connection = Новый COMОбъект("ADODB.Connection");
// Установка соединения с MySQL...
CommandSQL = Новый COMОбъект("ADODB.Command");
CommandSQL.ActiveConnection = Connection;
РазмерПорции = 100; // Оптимальный размер пакета для баланса скорости и объема памяти
ШапкаЗапроса = "INSERT INTO tested (name, article, quantity, price) VALUES ";
ШаблонСтрокиЗапроса = "('%1','%2','%3','%4'),";
НомерСтроки = 1;
РазмерВыборки = Выборка.Количество(); // Получаем общее число записей
ТекстЗапроса = ШапкаЗапроса;
Пока Выборка.Следующий() Цикл
Наименование = Строка(Выборка.Номенклатура);
// Экранируем одинарные кавычки во избежание синтаксических ошибок SQL
Наименование = СтрЗаменить(Наименование, "'", "''");
Артикул = Строка(Выборка.Артикул);
Количество = Число(Выборка.Остаток);
Цена = Формат(Выборка.Цена, "ЧГ=");
// Формируем очередную строку значений
ТекстЗапроса = ТекстЗапроса + Символы.ПС + СтрШаблон(ШаблонСтрокиЗапроса, Наименование, Артикул, Количество, Цена);
Если (НомерСтроки % РазмерПорции = 0) ИЛИ (НомерСтроки >= РазмерВыборки) Тогда
// Заменяем последнюю запятую на точку с запятой для завершения запроса
ТекстЗапроса = Лев(ТекстЗапроса, СтрДлина(ТекстЗапроса) - 1) + ";";
CommandSQL.CommandText = ТекстЗапроса;
Попытка
CommandSQL.Execute();
Исключение
ЗаписьЖурналаРегистрации("Ошибка записи в MySQL", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
КонецПопытки;
// Сброс буфера для следующей порции
ТекстЗапроса = ШапкаЗапроса;
КонецЕсли;
НомерСтроки = НомерСтроки + 1;
КонецЦикла;
Этот простой шаг сократит время выполнения операции с 16 минут до 30–60 секунд, поскольку сетевые задержки практически перестанут влиять на общий процесс выгрузки.
Часто медленная работа выгрузки связана со сложными, неоптимальными запросами в самой 1С. Вложенные виртуальные таблицы «на лету» без использования временных таблиц заставляют СУБД 1С строить неоптимальные планы выполнения. Давайте проанализируем и оптимизируем исходный запрос.
Вместо громоздкого вложенного запроса с избыточными группировками применим классическое левое соединение виртуальной таблицы остатков и среза последних цен:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Номенклатура КАК Номенклатура,
| Остатки.Номенклатура.Наименование КАК Наименование,
| Остатки.Номенклатура.Артикул КАК Артикул,
| Остатки.Номенклатура.Производитель КАК Производитель,
| Остатки.КоличествоОстаток КАК Остаток,
| ЕСТЬNULL(Цены.Цена, 0) * 1.33 КАК Цена
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| ,
| КоличествоОстаток > 0
| ) КАК Остатки
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| ,
| ТипЦен = &ТипЦенРозница
| ) КАК Цены
| ПО Остатки.Номенклатура = Цены.Номенклатура
|ГДЕ
| ЕСТЬNULL(Цены.Цена, 0) > 0";
Запрос.УстановитьПараметр("ТипЦенРозница", Справочники.ТипыЦенНоменклатуры.НайтиПоНаименованию("Закупочная продажа", Истина));
Выборка = Запрос.Выполнить().Выбрать();
Почему этот запрос эффективнее? Мы избавились от тройной вложенности и избыточных группировок СУММА() по полям, которые по своей природе уникальны или уже сгруппированы в виртуальных таблицах. Такой запрос отработает на сервере 1С за доли секунды.
Если вам требуется абсолютно мгновенный импорт (буквально за 1–2 секунды для десятков тысяч строк), то лучшим выбором будет использование встроенного механизма MySQL для загрузки текстовых файлов. Команда LOAD DATA LOCAL INFILE оптимизирована на уровне ядра СУБД и работает в сотни раз быстрее любых вариантов INSERT.
Разберем по шагам эту схему работы:
Посмотрим на пример кода реализации данного метода в 1С:
// Шаг 1. Формируем локальный текстовый файл на диске сервера 1С
ПутьКФайлу = "C:\temp\export_goods.csv";
ЗаписьТекста = Новый ЗаписьТекста(ПутьКФайлу, КодировкаТекста.UTF8);
Пока Выборка.Следующий() Цикл
СтрокаДанных = Строка(Выборка.Номенклатура) + ";" +
Строка(Выборка.Артикул) + ";" +
Формат(Выборка.Остаток, "ЧГ=") + ";" +
Формат(Выборка.Цена, "ЧГ=");
ЗаписьТекста.ЗаписатьСтроку(СтрокаДанных);
КонецЦикла;
ЗаписьТекста.Закрыть();
// Шаг 2. Загружаем файл в базу данных MySQL одним запросом
// Обратите внимание на использование двойных обратных слэшей в пути для MySQL
ПутьКФайлуSQL = СтрЗаменить(ПутьКФайлу, "\", "/");
SQLКоманда = "LOAD DATA LOCAL INFILE '" + ПутьКФайлуSQL + "' " +
"INTO TABLE tested " +
"FIELDS TERMINATED BY ';' " +
"LINES TERMINATED BY '\r\n' " +
"(name, article, quantity, price);";
Connection.Execute(SQLКоманда);
Важное примечание: Для работы этого метода в настройках сервера MySQL (файл конфигурации my.cnf или my.ini) должен быть включен параметр local_infile = 1, а также соответствующая опция должна быть активирована на стороне клиента (в строке подключения ODBC-драйвера).
Прямое подключение к базе данных сайта из внешней сети через порт 3306 создает колоссальную угрозу безопасности («голая жопа» в интернет, как выразились участники обсуждения). Любой злоумышленник может попытаться подобрать пароль к СУБД или использовать уязвимости в коде базы данных.
Наиболее надежный, безопасный и современный подход — закрыть порт MySQL для внешних подключений, а обмен выполнять через HTTP-сервис (API-эндпоинт), написанный на стороне сайта (например, на PHP, Python или Node.js). 1С в данном сценарии выступает в роли HTTP-клиента.
Рассмотрим по шагам этот процесс:
ЗаписьJSON (если используется режим совместимости с 8.3).Посмотрим на пример отправки данных из 1С с помощью объекта HTTPСоединение:
// Формируем тело запроса в формате JSON (упрощенный пример)
ТекстJSON = "[";
Пока Выборка.Следующий() Цикл
ТекстJSON = ТекстJSON + "{" +
"""name"":""" + СтрЗаменить(Строка(Выборка.Номенклатура), """", "\""") + """," +
"""article"":""" + Строка(Выборка.Артикул) + """," +
"""quantity"":" + Формат(Выборка.Остаток, "ЧГ=") + "," +
"""price"":" + Формат(Выборка.Цена, "ЧГ=") + "},";
КонецЦикла;
// Убираем хвостовую запятую и закрываем массив
ТекстJSON = Лев(ТекстJSON, СтрДлина(ТекстJSON) - 1) + "]";
// Настраиваем HTTP-соединение
СерверСайта = "mysite.ru";
HTTPСоединение = Новый HTTPСоединение(СерверСайта, , , , , 30); // Таймаут 30 секунд
HTTPЗапрос = Новый HTTPЗапрос("/api/import_goods.php");
HTTPЗапрос.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
HTTPЗапрос.УстановитьТелоИзСтроки(ТекстJSON, КодировкаТекста.UTF8, ИспользованиеByteOrderMark.НеИспользовать);
Попытка
HTTPОтвет = HTTPСоединение.ОтправитьДляОбработки(HTTPЗапрос);
Если HTTPОтвет.КодСостояния = 200 Тогда
Сообщить("Импорт успешно завершен! Ответ сервера: " + HTTPОтвет.ПолучитьТелоКакСтроку());
Иначе
Сообщить("Ошибка импорта. Код состояния сервера: " + HTTPОтвет.КодСостояния);
КонецЕсли;
Исключение
Сообщить("Не удалось установить связь с сервером: " + ОписаниеОшибки());
КонецПопытки;
Если вы хотите максимально изолировать систему 1С от сайта и гарантировать стабильную работу даже при нестабильном интернет-соединении, используйте выгрузку через промежуточный FTP-сервер. Этот метод прост в реализации и абсолютно надежен.
Схема обмена выглядит следующим образом:
FTPСоединение файл копируется на FTP-сервер сайта.Этот способ полностью убирает зависимость 1С от доступности базы данных MySQL «здесь и сейчас», а также решает любые проблемы с производительностью на стороне клиента 1С.
Мы проанализировали различные варианты ускорения обмена. Чтобы выбрать оптимальный способ для вашей задачи, ориентируйтесь на следующие рекомендации: