From acea94e898acbb3d0533d88976d6fbfb3253b1f7 Mon Sep 17 00:00:00 2001 From: sfaqer Date: Wed, 7 Oct 2026 09:00:48 +1000 Subject: [PATCH] =?UTF-8?q?fix(inmemory):=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=BF=D0=B0=D0=B4=D0=B0=D0=B5=D1=82=20?= =?UTF-8?q?=D0=B2=20=D0=BE=D0=B1=D1=89=D1=83=D1=8E=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D1=83=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4?= =?UTF-8?q?=D0=BD=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сохранение добавляло строку в общую таблицу базы раньше, чем вычисляло значения колонок. Если вычисление падало (например, в ссылочном поле не сущность), в таблице оставалась недописанная строка, и поиск ломался у всех потоков этой базы. Теперь объект строки и значения колонок собираются до блокировки, а под блокировкой выдается идентификатор и строка добавляется последней; при сбое сборки идентификатор не выдается. Co-Authored-By: Claude Opus 5.5 --- ...20\272\321\202\320\276\321\200InMemory.os" | 60 ++++++++++++++---- ...20\272\321\202\320\276\321\200InMemory.os" | 61 +++++++++++++++++++ 2 files changed, 110 insertions(+), 11 deletions(-) diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" index 8b5c9d4b..f4d23a01 100644 --- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" @@ -182,10 +182,21 @@ Процедура Сохранить(ОбъектМодели, Сущность) Экспорт Событие = НачатьЗапрос("INSERT", ОбъектМодели); + + // Строка базы собирается до блокировки: конструктор класса сущности - пользовательский код, а + // значения колонок берутся из полей сохраняемого объекта и базы не касаются. Упади сборка, в + // общей таблице не останется недописанной строки, а идентификатор не будет выдан. + Попытка + СущностьВБД = СобратьСущностьВБД(ОбъектМодели, Сущность); + Исключение + ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); + ВызватьИсключение; + КонецПопытки; + Блокировка.Запись().Заблокировать(); Попытка - ЗаписатьСущность(ОбъектМодели, Сущность); + ЗаписатьСущность(ОбъектМодели, Сущность, СущностьВБД); Исключение Блокировка.Запись().Разблокировать(); ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); @@ -197,13 +208,41 @@ КонецПроцедуры +// Создает объект строки базы и переносит в него значения всех колонок, кроме идентификатора: +// идентификатор может выдать только запись, под блокировкой. +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. +// Сущность - Произвольный - Сохраняемая сущность. +// +// Возвращаемое значение: +// Произвольный - Объект строки базы без идентификатора. +// +Функция СобратьСущностьВБД(ОбъектМодели, Сущность) + + СущностьВБД = Новый (ОбъектМодели.ТипСущности()); + ИмяПоляИдентификатора = ОбъектМодели.Идентификатор().ИмяПоля; + + Для Каждого Колонка Из ОбъектМодели.Колонки() Цикл + Если Колонка.ИмяПоля = ИмяПоляИдентификатора Тогда + Продолжить; + КонецЕсли; + Значение = ОбъектМодели.ПолучитьПриведенноеЗначениеПоля(Сущность, Колонка.ИмяПоля); + Рефлектор.УстановитьСвойство(СущностьВБД, Колонка.ИмяПоля, Значение); + КонецЦикла; + + Возврат СущностьВБД; + +КонецФункции + // Записывает сущность в таблицу кеша. // // Параметры: // ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. // Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД. +// СущностьВБД - Произвольный - Объект строки базы со значениями колонок, см. СобратьСущностьВБД. // -Процедура ЗаписатьСущность(ОбъектМодели, Сущность) +Процедура ЗаписатьСущность(ОбъектМодели, Сущность, СущностьВБД) ИмяТаблицы = ОбъектМодели.ИмяТаблицы(); @@ -237,25 +276,24 @@ УчестьВыданныйИдентификатор(ИмяТаблицы, Идентификатор); + ИмяПоляИдентификатора = ОбъектМодели.Идентификатор().ИмяПоля; + Рефлектор.УстановитьСвойство( + СущностьВБД, + ИмяПоляИдентификатора, + ОбъектМодели.ПолучитьПриведенноеЗначениеПоля(Сущность, ИмяПоляИдентификатора) + ); + Если ТипЗнч(Идентификатор) = Тип("Число") Тогда Идентификатор = Формат(Идентификатор, "ЧГ="); КонецЕсли; + // Строка добавляется последней, когда все ее значения уже готовы. СтрокаТЗ = Таблица.Найти(Идентификатор, "_Идентификатор"); Если СтрокаТЗ = Неопределено Тогда СтрокаТЗ = Таблица.Добавить(); КонецЕсли; - СущностьВБД = Новый (ОбъектМодели.ТипСущности()); - - Для Каждого Колонка Из ОбъектМодели.Колонки() Цикл - Значение = ОбъектМодели.ПолучитьПриведенноеЗначениеПоля( - Сущность, - Колонка.ИмяПоля); - Рефлектор.УстановитьСвойство(СущностьВБД, Колонка.ИмяПоля, Значение); - КонецЦикла; - СтрокаТЗ._Идентификатор = Идентификатор; СтрокаТЗ._Сущность = СущностьВБД; diff --git "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" index a47168c3..73c77138 100644 --- "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" +++ "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200InMemory.os" @@ -653,3 +653,64 @@ Ожидаем.Что(НайденныеСтроки, "Коннектор работает после ошибок").ИмеетДлину(1); КонецПроцедуры + +// Таблица общая у всех коннекторов подключения. Сохранение, упавшее на вычислении значения колонки, +// не должно оставить в ней недописанную строку: иначе поиск ломается у всех, кто работает с базой. +// +&Тест +Процедура УпавшееСохранениеНеОставляетНедописаннойСтроки() Экспорт + + // Дано + МодельДанных = Новый МодельДанных(); + ОбъектМодели = МодельДанных.СоздатьОбъектМодели(Тип("СущностьСоВсемиТипамиКолонок")); + Коннектор.ИнициализироватьТаблицу(ОбъектМодели); + + Сохраненная = Новый СущностьСоВсемиТипамиКолонок; + Сохраненная.Целое = 1; + Коннектор.Сохранить(ОбъектМодели, Сохраненная); + + Сломанная = Новый СущностьСоВсемиТипамиКолонок; + Сломанная.Целое = 2; + // В ссылочном поле не сущность: значение колонки не вычислить. + Сломанная.Ссылка = "не сущность"; + + ПараметрыСохранения = Новый Массив; + ПараметрыСохранения.Добавить(ОбъектМодели); + ПараметрыСохранения.Добавить(Сломанная); + + // Когда + Ожидаем.Что(Коннектор, "Сохранение с неверной ссылкой").Метод("Сохранить", ПараметрыСохранения) + .ВыбрасываетИсключение(); + + // Тогда + НайденныеСтроки = Коннектор.НайтиСтрокиВТаблице(ОбъектМодели, Новый ОпцииПоиска()); + Ожидаем.Что(НайденныеСтроки, "Поиск видит только сохраненную строку").ИмеетДлину(1); + + ПараметрыЗадания = Новый Массив; + ПараметрыЗадания.Добавить(СтрокаСоединения); + ПараметрыЗадания.Добавить(ОбъектМодели); + Задание = ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПосчитатьСтрокиВДругомПотоке", ПараметрыЗадания, Истина); + ТестовыеУтилиты.ДождатьсяЗадания(Задание); + + Ожидаем.Что(Задание.ИнформацияОбОшибке, "Поиск в другом потоке не упал").Равно(Неопределено); + Ожидаем.Что(Задание.Результат, "Другой поток видит только сохраненную строку").Равно(1); + +КонецПроцедуры + +// Считает строки таблицы своим коннектором той же базы. Выполняется фоновым заданием. +// +// Параметры: +// СтрокаСоединенияБазы - Строка - Строка подключения базы теста. +// ОбъектМодели - ОбъектМодели - Модель таблицы. +// +// Возвращаемое значение: +// Число - Число строк таблицы. +// +Функция ПосчитатьСтрокиВДругомПотоке(Знач СтрокаСоединенияБазы, ОбъектМодели) Экспорт + + КоннекторПотока = Новый КоннекторInMemory(); + КоннекторПотока.Открыть(СтрокаСоединенияБазы, Новый Массив); + + Возврат КоннекторПотока.НайтиСтрокиВТаблице(ОбъектМодели, Новый ОпцииПоиска()).Количество(); + +КонецФункции