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 388e077..3997e01 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 6a22b4b..4cf4b8e 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 3e20a0c..48dd77e 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; + МенеджерЖурнала.Сохранить(Журнал); + +КонецПроцедуры