diff --git a/docs/Thread-Safe-Transactions.md b/docs/Thread-Safe-Transactions.md new file mode 100644 index 0000000..f0f0f9b --- /dev/null +++ b/docs/Thread-Safe-Transactions.md @@ -0,0 +1,103 @@ +# Thread-Safe Transactions + +Starting from version 3.5.0, the Entity library supports thread-safe transactions through connection pooling with a simplified API. + +## Problem + +In the previous version, all entity managers and repositories shared a single connector instance. This caused race conditions when multiple threads used transactions: + +```bsl +// Thread 1 and Thread 2 both use the same connector +Thread1: МенеджерСущностей.НачатьТранзакцию(); +Thread2: МенеджерСущностей.НачатьТранзакцию(); // Overwrites Thread 1's transaction +Thread1: МенеджерСущностей.ЗафиксироватьТранзакцию(); // Commits Thread 2's work instead! +``` + +## Solution + +The library now supports connection pooling with automatic context management. Each transaction gets its own context and connector, ensuring thread safety without complex parameter passing. + +## Usage + +### Creating Entity Manager with Connection Pool + +```bsl +// Create entity manager with connection pool of size 5 +МенеджерСущностей = Новый МенеджерСущностей( + Тип("КоннекторPostgreSQL"), + "Host=localhost;Port=5432;Database=test", + Неопределено, // Parameters + 5 // Pool size +); +``` + +### Simplified Thread-Safe Transactions + +```bsl +// Simple approach - library automatically manages contexts +КонтекстID = МенеджерСущностей.НачатьТранзакцию(); + +// All CRUD operations can optionally use the context +МенеджерСущностей.Сохранить(Сущность, КонтекстID); +ЗагруженныеСущности = МенеджерСущностей.Получить(Тип("МояСущность"), Неопределено, КонтекстID); + +// Commit the transaction +МенеджерСущностей.ЗафиксироватьТранзакцию(КонтекстID); +``` + +### Multiple Concurrent Transactions + +```bsl +// Thread 1: +КонтекстID1 = МенеджерСущностей.НачатьТранзакцию(); +МенеджерСущностей.Сохранить(Сущность1, КонтекстID1); + +// Thread 2: Independent transaction +КонтекстID2 = МенеджерСущностей.НачатьТранзакцию(); +МенеджерСущностей.Сохранить(Сущность2, КонтекстID2); + +// Each thread commits independently +МенеджерСущностей.ЗафиксироватьТранзакцию(КонтекстID1); // Thread 1 +МенеджерСущностей.ОтменитьТранзакцию(КонтекстID2); // Thread 2 +``` + +## Backward Compatibility + +All existing code continues to work without modification: + +```bsl +// Old API still works (uses shared connector) +МенеджерСущностей = Новый МенеджерСущностей(Тип("КоннекторPostgreSQL"), "connection_string"); +МенеджерСущностей.НачатьТранзакцию(); +МенеджерСущностей.Сохранить(Сущность); +МенеджерСущностей.ЗафиксироватьТранзакцию(); +``` + +### CRUD Operations + +All CRUD operations support optional context parameters: + +```bsl +// Without context (uses default connector) +МенеджерСущностей.Сохранить(Сущность); +Результат = МенеджерСущностей.Получить(Тип("МояСущность")); + +// With context (uses transaction-specific connector) +МенеджерСущностей.Сохранить(Сущность, КонтекстID); +Результат = МенеджерСущностей.Получить(Тип("МояСущность"), ОпцииПоиска, КонтекстID); +МенеджерСущностей.Удалить(Сущность, КонтекстID); +``` + +## Connection Pool Configuration + +- **Pool Size**: Determines the maximum number of concurrent connections +- **Default**: Pool is disabled (backward compatibility) +- **Recommendation**: Set pool size to expected number of concurrent threads + +## Key Features + +- **Simplified API**: No need to manually manage connection objects +- **Automatic Context Management**: Connectors are automatically assigned to contexts +- **Backward Compatibility**: All existing code works unchanged +- **Thread Safety**: Each transaction context is isolated +- **Performance**: Connection reuse minimizes database connection overhead \ No newline at end of file diff --git "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\260\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\260\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" index b759f5a..b013399 100644 --- "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\260\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" +++ "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\260\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" @@ -3,32 +3,33 @@ Перем Хранилища; Перем Семафор; -Функция Получить(ОбъектМодели, Коннектор) Экспорт - - Семафор.Захватить(); - - СвойстваКоннектора = РаботаСКоннекторами.ПолучитьСвойстваКоннектора(Коннектор); - - КлючХранилища = ПолучитьКлючХранилища( - ОбъектМодели.ТипСущности(), - ТипЗнч(Коннектор), - СвойстваКоннектора.СтрокаСоединения - ); - ХранилищеСущностей = Хранилища.Получить(КлючХранилища); - Если ХранилищеСущностей = Неопределено Тогда - ХранилищеСущностей = Новый ХранилищеСущностей( - ОбъектМодели, - ТипЗнч(Коннектор), - СвойстваКоннектора.СтрокаСоединения, - СвойстваКоннектора.Параметры - ); - Хранилища.Вставить(КлючХранилища, ХранилищеСущностей); - КонецЕсли; - - Семафор.Освободить(); - - Возврат ХранилищеСущностей; - +Функция Получить(ОбъектМодели, Коннектор, МенеджерСущностей = Неопределено) Экспорт + + Семафор.Захватить(); + + СвойстваКоннектора = РаботаСКоннекторами.ПолучитьСвойстваКоннектора(Коннектор); + + КлючХранилища = ПолучитьКлючХранилища( + ОбъектМодели.ТипСущности(), + ТипЗнч(Коннектор), + СвойстваКоннектора.СтрокаСоединения + ); + ХранилищеСущностей = Хранилища.Получить(КлючХранилища); + Если ХранилищеСущностей = Неопределено Тогда + ХранилищеСущностей = Новый ХранилищеСущностей( + ОбъектМодели, + ТипЗнч(Коннектор), + СвойстваКоннектора.СтрокаСоединения, + СвойстваКоннектора.Параметры, + МенеджерСущностей + ); + Хранилища.Вставить(КлючХранилища, ХранилищеСущностей); + КонецЕсли; + + Семафор.Освободить(); + + Возврат ХранилищеСущностей; + КонецФункции Процедура Закрыть(ТипКоннектора, СтрокаСоединения, ПараметрыКоннектора) Экспорт diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" index b90e0f9..8f14727 100644 --- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\234\320\265\320\275\320\265\320\264\320\266\320\265\321\200\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" @@ -13,6 +13,13 @@ Перем СтрокаСоединенияКоннектора; Перем ПараметрыКоннектора; +// Пул соединений для потокобезопасной работы с транзакциями +Перем ПулСоединений; +Перем РазмерПулаСоединений; + +// Управление контекстами транзакций +Перем ТекущиеКонтексты; // Соответствие: КонтекстID -> Истина (активные контексты) + Перем Лог; // Конструктор объекта МенеджерСущностей. @@ -21,8 +28,9 @@ // ТипКоннектора - Тип - Тип класса, реализующего интерфейс Коннектор. // СтрокаСоединения - Строка - Строка соединения к БД, к которой подключается коннектор. // ППараметрыКоннектора - Массив - Массив дополнительных параметров коннектора. Содержимое произвольное. +// РРазмерПулаСоединений - Число - Размер пула соединений для потокобезопасной работы (по умолчанию 5) // -Процедура ПриСозданииОбъекта(Знач ТипКоннектора, Знач СтрокаСоединения = "", Знач ППараметрыКоннектора = Неопределено) +Процедура ПриСозданииОбъекта(Знач ТипКоннектора, Знач СтрокаСоединения = "", Знач ППараметрыКоннектора = Неопределено, Знач РРазмерПулаСоединений = 5) Лог = Логирование.ПолучитьЛог("oscript.lib.entity.manager"); Лог.Отладка("Инициализация менеджера сущностей с коннектором %1", ТипКоннектора); ПроверитьПоддержкуИнтерфейсаКоннектора(ТипКоннектора); @@ -37,6 +45,12 @@ Иначе ПараметрыКоннектора = ППараметрыКоннектора; КонецЕсли; + + РазмерПулаСоединений = РРазмерПулаСоединений; + ПулСоединений = Неопределено; // Будет инициализирован в методе Инициализировать() + + // Инициализация управления контекстами + ТекущиеКонтексты = Новый Соответствие; КонецПроцедуры // Регистрирует переданный тип класса-сценария в модели данных. @@ -65,6 +79,12 @@ РаботаСКоннекторами.ОткрытьКоннектор(Коннектор, СтрокаСоединенияКоннектора, ПараметрыКоннектора); + // Инициализируем пул соединений только если задан положительный размер + Если РазмерПулаСоединений > 0 Тогда + ТипКоннектора = ТипЗнч(Коннектор); + ПулСоединений = Новый ПулСоединений(РазмерПулаСоединений, ТипКоннектора, СтрокаСоединенияКоннектора, ПараметрыКоннектора); + КонецЕсли; + ОбъектыМодели = МодельДанных.ПолучитьОбъектыМодели(); Для Каждого ОбъектМодели Из ОбъектыМодели Цикл @@ -100,6 +120,7 @@ КонецФункции // Сохраняет сущность в БД. +// Автоматически определяет контекст выполнения (основной поток или фоновое задание). // // Параметры: // Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД. @@ -108,10 +129,15 @@ ТипСущности = АктивнаяЗапись.ТипСущности(Сущность); ОбъектМодели = МодельДанных.Получить(ТипСущности); ПулСущностей = ПолучитьПулСущностей(ТипСущности); - РаботаСКоннекторами.Сохранить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); + + // Автоматически выбираем коннектор в зависимости от контекста + КоннекторДляОперации = ПолучитьКоннекторДляТекущегоКонтекста(); + + РаботаСКоннекторами.Сохранить(КоннекторДляОперации, ОбъектМодели, ПулСущностей, Сущность); КонецПроцедуры // Осуществляет поиск сущностей переданного типа по идентификатору. +// Автоматически определяет контекст выполнения (основной поток или фоновое задание). // // Параметры: // ТипСущности - Тип - Тип искомой сущности. @@ -131,10 +157,15 @@ Если ОпцииПоиска = Неопределено Тогда ОпцииПоиска = Новый ОпцииПоиска; КонецЕсли; - Возврат РаботаСКоннекторами.Получить(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); + + // Автоматически выбираем коннектор в зависимости от контекста + КоннекторДляОперации = ПолучитьКоннекторДляТекущегоКонтекста(); + + Возврат РаботаСКоннекторами.Получить(КоннекторДляОперации, ОбъектМодели, ПулСущностей, ОпцииПоиска); КонецФункции // Осуществляет поиск сущности переданного типа по идентификатору. +// Автоматически определяет контекст выполнения (основной поток или фоновое задание). // // Параметры: // ТипСущности - Тип - Тип искомой сущности. @@ -155,10 +186,15 @@ Если ОпцииПоиска = Неопределено Тогда ОпцииПоиска = Новый ОпцииПоиска; КонецЕсли; - Возврат РаботаСКоннекторами.ПолучитьОдно(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); + + // Автоматически выбираем коннектор в зависимости от контекста + КоннекторДляОперации = ПолучитьКоннекторДляТекущегоКонтекста(); + + Возврат РаботаСКоннекторами.ПолучитьОдно(КоннекторДляОперации, ОбъектМодели, ПулСущностей, ОпцииПоиска); КонецФункции // Выполняет удаление сущности из базы данных. +// Автоматически определяет контекст выполнения (основной поток или фоновое задание). // Сущность должна иметь заполненный идентификатор. // // Параметры: @@ -168,7 +204,11 @@ ТипСущности = АктивнаяЗапись.ТипСущности(Сущность); ОбъектМодели = МодельДанных.Получить(ТипСущности); ПулСущностей = ПолучитьПулСущностей(ТипСущности); - РаботаСКоннекторами.Удалить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); + + // Автоматически выбираем коннектор в зависимости от контекста + КоннекторДляОперации = ПолучитьКоннекторДляТекущегоКонтекста(); + + РаботаСКоннекторами.Удалить(КоннекторДляОперации, ОбъектМодели, ПулСущностей, Сущность); КонецПроцедуры // Выполняет очистку полную данных библиотеки. @@ -176,6 +216,12 @@ // Процедура Закрыть() Экспорт РаботаСКоннекторами.ЗакрытьКоннектор(Коннектор); + + // Закрываем пул соединений + Если ПулСоединений <> Неопределено Тогда + ПулСоединений.ЗакрытьВсеСоединения(); + КонецЕсли; + МодельДанных.Очистить(); СвойстваКоннектора = РаботаСКоннекторами.ПолучитьСвойстваКоннектора(Коннектор); ХранилищаСущностей.Закрыть(ТипЗнч(Коннектор), СвойстваКоннектора.СтрокаСоединения, СвойстваКоннектора.Параметры); @@ -184,21 +230,57 @@ КонецПроцедуры // Посылает коннектору запрос на начало транзакции. +// Автоматически определяет контекст выполнения (основной поток или фоновое задание). // Процедура НачатьТранзакцию() Экспорт - РаботаСКоннекторами.НачатьТранзакцию(Коннектор); + Если ПулСоединений <> Неопределено Тогда + // Автоматически определяем контекст выполнения + КонтекстID = ПолучитьТекущийКонтекстID(); + ТекущиеКонтексты[КонтекстID] = Истина; + + // Получаем соединение для контекста и начинаем транзакцию + Соединение = ПулСоединений.ПолучитьСоединениеДляКонтекста(КонтекстID); + Соединение.НачатьТранзакцию(); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.НачатьТранзакцию(Коннектор); + КонецЕсли; КонецПроцедуры // Посылает коннектору запрос на фиксацию транзакции. +// Автоматически определяет контекст выполнения. // Процедура ЗафиксироватьТранзакцию() Экспорт - РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор); + Если ПулСоединений <> Неопределено Тогда + // Автоматически определяем текущий контекст + КонтекстID = ПолучитьТекущийКонтекстID(); + + // Фиксируем транзакцию и освобождаем контекст + Соединение = ПулСоединений.ПолучитьСоединениеДляКонтекста(КонтекстID); + Соединение.ЗафиксироватьТранзакцию(); + ЗавершитьКонтекст(КонтекстID); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор); + КонецЕсли; КонецПроцедуры // Посылает коннектору запрос на отмену транзакции. +// Автоматически определяет контекст выполнения. // Процедура ОтменитьТранзакцию() Экспорт - РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + Если ПулСоединений <> Неопределено Тогда + // Автоматически определяем текущий контекст + КонтекстID = ПолучитьТекущийКонтекстID(); + + // Отменяем транзакцию и освобождаем контекст + Соединение = ПулСоединений.ПолучитьСоединениеДляКонтекста(КонтекстID); + Соединение.ОтменитьТранзакцию(); + ЗавершитьКонтекст(КонтекстID); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + КонецЕсли; КонецПроцедуры // Возвращает текущий активный коннектор. @@ -211,6 +293,109 @@ Возврат Коннектор; КонецФункции +// Получить соединение из пула для потокобезопасных операций +// +// Возвращаемое значение: +// Соединение - СоединениеСущности из пула для выполнения операций БД +// +Функция ПолучитьСоединение() Экспорт + Если ПулСоединений = Неопределено Тогда + ВызватьИсключение("Менеджер сущностей не инициализирован. Вызовите метод Инициализировать()"); + КонецЕсли; + + Возврат ПулСоединений.ПолучитьСоединение(); +КонецФункции + +// Вернуть соединение в пул после использования +// +// Параметры: +// Соединение - СоединениеСущности - Соединение для возврата в пул +// +Процедура ВернутьСоединение(Соединение) Экспорт + Если ПулСоединений <> Неопределено Тогда + ПулСоединений.ВернутьСоединение(Соединение); + КонецЕсли; +КонецПроцедуры + +// Получить идентификатор текущего контекста выполнения +// Автоматически определяет, выполняется ли код в основном потоке или фоновом задании +// +// Возвращаемое значение: +// Строка - Идентификатор контекста: +// - Для фоновых заданий: УникальныйИдентификатор задания +// - Для основного потока: специальная константа "MainThread" +// +Функция ПолучитьТекущийКонтекстID() + Попытка + // Пытаемся получить текущее фоновое задание + ТекущееЗадание = ФоновыеЗадания.ПолучитьТекущее(); + + Если ТекущееЗадание <> Неопределено Тогда + // Выполняемся в фоновом задании - используем его УникальныйИдентификатор + Возврат Строка(ТекущееЗадание.УникальныйИдентификатор); + Иначе + // Выполняемся в основном потоке + Возврат "MainThread"; + КонецЕсли; + Исключение + // Если API ФоновыеЗадания недоступно - используем основной поток + Возврат "MainThread"; + КонецПопытки; +КонецФункции + +// Завершить контекст и освободить связанные ресурсы +// +// Параметры: +// КонтекстID - Строка - Идентификатор завершаемого контекста +// +Процедура ЗавершитьКонтекст(КонтекстID) + ТекущиеКонтексты.Удалить(КонтекстID); + ПулСоединений.ОсвободитьКонтекст(КонтекстID); +КонецПроцедуры + +// Получить коннектор для текущего контекста выполнения +// Автоматически определяет контекст и выбирает подходящий коннектор +// +// Возвращаемое значение: +// АбстрактныйКоннектор - Коннектор для выполнения операции +// +Функция ПолучитьКоннекторДляТекущегоКонтекста() + Если ПулСоединений = Неопределено Тогда + // Режим обратной совместимости - пул не используется + Возврат Коннектор; + КонецЕсли; + + // Определяем текущий контекст + КонтекстID = ПолучитьТекущийКонтекстID(); + + // Проверяем, есть ли активная транзакция для данного контекста + Если ТекущиеКонтексты.Получить(КонтекстID) <> Неопределено Тогда + // Используем коннектор из активной транзакции + Соединение = ПулСоединений.ПолучитьСоединениеДляКонтекста(КонтекстID); + Возврат Соединение.ПолучитьКоннектор(); + Иначе + // Нет активной транзакции - используем общий коннектор + Возврат Коннектор; + КонецЕсли; +КонецФункции + +// Получить коннектор для контекста (для операций в рамках транзакции) +// +// Параметры: +// КонтекстID - Строка - Идентификатор контекста транзакции +// +// Возвращаемое значение: +// АбстрактныйКоннектор - Коннектор, привязанный к контексту +// +Функция ПолучитьКоннекторДляКонтекста(КонтекстID) + Если ПулСоединений = Неопределено Тогда + Возврат Коннектор; + КонецЕсли; + + Соединение = ПулСоединений.ПолучитьСоединениеДляКонтекста(КонтекстID); + Возврат Соединение.ПолучитьКоннектор(); +КонецФункции + // Получает ХранилищеСущностей, привязанное к переданному типу сущности. // // Параметры: @@ -223,7 +408,8 @@ ОбъектМодели = МодельДанных.Получить(ТипСущности); ХранилищеСущностей = ХранилищаСущностей.Получить( ОбъектМодели, - Коннектор + Коннектор, + ЭтотОбъект // Передаем ссылку на менеджер для доступа к пулу соединений ); Возврат ХранилищеСущностей; КонецФункции diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os" new file mode 100644 index 0000000..bfef30b --- /dev/null +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\237\321\203\320\273\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\271.os" @@ -0,0 +1,216 @@ +// Пул соединений для обеспечения потокобезопасности транзакций +// Использует автоматическое назначение коннекторов по контексту выполнения + +Перем ДоступныеСоединения; // Очередь доступных соединений +Перем Карта_КонтекстНаКоннектор; // Соответствие: КонтекстID -> СоединениеСущности +Перем Карта_КоннекторНаКонтекст; // Соответствие: СоединениеСущности -> КонтекстID +Перем РазмерПула; +Перем ТипКоннектора; +Перем СтрокаСоединения; +Перем ПараметрыКоннектора; + +// Конструктор пула соединений +// +// Параметры: +// ТПРазмерПула - Число - Максимальное количество соединений в пуле +// ТПТипКоннектора - Тип - Тип коннектора для создания соединений +// ТПСтрокаСоединения - Строка - Строка соединения для коннекторов +// ТППараметрыКоннектора - Массив - Параметры для коннекторов +// +Процедура ПриСозданииОбъекта(ТПРазмерПула, ТПТипКоннектора, ТПСтрокаСоединения, ТППараметрыКоннектора) + РазмерПула = ТПРазмерПула; + ТипКоннектора = ТПТипКоннектора; + СтрокаСоединения = ТПСтрокаСоединения; + ПараметрыКоннектора = ТППараметрыКоннектора; + + ДоступныеСоединения = Новый Массив; + Карта_КонтекстНаКоннектор = Новый Соответствие; + Карта_КоннекторНаКонтекст = Новый Соответствие; + + // Создаем начальный набор соединений + Счетчик = 0; + Пока Счетчик < РазмерПула Цикл + Соединение = СоздатьНовоеСоединение(); + ДоступныеСоединения.Добавить(Соединение); + Счетчик = Счетчик + 1; + КонецЦикла; +КонецПроцедуры + +// Получить соединение из пула +// +// Возвращаемое значение: +// СоединениеСущности - Свободное соединение для выполнения операций +// +Функция ПолучитьСоединение() Экспорт + Если ДоступныеСоединения.Количество() > 0 Тогда + Соединение = ДоступныеСоединения[0]; + ДоступныеСоединения.Удалить(0); + Возврат Соединение; + Иначе + // Если нет доступных соединений, создаем новое временное + Возврат СоздатьНовоеСоединение(); + КонецЕсли; +КонецФункции + +// Получить соединение для контекста (автоматическое назначение) +// +// Параметры: +// КонтекстID - Строка - Идентификатор контекста выполнения +// +// Возвращаемое значение: +// СоединениеСущности - Соединение, привязанное к контексту +// +Функция ПолучитьСоединениеДляКонтекста(КонтекстID) Экспорт + // Проверяем, есть ли уже соединение для данного контекста + Если Карта_КонтекстНаКоннектор.Получить(КонтекстID) <> Неопределено Тогда + Возврат Карта_КонтекстНаКоннектор[КонтекстID]; + КонецЕсли; + + // Очищаем завершенные контексты перед выделением нового соединения + ОчиститьЗавершенныеКонтексты(); + + // Получаем новое соединение + Соединение = ПолучитьСоединение(); + + // Привязываем к контексту + Карта_КонтекстНаКоннектор[КонтекстID] = Соединение; + Карта_КоннекторНаКонтекст[Соединение] = КонтекстID; + + Возврат Соединение; +КонецФункции + +// Вернуть соединение в пул +// +// Параметры: +// Соединение - СоединениеСущности - Соединение для возврата в пул +// +Процедура ВернутьСоединение(Соединение) Экспорт + // Удаляем привязку к контексту + КонтекстID = Карта_КоннекторНаКонтекст.Получить(Соединение); + Если КонтекстID <> Неопределено Тогда + Карта_КонтекстНаКоннектор.Удалить(КонтекстID); + Карта_КоннекторНаКонтекст.Удалить(Соединение); + КонецЕсли; + + Если ДоступныеСоединения.Количество() < РазмерПула Тогда + // Сбрасываем состояние транзакции перед возвратом в пул + Соединение.СброситьТранзакцию(); + ДоступныеСоединения.Добавить(Соединение); + Иначе + // Пул переполнен, закрываем соединение + Соединение.Закрыть(); + КонецЕсли; +КонецПроцедуры + +// Освободить соединение контекста +// +// Параметры: +// КонтекстID - Строка - Идентификатор контекста для освобождения +// +Процедура ОсвободитьКонтекст(КонтекстID) Экспорт + Соединение = Карта_КонтекстНаКоннектор.Получить(КонтекстID); + Если Соединение <> Неопределено Тогда + ВернутьСоединение(Соединение); + КонецЕсли; +КонецПроцедуры + +// Закрыть все соединения в пуле +// +Процедура ЗакрытьВсеСоединения() Экспорт + // Закрываем все активные соединения в контекстах + Для Каждого Элемент Из Карта_КонтекстНаКоннектор Цикл + Соединение = Элемент.Значение; + Соединение.Закрыть(); + КонецЦикла; + + // Закрываем соединения в пуле + Для Каждого Соединение Из ДоступныеСоединения Цикл + Соединение.Закрыть(); + КонецЦикла; + + ДоступныеСоединения.Очистить(); + Карта_КонтекстНаКоннектор.Очистить(); + Карта_КоннекторНаКонтекст.Очистить(); +КонецПроцедуры + +Процедура ОчиститьЗавершенныеКонтексты() + КонтекстыДляПроверки = Новый Массив; + Для Каждого Элемент Из Карта_КонтекстНаКоннектор Цикл + КонтекстыДляПроверки.Добавить(Элемент.Ключ); + КонецЦикла; + Для Каждого КонтекстID Из КонтекстыДляПроверки Цикл + Если ПроверитьЗавершениеКонтекста(КонтекстID) Тогда + ОсвободитьКонтекст(КонтекстID); + КонецЕсли; + КонецЦикла; +КонецПроцедуры + +// Проверить, завершен ли контекст (фоновое задание или основной поток) +// +// Параметры: +// КонтекстID - Строка - Идентификатор контекста для проверки +// +// Возвращаемое значение: +// Булево - Истина, если контекст завершен и может быть освобожден +// +Функция ПроверитьЗавершениеКонтекста(КонтекстID) + // Проверяем, является ли контекст фоновым заданием + // Контексты фоновых заданий имеют специальный формат или могут быть идентифицированы + + // Простая эвристика: если контекст содержит UUID-подобную структуру, + // предполагаем, что это может быть ID фонового задания + Если СтрДлина(КонтекстID) = 36 И СтрЧислоВхождений(КонтекстID, "-") = 4 Тогда + // Похоже на UUID фонового задания - проверяем через API + Возврат ПроверитьСтатусФоновогоЗадания(КонтекстID); + КонецЕсли; + + // Для обычных контекстов (типа "Контекст_123") используем временную эвристику + // В реальной реализации здесь может быть проверка потоков + Возврат Ложь; // Предполагаем, что контекст еще активен +КонецФункции + +// Проверить статус фонового задания по ID +// +// Параметры: +// ЗаданиеID - Строка - Идентификатор фонового задания +// +// Возвращаемое значение: +// Булево - Истина, если задание завершено +// +Функция ПроверитьСтатусФоновогоЗадания(ЗаданиеID) + Попытка + // Получаем актуальный список заданий. Если задание отсутствует - считаем завершенным. + МассивЗаданий = ФоновыеЗадания.ПолучитьФоновыеЗадания(); + Для Каждого Задание Из МассивЗаданий Цикл + Если Задание.УникальныйИдентификатор <> ЗаданиеID Тогда + Продолжить; + КонецЕсли; + Попытка + Сост = Задание.Состояние; + Исключение + // Не удалось получить состояние - считаем активным + Возврат Ложь; + КонецПопытки; + Если Сост = СостояниеФоновогоЗадания.Активно Тогда + Возврат Ложь; + Иначе + Возврат Истина; + КонецЕсли; + КонецЦикла; + + // Не нашли задание - вероятно завершено и очищено менеджером + Возврат Истина; + Исключение + // При любой ошибке (например, свойство недоступно) возвращаем Ложь - не освобождаем раньше времени + Возврат Ложь; + КонецПопытки; +КонецФункции + +// Создать новое соединение +// +// Возвращаемое значение: +// СоединениеСущности - Новое соединение с открытым коннектором +// +Функция СоздатьНовоеСоединение() + Возврат Новый СоединениеСущности(ТипКоннектора, СтрокаСоединения, ПараметрыКоннектора); +КонецФункции \ No newline at end of file diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" new file mode 100644 index 0000000..ec08535 --- /dev/null +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\241\320\276\320\265\320\264\320\270\320\275\320\265\320\275\320\270\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" @@ -0,0 +1,135 @@ +#Использовать "../internal" + +// Соединение - обертка над коннектором для обеспечения потокобезопасности +// Каждое соединение содержит собственный экземпляр коннектора и состояние транзакции + +Перем Коннектор; +Перем ВТранзакции; + +// Конструктор соединения +// +// Параметры: +// ТипКоннектора - Тип - Тип коннектора для создания +// СтрокаСоединения - Строка - Строка соединения для коннектора +// ПараметрыКоннектора - Массив - Параметры для коннектора +// +Процедура ПриСозданииОбъекта(ТипКоннектора, СтрокаСоединения, ПараметрыКоннектора) + Коннектор = РаботаСКоннекторами.СоздатьКоннектор(ТипКоннектора); + РаботаСКоннекторами.ОткрытьКоннектор(Коннектор, СтрокаСоединения, ПараметрыКоннектора); + ВТранзакции = Ложь; +КонецПроцедуры + +// Получить коннектор для выполнения операций +// +// Возвращаемое значение: +// АбстрактныйКоннектор - Коннектор для работы с БД +// +Функция ПолучитьКоннектор() Экспорт + Возврат Коннектор; +КонецФункции + +// Начать транзакцию +// +Процедура НачатьТранзакцию() Экспорт + Если НЕ ВТранзакции Тогда + РаботаСКоннекторами.НачатьТранзакцию(Коннектор); + ВТранзакции = Истина; + КонецЕсли; +КонецПроцедуры + +// Зафиксировать транзакцию +// +Процедура ЗафиксироватьТранзакцию() Экспорт + Если ВТранзакции Тогда + РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор); + ВТранзакции = Ложь; + КонецЕсли; +КонецПроцедуры + +// Отменить транзакцию +// +Процедура ОтменитьТранзакцию() Экспорт + Если ВТранзакции Тогда + РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + ВТранзакции = Ложь; + КонецЕсли; +КонецПроцедуры + +// Проверить, находится ли соединение в транзакции +// +// Возвращаемое значение: +// Булево - Истина, если транзакция активна +// +Функция ВТранзакции() Экспорт + Возврат ВТранзакции; +КонецФункции + +// Сбросить состояние транзакции (для возврата в пул) +// +Процедура СброситьТранзакцию() Экспорт + Если ВТранзакции Тогда + Попытка + РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + Исключение + // Игнорируем ошибки при сбросе транзакции + КонецПопытки; + ВТранзакции = Ложь; + КонецЕсли; +КонецПроцедуры + +// Закрыть соединение +// +Процедура Закрыть() Экспорт + СброситьТранзакцию(); + РаботаСКоннекторами.ЗакрытьКоннектор(Коннектор); +КонецПроцедуры + +// Сохранить сущность через данное соединение +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Модель сущности +// ПулСущностей - Соответствие - Пул сущностей +// Сущность - Произвольный - Сохраняемая сущность +// +Процедура Сохранить(ОбъектМодели, ПулСущностей, Сущность) Экспорт + РаботаСКоннекторами.Сохранить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); +КонецПроцедуры + +// Получить сущности через данное соединение +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Модель сущности +// ПулСущностей - Соответствие - Пул сущностей +// ОпцииПоиска - ОпцииПоиска - Опции поиска +// +// Возвращаемое значение: +// Массив - Найденные сущности +// +Функция Получить(ОбъектМодели, ПулСущностей, ОпцииПоиска) Экспорт + Возврат РаботаСКоннекторами.Получить(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); +КонецФункции + +// Получить одну сущность через данное соединение +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Модель сущности +// ПулСущностей - Соответствие - Пул сущностей +// ОпцииПоиска - ОпцииПоиска - Опции поиска +// +// Возвращаемое значение: +// Произвольный - Найденная сущность или Неопределено +// +Функция ПолучитьОдно(ОбъектМодели, ПулСущностей, ОпцииПоиска) Экспорт + Возврат РаботаСКоннекторами.ПолучитьОдно(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); +КонецФункции + +// Удалить сущность через данное соединение +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Модель сущности +// ПулСущностей - Соответствие - Пул сущностей +// Сущность - Произвольный - Удаляемая сущность +// +Процедура Удалить(ОбъектМодели, ПулСущностей, Сущность) Экспорт + РаботаСКоннекторами.Удалить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); +КонецПроцедуры \ No newline at end of file diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" index 23f6295..9259ab8 100644 --- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\245\321\200\320\260\320\275\320\270\320\273\320\270\321\211\320\265\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\265\320\271.os" @@ -1,15 +1,17 @@ #Использовать "../internal" -Перем ОбъектМодели; -Перем Коннектор; -Перем ПулСущностей; - -Процедура ПриСозданииОбъекта(Знач ПОбъектМодели, Знач ТипКоннектора, Знач СтрокаСоединения, ПараметрыКоннектора) - ОбъектМодели = ПОбъектМодели; - Коннектор = РаботаСКоннекторами.СоздатьКоннектор(ТипКоннектора); - - РаботаСКоннекторами.ОткрытьКоннектор(Коннектор, СтрокаСоединения, ПараметрыКоннектора); - ПулСущностей = Новый Соответствие(); +Перем ОбъектМодели; +Перем Коннектор; +Перем ПулСущностей; +Перем МенеджерСущностей; // Ссылка на менеджер для доступа к пулу соединений + +Процедура ПриСозданииОбъекта(Знач ПОбъектМодели, Знач ТипКоннектора, Знач СтрокаСоединения, ПараметрыКоннектора, Знач ПМенеджерСущностей = Неопределено) + ОбъектМодели = ПОбъектМодели; + Коннектор = РаботаСКоннекторами.СоздатьКоннектор(ТипКоннектора); + + РаботаСКоннекторами.ОткрытьКоннектор(Коннектор, СтрокаСоединения, ПараметрыКоннектора); + ПулСущностей = Новый Соответствие(); + МенеджерСущностей = ПМенеджерСущностей; КонецПроцедуры // Создает экземпляр сущности, расширенный методами паттерна Active Record. @@ -22,13 +24,20 @@ Возврат АктивнаяЗапись.СоздатьИзХранилища(ОбъектМодели, ЭтотОбъект); КонецФункции -// Сохраняет сущность в БД. -// -// Параметры: -// Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД. -// -Процедура Сохранить(Сущность) Экспорт - РаботаСКоннекторами.Сохранить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); +// Сохраняет сущность в БД. +// +// Параметры: +// Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД. +// Соединение - СоединениеСущности - Соединение для выполнения операции (необязательно) +// +Процедура Сохранить(Сущность, Соединение = Неопределено) Экспорт + Если Соединение <> Неопределено Тогда + // Используем переданное соединение для потокобезопасной операции + Соединение.Сохранить(ОбъектМодели, ПулСущностей, Сущность); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.Сохранить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); + КонецЕсли; КонецПроцедуры @@ -45,12 +54,18 @@ // Массив - Массив найденных сущностей. В качестве элементов массива выступают // экземпляры класса с типом, привязанным к ХранилищуСущностей, с заполненными значениями полей. // -Функция Получить(Знач ОпцииПоиска = Неопределено) Экспорт - Если ОпцииПоиска = Неопределено Тогда - ОпцииПоиска = Новый ОпцииПоиска(); - КонецЕсли; - - Возврат РаботаСКоннекторами.Получить(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); +Функция Получить(Знач ОпцииПоиска = Неопределено, Соединение = Неопределено) Экспорт + Если ОпцииПоиска = Неопределено Тогда + ОпцииПоиска = Новый ОпцииПоиска(); + КонецЕсли; + + Если Соединение <> Неопределено Тогда + // Используем переданное соединение для потокобезопасной операции + Возврат Соединение.Получить(ОбъектМодели, ПулСущностей, ОпцииПоиска); + Иначе + // Обратная совместимость - используем общий коннектор + Возврат РаботаСКоннекторами.Получить(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); + КонецЕсли; КонецФункции // Осуществляет поиск сущности типа, привязанного к ХранилищуСущностей, по идентификатору. @@ -67,22 +82,35 @@ // Произвольный - Если сущность была найдена, то возвращается экземпляр класса с типом, // привязанным к ХранилищуСущностей, с заполненными значениями полей. Иначе возвращается "Неопределено". // -Функция ПолучитьОдно(Знач ОпцииПоиска = Неопределено) Экспорт - Если ОпцииПоиска = Неопределено Тогда - ОпцииПоиска = Новый ОпцииПоиска(); - КонецЕсли; - - Возврат РаботаСКоннекторами.ПолучитьОдно(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); +Функция ПолучитьОдно(Знач ОпцииПоиска = Неопределено, Соединение = Неопределено) Экспорт + Если ОпцииПоиска = Неопределено Тогда + ОпцииПоиска = Новый ОпцииПоиска(); + КонецЕсли; + + Если Соединение <> Неопределено Тогда + // Используем переданное соединение для потокобезопасной операции + Возврат Соединение.ПолучитьОдно(ОбъектМодели, ПулСущностей, ОпцииПоиска); + Иначе + // Обратная совместимость - используем общий коннектор + Возврат РаботаСКоннекторами.ПолучитьОдно(Коннектор, ОбъектМодели, ПулСущностей, ОпцииПоиска); + КонецЕсли; КонецФункции -// Выполняет удаление сущности из базы данных. -// Сущность должна иметь заполненный идентификатор. -// -// Параметры: -// Сущность - Произвольный - Удаляемая сущность -// -Процедура Удалить(Сущность) Экспорт - РаботаСКоннекторами.Удалить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); +// Выполняет удаление сущности из базы данных. +// Сущность должна иметь заполненный идентификатор. +// +// Параметры: +// Сущность - Произвольный - Удаляемая сущность +// Соединение - СоединениеСущности - Соединение для выполнения операции (необязательно) +// +Процедура Удалить(Сущность, Соединение = Неопределено) Экспорт + Если Соединение <> Неопределено Тогда + // Используем переданное соединение для потокобезопасной операции + Соединение.Удалить(ОбъектМодели, ПулСущностей, Сущность); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.Удалить(Коннектор, ОбъектМодели, ПулСущностей, Сущность); + КонецЕсли; КонецПроцедуры // Выполняет очистку текущего Хранилища сущностей. @@ -93,22 +121,56 @@ ПулСущностей.Очистить(); КонецПроцедуры -// Посылает коннектору запрос на начало транзакции. -// -Процедура НачатьТранзакцию() Экспорт - РаботаСКоннекторами.НачатьТранзакцию(Коннектор); -КонецПроцедуры - -// Посылает коннектору запрос на фиксацию транзакции. -// -Процедура ЗафиксироватьТранзакцию() Экспорт - РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор); -КонецПроцедуры +// Посылает коннектору запрос на начало транзакции. +// Если доступен менеджер с пулом соединений, получает соединение из пула. +// +// Возвращаемое значение: +// Соединение - СоединениеСущности для выполнения транзакционных операций (если используется пул) +// Неопределено - Если пул недоступен, транзакция выполняется на общем коннекторе +// +Функция НачатьТранзакцию() Экспорт + Если МенеджерСущностей <> Неопределено Тогда + // Используем пул соединений для потокобезопасности + Соединение = МенеджерСущностей.ПолучитьСоединение(); + Соединение.НачатьТранзакцию(); + Возврат Соединение; + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.НачатьТранзакцию(Коннектор); + Возврат Неопределено; + КонецЕсли; +КонецФункции -// Посылает коннектору запрос на отмену транзакции. -// -Процедура ОтменитьТранзакцию() Экспорт - РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); +// Посылает коннектору запрос на фиксацию транзакции. +// +// Параметры: +// Соединение - СоединениеСущности - Соединение с активной транзакцией (при использовании пула) +// +Процедура ЗафиксироватьТранзакцию(Соединение = Неопределено) Экспорт + Если Соединение <> Неопределено Тогда + // Фиксируем транзакцию и возвращаем соединение в пул + Соединение.ЗафиксироватьТранзакцию(); + МенеджерСущностей.ВернутьСоединение(Соединение); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.ЗафиксироватьТранзакцию(Коннектор); + КонецЕсли; +КонецПроцедуры + +// Посылает коннектору запрос на отмену транзакции. +// +// Параметры: +// Соединение - СоединениеСущности - Соединение с активной транзакцией (при использовании пула) +// +Процедура ОтменитьТранзакцию(Соединение = Неопределено) Экспорт + Если Соединение <> Неопределено Тогда + // Отменяем транзакцию и возвращаем соединение в пул + Соединение.ОтменитьТранзакцию(); + МенеджерСущностей.ВернутьСоединение(Соединение); + Иначе + // Обратная совместимость - используем общий коннектор + РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + КонецЕсли; КонецПроцедуры // Возвращает текущий активный коннектор. diff --git "a/tests/\320\234\320\275\320\276\320\263\320\276\320\277\320\276\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214\320\244\320\276\320\275\320\276\320\262\321\213\320\265\320\227\320\260\320\264\320\260\320\275\320\270\321\217.os" "b/tests/\320\234\320\275\320\276\320\263\320\276\320\277\320\276\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214\320\244\320\276\320\275\320\276\320\262\321\213\320\265\320\227\320\260\320\264\320\260\320\275\320\270\321\217.os" new file mode 100644 index 0000000..30bb9a7 --- /dev/null +++ "b/tests/\320\234\320\275\320\276\320\263\320\276\320\277\320\276\321\202\320\276\321\207\320\275\320\276\321\201\321\202\321\214\320\244\320\276\320\275\320\276\320\262\321\213\320\265\320\227\320\260\320\264\320\260\320\275\320\270\321\217.os" @@ -0,0 +1,376 @@ +// BSLLS:MagicNumber-off +// BSLLS:LatinAndCyrillicSymbolInWord-off +// BSLLS:DuplicateStringLiteral-off +// BSLLS:LineLength-off + +#Использовать ".." +#Использовать "utils" + +Перем МенеджерСущностей; +Перем СтрокаСоединенияКоннектора; // Для использования в тестах с пулом соединений + +Процедура ПередЗапускомТеста() Экспорт + + ЗапускатьТестыPostgres = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_POSTGRES_TESTS", "true"); + ЗапускатьТестыSQLite = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_SQLITE_TESTS", "true"); + + ВыполнятьСбросТаблиц = Ложь; + Если ЗапускатьТестыSQLite = "true" Тогда + СтрокаСоединения = "FullUri=file::memory:?cache=shared"; + //СтрокаСоединения = "Data Source=test.db"; + ТипКоннектора = Тип("КоннекторSQLite"); + ИначеЕсли ЗапускатьТестыPostgres = "true" Тогда + Хост = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_HOST", "localhost"); + Порт = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_PORT", "5432"); + Пользователь = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_USERNAME", "postgres"); + Пароль = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_PASSWORD", "postgres"); + ИмяБД = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("POSTGRES_DATABASE", "postgres"); + СтрокаСоединения = СтрШаблон( + "Host=%1;Username=%2;Password=%3;Database=%4;port=%5;", + Хост, + Пользователь, + Пароль, + ИмяБД, + Порт + ); + ТипКоннектора = Тип("КоннекторPostgreSQL"); + ВыполнятьСбросТаблиц = Истина; + Иначе + ВызватьИсключение "Нет доступного коннектора для тестирования менеджера сущностей"; + КонецЕсли; + + // Сохраняем строку соединения для использования в тестах с пулом + СтрокаСоединенияКоннектора = СтрокаСоединения; + + МенеджерСущностей = Новый МенеджерСущностей(ТипКоннектора, СтрокаСоединения); + + Если ВыполнятьСбросТаблиц Тогда + Коннектор = МенеджерСущностей.ПолучитьКоннектор(); + Коннектор.Открыть(СтрокаСоединения, Новый Массив); + ТестовыеУтилиты.УдалитьТаблицыВБазеДанных(Коннектор); + Коннектор.Закрыть(); + КонецЕсли; + + ПодключитьСценарий( + ОбъединитьПути( + ТекущийКаталог(), + "tests", + "fixtures", + "Автор.os" + ), + "Автор" + ); + ПодключитьСценарий( + ОбъединитьПути( + ТекущийКаталог(), + "tests", + "fixtures", + "СущностьБезГенерируемогоИдентификатора.os" + ), + "СущностьБезГенерируемогоИдентификатора" + ); + ПодключитьСценарий( + ОбъединитьПути( + ТекущийКаталог(), + "tests", + "fixtures", + "СущностьСоВсемиТипамиКолонок.os" + ), + "СущностьСоВсемиТипамиКолонок" + ); + + МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьБезГенерируемогоИдентификатора")); + МенеджерСущностей.ДобавитьКлассВМодель(Тип("Автор")); + МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьСоВсемиТипамиКолонок")); + + МенеджерСущностей.Инициализировать(); + +КонецПроцедуры + +Процедура ПослеЗапускаТеста() Экспорт + МенеджерСущностей.Закрыть(); + МенеджерСущностей = Неопределено; +КонецПроцедуры + +&Тест +Процедура НезависимыеТранзакцииВРазныхКонтекстах() Экспорт + // Проверяем, что транзакции в разных контекстах не влияют друг на друга + + // Контекст 1 - имитирует основной поток + МенеджерСущностей.НачатьТранзакцию(); + + // Сохраняем сущность в основном потоке + Автор1 = Новый Автор; + Автор1.Имя = "Первый"; + Автор1.ВтороеИмя = "Автор"; + МенеджерСущностей.Сохранить(Автор1); + + // TODO: Для полного тестирования нужно реализовать симуляцию фонового задания + // Пока проверяем только основной поток + + // Фиксируем транзакцию основного потока + МенеджерСущностей.ЗафиксироватьТранзакцию(); + + // Проверяем результат - должен остаться автор из основного потока + Результат = МенеджерСущностей.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы"); + Ожидаем.Что(Результат, "Должен остаться автор из основного потока").ИмеетДлину(1); + Ожидаем.Что(Результат[0].Имя, "Должен остаться автор из основного потока").Равно("Первый"); +КонецПроцедуры + +&Тест +Процедура МногократныеНезависимыеТранзакции() Экспорт + // Проверяем работу нескольких независимых транзакций подряд + + // Создаем несколько транзакций в основном потоке (в реальности это могли бы быть разные фоновые задания) + Для Индекс = 1 По 3 Цикл + МенеджерСущностей.НачатьТранзакцию(); + + // В каждой транзакции сохраняем автора + Автор = Новый Автор; + Автор.Имя = "Автор" + Индекс; + Автор.ВтороеИмя = "Контекст" + Индекс; + МенеджерСущностей.Сохранить(Автор); + + // Фиксируем транзакцию + МенеджерСущностей.ЗафиксироватьТранзакцию(); + КонецЦикла; + + // Проверяем, что все авторы сохранились + Результат = МенеджерСущностей.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы ORDER BY Имя"); + Ожидаем.Что(Результат, "Должны сохраниться все три автора").ИмеетДлину(3); + + Для Индекс = 0 По 2 Цикл + ОжидаемоеИмя = "Автор" + (Индекс + 1); + Ожидаем.Что(Результат[Индекс].Имя, "Автор должен быть сохранен корректно").Равно(ОжидаемоеИмя); + КонецЦикла; +КонецПроцедуры + +&Тест +Процедура ОбратнаяСовместимостьБезКонтекстов() Экспорт + // Проверяем, что старый API без указания контекстов продолжает работать + + МенеджерСущностей.НачатьТранзакцию(); // Без возврата КонтекстID + + Автор = Новый Автор; + Автор.Имя = "Старый"; + Автор.ВтороеИмя = "API"; + МенеджерСущностей.Сохранить(Автор); // Без указания контекста + + МенеджерСущностей.ЗафиксироватьТранзакцию(); // Без указания контекста + + // Проверяем результат + Результат = МенеджерСущностей.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы"); + Ожидаем.Что(Результат, "Автор должен быть сохранен").ИмеетДлину(1); + Ожидаем.Что(Результат[0].Имя, "Имя автора должно быть корректным").Равно("Старый"); +КонецПроцедуры + +&Тест +Процедура СмешанноеИспользованиеКонтекстовИБезКонтекстов() Экспорт + // Проверяем совместимость API с автоматическим определением контекстов + + // Транзакция 1 + МенеджерСущностей.НачатьТранзакцию(); + + Автор1 = Новый Автор; + Автор1.Имя = "Первый"; + Автор1.ВтороеИмя = "Автор"; + МенеджерСущностей.Сохранить(Автор1); + + МенеджерСущностей.ЗафиксироватьТранзакцию(); + + // Транзакция 2 + МенеджерСущностей.НачатьТранзакцию(); + + Автор2 = Новый Автор; + Автор2.Имя = "Второй"; + Автор2.ВтороеИмя = "Автор"; + МенеджерСущностей.Сохранить(Автор2); + + МенеджерСущностей.ЗафиксироватьТранзакцию(); + + // Проверяем результат + Результат = МенеджерСущностей.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы ORDER BY Имя"); + Ожидаем.Что(Результат, "Должны быть сохранены оба автора").ИмеетДлину(2); + Ожидаем.Что(Результат[0].Имя, "Первый автор").Равно("Второй"); + Ожидаем.Что(Результат[1].Имя, "Второй автор").Равно("Первый"); +КонецПроцедуры + +&Тест +Процедура СимуляцияФоновогоЗаданияСОшибкой() Экспорт + // Имитируем ситуацию с транзакциями и откатом + + // Основной поток начинает транзакцию + МенеджерСущностей.НачатьТранзакцию(); + + Автор1 = Новый Автор; + Автор1.Имя = "Основной"; + Автор1.ВтороеИмя = "Поток"; + МенеджерСущностей.Сохранить(Автор1); + + // Основной поток завершается успешно + МенеджерСущностей.ЗафиксироватьТранзакцию(); + + // Симулируем ошибочную транзакцию + МенеджерСущностей.НачатьТранзакцию(); + + Автор2 = Новый Автор; + Автор2.Имя = "Ошибочный"; + Автор2.ВтороеИмя = "Автор"; + МенеджерСущностей.Сохранить(Автор2); + + // Симулируем ошибку - откатываем транзакцию + МенеджерСущностей.ОтменитьТранзакцию(); + + // Проверяем, что сохранился только автор из первой транзакции + Результат = МенеджерСущностей.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы"); + Ожидаем.Что(Результат, "Должен остаться только автор из первой транзакции").ИмеетДлину(1); + Ожидаем.Что(Результат[0].Имя, "Должен остаться правильный автор").Равно("Основной"); +КонецПроцедуры + +// Тест для демонстрации реальной работы с ФоновыеЗадания API +// Проверяет многопоточную работу менеджера сущностей в режиме фоновых заданий +&Тест +Процедура МногопоточнаяРаботаСФоновымиЗаданиями() Экспорт + + // Создаем менеджер с пулом соединений для тестирования + ТипКоннекторы = ТипЗнч(МенеджерСущностей.ПолучитьКоннектор()); + МенеджерСПулом = Новый МенеджерСущностей( + ТипКоннекторы, + СтрокаСоединенияКоннектора, // Используем сохраненную строку соединения из ПередЗапускомТеста + Неопределено, + 3 // Пул из 3 соединений + ); + МенеджерСПулом.ДобавитьКлассВМодель(Тип("Автор")); + МенеджерСПулом.Инициализировать(); + + // Начинаем транзакцию в основном потоке + МенеджерСПулом.НачатьТранзакцию(); + + Автор1 = Новый Автор; + Автор1.Имя = "Основной"; + Автор1.ВтороеИмя = "Поток"; + МенеджерСПулом.Сохранить(Автор1); + + // Запускаем несколько фоновых заданий параллельно + МассивЗаданий = Новый Массив; + + Для Индекс = 1 По 2 Цикл + ПараметрыЗадания = Новый Структура; + ПараметрыЗадания.Вставить("МенеджерСущностей", МенеджерСПулом); + ПараметрыЗадания.Вставить("НомерЗадания", Индекс); + + ФоновоеЗадание = ФоновыеЗадания.Выполнить("ФоновоеЗаданиеСозданияАвтораСПулом", ПараметрыЗадания); + МассивЗаданий.Добавить(ФоновоеЗадание); + КонецЦикла; + + // Ждем завершения всех фоновых заданий (максимум 10 секунд) + ОжидатьЗавершенияВсехЗаданий(МассивЗаданий, 100, 100); + + // Фиксируем транзакцию основного потока + МенеджерСПулом.ЗафиксироватьТранзакцию(); + + // Проверяем результат + Результат = МенеджерСПулом.ПолучитьКоннектор().ВыполнитьЗапрос("SELECT * FROM Авторы ORDER BY Имя"); + + // Подсчитываем успешно завершенные задания + КоличествоУспешныхЗаданий = 0; + Для Каждого Задание Из МассивЗаданий Цикл + Если Задание.Состояние = СостояниеФоновогоЗадания.Завершено Тогда + КоличествоУспешныхЗаданий = КоличествоУспешныхЗаданий + 1; + КонецЕсли; + КонецЦикла; + + ОжидаемоеКоличествоАвторов = 1 + КоличествоУспешныхЗаданий; // Основной поток + успешные задания + Ожидаем.Что(Результат, "Количество авторов должно соответствовать успешно завершенным операциям").ИмеетДлину(ОжидаемоеКоличествоАвторов); + + // Проверяем, что автор из основного потока точно есть + НайденОсновнойАвтор = Ложь; + Для Каждого Строка Из Результат Цикл + Если Строка.Имя = "Основной" Тогда + НайденОсновнойАвтор = Истина; + Прервать; + КонецЕсли; + КонецЦикла; + Ожидаем.Что(НайденОсновнойАвтор, "Автор из основного потока должен быть сохранен").ЭтоИстина(); + + МенеджерСПулом.Закрыть(); +КонецПроцедуры + +// Ожидать завершения всех переданных фоновых заданий +// +// Параметры: +// Задания - Массив - Массив фоновых заданий (объекты типа ФоновоеЗадание) +// МаксИтераций - Число - Максимальное количество итераций ожидания +// ИнтервалМС - Число - Пауза между итерациями в миллисекундах +Процедура ОжидатьЗавершенияВсехЗаданий(Задания, МаксИтераций, ИнтервалМС) + СчетчикОжидания = 0; + Пока СчетчикОжидания < МаксИтераций Цикл + ВсеЗавершены = Истина; + Для Каждого Задание Из Задания Цикл + Если Задание.Состояние = СостояниеФоновогоЗадания.Активно Тогда + ВсеЗавершены = Ложь; + Прервать; + КонецЕсли; + КонецЦикла; + Если ВсеЗавершены Тогда + Прервать; + КонецЕсли; + Приостановить(ИнтервалМС); + СчетчикОжидания = СчетчикОжидания + 1; + КонецЦикла; +КонецПроцедуры + + +// Процедура для выполнения в фоновом задании +// Создание автора в отдельной транзакции (используется в тестах фоновых заданий) +// Параметры: +// ПараметрыЗадания - Структура - Данные для выполнения (МенеджерСущностей, НомерЗадания) +Процедура ФоновоеЗаданиеСозданияАвтора(ПараметрыЗадания) Экспорт + МенеджерСущностей = ПараметрыЗадания.МенеджерСущностей; + + // Начинаем транзакцию в фоновом задании (контекст определяется автоматически) + МенеджерСущностей.НачатьТранзакцию(); + + Попытка + Автор = Новый Автор; + Автор.Имя = "Фоновое"; + Автор.ВтороеИмя = "Задание"; + МенеджерСущностей.Сохранить(Автор); + + // Фиксируем транзакцию + МенеджерСущностей.ЗафиксироватьТранзакцию(); + Исключение + // В случае ошибки откатываем транзакцию + МенеджерСущностей.ОтменитьТранзакцию(); + ВызватьИсключение; + КонецПопытки; +КонецПроцедуры + +// Процедура для выполнения в фоновом задании с пулом соединений +// Создание автора при активном пуле соединений (используется в тестах) +// Параметры: +// ПараметрыЗадания - Структура - Данные для выполнения (МенеджерСущностей, НомерЗадания) +Процедура ФоновоеЗаданиеСозданияАвтораСПулом(ПараметрыЗадания) Экспорт + МенеджерСущностей = ПараметрыЗадания.МенеджерСущностей; + НомерЗадания = ПараметрыЗадания.НомерЗадания; + + // Начинаем транзакцию в фоновом задании (контекст определяется автоматически) + МенеджерСущностей.НачатьТранзакцию(); + + Попытка + Автор = Новый Автор; + Автор.Имя = "Фоновое" + НомерЗадания; + Автор.ВтороеИмя = "Задание"; + МенеджерСущностей.Сохранить(Автор); + + // Имитируем некоторую работу + Приостановить(10 + НомерЗадания * 5); // Разные времена выполнения + + // Фиксируем транзакцию + МенеджерСущностей.ЗафиксироватьТранзакцию(); + Исключение + // В случае ошибки откатываем транзакцию + МенеджерСущностей.ОтменитьТранзакцию(); + ВызватьИсключение; + КонецПопытки; +КонецПроцедуры \ No newline at end of file