Как правильно закрывать соединения с внешней базой SQL через ADO в 1С 8.3 Управляемые формы?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При работе с внешними базами данных SQL из 1С посредством технологии ADO (ActiveX Data Objects) одной из частых и критически важных проблем является некорректное управление соединениями. Если не закрывать соединения правильно, это может привести к утечкам ресурсов на SQL-сервере, превышению лимита подключений и, как следствие, к ошибкам в работе информационной системы. В этой статье мы подробно разберем, как эффективно управлять ADO-соединениями, предотвращая утечки и обеспечивая стабильную работу.

Мы выясним причины возникновения проблемы с накапливающимися соединениями, рассмотрим основные принципы работы с COM-объектами в 1С и предоставим несколько вариантов решения, адаптированных под различные сценарии использования.

Причина возникновения проблемы: некорректное закрытие COM-объектов ADO

Основная причина, по которой соединения с SQL-сервером не закрываются, даже если вы вызываете метод Close() для объектов ADODB.Connection и ADODB.Recordset, заключается в особенностях работы с COM-объектами в 1С. В 1С, как и в других языках, работающих с COM, недостаточно просто вызвать метод Close(). Необходимо также убедиться, что ссылки на COM-объекты полностью освобождены. Если этого не сделать, объект может оставаться в памяти до тех пор, пока сборщик мусора 1С не освободит его, что может произойти не сразу, а значит, и соединение с базой данных останется открытым.

Таким образом, мы должны не только закрывать соединение методом Close(), но и явно обнулять переменные, которые хранят ссылки на эти COM-объекты. Это даст понять системе, что объекты больше не нужны, и ресурсы могут быть освобождены.

Решение 1: Базовый механизм корректного закрытия ADO-соединений

Рассмотрим основной и наиболее важный принцип управления ADO-соединениями. Он включает в себя создание COM-объектов, выполнение операций и, что критично, их гарантированное закрытие и обнуление, независимо от того, произошли ли ошибки в процессе работы.

  1. Объявляем переменные: В начале процедуры объявляем переменные для ADODB.Connection и ADODB.Recordset и сразу присваиваем им значение Неопределено. Это хорошая практика, чтобы избежать потенциальных проблем, если объекты не будут инициализированы по какой-либо причине.
  2. Используем блок Попытка...Исключение...КонецПопытки: Все операции по подключению, выполнению запросов и обработке данных должны быть заключены в этот блок. Это позволит нам корректно отлавливать и обрабатывать ошибки, не прерывая выполнение программы неконтролируемо.
  3. Выполняем операции с базой данных: Создаем объекты, открываем соединение, выполняем запрос, обрабатываем данные.
  4. Гарантированное закрытие и обнуление: После блока Попытка...Исключение...КонецПопытки мы должны разместить код, который гарантированно закроет соединения и обнулит переменные. Это должно произойти в любом случае – была ли ошибка или нет.

Посмотрим на пример кода, который демонстрирует эти принципы:


Процедура ПодключитьсяИСделатьЗапросКорректно()
    // 1. Объявляем переменные и инициализируем их Неопределено
    Перем ADODBConnection;
    Перем ADODBRecordSet;
    ADODBConnection = Неопределено;
    ADODBRecordSet = Неопределено;

    Попытка
        // 3. Создание и открытие соединения
        ADODBConnection = Новый COMОбъект("ADODB.Connection");
        СтрокаСоединения = "Provider=SQLOLEDB.1;Data Source=SQLSERVER;Initial Catalog=ExternalDB;Integrated Security=SSPI;Connect Timeout=30;";
        ADODBConnection.Open(СтрокаСоединения);
        Сообщить("Соединение с SQL базой открыто.");

        // 3. Создание и выполнение запроса
        ADODBRecordSet = Новый COMОбъект("ADODB.Recordset");
        SQLЗапрос = "SELECT TOP 10 ID, FieldName FROM MyTable ORDER BY ID DESC"; // Пример запроса
        ADODBRecordSet.Open(SQLЗапрос, ADODBConnection);
        Сообщить("Запрос к SQL базе выполнен.");

        // 3. Обработка данных
        Если НЕ ADODBRecordSet.EOF Тогда
            Сообщить("Получены данные:");
            Пока НЕ ADODBRecordSet.EOF Цикл
                Сообщить("ID: " + ADODBRecordSet.Fields("ID").Value + ", Название: " + ADODBRecordSet.Fields("FieldName").Value);
                ADODBRecordSet.MoveNext();
            КонецЦикла;
        Иначе
            Сообщить("Данные не найдены.");
        КонецЕсли;

    Исключение
        // 2. Обработка ошибок, если что-то пошло не так
        Сообщить("Произошла ошибка при работе с внешней базой: " + ОписаниеОшибки());
        // Дополнительная логика обработки ошибок, например, запись в журнал
    КонецПопытки;
    
    // 4. ГАРАНТИРОВАННОЕ ЗАКРЫТИЕ И ОБНУЛЕНИЕ ОБЪЕКТОВ
    // Этот блок будет выполнен в любом случае - с ошибкой или без.

    // Закрываем и обнуляем RecordSet
    Если ADODBRecordSet <> Неопределено Тогда
        // Проверяем состояние RecordSet перед закрытием. adStateOpen = 1
        Если ADODBRecordSet.State = 1 Тогда 
            ADODBRecordSet.Close();
            Сообщить("RecordSet закрыт.");
        КонецЕсли;
        ADODBRecordSet = Неопределено; // Обнуляем ссылку
        Сообщить("RecordSet обнулен.");
    КонецЕсли;

    // Закрываем и обнуляем Connection
    Если ADODBConnection <> Неопределено Тогда
        // Проверяем состояние Connection перед закрытием. adStateOpen = 1
        Если ADODBConnection.State = 1 Тогда 
            ADODBConnection.Close();
            Сообщить("Соединение с SQL базой закрыто.");
        КонецЕсли;
        ADODBConnection = Неопределено; // Обнуляем ссылку
        Сообщить("Соединение с SQL базой обнулено.");
    КонецЕсли;
КонецПроцедуры

Мы видим, что в данном подходе критически важным является проверка свойства State у объектов ADODB.Connection и ADODB.Recordset. Это свойство показывает текущее состояние объекта (например, открыт он или закрыт). Константа adStateOpen имеет значение 1. Проверка ADODBRecordSet.State = 1 позволяет избежать ошибки при попытке закрыть уже закрытое соединение или рекордсет.

Решение 2: Оптимизация для циклической работы с внешней базой

Часто возникает необходимость выполнять множество запросов к внешней базе данных в цикле, например, для обработки списка элементов. Если мы будем открывать и закрывать ADODB.Connection на каждой итерации цикла, это значительно замедлит работу из-за накладных расходов на установление нового соединения. Более эффективным подходом является открытие соединения один раз до начала цикла и его закрытие после завершения всех итераций. При этом ADODB.Recordset должен быть корректно закрыт и обнулен внутри каждой итерации цикла, если он используется.

Рассмотрим структуру кода для такого сценария:

  1. Открываем соединение вне цикла: Объект ADODB.Connection создаем и открываем до входа в цикл.
  2. Внутри цикла: Для каждой итерации мы создаем новый объект ADODB.Recordset, выполняем запрос, обрабатываем данные, а затем ОБЯЗАТЕЛЬНО закрываем и обнуляем ADODB.Recordset.
  3. Закрываем соединение после цикла: После того как все итерации завершены, закрываем и обнуляем объект ADODB.Connection.
  4. Два уровня обработки ошибок: Внешний блок Попытка...Исключение для общих ошибок соединения и внутренний для ошибок обработки отдельных элементов.

Посмотрим на пример:


Процедура ЦиклическийЗапросКВнешнейБазеОптимизированный()
    Перем ADODBConnection;
    ADODBConnection = Неопределено;

    Попытка
        // 1. Открываем соединение ОДИН РАЗ до начала цикла
        ADODBConnection = Новый COMОбъект("ADODB.Connection");
        СтрокаСоединения = "Provider=SQLOLEDB.1;Data Source=SQLSERVER;Initial Catalog=ExternalDB;Integrated Security=SSPI;Connect Timeout=30;";
        ADODBConnection.Open(СтрокаСоединения);
        Сообщить("Соединение с SQL базой открыто для циклической обработки.");

        // Предположим, у нас есть список элементов для обработки
        МойСписок = Новый Массив();
        МойСписок.Добавить(101); // ID элемента 1
        МойСписок.Добавить(102); // ID элемента 2
        МойСписок.Добавить(103); // ID элемента 3

        Для Каждого ЭлементID Из МойСписок Цикл
            Перем ADODBRecordSet;
            ADODBRecordSet = Неопределено;

            Попытка
                // 2. Внутри цикла создаем и используем RecordSet
                SQLЗапрос = "SELECT FieldName FROM MyTable WHERE ID = " + ЭлементID;
                ADODBRecordSet = Новый COMОбъект("ADODB.Recordset");
                // Открываем RecordSet, используя уже открытое соединение
                ADODBRecordSet.Open(SQLЗапрос, ADODBConnection);

                Если НЕ ADODBRecordSet.EOF Тогда
                    Сообщить("Обработка элемента ID " + ЭлементID + ": Найдено значение '" + ADODBRecordSet.Fields("FieldName").Value + "'");
                Иначе
                    Сообщить("Обработка элемента ID " + ЭлементID + ": Данные не найдены.");
                КонецЕсли;

            Исключение
                // 2. Обработка ошибок для конкретного элемента
                Сообщить("Ошибка при обработке элемента ID " + ЭлементID + ": " + ОписаниеОшибки());
            КонецПопытки;

            // 2. Закрытие и обнуление RecordSet ВНУТРИ ЦИКЛА
            Если ADODBRecordSet <> Неопределено Тогда
                Если ADODBRecordSet.State = 1 Тогда
                    ADODBRecordSet.Close();
                КонецЕсли;
                ADODBRecordSet = Неопределено;
            КонецЕсли;
        КонецЦикла;

    Исключение
        // Обработка общих ошибок при работе с внешней базой (например, проблема с подключением)
        Сообщить("Общая ошибка при работе с внешней базой в цикле: " + ОписаниеОшибки());
    КонецПопытки;

    // 3. Закрытие и обнуление Connection ПОСЛЕ ЗАВЕРШЕНИЯ ВСЕГО ЦИКЛА
    Если ADODBConnection <> Неопределено Тогда
        Если ADODBConnection.State = 1 Тогда
            ADODBConnection.Close();
            Сообщить("Соединение с SQL базой закрыто после циклической обработки.");
        КонецЕсли;
        ADODBConnection = Неопределено;
    КонецЕсли;
КонецПроцедуры

Такой подход значительно снижает нагрузку на SQL-сервер и повышает производительность за счет минимизации количества операций открытия/закрытия соединения.

Решение 3: Работа с транзакциями ADO для обеспечения целостности данных

В некоторых случаях нам требуется выполнить несколько операций изменения данных (INSERT, UPDATE, DELETE) в внешней SQL базе таким образом, чтобы они были атомарными: либо все операции успешно завершаются (фиксируются), либо ни одна из них не применяется (откатываются). Для этого в ADO предусмотрен механизм транзакций.

Мы будем использовать следующие методы объекта ADODB.Connection:

  1. BeginTrans(): Начинает новую транзакцию.
  2. CommitTrans(): Фиксирует все изменения, сделанные в рамках текущей транзакции.
  3. RollbackTrans(): Откатывает все изменения, сделанные в рамках текущей транзакции, отменяя их.

Важно помнить, что транзакции увеличивают нагрузку на базу данных и блокировки, поэтому используйте их только тогда, когда это действительно необходимо для обеспечения целостности данных.

Рассмотрим пример использования транзакций:


Процедура ВыполнитьТранзакциюSQLКорректно()
    Перем ADODBConnection;
    ADODBConnection = Неопределено;

    Попытка
        ADODBConnection = Новый COMОбъект("ADODB.Connection");
        СтрокаСоединения = "Provider=SQLOLEDB.1;Data Source=SQLSERVER;Initial Catalog=ExternalDB;Integrated Security=SSPI;Connect Timeout=30;";
        ADODBConnection.Open(СтрокаСоединения);
        Сообщить("Соединение с SQL базой открыто для транзакции.");

        // 1. НАЧИНАЕМ ТРАНЗАКЦИЮ
        ADODBConnection.BeginTrans();
        Сообщить("Транзакция ADO начата.");

        // Выполняем несколько команд изменения данных
        // Эти изменения не будут видны другим пользователям до фиксации транзакции
        Сообщить("Выполняем INSERT в таблицу TableA...");
        ADODBConnection.Execute("INSERT INTO TableA (Col1, Col2) VALUES ('Значение1', 'Значение2')");
        Сообщить("Выполняем UPDATE таблицы TableB...");
        ADODBConnection.Execute("UPDATE TableB SET Col2 = 'НовоеЗначение' WHERE ID = 10");
        // Предположим, здесь может произойти какая-то ошибка, 
        // или условие, при котором мы решаем откатить транзакцию
        // Например: Если ПроизошлаНепредвиденнаяОшибка Тогда Ошибка("Принудительный откат"); КонецЕсли;
        
        // 2. ФИКСИРУЕМ ТРАНЗАКЦИЮ, если все операции прошли успешно
        ADODBConnection.CommitTrans();
        Сообщить("Транзакция успешно зафиксирована. Все изменения сохранены.");

    Исключение
        // Если произошла ошибка, мы должны ОТКАТИТЬ ТРАНЗАКЦИЮ
        Если ADODBConnection <> Неопределено И ADODBConnection.State = 1 Тогда
            // Проверяем, была ли транзакция начата, прежде чем откатывать
            // ADODB.Connection не имеет прямого свойства "IsInTransaction",
            // но TransactionLevel будет больше 0, если транзакция активна.
            Если ADODBConnection.TransactionLevel > 0 Тогда 
                ADODBConnection.RollbackTrans();
                Сообщить("Ошибка при выполнении транзакции. Транзакция ОТКАТАНА. Изменения не применены.");
            Иначе
                Сообщить("Произошла ошибка, но транзакция не была активна или уже откатана.");
            КонецЕсли;
        КонецЕсли;
        Сообщить("Произошла ошибка: " + ОписаниеОшибки());
    КонецПопытки;

    // Гарантированное закрытие и обнуление Connection
    Если ADODBConnection <> Неопределено Тогда
        Если ADODBConnection.State = 1 Тогда
            ADODBConnection.Close();
            Сообщить("Соединение с SQL базой закрыто после работы с транзакцией.");
        КонецЕсли;
        ADODBConnection = Неопределено;
    КонецЕсли;
КонецПроцедуры

В этом примере мы видим, как блок Попытка...Исключение используется для управления транзакциями. Если в процессе выполнения команд возникает ошибка, то в секции Исключение мы вызываем RollbackTrans(), чтобы отменить все незафиксированные изменения. Если же все проходит успешно, то в конце блока Попытка вызывается CommitTrans().

Важные моменты и дополнительные советы

Следуя этим рекомендациям, мы можем значительно повысить надежность и производительность наших решений, взаимодействующих с внешними базами данных SQL из 1С, и избежать распространенных проблем с утечками соединений и ресурсами. При необходимости организовать обмен без ручного написания кода взаимодействия можно использовать подсистему выгрузки данных во внешнюю базу SQL.

Правильное управление COM-объектами и ADO-соединениями является фундаментальным навыком для программиста 1С, работающего с интеграциями, и позволяет создавать стабильные и масштабируемые системы.

← На главную