From e9cf517688fd5e84641e42a8746e3b53da494df1 Mon Sep 17 00:00:00 2001 From: sfaqer Date: Wed, 7 Oct 2026 08:55:15 +1000 Subject: [PATCH] =?UTF-8?q?fix(sqlite):=20=D0=B8=D0=B4=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D1=80=20=D0=B2=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=BA=D0=B8=20=D1=87=D0=B8=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=D1=81=D1=8F=20=D0=B4=D0=BE=20=D1=80=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D1=8B=D0=BB=D0=BA=D0=B8=20=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Наблюдатель после INSERT, который пишет в ту же базу через менеджер, получает то же соединение и своей вставкой сдвигал last_insert_rowid: сохраненная сущность получала идентификатор чужой строки. Новая АбстрактныйКоннекторSQL.ВставитьСНаблюдением читает идентификатор до ЗавершитьЗапрос. Co-Authored-By: Claude Opus 5.5 --- ...265\320\272\321\202\320\276\321\200SQL.os" | 31 ++++++++ ...\320\272\321\202\320\276\321\200SQLite.os" | 9 ++- ...\320\272\321\202\320\276\321\200SQLite.os" | 75 +++++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) diff --git "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os" "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os" index 388e077e..3997e01a 100644 --- "a/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os" +++ "b/src/\320\232\320\273\320\260\321\201\321\201\321\213/\320\220\320\261\321\201\321\202\321\200\320\260\320\272\321\202\320\275\321\213\320\271\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQL.os" @@ -65,6 +65,37 @@ КонецФункции +// Выполняет подготовленную вставку, сообщая о ней наблюдателям, и возвращает идентификатор +// вставленной строки. +// +// Идентификатор читается до рассылки события вставки: он относится к соединению, а наблюдатель, +// пишущий в ту же базу, работает тем же соединением, и его вставка подменила бы идентификатор. +// +// Параметры: +// Запрос - Запрос - Подготовленный запрос вставки. +// ИмяТаблицы - Строка - Таблица вставки. +// +// Возвращаемое значение: +// Число - Идентификатор вставленной строки. +// +Функция ВставитьСНаблюдением(Запрос, Знач ИмяТаблицы) Экспорт + + Событие = НачатьЗапрос("INSERT", ИмяТаблицы, Запрос.Текст); + + Попытка + Запрос.ВыполнитьКоманду(); + Идентификатор = Коннектор.КонструкторКоннектора.ИДПоследнейДобавленнойЗаписи(Коннектор.Соединение, Запрос); + Исключение + ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); + ВызватьИсключение; + КонецПопытки; + + ЗавершитьЗапрос(Событие); + + Возврат Идентификатор; + +КонецФункции + Функция НачатьЗапрос(Знач ОперацияСУБД, Знач ИмяТаблицы, Знач ТекстЗапроса) Если Наблюдатели = Неопределено Тогда 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\200SQLite.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\200SQLite.os" index 6a22b4bd..4cf4b8ed 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\200SQLite.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\200SQLite.os" @@ -184,20 +184,23 @@ Лог.Отладка("Сохранение сущности с типом %1:%2%3", ОбъектМодели.ТипСущности(), Символы.ПС, ТекстЗапроса); Запрос.Текст = ТекстЗапроса; - КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь); // Идентификатор объекта перезаписывается только тогда, когда его выдала СУБД. Если он был // задан вызывающим кодом, объект уже несет верное значение. // // last_insert_rowid относится к соединению, а соединение занято одним потоком исполнения, - // поэтому вставка и получение идентификатора не разъезжаются и без внешней блокировки. + // поэтому вставку и получение идентификатора не разделит другой поток. Разделить их мог бы + // наблюдатель вставки, пишущий в ту же базу тем же соединением, поэтому идентификатор читается + // до рассылки события, см. ВставитьСНаблюдением. Если СоздаватьНовыйИдентификатор Тогда - ИДПоследнейДобавленнойЗаписи = КонструкторКоннектора.ИДПоследнейДобавленнойЗаписи(Соединение, Запрос); + ИДПоследнейДобавленнойЗаписи = КоннекторSQL.ВставитьСНаблюдением(Запрос, ИмяТаблицы); ОбъектМодели.УстановитьЗначениеКолонкиВПоле( Сущность, ОбъектМодели.Идентификатор().ИмяКолонки, ИДПоследнейДобавленнойЗаписи ); + Иначе + КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь); КонецЕсли; // TODO: Для полей с автоинкрементом - получить значения из базы. diff --git "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" index 3e20a0c7..48dd77e9 100644 --- "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" +++ "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.os" @@ -10,6 +10,9 @@ // ИсточникДанных - Источник менеджера набора: создается в подготовке, закрывается после теста. Перем Источник; Перем Коннектор; +// МенеджерСущностей - Менеджер теста записи журнала наблюдателем; см. +// ЗаписьНаблюдателяПослеВставкиНеСдвигаетИдентификатор. +Перем МенеджерЖурнала; &ВключенЕсли("() -> ЗапускатьТестыSQLite()") &ТестовыйНабор @@ -563,3 +566,75 @@ Ожидаем.Что(НайденныеСтроки, "Повторное сохранение обновило строку, а не добавило").ИмеетДлину(2); КонецПроцедуры + +// Наблюдатель, который после вставки пишет журнал в ту же базу, работает тем же соединением и +// своей вставкой сдвигает last_insert_rowid. Сохраненная сущность должна получить свой +// идентификатор, а не идентификатор строки журнала. +// +&Тест +Процедура ЗаписьНаблюдателяПослеВставкиНеСдвигаетИдентификатор() Экспорт + + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "НаблюдательСДействием.os"), + "НаблюдательСДействием" + ); + + // Пул из одного соединения: база в памяти своя у каждого соединения. + ИсточникЖурнала = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), "Data Source=:memory:", , 1); + ИсточникЖурнала.УстановитьАвтоЗакрытие(Ложь); + + Попытка + ПроверитьИдентификаторПриЗаписиНаблюдателя(ИсточникЖурнала); + Исключение + Ошибка = ИнформацияОбОшибке(); + ИсточникЖурнала.Закрыть(); + ВызватьИсключение; + КонецПопытки; + + ИсточникЖурнала.Закрыть(); + +КонецПроцедуры + +// Сохраняет автора, пока наблюдатель пишет после вставки строку журнала, и проверяет +// идентификатор автора. +// +// Параметры: +// ИсточникЖурнала - ИсточникДанных - Источник с базой в памяти. +// +Процедура ПроверитьИдентификаторПриЗаписиНаблюдателя(ИсточникЖурнала) + + МенеджерЖурнала = ТестовыеУтилиты.МенеджерАвторов(ИсточникЖурнала); + + // Наблюдатель зовет действие в потоке теста, поэтому поля набора ему доступны. + Наблюдатель = Новый НаблюдательСДействием( + "Запрос", + "INSERT", + "После", + Новый Действие(ЭтотОбъект, "ЗаписатьЖурнал"), + Истина + ); + ИсточникЖурнала.ДобавитьНаблюдателя(Наблюдатель); + + Автор = Новый Автор; + Автор.Имя = "Пушкин"; + МенеджерЖурнала.Сохранить(Автор); + + Ожидаем.Что(Наблюдатель.Выполнено(), "Наблюдатель записал журнал").Равно(1); + Ожидаем.Что(Наблюдатель.Ошибки(), "Запись журнала прошла").ИмеетДлину(0); + + СтрокиАвторов = ТестовыеУтилиты.ВыполнитьЗапрос(МенеджерЖурнала, "SELECT Идентификатор FROM Авторы"); + Ожидаем.Что(СтрокиАвторов, "Автор сохранен").ИмеетДлину(1); + Ожидаем.Что(Автор.ВнутреннийИдентификатор, "Автор получил идентификатор своей строки") + .Равно(СтрокиАвторов[0].Идентификатор); + +КонецПроцедуры + +// Сохраняет строку журнала менеджером теста. Действие наблюдателя. +// +Процедура ЗаписатьЖурнал() Экспорт + + Журнал = Новый СущностьБезГенерируемогоИдентификатора; + Журнал.ВнутреннийИдентификатор = 1000; + МенеджерЖурнала.Сохранить(Журнал); + +КонецПроцедуры