From 1be6a367b5a0705491736225452c38fb5c5094cc Mon Sep 17 00:00:00 2001 From: sfaqer Date: Fri, 2 Oct 2026 16:39:22 +1000 Subject: [PATCH 1/2] =?UTF-8?q?fix(connector):=20=D1=86=D0=B5=D0=BB=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B4=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B0=D1=80?= =?UTF-8?q?=D0=B0=D0=BB=D0=BB=D0=B5=D0=BB=D1=8C=D0=BD=D0=BE=D0=B9=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Транзакция, брошенная алгоритмом прямого доступа, откатывается при возврате соединения в пул: SQL-коннектор сам ведет признак транзакции по выполненным запросам (BEGIN/START, COMMIT/END/ROLLBACK, кроме ROLLBACK TO), пул спрашивает его через ВТранзакции(). Раньше такая транзакция уходила с соединением к другому потоку, и его COMMIT фиксировал чужие строки. Сохранение сущности с подчиненными таблицами идет через ВыполнитьМонопольно: SQL - своя транзакция (SQLite - BEGIN IMMEDIATE), InMemory - блокировка записи базы, JSON - блокировка записи каталога вместо блокировки файла. Параллельные сохранения одной сущности больше не смешивают строки коллекций, чтение не видит коллекцию наполовину. InMemory собирает строку до блокировки и добавляет ее последней: упавшее сохранение не оставляет недописанной строки. SQLite читает идентификатор вставки до рассылки события: наблюдатель, пишущий в ту же базу, его больше не подменяет. BREAKING CHANGE: в интерфейсе АбстрактныйКоннектор обязательные методы ВТранзакции() и ВыполнитьМонопольно(Действие); сторонний коннектор без них отвергается при создании ИсточникДанных (Major.Connector). Co-Authored-By: Claude Opus 5.5 --- README.md | 4 + ...20\265\320\272\321\202\320\276\321\200.md" | 36 ++ ...65\320\272\321\202\320\276\321\200JSON.md" | 31 +- ...\320\272\321\202\320\276\321\200SQLite.md" | 29 ++ ...21\202\320\260\320\241\320\221\320\224.md" | 53 +++ ...20\275\320\276\321\201\321\202\320\270.os" | 68 ++++ ...20\270\320\265\320\241\320\221\320\224.os" | 28 +- ...20\276\321\200\320\260\320\274\320\270.os" | 54 +-- ...20\265\320\272\321\202\320\276\321\200.os" | 29 +- ...265\320\272\321\202\320\276\321\200SQL.os" | 170 +++++++++- ...20\272\321\202\320\276\321\200InMemory.os" | 93 +++++- ...65\320\272\321\202\320\276\321\200JSON.os" | 62 +++- ...\272\321\202\320\276\321\200PostgreSQL.os" | 20 ++ ...\320\272\321\202\320\276\321\200SQLite.os" | 39 ++- ...20\275\320\265\320\275\320\270\321\217.os" | 48 +++ ...20\264\320\265\320\275\320\270\321\217.os" | 8 + ...20\260\320\272\321\206\320\270\320\270.os" | 48 +++ ...20\260\320\265\320\274\321\213\320\271.os" | 8 + ...20\260\320\275\320\270\320\265\320\274.os" | 8 + ...21\207\320\265\320\275\320\270\320\270.os" | 8 + ...20\265\321\201\321\202\320\276\320\262.os" | 19 ++ ...21\200\321\213\321\202\320\270\320\265.os" | 18 + ...20\277\321\200\320\276\321\201\320\260.os" | 29 ++ ...20\272\321\206\320\270\320\265\320\271.os" | 20 ++ ...20\262\321\202\320\276\321\200\320\260.os" | 31 ++ ...21\202\320\276\321\200\320\276\320\274.os" | 188 ++++++++++- ...20\260\320\275\320\275\321\213\321\205.os" | 44 +++ ...20\272\321\202\320\276\321\200InMemory.os" | 61 ++++ ...65\320\272\321\202\320\276\321\200JSON.os" | 12 +- ...\320\272\321\202\320\276\321\200SQLite.os" | 75 +++++ ...20\265\320\272\321\206\320\270\320\271.os" | 309 ++++++++++++++++++ ...\320\272\321\206\320\270\320\270SQLite.os" | 150 +++++++++ 32 files changed, 1706 insertions(+), 94 deletions(-) create mode 100644 "src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\320\270\321\201\321\214\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" create mode 100644 "tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\234\320\276\320\275\320\276\320\277\320\276\320\273\321\214\320\275\320\276\320\263\320\276\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" create mode 100644 "tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\237\321\200\320\270\320\267\320\275\320\260\320\272\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270.os" create mode 100644 "tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\260\321\203\320\267\321\213\320\237\320\276\321\201\320\273\320\265\320\227\320\260\320\277\321\200\320\276\321\201\320\260.os" create mode 100644 "tests/fixtures/\320\241\321\203\321\211\320\275\320\276\321\201\321\202\321\214\320\241\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\265\320\271.os" create mode 100644 "tests/fixtures/\320\247\321\202\320\265\320\275\320\270\320\265\320\230\320\227\320\260\320\277\320\270\321\201\321\214\320\220\320\262\321\202\320\276\321\200\320\260.os" create mode 100644 "tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\320\276\320\265\320\241\320\276\321\205\321\200\320\260\320\275\320\265\320\275\320\270\320\265\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\271.os" create mode 100644 "tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\321\213\320\265\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270SQLite.os" diff --git a/README.md b/README.md index 09f7894e..6d87a459 100644 --- a/README.md +++ b/README.md @@ -572,6 +572,10 @@ Например, `КоннекторJSON` не умеет работать с транзакциями, однако, он имеет соответствующие методы, выводящие диагностические сообщения при их вызове. +Коннектор с транзакциями **обязан** сам знать, открыта ли транзакция, и отвечать на это методом `ВТранзакции()`: транзакцию может начать текстом запроса и бросить алгоритм `ВыполнитьСКоннектором`, и пул откатывает ее по этому ответу, прежде чем соединение достанется другому потоку. Коннектор без транзакций отвечает `Ложь`. + +Сохранение сущности с подчиненными таблицами - несколько запросов, и библиотека выполняет их методом коннектора `ВыполнитьМонопольно(Действие)`: коннектор **обязан** выполнить действие так, чтобы другие потоки не видели его промежуточных состояний, - в транзакции или под своей блокировкой. Коннектор, не умеющий изолировать, просто выполняет действие. + Коннектор **может** дополнительно реализовать интерфейс [`НаблюдаемыйКоннектор`](docs/НаблюдаемыйКоннектор.md): тогда наблюдатели источника данных получают события его запросов к СУБД и описание соединения. Встроенные коннекторы реализуют оба интерфейса. Коннектор без второго интерфейса работает как раньше. > Важно! diff --git "a/docs/\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\200.md" "b/docs/\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\200.md" index 77049d42..b2933bd6 100644 --- "a/docs/\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\200.md" +++ "b/docs/\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\200.md" @@ -64,6 +64,42 @@ Процедура ОтменитьТранзакцию() Экспорт ``` +### ВТранзакции + +Коннектор с транзакциями ведет признак сам: транзакцию может начать и бросить алгоритм +`ВыполнитьСКоннектором` мимо библиотеки, и узнать о ней пул может только у коннектора. Коннектор +без транзакций отвечает `Ложь`. + +```bsl +// Определяет, начата ли этим коннектором транзакция, которая еще не завершена: методом +// коннектора или текстом запроса прямого доступа. +// +// По ответу пул откатывает транзакцию, брошенную алгоритмом ВыполнитьСКоннектором, прежде чем +// соединение достанется другому потоку исполнения. Коннектор без транзакций отвечает Ложь. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт +``` + +### ВыполнитьМонопольно + +Библиотека поручает коннектору операции из нескольких запросов - сохранение сущности с +подчиненными таблицами: другие потоки не должны видеть их промежуточных состояний. Встроенные +коннекторы SQL выполняют действие в своей транзакции (внутри уже начатой - просто выполняют), +InMemory и JSON - под блокировкой записи базы или каталога, без отката. Коннектор, не умеющий +изолировать, просто выполняет действие. + +```bsl +// Выполняет действие так, чтобы другие потоки исполнения не видели его промежуточных состояний. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт +``` + ### ИнициализироватьТаблицу ```bsl diff --git "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.md" "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.md" index 6dc1e1e9..f858a1fd 100644 --- "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.md" +++ "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.md" @@ -64,6 +64,31 @@ Процедура ОтменитьТранзакцию() Экспорт ``` +### ВТранзакции + +```bsl +// Определяет, начата ли транзакция. Транзакций коннектор не поддерживает. +// +// Возвращаемое значение: +// Булево - Всегда Ложь. +// +Функция ВТранзакции() Экспорт +``` + +### ВыполнитьМонопольно + +```bsl +// Выполняет действие монопольно: под блокировкой записи каталога базы. Другие потоки процесса +// не читают и не пишут таблицы этого каталога, пока действие не закончится. +// +// Отката нет: если действие упало посередине, записанное до сбоя остается в файлах. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт +``` + ### ИнициализироватьТаблицу ```bsl @@ -104,9 +129,9 @@ ```bsl // Осуществляет поиск строк в таблице по указанному отбору. // -// Файл таблицы читается под блокировкой чтения: параллельные чтения одной таблицы не мешают -// друг другу, но не пересекаются с записью, поэтому одновременные чтение и запись из разных -// потоков одного процесса не конфликтуют за файл. +// Файл таблицы читается под блокировкой чтения каталога: параллельные чтения не мешают друг +// другу, но не пересекаются с записью, поэтому одновременные чтение и запись из разных потоков +// одного процесса не конфликтуют за файл, а монопольное действие не видно наполовину. // // Параметры: // ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. diff --git "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" index 03224015..f9e202e8 100644 --- "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" +++ "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" @@ -45,6 +45,11 @@ ```bsl // Начинает новую транзакцию в БД. // +// Транзакция сразу берет блокировку записи (BEGIN IMMEDIATE). Отложенная взяла бы ее только на +// первой записи, и две транзакции, обе успевшие прочитать, ждали бы друг друга до ошибки +// "database is locked". Немедленная ждет предшественницу на начале - не дольше срока ожидания +// соединения, параметр строки соединения "Default Timeout", по умолчанию 30 секунд. +// Процедура НачатьТранзакцию() Экспорт ``` @@ -64,6 +69,30 @@ Процедура ОтменитьТранзакцию() Экспорт ``` +### ВТранзакции + +```bsl +// Определяет, начата ли этим коннектором транзакция, которая еще не завершена: методом +// коннектора или текстом запроса. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт +``` + +### ВыполнитьМонопольно + +```bsl +// Выполняет действие в транзакции: в своей, если транзакция еще не начата, иначе - в уже +// начатой. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт +``` + ### ИнициализироватьТаблицу ```bsl diff --git "a/docs/\320\237\320\276\321\202\320\276\320\272\320\276\320\261\320\265\320\267\320\276\320\277\320\260\321\201\320\275\320\260\321\217\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\221\320\224.md" "b/docs/\320\237\320\276\321\202\320\276\320\272\320\276\320\261\320\265\320\267\320\276\320\277\320\260\321\201\320\275\320\260\321\217\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\221\320\224.md" index c2994f84..82122b17 100644 --- "a/docs/\320\237\320\276\321\202\320\276\320\272\320\276\320\261\320\265\320\267\320\276\320\277\320\260\321\201\320\275\320\260\321\217\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\221\320\224.md" +++ "b/docs/\320\237\320\276\321\202\320\276\320\272\320\276\320\261\320\265\320\267\320\276\320\277\320\260\321\201\320\275\320\260\321\217\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\221\320\224.md" @@ -133,6 +133,33 @@ PostgreSQL это закрывает параметр строки соедин КонецПопытки; ``` +### Сохранение с подчиненными таблицами + +Сущность с подчиненными таблицами сохраняется несколькими запросами: строка сущности, удаление +прежних строк коллекций, вставка новых. Библиотека выполняет их монопольно, методом коннектора +`ВыполнитьМонопольно`, поэтому параллельные сохранения одной сущности не смешивают строки коллекций, +а чтение не видит коллекцию наполовину: + +- SQLite и PostgreSQL сохраняют в своей транзакции, если контекст транзакцию не начинал, иначе - в + его транзакции. Наблюдатели видят запросы `BEGIN` и `COMMIT`, событий транзакции нет. Своя + транзакция SQLite сразу берет блокировку записи (`BEGIN IMMEDIATE`) и ждет занятую базу не + дольше `Default Timeout` строки соединения, по умолчанию 30 секунд. +- InMemory сохраняет под блокировкой записи базы, JSON - под блокировкой записи каталога: чтения и + записи других потоков этой базы ждут. Отката нет: сохранение, упавшее посередине, оставит + записанное до сбоя. +- Наблюдатели запросов такого сохранения вызываются, пока коннектор держит транзакцию или + блокировку: работа наблюдателя с той же базой из другого потока будет ждать конца сохранения. + +Блокировка JSON одна на каталог: записи разных таблиц одного каталога идут по одной, чтения - +параллельно. + +Потерянное обновление это не лечит: из двух сохранений одной сущности остается последнее. + +Транзакции SQLite, начатые `НачатьТранзакцию`, отложенные: блокировку записи они берут на первой +записи, поэтому параллельные читающие транзакции друг другу не мешают. Но две транзакции, обе +успевшие прочитать и затем пишущие, ждут друг друга до ошибки `database is locked`. Такую работу +выполняйте монопольно - методом коннектора `ВыполнитьМонопольно` в `ВыполнитьСКоннектором`. + ### Прямой доступ к БД Коннектор наружу не выдается: соединение принадлежит пулу, и вернуть его должен тот, кто взял. @@ -158,6 +185,32 @@ PostgreSQL это закрывает параметр строки соедин Если текущий контекст исполнения начал транзакцию, алгоритм получает коннектор этой транзакции и попадает в нее. +Транзакцию для алгоритма открывайте менеджером: `МенеджерСущностей.НачатьТранзакцию()` до +`ВыполнитьСКоннектором`, фиксация или отмена - после. Транзакция, которую алгоритм начал сам - +методом коннектора или запросом `BEGIN`, - и не завершил, откатывается при возврате соединения в +пул: иначе она ушла бы с соединением к другому контексту, и его фиксация записала бы строки +алгоритма. Наблюдатели увидят запрос `ROLLBACK`, события транзакции у нее нет: начинала ее не +библиотека. + +```bsl +// Удаление не применится: транзакция брошена алгоритмом и откатывается при возврате соединения +МенеджерСущностей.ВыполнитьСКоннектором( + "Коннектор -> Коннектор.ВыполнитьЗапрос(""BEGIN""); Коннектор.ВыполнитьЗапрос(Текст)", + Новый Структура("Текст", "DELETE FROM Авторы") +); +``` + +О транзакции коннектор узнает по первому оператору каждого выполненного запроса: `BEGIN` и +`START TRANSACTION` ее начинают, `COMMIT`, `END` и `ROLLBACK` завершают, откат к точке сохранения +(`ROLLBACK TO`) не завершает. Не видны ему транзакции, начатые или завершенные вторым оператором +многооператорного текста, процедурой на стороне СУБД, а в SQLite - еще и `SAVEPOINT` вне транзакции +и `RELEASE` последней точки сохранения. О транзакции пулу сообщает сам коннектор методом +`ВТранзакции()` интерфейса `АбстрактныйКоннектор`: сторонний коннектор с транзакциями должен вести +этот признак сам. + +Коннектор не храните вне алгоритма: после выхода из него соединение принадлежит пулу и может +работать в другом контексте. + Прежний `ПолучитьКоннектор()` удален: он отдавал соединение наружу, не имея способа получить его обратно, поэтому соединение оставалось за контекстом навсегда. diff --git "a/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\320\270\321\201\321\214\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\320\270\321\201\321\214\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" new file mode 100644 index 00000000..03960736 --- /dev/null +++ "b/src/internal/\320\232\320\273\320\260\321\201\321\201\321\213/\320\227\320\260\320\277\320\270\321\201\321\214\320\241\321\203\321\211\320\275\320\276\321\201\321\202\320\270.os" @@ -0,0 +1,68 @@ +// Запись сущности в таблицы коннектора: строка сущности и строки ее подчиненных таблиц. +// +// Записывается одним действием, чтобы коннектор мог выполнить его монопольно, см. +// АбстрактныйКоннектор.ВыполнитьМонопольно: иначе два сохранения одной сущности перемежались бы, +// а чтение видело бы коллекцию между удалением прежних строк и вставкой новых. + +// АбстрактныйКоннектор - Коннектор занятого соединения. +Перем Коннектор; +// ОбъектМодели - Объект модели сущности. +Перем ОбъектМодели; +// Произвольный - Сохраняемая сущность. +Перем Сущность; + +// Параметры: +// ПКоннектор - АбстрактныйКоннектор - Коннектор занятого соединения. +// ПОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. +// ПСущность - Произвольный - Сохраняемая сущность. +// +Процедура ПриСозданииОбъекта(ПКоннектор, ПОбъектМодели, ПСущность) + Коннектор = ПКоннектор; + ОбъектМодели = ПОбъектМодели; + Сущность = ПСущность; +КонецПроцедуры + +// Записывает строку сущности и строки ее подчиненных таблиц. +// +Процедура Записать() Экспорт + Коннектор.Сохранить(ОбъектМодели, Сущность); + СохранитьПодчиненныеТаблицы(); +КонецПроцедуры + +// Перезаписывает строки подчиненных таблиц сущности: старые строки удаляются, текущее +// содержимое коллекций сохраняется заново. +// +Процедура СохранитьПодчиненныеТаблицы() + + Для Каждого ПодчиненнаяТаблица Из ОбъектМодели.ПодчиненныеТаблицы() Цикл + + ОбъектМоделиЭлементКоллекции = ОбработкаКоллекций.ПолучитьОбъектМоделиДляПодчиненнойТаблицы( + ОбъектМодели, + ПодчиненнаяТаблица + ); + + // Очистка подчиненной таблицы + ОпцииПоиска = Новый ОпцииПоиска(); + ОпцииПоиска.Отбор("ref", ВидСравнения.Равно, ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность)); + Коннектор.УдалитьСтрокиВТаблице(ОбъектМоделиЭлементКоллекции, ОпцииПоиска); + + РасширяемоеПоле = ОбъектМодели.ПолучитьЗначениеПоля(Сущность, ПодчиненнаяТаблица.ИмяПоля); + + Если НЕ ЗначениеЗаполнено(РасширяемоеПоле) Тогда + Продолжить; + КонецЕсли; + + КоллекцияСущностей = ОбработкаКоллекций.ПреобразоватьКоллекциюККоллекцииСущностей( + РасширяемоеПоле, + Сущность, + ОбъектМодели, + ПодчиненнаяТаблица + ); + + Для Каждого ЭлементКоллекции Из КоллекцияСущностей Цикл + Коннектор.Сохранить(ОбъектМоделиЭлементКоллекции, ЭлементКоллекции); + КонецЦикла; + + КонецЦикла; + +КонецПроцедуры diff --git "a/src/internal/\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\320\221\320\224.os" "b/src/internal/\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\320\221\320\224.os" index 2b883637..16b6c9d4 100644 --- "a/src/internal/\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\320\221\320\224.os" +++ "b/src/internal/\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\320\221\320\224.os" @@ -209,9 +209,10 @@ // Отмечает возврат соединения в пул. // // Незавершенная транзакция при этом откатывается: соединение уходит следующему потоку, и -// оставлять на нем чужую начатую транзакцию нельзя. Ошибка отката наружу не идет - соединение -// возвращается в пул в любом случае, иначе оно было бы потеряно, - но соединение после нее -// непригодно так же, как после неудачного явного завершения транзакции. +// оставлять на нем чужую начатую транзакцию нельзя. Откатывается и транзакция, которую начал и +// бросил алгоритм прямого доступа мимо этого соединения: о ней знает коннектор. Ошибка отката +// наружу не идет - соединение возвращается в пул в любом случае, иначе оно было бы потеряно, - но +// соединение после нее непригодно так же, как после неудачного явного завершения транзакции. // Процедура ОтметитьВозврат() Экспорт @@ -227,6 +228,8 @@ КонецПопытки; ВТранзакции = Ложь; ЗавершитьВерхнееСобытиеТранзакции("abandoned", ОшибкаОтката); + Иначе + ОткатитьТранзакциюКоннектора(); КонецЕсли; // Остаток стопки - события начал, которым не досталось своего завершения: их закрывает @@ -239,6 +242,25 @@ КонецПроцедуры +// Откатывает транзакцию, которую начал мимо этого соединения и не завершил алгоритм прямого +// доступа. +// +// Начинала ее не библиотека, поэтому события транзакции у нее нет: наблюдатели увидят только +// запрос отката. Если коннектор не смог ответить или откатить, состояние транзакции неизвестно и +// соединение непригодно. +// +Процедура ОткатитьТранзакциюКоннектора() + + Попытка + Если Коннектор.ВТранзакции() Тогда + РаботаСКоннекторами.ОтменитьТранзакцию(Коннектор); + КонецЕсли; + Исключение + ИспортитьПослеНеудачногоЗавершения(); + КонецПопытки; + +КонецПроцедуры + // Закрывает соединение. // Процедура Закрыть() Экспорт diff --git "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os" "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os" index 5cd1bc2c..9d9d53dd 100644 --- "a/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os" +++ "b/src/internal/\320\234\320\276\320\264\321\203\320\273\320\270/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\260\320\274\320\270.os" @@ -38,8 +38,15 @@ Попытка Коннектор = Соединение.ПолучитьКоннектор(); - Коннектор.Сохранить(ОбъектМодели, Сущность); - СохранитьПодчиненныеТаблицы(Коннектор, ОбъектМодели, Сущность); + Запись = Новый ЗаписьСущности(Коннектор, ОбъектМодели, Сущность); + + // Сущность с подчиненными таблицами пишется несколькими запросами, и другие потоки не + // должны видеть промежуточных состояний. Строка без подчиненных таблиц - один запрос. + Если ОбъектМодели.ПодчиненныеТаблицы().Количество() = 0 Тогда + Запись.Записать(); + Иначе + Коннектор.ВыполнитьМонопольно(Новый Действие(Запись, "Записать")); + КонецЕсли; // Пул сущностей обновляется до возврата соединения: экземпляр на идентификатор должен // остаться один, а возврат идет уже после работы с СУБД и может упасть на закрытии @@ -360,49 +367,6 @@ КонецПроцедуры -// Перезаписывает строки подчиненных таблиц сущности: старые строки удаляются, текущее -// содержимое коллекций сохраняется заново. -// -// Параметры: -// Коннектор - АбстрактныйКоннектор - Коннектор занятого соединения. -// ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. -// Сущность - Произвольный - Сохраняемая сущность. -// -Процедура СохранитьПодчиненныеТаблицы(Коннектор, ОбъектМодели, Сущность) - - Для Каждого ПодчиненнаяТаблица Из ОбъектМодели.ПодчиненныеТаблицы() Цикл - - ОбъектМоделиЭлементКоллекции = ОбработкаКоллекций.ПолучитьОбъектМоделиДляПодчиненнойТаблицы( - ОбъектМодели, - ПодчиненнаяТаблица - ); - - // Очистка подчиненной таблицы - ОпцииПоиска = Новый ОпцииПоиска(); - ОпцииПоиска.Отбор("ref", ВидСравнения.Равно, ОбъектМодели.ПолучитьЗначениеИдентификатора(Сущность)); - Коннектор.УдалитьСтрокиВТаблице(ОбъектМоделиЭлементКоллекции, ОпцииПоиска); - - РасширяемоеПоле = ОбъектМодели.ПолучитьЗначениеПоля(Сущность, ПодчиненнаяТаблица.ИмяПоля); - - Если НЕ ЗначениеЗаполнено(РасширяемоеПоле) Тогда - Продолжить; - КонецЕсли; - - КоллекцияСущностей = ОбработкаКоллекций.ПреобразоватьКоллекциюККоллекцииСущностей( - РасширяемоеПоле, - Сущность, - ОбъектМодели, - ПодчиненнаяТаблица - ); - - Для Каждого ЭлементКоллекции Из КоллекцияСущностей Цикл - Коннектор.Сохранить(ОбъектМоделиЭлементКоллекции, ЭлементКоллекции); - КонецЦикла; - - КонецЦикла; - -КонецПроцедуры - // Собирает сущность по прочитанной строке таблицы: берет экземпляр из пула сущностей, // заполняет колонки и подчиненные таблицы. // 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\200.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\200.os" index 9fdf764a..dbfdfa35 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\200.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\200.os" @@ -53,7 +53,34 @@ // Отменяет открытую транзакцию в БД. // Процедура ОтменитьТранзакцию() Экспорт - + +КонецПроцедуры + +// Определяет, начата ли этим коннектором транзакция, которая еще не завершена: методом +// коннектора или текстом запроса прямого доступа. +// +// По ответу пул откатывает транзакцию, брошенную алгоритмом ВыполнитьСКоннектором, прежде чем +// соединение достанется другому потоку исполнения. Коннектор без транзакций отвечает Ложь. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +// Выполняет действие так, чтобы другие потоки исполнения не видели его промежуточных состояний. +// +// Библиотека выполняет так операции из нескольких запросов - сохранение сущности с подчиненными +// таблицами. Коннектор с транзакциями выполняет действие в своей транзакции, а внутри уже +// начатой - просто выполняет; коннектор без транзакций - под своей блокировкой записи. Коннектор, +// не умеющий изолировать, просто выполняет действие. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + Действие.Выполнить(); КонецПроцедуры // Создает таблицу в БД по данным модели. 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..f088625b 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" @@ -6,10 +6,14 @@ // НаблюдателиИсточникаДанных - Наблюдатели пула, которым сообщается о каждом запросе. Неопределено, // пока коннектор работает вне пула. Перем Наблюдатели; +// Булево - Транзакция начата запросом этого коннектора и еще не завершена, см. ОтметитьТранзакцию. +// Соединение занято одним потоком исполнения, поэтому признак читает и пишет только он. +Перем ТранзакцияОткрыта; Процедура ПриСозданииОбъекта(пКоннектор, пЛог = Неопределено) Коннектор = пКоннектор; + ТранзакцияОткрыта = Ложь; Если ЗначениеЗаполнено(пЛог) Тогда Лог = пЛог; Иначе @@ -59,12 +63,172 @@ ВызватьИсключение; КонецПопытки; + ОтметитьТранзакцию(ОперацияСУБД, Запрос.Текст); ЗавершитьЗапрос(Событие, , ?(ВернутьРезультат, Результат.Количество(), Неопределено)); Возврат Результат; КонецФункции +// Выполняет подготовленную вставку, сообщая о ней наблюдателям, и возвращает идентификатор +// вставленной строки. +// +// Идентификатор читается до рассылки события вставки: он относится к соединению, а наблюдатель, +// пишущий в ту же базу, работает тем же соединением, и его вставка подменила бы идентификатор. +// +// Параметры: +// Запрос - Запрос - Подготовленный запрос вставки. +// ИмяТаблицы - Строка - Таблица вставки. +// +// Возвращаемое значение: +// Число - Идентификатор вставленной строки. +// +Функция ВставитьСНаблюдением(Запрос, Знач ИмяТаблицы) Экспорт + + Событие = НачатьЗапрос("INSERT", ИмяТаблицы, Запрос.Текст); + + Попытка + Запрос.ВыполнитьКоманду(); + Идентификатор = Коннектор.КонструкторКоннектора.ИДПоследнейДобавленнойЗаписи(Коннектор.Соединение, Запрос); + Исключение + ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); + ВызватьИсключение; + КонецПопытки; + + ЗавершитьЗапрос(Событие); + + Возврат Идентификатор; + +КонецФункции + +// Выполняет действие в транзакции: в своей, если транзакция еще не начата, иначе - в уже +// начатой. +// +// Своя транзакция начинается переданной командой и фиксируется методом коннектора, сбой действия +// ее отменяет. Если отмена не удалась, признак транзакции остается, и ее откатит пул при возврате +// соединения. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// КомандаНачала - Строка - Команда начала своей транзакции: монопольному действию СУБД может +// требоваться не та транзакция, что начинает НачатьТранзакцию. +// +Процедура ВыполнитьМонопольно(Действие, Знач КомандаНачала) Экспорт + + Если ТранзакцияОткрыта Тогда + Действие.Выполнить(); + Возврат; + КонецЕсли; + + Запрос = Коннектор.КонструкторКоннектора.НовыйЗапрос(Коннектор.Соединение); + Запрос.Текст = КомандаНачала; + ВыполнитьСНаблюдением(Запрос, "BEGIN", "", Ложь); + + Попытка + Действие.Выполнить(); + Исключение + Ошибка = ИнформацияОбОшибке(); + ОтменитьТранзакциюПослеОшибки(); + ВызватьИсключение; + КонецПопытки; + + Коннектор.ЗафиксироватьТранзакцию(); + +КонецПроцедуры + +// Отменяет свою транзакцию после ошибки действия, не подменяя эту ошибку своей. +// +// Ошибку отмены наблюдатели получают событием запроса ROLLBACK, а транзакция остается отмеченной +// открытой: ее откатит пул при возврате соединения или признает соединение непригодным. +// +Процедура ОтменитьТранзакциюПослеОшибки() + + Попытка + Коннектор.ОтменитьТранзакцию(); + Исключение + // Состояние транзакции неизвестно: признак остается, наружу идет ошибка действия. + ТранзакцияОткрыта = Истина; + КонецПопытки; + +КонецПроцедуры + +// Определяет, начата ли запросом этого коннектора транзакция, которая еще не завершена. +// +// Признак знает и о транзакции, начатой мимо библиотеки: методом коннектора или текстом запроса +// в прямом доступе. Пул по нему откатывает транзакцию, брошенную алгоритмом, прежде чем отдать +// соединение другому потоку исполнения. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт + Возврат ТранзакцияОткрыта; +КонецФункции + +// Отмечает начало или завершение транзакции успешно выполненным запросом. +// +// Начало - BEGIN и START TRANSACTION, завершение - COMMIT, END и ROLLBACK; откат к точке +// сохранения транзакцию не завершает. Неудавшийся запрос признак не меняет: в каком состоянии +// осталась транзакция, неизвестно, и лишний откат при возврате соединения в пул безопаснее, чем +// пропущенный. +// +// Признак ведется по первому оператору текста. Транзакцию, завершенную вторым оператором +// многооператорного текста или процедурой на стороне СУБД, он не видит. +// +// Параметры: +// ОперацияСУБД - Строка - Операция выполненного запроса. +// ТекстЗапроса - Строка - Текст выполненного запроса. +// +Процедура ОтметитьТранзакцию(Знач ОперацияСУБД, Знач ТекстЗапроса) + + Если ОперацияСУБД = "BEGIN" ИЛИ ОперацияСУБД = "START" Тогда + ТранзакцияОткрыта = Истина; + ИначеЕсли ОперацияСУБД = "COMMIT" ИЛИ ОперацияСУБД = "END" Тогда + ТранзакцияОткрыта = Ложь; + ИначеЕсли ОперацияСУБД = "ROLLBACK" И НЕ ЭтоОткатКТочкеСохранения(ТекстЗапроса) Тогда + ТранзакцияОткрыта = Ложь; + КонецЕсли; + +КонецПроцедуры + +// Определяет, откатывает ли ROLLBACK к точке сохранения: ROLLBACK [WORK | TRANSACTION] TO ... +// +// Параметры: +// ТекстЗапроса - Строка - Текст запроса ROLLBACK. +// +// Возвращаемое значение: +// Булево - Истина, если откат к точке сохранения. +// +Функция ЭтоОткатКТочкеСохранения(Знач ТекстЗапроса) + + Слова = СловаЗапроса(ТекстЗапроса); + + НомерСлова = 1; + Если Слова.Количество() > НомерСлова И (Слова[НомерСлова] = "WORK" ИЛИ Слова[НомерСлова] = "TRANSACTION") Тогда + НомерСлова = НомерСлова + 1; + КонецЕсли; + + Возврат Слова.Количество() > НомерСлова И Слова[НомерСлова] = "TO"; + +КонецФункции + +// Разбивает текст запроса на слова в верхнем регистре. Разделители - пробельные символы и точка +// с запятой. +// +// Параметры: +// ТекстЗапроса - Строка - Текст запроса или команды. +// +// Возвращаемое значение: +// Массив из Строка - Слова текста. +// +Функция СловаЗапроса(Знач ТекстЗапроса) + + Разделители = " ;" + Символы.Таб + Символы.ПС + Символы.ВК; + + Возврат СтрРазделить(ВРег(ТекстЗапроса), Разделители, Ложь); + +КонецФункции + Функция НачатьЗапрос(Знач ОперацияСУБД, Знач ИмяТаблицы, Знач ТекстЗапроса) Если Наблюдатели = Неопределено Тогда @@ -95,15 +259,15 @@ // Функция ОперацияИзТекста(Знач ТекстЗапроса) - Слова = СтрРазделить(СокрЛП(СтрЗаменить(ТекстЗапроса, Символы.ПС, " ")), " ", Ложь); + Слова = СловаЗапроса(ТекстЗапроса); Если Слова.Количество() = 0 Тогда Возврат ""; КонецЕсли; - Операция = ВРег(Слова[0]); + Операция = Слова[0]; ОперацияНадСхемой = Операция = "CREATE" ИЛИ Операция = "DROP" ИЛИ Операция = "ALTER"; Если ОперацияНадСхемой И Слова.Количество() > 1 Тогда - Операция = Операция + " " + ВРег(Слова[1]); + Операция = Операция + " " + Слова[1]; КонецЕсли; Возврат Операция; 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..afe6acaa 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" @@ -93,6 +93,39 @@ ВызватьИсключение "Не поддерживается"; КонецПроцедуры +// Определяет, начата ли транзакция. Транзакций коннектор не поддерживает. +// +// Возвращаемое значение: +// Булево - Всегда Ложь. +// +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +// Выполняет действие монопольно: под блокировкой записи базы. Другие потоки не читают и не пишут +// таблицы этой базы, пока действие не закончится; операции самого действия берут ту же блокировку +// повторно. +// +// Отката нет: если действие упало посередине, записанное до сбоя остается в таблицах. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + + Блокировка.Запись().Заблокировать(); + + Попытка + Действие.Выполнить(); + Исключение + Блокировка.Запись().Разблокировать(); + ВызватьИсключение; + КонецПопытки; + + Блокировка.Запись().Разблокировать(); + +КонецПроцедуры + // Создает таблицу в БД по данным модели. // // Параметры: @@ -182,10 +215,21 @@ Процедура Сохранить(ОбъектМодели, Сущность) Экспорт Событие = НачатьЗапрос("INSERT", ОбъектМодели); + + // Строка базы собирается до блокировки: конструктор класса сущности - пользовательский код, а + // значения колонок берутся из полей сохраняемого объекта и базы не касаются. Упади сборка, в + // общей таблице не останется недописанной строки, а идентификатор не будет выдан. + Попытка + СущностьВБД = СобратьСущностьВБД(ОбъектМодели, Сущность); + Исключение + ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); + ВызватьИсключение; + КонецПопытки; + Блокировка.Запись().Заблокировать(); Попытка - ЗаписатьСущность(ОбъектМодели, Сущность); + ЗаписатьСущность(ОбъектМодели, Сущность, СущностьВБД); Исключение Блокировка.Запись().Разблокировать(); ЗавершитьЗапрос(Событие, ИнформацияОбОшибке()); @@ -197,13 +241,41 @@ КонецПроцедуры +// Создает объект строки базы и переносит в него значения всех колонок, кроме идентификатора: +// идентификатор может выдать только запись, под блокировкой. +// +// Параметры: +// ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. +// Сущность - Произвольный - Сохраняемая сущность. +// +// Возвращаемое значение: +// Произвольный - Объект строки базы без идентификатора. +// +Функция СобратьСущностьВБД(ОбъектМодели, Сущность) + + СущностьВБД = Новый (ОбъектМодели.ТипСущности()); + ИмяПоляИдентификатора = ОбъектМодели.Идентификатор().ИмяПоля; + + Для Каждого Колонка Из ОбъектМодели.Колонки() Цикл + Если Колонка.ИмяПоля = ИмяПоляИдентификатора Тогда + Продолжить; + КонецЕсли; + Значение = ОбъектМодели.ПолучитьПриведенноеЗначениеПоля(Сущность, Колонка.ИмяПоля); + Рефлектор.УстановитьСвойство(СущностьВБД, Колонка.ИмяПоля, Значение); + КонецЦикла; + + Возврат СущностьВБД; + +КонецФункции + // Записывает сущность в таблицу кеша. // // Параметры: // ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. // Сущность - Произвольный - Объект (экземпляр класса, зарегистрированного в модели) для сохранения в БД. +// СущностьВБД - Произвольный - Объект строки базы со значениями колонок, см. СобратьСущностьВБД. // -Процедура ЗаписатьСущность(ОбъектМодели, Сущность) +Процедура ЗаписатьСущность(ОбъектМодели, Сущность, СущностьВБД) ИмяТаблицы = ОбъектМодели.ИмяТаблицы(); @@ -237,25 +309,24 @@ УчестьВыданныйИдентификатор(ИмяТаблицы, Идентификатор); + ИмяПоляИдентификатора = ОбъектМодели.Идентификатор().ИмяПоля; + Рефлектор.УстановитьСвойство( + СущностьВБД, + ИмяПоляИдентификатора, + ОбъектМодели.ПолучитьПриведенноеЗначениеПоля(Сущность, ИмяПоляИдентификатора) + ); + Если ТипЗнч(Идентификатор) = Тип("Число") Тогда Идентификатор = Формат(Идентификатор, "ЧГ="); КонецЕсли; + // Строка добавляется последней, когда все ее значения уже готовы. СтрокаТЗ = Таблица.Найти(Идентификатор, "_Идентификатор"); Если СтрокаТЗ = Неопределено Тогда СтрокаТЗ = Таблица.Добавить(); КонецЕсли; - СущностьВБД = Новый (ОбъектМодели.ТипСущности()); - - Для Каждого Колонка Из ОбъектМодели.Колонки() Цикл - Значение = ОбъектМодели.ПолучитьПриведенноеЗначениеПоля( - Сущность, - Колонка.ИмяПоля); - Рефлектор.УстановитьСвойство(СущностьВБД, Колонка.ИмяПоля, Значение); - КонецЦикла; - СтрокаТЗ._Идентификатор = Идентификатор; СтрокаТЗ._Сущность = СущностьВБД; 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\200JSON.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\200JSON.os" index a0498f6a..5623d0a2 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\200JSON.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\200JSON.os" @@ -132,6 +132,52 @@ ВызватьИсключение "Не поддерживается"; КонецПроцедуры +// Определяет, начата ли транзакция. Транзакций коннектор не поддерживает. +// +// Возвращаемое значение: +// Булево - Всегда Ложь. +// +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +// Выполняет действие монопольно: под блокировкой записи каталога базы. Другие потоки процесса +// не читают и не пишут таблицы этого каталога, пока действие не закончится. +// +// Отката нет: если действие упало посередине, записанное до сбоя остается в файлах. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + + Блокировка = БлокировкаКаталога().Запись(); + Блокировка.Заблокировать(); + + Попытка + Действие.Выполнить(); + Исключение + Блокировка.Разблокировать(); + ВызватьИсключение; + КонецПопытки; + + Блокировка.Разблокировать(); + +КонецПроцедуры + +// Возвращает блокировку каталога базы - общую на процесс для всех коннекторов этого каталога. +// +// Блокировка одна на каталог, а не на файл таблицы: монопольное действие пишет несколько таблиц, +// и изолировать его по отдельным файлам нельзя без взаимных блокировок. Чтения таблиц каталога +// идут параллельно, записи - по одной. +// +// Возвращаемое значение: +// БлокировкаЧтенияЗаписи - Блокировка каталога. +// +Функция БлокировкаКаталога() + Возврат БлокировкиЧтенияЗаписи.Получить(БазовыйКаталог); +КонецФункции + // Создает таблицу в БД по данным модели. // // Параметры: @@ -141,7 +187,7 @@ Событие = НачатьЗапрос("CREATE TABLE", ОбъектМодели); ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись(); + Блокировка = БлокировкаКаталога().Запись(); Блокировка.Заблокировать(); Попытка @@ -175,7 +221,7 @@ Процедура Сохранить(ОбъектМодели, Сущность) Экспорт Событие = НачатьЗапрос("INSERT", ОбъектМодели); ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись(); + Блокировка = БлокировкаКаталога().Запись(); Блокировка.Заблокировать(); Попытка @@ -236,7 +282,7 @@ Событие = НачатьЗапрос("DELETE", ОбъектМодели); ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись(); + Блокировка = БлокировкаКаталога().Запись(); Блокировка.Заблокировать(); Попытка @@ -274,9 +320,9 @@ // Осуществляет поиск строк в таблице по указанному отбору. // -// Файл таблицы читается под блокировкой чтения: параллельные чтения одной таблицы не мешают -// друг другу, но не пересекаются с записью, поэтому одновременные чтение и запись из разных -// потоков одного процесса не конфликтуют за файл. +// Файл таблицы читается под блокировкой чтения каталога: параллельные чтения не мешают друг +// другу, но не пересекаются с записью, поэтому одновременные чтение и запись из разных потоков +// одного процесса не конфликтуют за файл, а монопольное действие не видно наполовину. // // Параметры: // ОбъектМодели - ОбъектМодели - Объект, содержащий описание класса-сущности и настроек таблицы БД. @@ -323,7 +369,7 @@ НайденныеСтроки = Новый Массив; ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Чтение(); + Блокировка = БлокировкаКаталога().Чтение(); Блокировка.Заблокировать(); Попытка @@ -483,7 +529,7 @@ Событие = НачатьЗапрос("DELETE", ОбъектМодели); ИмяФайла = ОбъединитьПути(БазовыйКаталог, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла).Запись(); + Блокировка = БлокировкаКаталога().Запись(); Блокировка.Заблокировать(); Попытка 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\200PostgreSQL.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\200PostgreSQL.os" index 30e0b43c..d7a5763e 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\200PostgreSQL.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\200PostgreSQL.os" @@ -100,6 +100,26 @@ ВыполнитьКоманду("ROLLBACK", "ROLLBACK;"); КонецПроцедуры +// Определяет, начата ли этим коннектором транзакция, которая еще не завершена: методом +// коннектора или текстом запроса. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт + Возврат КоннекторSQL.ВТранзакции(); +КонецФункции + +// Выполняет действие в транзакции: в своей, если транзакция еще не начата, иначе - в уже +// начатой. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + КоннекторSQL.ВыполнитьМонопольно(Действие, "START TRANSACTION;"); +КонецПроцедуры + // Выполняет команду без таблицы, сообщая о ней наблюдателям. // // Параметры: 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..adb7f9df 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" @@ -84,6 +84,11 @@ // Начинает новую транзакцию в БД. // +// Транзакция отложенная: блокировку записи она берет на первой записи, поэтому параллельные +// читающие транзакции друг другу не мешают. Две транзакции, обе успевшие прочитать и затем +// пишущие, ждут друг друга до ошибки "database is locked" - такую работу следует выполнять +// монопольно, см. ВыполнитьМонопольно. +// Процедура НачатьТранзакцию() Экспорт ВыполнитьКоманду("BEGIN", "BEGIN TRANSACTION;"); КонецПроцедуры @@ -100,6 +105,31 @@ ВыполнитьКоманду("ROLLBACK", "ROLLBACK TRANSACTION;"); КонецПроцедуры +// Определяет, начата ли этим коннектором транзакция, которая еще не завершена: методом +// коннектора или текстом запроса. +// +// Возвращаемое значение: +// Булево - Истина, если транзакция начата и не завершена. +// +Функция ВТранзакции() Экспорт + Возврат КоннекторSQL.ВТранзакции(); +КонецФункции + +// Выполняет действие в транзакции: в своей, если транзакция еще не начата, иначе - в уже +// начатой. +// +// Своя транзакция сразу берет блокировку записи (BEGIN IMMEDIATE): действие, читающее перед +// записью, в отложенной транзакции ждало бы такое же параллельное действие до ошибки +// "database is locked". Немедленная ждет предшественницу на начале - не дольше срока ожидания +// соединения, параметр строки соединения "Default Timeout", по умолчанию 30 секунд. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + КоннекторSQL.ВыполнитьМонопольно(Действие, "BEGIN IMMEDIATE TRANSACTION;"); +КонецПроцедуры + // Выполняет команду без таблицы, сообщая о ней наблюдателям. // // Параметры: @@ -184,20 +214,23 @@ Лог.Отладка("Сохранение сущности с типом %1:%2%3", ОбъектМодели.ТипСущности(), Символы.ПС, ТекстЗапроса); Запрос.Текст = ТекстЗапроса; - КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь); // Идентификатор объекта перезаписывается только тогда, когда его выдала СУБД. Если он был // задан вызывающим кодом, объект уже несет верное значение. // // last_insert_rowid относится к соединению, а соединение занято одним потоком исполнения, - // поэтому вставка и получение идентификатора не разъезжаются и без внешней блокировки. + // поэтому вставку и получение идентификатора не разделит другой поток. Разделить их мог бы + // наблюдатель вставки, пишущий в ту же базу тем же соединением, поэтому идентификатор читается + // до рассылки события, см. ВставитьСНаблюдением. Если СоздаватьНовыйИдентификатор Тогда - ИДПоследнейДобавленнойЗаписи = КонструкторКоннектора.ИДПоследнейДобавленнойЗаписи(Соединение, Запрос); + ИДПоследнейДобавленнойЗаписи = КоннекторSQL.ВставитьСНаблюдением(Запрос, ИмяТаблицы); ОбъектМодели.УстановитьЗначениеКолонкиВПоле( Сущность, ОбъектМодели.Идентификатор().ИмяКолонки, ИДПоследнейДобавленнойЗаписи ); + Иначе + КоннекторSQL.ВыполнитьСНаблюдением(Запрос, "INSERT", ИмяТаблицы, Ложь); КонецЕсли; // TODO: Для полей с автоинкрементом - получить значения из базы. diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\234\320\276\320\275\320\276\320\277\320\276\320\273\321\214\320\275\320\276\320\263\320\276\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\234\320\276\320\275\320\276\320\277\320\276\320\273\321\214\320\275\320\276\320\263\320\276\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" new file mode 100644 index 00000000..e98c8904 --- /dev/null +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\234\320\276\320\275\320\276\320\277\320\276\320\273\321\214\320\275\320\276\320\263\320\276\320\222\321\213\320\277\320\276\320\273\320\275\320\265\320\275\320\270\321\217.os" @@ -0,0 +1,48 @@ +// Коннектор, реализующий все методы АбстрактныйКоннектор, кроме ВыполнитьМонопольно. Проверяет, +// что источник отвергает коннектор, которому библиотека не может поручить сохранение сущности с +// подчиненными таблицами. + +// BSLLS:UnusedParameters-off + +&Реализует("АбстрактныйКоннектор") +Процедура ПриСозданииОбъекта() +КонецПроцедуры + +Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт +КонецПроцедуры + +Процедура Закрыть() Экспорт +КонецПроцедуры + +Функция Открыт() Экспорт + Возврат Истина; +КонецФункции + +Процедура НачатьТранзакцию() Экспорт +КонецПроцедуры + +Процедура ЗафиксироватьТранзакцию() Экспорт +КонецПроцедуры + +Процедура ОтменитьТранзакцию() Экспорт +КонецПроцедуры + +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт +КонецПроцедуры + +Процедура Сохранить(ОбъектМодели, Сущность) Экспорт +КонецПроцедуры + +Процедура Удалить(ОбъектМодели, Сущность) Экспорт +КонецПроцедуры + +Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт + Возврат Новый Массив; +КонецФункции + +Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт +КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os" index 92937970..80edc9a4 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\235\320\260\320\261\320\273\321\216\320\264\320\265\320\275\320\270\321\217.os" @@ -34,6 +34,14 @@ Внутренний.ОтменитьТранзакцию(); КонецПроцедуры +Функция ВТранзакции() Экспорт + Возврат Внутренний.ВТранзакции(); +КонецФункции + +Процедура ВыполнитьМонопольно(Действие) Экспорт + Внутренний.ВыполнитьМонопольно(Действие); +КонецПроцедуры + Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт Внутренний.ИнициализироватьТаблицу(ОбъектМодели); КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\237\321\200\320\270\320\267\320\275\320\260\320\272\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\237\321\200\320\270\320\267\320\275\320\260\320\272\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270.os" new file mode 100644 index 00000000..37c5354a --- /dev/null +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\221\320\265\320\267\320\237\321\200\320\270\320\267\320\275\320\260\320\272\320\260\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270.os" @@ -0,0 +1,48 @@ +// Коннектор, реализующий все методы АбстрактныйКоннектор, кроме ВТранзакции. Проверяет, что +// источник отвергает коннектор, не сообщающий о своей транзакции: пул не смог бы откатить +// транзакцию, брошенную алгоритмом прямого доступа. + +// BSLLS:UnusedParameters-off + +&Реализует("АбстрактныйКоннектор") +Процедура ПриСозданииОбъекта() +КонецПроцедуры + +Процедура Открыть(СтрокаСоединения, ПараметрыКоннектора) Экспорт +КонецПроцедуры + +Процедура Закрыть() Экспорт +КонецПроцедуры + +Функция Открыт() Экспорт + Возврат Истина; +КонецФункции + +Процедура НачатьТранзакцию() Экспорт +КонецПроцедуры + +Процедура ЗафиксироватьТранзакцию() Экспорт +КонецПроцедуры + +Процедура ОтменитьТранзакцию() Экспорт +КонецПроцедуры + +Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт +КонецПроцедуры + +Процедура Сохранить(ОбъектМодели, Сущность) Экспорт +КонецПроцедуры + +Процедура Удалить(ОбъектМодели, Сущность) Экспорт +КонецПроцедуры + +Функция НайтиСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт + Возврат Новый Массив; +КонецФункции + +Процедура УдалитьСтрокиВТаблице(ОбъектМодели, ОпцииПоиска) Экспорт +КонецПроцедуры + +Процедура ВыполнитьМонопольно(Действие) Экспорт + Действие.Выполнить(); +КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os" index edae5cd0..955cb682 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\235\320\265\320\277\320\276\320\273\320\275\320\276\320\235\320\260\320\261\320\273\321\216\320\264\320\260\320\265\320\274\321\213\320\271.os" @@ -40,6 +40,14 @@ Внутренний.ОтменитьТранзакцию(); КонецПроцедуры +Функция ВТранзакции() Экспорт + Возврат Внутренний.ВТранзакции(); +КонецФункции + +Процедура ВыполнитьМонопольно(Действие) Экспорт + Внутренний.ВыполнитьМонопольно(Действие); +КонецПроцедуры + Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт Внутренний.ИнициализироватьТаблицу(ОбъектМодели); КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os" index 622daf55..eb640acd 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\233\320\276\320\274\320\260\320\275\321\213\320\274\320\236\320\277\320\270\321\201\320\260\320\275\320\270\320\265\320\274.os" @@ -60,6 +60,14 @@ Внутренний.ОтменитьТранзакцию(); КонецПроцедуры +Функция ВТранзакции() Экспорт + Возврат Внутренний.ВТранзакции(); +КонецФункции + +Процедура ВыполнитьМонопольно(Действие) Экспорт + Внутренний.ВыполнитьМонопольно(Действие); +КонецПроцедуры + Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт Внутренний.ИнициализироватьТаблицу(ОбъектМодели); КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\274\320\237\321\200\320\270\320\237\320\276\320\264\320\272\320\273\321\216\321\207\320\265\320\275\320\270\320\270.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\274\320\237\321\200\320\270\320\237\320\276\320\264\320\272\320\273\321\216\321\207\320\265\320\275\320\270\320\270.os" index 5832a367..97997a2d 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\274\320\237\321\200\320\270\320\237\320\276\320\264\320\272\320\273\321\216\321\207\320\265\320\275\320\270\320\270.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\241\320\276\320\261\321\213\321\202\320\270\320\265\320\274\320\237\321\200\320\270\320\237\320\276\320\264\320\272\320\273\321\216\321\207\320\265\320\275\320\270\320\270.os" @@ -53,6 +53,14 @@ Внутренний.ОтменитьТранзакцию(); КонецПроцедуры +Функция ВТранзакции() Экспорт + Возврат Внутренний.ВТранзакции(); +КонецФункции + +Процедура ВыполнитьМонопольно(Действие) Экспорт + Внутренний.ВыполнитьМонопольно(Действие); +КонецПроцедуры + Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт Внутренний.ИнициализироватьТаблицу(ОбъектМодели); КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os" index dd71cc39..82d831cb 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\271\320\224\320\273\321\217\320\242\320\265\321\201\321\202\320\276\320\262.os" @@ -105,6 +105,25 @@ КонецПроцедуры +// О своей транзакции коннектор не сообщает: он изображает транзакции для событий соединения, а +// откат брошенной алгоритмом транзакции проверяется на настоящих СУБД. +// +// Возвращаемое значение: +// Булево - Всегда Ложь. +// +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +// Изоляцию дает база в памяти, которой делегируются данные. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + Внутренний.ВыполнитьМонопольно(Действие); +КонецПроцедуры + Процедура ИнициализироватьТаблицу(ОбъектМодели) Экспорт Внутренний.ИнициализироватьТаблицу(ОбъектМодели); КонецПроцедуры diff --git "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os" "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os" index cf139d8d..b49e4622 100644 --- "a/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os" +++ "b/tests/fixtures/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\243\320\277\321\200\320\260\320\262\320\273\321\217\320\265\320\274\320\276\320\265\320\236\321\202\320\272\321\200\321\213\321\202\320\270\320\265.os" @@ -94,6 +94,24 @@ КонецЕсли; КонецПроцедуры +// Состояния транзакции коннектор не изображает, см. НачатьТранзакцию. +// +// Возвращаемое значение: +// Булево - Всегда Ложь. +// +Функция ВТранзакции() Экспорт + Возврат Ложь; +КонецФункции + +// Данных у коннектора нет, изолировать нечего. +// +// Параметры: +// Действие - Действие - Действие без параметров. +// +Процедура ВыполнитьМонопольно(Действие) Экспорт + Действие.Выполнить(); +КонецПроцедуры + // Закрывает соединение, падая, если так задано параметрами. // Процедура Закрыть() Экспорт diff --git "a/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\260\321\203\320\267\321\213\320\237\320\276\321\201\320\273\320\265\320\227\320\260\320\277\321\200\320\276\321\201\320\260.os" "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\260\321\203\320\267\321\213\320\237\320\276\321\201\320\273\320\265\320\227\320\260\320\277\321\200\320\276\321\201\320\260.os" new file mode 100644 index 00000000..7c966d84 --- /dev/null +++ "b/tests/fixtures/\320\235\320\260\320\261\320\273\321\216\320\264\320\260\321\202\320\265\320\273\321\214\320\237\320\260\321\203\320\267\321\213\320\237\320\276\321\201\320\273\320\265\320\227\320\260\320\277\321\200\320\276\321\201\320\260.os" @@ -0,0 +1,29 @@ +// Наблюдатель, который после запроса заданной операции делает паузу. +// +// Расширяет окно между запросами одной операции библиотеки: так перемежение двух операций, +// которое без изоляции случается редко, проявляется надежно. Пауза идет в потоке, выполнившем +// запрос. Состояния, кроме неизменяемых настроек, у наблюдателя нет. + +// Строка - Операция запроса, после которого делается пауза: DELETE, INSERT и т.п. +Перем ОперацияЗапроса; +// Число - Пауза в миллисекундах. +Перем Пауза; + +&Реализует("НаблюдательИсточникаДанных") +Процедура ПриСозданииОбъекта(Знач ПОперацияЗапроса, Знач ППауза) + ОперацияЗапроса = ПОперацияЗапроса; + Пауза = ППауза; +КонецПроцедуры + +Процедура ПередСобытием(Событие) Экспорт // BSLLS:UnusedParameters-off +КонецПроцедуры + +Процедура ПослеСобытия(Событие) Экспорт + + Если Событие.Вид() <> "Запрос" ИЛИ Событие.Операция() <> ОперацияЗапроса Тогда + Возврат; + КонецЕсли; + + Приостановить(Пауза); + +КонецПроцедуры diff --git "a/tests/fixtures/\320\241\321\203\321\211\320\275\320\276\321\201\321\202\321\214\320\241\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\265\320\271.os" "b/tests/fixtures/\320\241\321\203\321\211\320\275\320\276\321\201\321\202\321\214\320\241\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\265\320\271.os" new file mode 100644 index 00000000..d317ee80 --- /dev/null +++ "b/tests/fixtures/\320\241\321\203\321\211\320\275\320\276\321\201\321\202\321\214\320\241\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\265\320\271.os" @@ -0,0 +1,20 @@ +// Сущность с одной подчиненной таблицей: сохранение пишет строку сущности и строки коллекции. + +&Идентификатор +&Колонка(Тип = "Целое") +Перем Код Экспорт; + +&Колонка(Тип = "Строка") +Перем Метка Экспорт; + +&ПодчиненнаяТаблица( + Тип = "Массив", + ТипЭлемента = "Строка", + ИмяТаблицы = "СущностиСКоллекцией_Элементы" +) +Перем Элементы Экспорт; + +&Сущность(ИмяТаблицы = "СущностиСКоллекцией") +Процедура ПриСозданииОбъекта() + +КонецПроцедуры diff --git "a/tests/fixtures/\320\247\321\202\320\265\320\275\320\270\320\265\320\230\320\227\320\260\320\277\320\270\321\201\321\214\320\220\320\262\321\202\320\276\321\200\320\260.os" "b/tests/fixtures/\320\247\321\202\320\265\320\275\320\270\320\265\320\230\320\227\320\260\320\277\320\270\321\201\321\214\320\220\320\262\321\202\320\276\321\200\320\260.os" new file mode 100644 index 00000000..f2613391 --- /dev/null +++ "b/tests/fixtures/\320\247\321\202\320\265\320\275\320\270\320\265\320\230\320\227\320\260\320\277\320\270\321\201\321\214\320\220\320\262\321\202\320\276\321\200\320\260.os" @@ -0,0 +1,31 @@ +// Действие для монопольного выполнения коннектором: прочитать авторов, дождаться, пока прочитает +// другой поток, и записать автора. Ждет другой поток не дольше секунды: поток, ждущий начала своей +// транзакции, до чтения не дойдет. + +// АбстрактныйКоннекторSQL - Коннектор, которым выполняются запросы. +Перем Коннектор; +// Строка - Имя записываемого автора. +Перем Имя; +// СигналСобытия - Отмечается после чтения. +Перем СвойСигнал; +// СигналСобытия - Чтение другого потока. +Перем ЧужойСигнал; + +Процедура ПриСозданииОбъекта(ПКоннектор, Знач ПИмя, ПСвойСигнал, ПЧужойСигнал) + Коннектор = ПКоннектор; + Имя = ПИмя; + СвойСигнал = ПСвойСигнал; + ЧужойСигнал = ПЧужойСигнал; +КонецПроцедуры + +// Читает, ждет чтения другого потока и пишет. +// +Процедура Записать() Экспорт + + Коннектор.ВыполнитьЗапрос("SELECT * FROM Авторы"); + СвойСигнал.Отметить(); + ЧужойСигнал.Дождаться(1000); + + Коннектор.ВыполнитьЗапрос(СтрШаблон("INSERT INTO Авторы (Имя, Фамилия) VALUES ('%1', 'Иванов')", Имя)); + +КонецПроцедуры diff --git "a/tests/\320\224\320\276\321\201\321\202\321\203\320\277\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\276\320\274.os" "b/tests/\320\224\320\276\321\201\321\202\321\203\320\277\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\276\320\274.os" index 140fcafa..35e98875 100644 --- "a/tests/\320\224\320\276\321\201\321\202\321\203\320\277\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\276\320\274.os" +++ "b/tests/\320\224\320\276\321\201\321\202\321\203\320\277\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200\320\276\320\274.os" @@ -11,6 +11,10 @@ // ИсточникДанных - Источник тестовой базы; закрывается после каждого теста. Перем Источник; +// Тип - Тип коннектора тестовой базы. +Перем ТипКоннектора; +// Строка - Строка соединения с тестовой базой. +Перем СтрокаСоединения; // Менеджер, который тесты создают заново перед каждым тестом. Перем МенеджерСущностей; // Хранилище авторов, полученное из менеджера. @@ -39,15 +43,6 @@ ВызватьИсключение "Нет доступного коннектора для тестирования доступа коннектором"; КонецЕсли; - Источник = Новый ИсточникДанных("Тесты", ТипКоннектора, СтрокаСоединения); - // oneunit исполняет подготовку и тест в разных потоках: источник живет до ПослеКаждого. - Источник.УстановитьАвтоЗакрытие(Ложь); - МенеджерСущностей = Новый МенеджерСущностей(Источник); - - Если ВыполнятьСбросТаблиц Тогда - ТестовыеУтилиты.СброситьТаблицы(МенеджерСущностей); - КонецЕсли; - ПодключитьСценарий( ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "Автор.os"), "Автор" @@ -57,13 +52,53 @@ "СущностьБезГенерируемогоИдентификатора" ); + СоздатьМенеджер(10, ВыполнятьСбросТаблиц); + + ИмяИзЗахваченногоОбъекта = "Захваченный"; + +КонецПроцедуры + +// Создает источник, менеджер с моделью авторов и хранилище авторов. +// +// Параметры: +// РазмерПула - Число - Размер пула соединений источника. +// ВыполнятьСбросТаблиц - Булево - Удалить таблицы базы до инициализации модели. +// +Процедура СоздатьМенеджер(Знач РазмерПула, Знач ВыполнятьСбросТаблиц = Ложь) + + Источник = Новый ИсточникДанных("Тесты", ТипКоннектора, СтрокаСоединения, , РазмерПула); + // oneunit исполняет подготовку и тест в разных потоках: источник живет до ПослеКаждого. + Источник.УстановитьАвтоЗакрытие(Ложь); + МенеджерСущностей = Новый МенеджерСущностей(Источник); + + Если ВыполнятьСбросТаблиц Тогда + ТестовыеУтилиты.СброситьТаблицы(МенеджерСущностей); + КонецЕсли; + МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьБезГенерируемогоИдентификатора")); МенеджерСущностей.ДобавитьКлассВМодель(Тип("Автор")); МенеджерСущностей.Инициализировать(); Хранилище = МенеджерСущностей.ПолучитьХранилищеСущностей(Тип("Автор")); - ИмяИзЗахваченногоОбъекта = "Захваченный"; + +КонецПроцедуры + +// Заменяет менеджер и источник теста новыми, с пулом из одного соединения: следующий захват +// гарантированно получит то же соединение, что и предыдущий. +// +// База SQLite в памяти живет, пока открыто хоть одно ее соединение. Новый источник открывается +// до закрытия прежнего, поэтому таблицы и строки переживают замену. +// +Процедура ЗаменитьМенеджерНаПулИзОдного() + + ПрежнийМенеджер = МенеджерСущностей; + ПрежнийИсточник = Источник; + + СоздатьМенеджер(1); + + ПрежнийМенеджер.Закрыть(); + ПрежнийИсточник.Закрыть(); КонецПроцедуры @@ -152,6 +187,62 @@ КонецПроцедуры +// Транзакция, которую алгоритм начал методом коннектора и не завершил, не уходит с соединением +// к другому потоку: иначе его фиксация записала бы и строки алгоритма. +// +&Тест +Процедура БрошеннаяАлгоритмомТранзакцияНеДостаетсяДругомуПотоку() Экспорт + ПроверитьОткатБрошеннойТранзакции("Коннектор -> Коннектор.НачатьТранзакцию(); Коннектор.ВыполнитьЗапрос(Запрос)"); +КонецПроцедуры + +// То же для транзакции, начатой текстом запроса. +// +&Тест +Процедура БрошеннаяЗапросомТранзакцияНеДостаетсяДругомуПотоку() Экспорт + ПроверитьОткатБрошеннойТранзакции( + "Коннектор -> Коннектор.ВыполнитьЗапрос(""BEGIN""); Коннектор.ВыполнитьЗапрос(Запрос)" + ); +КонецПроцедуры + +// Текст запроса узнается и в нижнем регистре, и с точкой с запятой. +// +&Тест +Процедура БрошеннаяЗапросомВНижнемРегистреТранзакцияНеДостаетсяДругомуПотоку() Экспорт + ПроверитьОткатБрошеннойТранзакции( + "Коннектор -> Коннектор.ВыполнитьЗапрос(""begin;""); Коннектор.ВыполнитьЗапрос(Запрос)" + ); +КонецПроцедуры + +// Откат к точке сохранения транзакцию не завершает: брошенная после него транзакция откатывается +// так же. +// +&Тест +Процедура ОткатКТочкеСохраненияНеЗавершаетБрошеннуюТранзакцию() Экспорт + ПроверитьОткатБрошеннойТранзакции( + "Коннектор -> Коннектор.ВыполнитьЗапрос(""BEGIN""); Коннектор.ВыполнитьЗапрос(Запрос); + |Коннектор.ВыполнитьЗапрос(""SAVEPOINT Точка""); Коннектор.ВыполнитьЗапрос(""ROLLBACK TO Точка"")" + ); +КонецПроцедуры + +// Алгоритм, начавший и сам завершивший транзакцию, ничего не теряет: его строка зафиксирована. +// +&Тест +Процедура ЗавершеннаяАлгоритмомТранзакцияСохраняется() Экспорт + + ЗаменитьМенеджерНаПулИзОдного(); + + МенеджерСущностей.ВыполнитьСКоннектором( + "Коннектор -> Коннектор.ВыполнитьЗапрос(""BEGIN""); Коннектор.ВыполнитьЗапрос(Запрос); + |Коннектор.ВыполнитьЗапрос(""commit;"")", + Новый Структура("Запрос", "INSERT INTO Авторы (Имя, Фамилия) VALUES ('Свой', 'Иванов')") + ); + + ЗафиксироватьАвтораВДругомПотоке("Чужой"); + + Ожидаем.Что(ИменаАвторов(), "Строки обоих потоков на месте").ИмеетДлину(2); + +КонецПроцедуры + // Ошибка внутри алгоритма не должна оставлять соединение занятым. // &Тест @@ -311,6 +402,83 @@ Возврат ТестовыеУтилиты.ВыполнитьЗапрос(МенеджерСущностей, "SELECT * FROM Авторы"); КонецФункции +// Читает имена всех авторов. +// +// Возвращаемое значение: +// Массив из Строка - Имена авторов. +// +Функция ИменаАвторов() + Возврат ПрочитатьАвторов().ВыгрузитьКолонку("Имя"); +КонецФункции + +// Выполняет алгоритм, бросающий начатую транзакцию со вставленной строкой, затем фиксирует свою +// транзакцию в другом потоке на том же соединении и проверяет, что в базе только строка +// другого потока. +// +// Параметры: +// Алгоритм - Строка - Лямбда-выражение: начинает транзакцию и выполняет запрос из контекста. +// +Процедура ПроверитьОткатБрошеннойТранзакции(Знач Алгоритм) + + ЗаменитьМенеджерНаПулИзОдного(); + + МенеджерСущностей.ВыполнитьСКоннектором( + Алгоритм, + Новый Структура("Запрос", "INSERT INTO Авторы (Имя, Фамилия) VALUES ('Брошенный', 'Иванов')") + ); + + ЗафиксироватьАвтораВДругомПотоке("Чужой"); + + Имена = ИменаАвторов(); + Ожидаем.Что(Имена, "Строка брошенной транзакции не зафиксирована чужой фиксацией").ИмеетДлину(1); + Ожидаем.Что(Имена[0], "Зафиксирована строка другого потока").Равно("Чужой"); + +КонецПроцедуры + +// Сохраняет автора в транзакции фонового задания и дожидается его. +// +// Параметры: +// Имя - Строка - Имя автора. +// +Процедура ЗафиксироватьАвтораВДругомПотоке(Знач Имя) + + Параметры = Новый Массив; + Параметры.Добавить(МенеджерСущностей); + Параметры.Добавить(Имя); + + Задания = Новый Массив; + Задания.Добавить(ФоновыеЗадания.Выполнить(ЭтотОбъект, "ЗафиксироватьАвтораВФоне", Параметры, Истина)); + + ТестовыеУтилиты.ПроверитьЗадания(Задания); + +КонецПроцедуры + +// Сохраняет автора в своей транзакции. Выполняется фоновым заданием. +// +// Параметры: +// Менеджер - МенеджерСущностей - Менеджер теста. +// Имя - Строка - Имя автора. +// +Процедура ЗафиксироватьАвтораВФоне(Менеджер, Знач Имя) Экспорт + + Автор = Новый Автор; + Автор.Имя = Имя; + Автор.ВтороеИмя = "Петров"; + + Менеджер.НачатьТранзакцию(); + + Попытка + Менеджер.Сохранить(Автор); + Исключение + Ошибка = ИнформацияОбОшибке(); + Менеджер.ОтменитьТранзакцию(); + ВызватьИсключение; + КонецПопытки; + + Менеджер.ЗафиксироватьТранзакцию(); + +КонецПроцедуры + // Строка соединения с тестовой базой PostgreSQL. // // Возвращаемое значение: diff --git "a/tests/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os" "b/tests/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os" index 40d7bd02..c65f1c8c 100644 --- "a/tests/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os" +++ "b/tests/\320\230\321\201\321\202\320\276\321\207\320\275\320\270\320\272\320\224\320\260\320\275\320\275\321\213\321\205.os" @@ -34,6 +34,50 @@ КонецПроцедуры +// ВТранзакции - обязательный метод интерфейса: по нему пул откатывает транзакцию, брошенную +// алгоритмом прямого доступа. +&Тест +Процедура КоннекторБезПризнакаТранзакцииОтвергается() Экспорт + + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "КоннекторБезПризнакаТранзакции.os"), + "КоннекторБезПризнакаТранзакции" + ); + + ТекстОшибки = ""; + Попытка + Источник = Новый ИсточникДанных("Основной", Тип("КоннекторБезПризнакаТранзакции")); // BSLLS:UnusedLocalVariable-off + Исключение + ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()); + КонецПопытки; + + Ожидаем.Что(ТекстОшибки, "Источник отвергает коннектор без ВТранзакции") + .Содержит("не реализует интерфейс коннектора"); + +КонецПроцедуры + +// ВыполнитьМонопольно - обязательный метод интерфейса: им библиотека изолирует сохранение +// сущности с подчиненными таблицами. +&Тест +Процедура КоннекторБезМонопольногоВыполненияОтвергается() Экспорт + + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "КоннекторБезМонопольногоВыполнения.os"), + "КоннекторБезМонопольногоВыполнения" + ); + + ТекстОшибки = ""; + Попытка + Источник = Новый ИсточникДанных("Основной", Тип("КоннекторБезМонопольногоВыполнения")); // BSLLS:UnusedLocalVariable-off + Исключение + ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()); + КонецПопытки; + + Ожидаем.Что(ТекстОшибки, "Источник отвергает коннектор без ВыполнитьМонопольно") + .Содержит("не реализует интерфейс коннектора"); + +КонецПроцедуры + // Пул создается вместе с источником, поэтому и размер пула проверяется при создании. &Тест Процедура НеверныйРазмерПулаОтвергаетсяПриСоздании() Экспорт 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(); + КоннекторПотока.Открыть(СтрокаСоединенияБазы, Новый Массив); + + Возврат КоннекторПотока.НайтиСтрокиВТаблице(ОбъектМодели, Новый ОпцииПоиска()).Количество(); + +КонецФункции diff --git "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os" "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os" index bd2f93c0..ff141818 100644 --- "a/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os" +++ "b/tests/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200JSON.os" @@ -681,7 +681,7 @@ // Тогда Ожидаем.Что(ТекстОшибки, "Чтение отсутствующей таблицы завершается ошибкой").Не_().Равно(""); - ПроверитьЧтоБлокировкаТаблицыСвободна(ОбъектМодели, "Блокировка освобождена после ошибки чтения"); + ПроверитьЧтоБлокировкаКаталогаСвободна("Блокировка освобождена после ошибки чтения"); КонецПроцедуры @@ -706,7 +706,7 @@ // Тогда Ожидаем.Что(ТекстОшибки, "Инициализация без каталога завершается ошибкой").Не_().Равно(""); - ПроверитьЧтоБлокировкаТаблицыСвободна(ОбъектМодели, "Блокировка освобождена после ошибки инициализации"); + ПроверитьЧтоБлокировкаКаталогаСвободна("Блокировка освобождена после ошибки инициализации"); КонецПроцедуры @@ -728,19 +728,17 @@ КонецПроцедуры -// Проверяет, что блокировка файла таблицы никем не удерживается. +// Проверяет, что блокировка каталога базы никем не удерживается: она одна на все таблицы каталога. // // Проверяются обе половины: утечка блокировки чтения не мешает захватить запись из того же // потока, поэтому одной проверки записи недостаточно. // // Параметры: -// ОбъектМодели - ОбъектМодели - Объект модели, определяющий имя файла таблицы. // Пояснение - Строка - Пояснение к проверке. // -Процедура ПроверитьЧтоБлокировкаТаблицыСвободна(ОбъектМодели, Пояснение) +Процедура ПроверитьЧтоБлокировкаКаталогаСвободна(Пояснение) - ИмяФайла = ОбъединитьПути(СтрокаСоединения, ОбъектМодели.ИмяТаблицы()) + ".json"; - Блокировка = БлокировкиЧтенияЗаписи.Получить(ИмяФайла); + Блокировка = БлокировкиЧтенияЗаписи.Получить(СтрокаСоединения); Ожидаем.Что(Блокировка.ЗахваченаЗапись(), Пояснение + ": запись").Равно(Ложь); Ожидаем.Что(Блокировка.КоличествоЗахватовЧтения(), Пояснение + ": чтение").Равно(0); 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; + МенеджерЖурнала.Сохранить(Журнал); + +КонецПроцедуры diff --git "a/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\320\276\320\265\320\241\320\276\321\205\321\200\320\260\320\275\320\265\320\275\320\270\320\265\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\271.os" "b/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\320\276\320\265\320\241\320\276\321\205\321\200\320\260\320\275\320\265\320\275\320\270\320\265\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\271.os" new file mode 100644 index 00000000..459bad71 --- /dev/null +++ "b/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\320\276\320\265\320\241\320\276\321\205\321\200\320\260\320\275\320\265\320\275\320\270\320\265\320\232\320\276\320\273\320\273\320\265\320\272\321\206\320\270\320\271.os" @@ -0,0 +1,309 @@ +// Параллельное сохранение одной сущности с подчиненной таблицей из разных потоков исполнения. +// +// Сохранение сущности с коллекцией - несколько запросов: строка сущности, удаление прежних строк +// коллекции, вставка новых. Без изоляции два сохранения одной сущности перемежаются, и в коллекции +// оказываются строки обоих, а чтение посередине видит коллекцию пустой. Тесты проверяют, что +// каждое чтение и итог видят коллекцию ровно из того числа элементов, которое сохраняется. +// +// SQLite - файловая база: база в памяти с общим кэшем параллельных писателей не допускает. +// +// BSLLS:MagicNumber-off +// BSLLS:LatinAndCyrillicSymbolInWord-off +// BSLLS:DuplicateStringLiteral-off +// BSLLS:UsingHardcodeNetworkAddress-off + +#Использовать ".." +#Использовать "utils" + +// Массив из ИсточникДанных - Источники, созданные тестом. +Перем СозданныеИсточники; +// МенеджерСущностей - Менеджер теста; закрывается после теста. +Перем МенеджерСущностей; +// Массив из Строка - Файлы и каталоги баз, которые тест создал и удаляет после себя. +Перем ФайлыБаз; + +&ПередКаждым +Процедура ПередЗапускомТеста() Экспорт + + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "СущностьСКоллекцией.os"), + "СущностьСКоллекцией" + ); + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "НаблюдательПаузыПослеЗапроса.os"), + "НаблюдательПаузыПослеЗапроса" + ); + + СозданныеИсточники = Новый Массив; + ФайлыБаз = Новый Массив; + МенеджерСущностей = Неопределено; + +КонецПроцедуры + +&ПослеКаждого +Процедура ПослеЗапускаТеста() Экспорт + + Если МенеджерСущностей <> Неопределено Тогда + МенеджерСущностей.Закрыть(); + МенеджерСущностей = Неопределено; + КонецЕсли; + + ТестовыеУтилиты.ЗакрытьИсточники(СозданныеИсточники); + + Для Каждого Путь Из ФайлыБаз Цикл + УдалитьФайлы(Путь); + КонецЦикла; + +КонецПроцедуры + +&Тест +Процедура ПараллельноеСохранениеКоллекцииInMemory() Экспорт + + ИмяБазы = "ПараллельноеСохранениеКоллекций"; + ХранилищеВПамяти.Очистить(ИмяБазы); + + ПроверитьПараллельноеСохранение(Тип("КоннекторInMemory"), ИмяБазы); + +КонецПроцедуры + +&Тест +Процедура ПараллельноеСохранениеКоллекцииJSON() Экспорт + + Каталог = ПолучитьИмяВременногоФайла(); + СоздатьКаталог(Каталог); + ФайлыБаз.Добавить(Каталог); + + ПроверитьПараллельноеСохранение(Тип("КоннекторJSON"), Каталог); + +КонецПроцедуры + +&Тест +&ВключенЕсли("() -> ЗапускатьТестыSQLite()") +Процедура ПараллельноеСохранениеКоллекцииSQLite() Экспорт + + ФайлБазы = ПолучитьИмяВременногоФайла("sqlite"); + ФайлыБаз.Добавить(ФайлБазы); + + ПроверитьПараллельноеСохранение(Тип("КоннекторSQLite"), "Data Source=" + ФайлБазы); + +КонецПроцедуры + +&Тест +&ВключенЕсли("() -> ЗапускатьТестыPostgreSQL()") +Процедура ПараллельноеСохранениеКоллекцииPostgreSQL() Экспорт + ПроверитьПараллельноеСохранение(Тип("КоннекторPostgreSQL"), СтрокаСоединенияPostgres(), Истина); +КонецПроцедуры + +// Определяет, доступна ли в текущем окружении база SQLite. +// +// Возвращаемое значение: +// Булево - Истина, если тест нужно выполнять. +// +Функция ЗапускатьТестыSQLite() Экспорт + Возврат ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_SQLITE_TESTS", "true") = "true"; +КонецФункции + +// Определяет, доступна ли в текущем окружении база PostgreSQL. +// +// Возвращаемое значение: +// Булево - Истина, если тест нужно выполнять. +// +Функция ЗапускатьТестыPostgreSQL() Экспорт + Возврат ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_POSTGRES_TESTS", "true") = "true"; +КонецФункции + +// Два задания сохраняют одну сущность с коллекцией, третье в это время ее читает. Каждое чтение и +// итог должны видеть коллекцию ровно из ЭлементовВКоллекции() элементов. +// +// Параметры: +// ТипКоннектора - Тип - Тип коннектора базы. +// СтрокаСоединения - Строка - Строка соединения с базой. +// СброситьТаблицы - Булево - Удалить таблицы базы до инициализации модели. +// +Процедура ПроверитьПараллельноеСохранение(Знач ТипКоннектора, Знач СтрокаСоединения, Знач СброситьТаблицы = Ложь) + + Источник = Новый ИсточникДанных("Тесты", ТипКоннектора, СтрокаСоединения); + СозданныеИсточники.Добавить(Источник); + // oneunit исполняет подготовку и тест в разных потоках: источник живет до ПослеКаждого. + Источник.УстановитьАвтоЗакрытие(Ложь); + + МенеджерСущностей = Новый МенеджерСущностей(Источник); + Если СброситьТаблицы Тогда + ТестовыеУтилиты.СброситьТаблицы(МенеджерСущностей); + КонецЕсли; + МенеджерСущностей.ДобавитьКлассВМодель(Тип("СущностьСКоллекцией")); + МенеджерСущностей.Инициализировать(); + + МенеджерСущностей.Сохранить(НоваяСущность("Начало")); + + // Пауза между удалением прежних строк коллекции и вставкой новых: без изоляции в это окно + // попадают и второе сохранение, и чтение. + Источник.ДобавитьНаблюдателя(Новый НаблюдательПаузыПослеЗапроса("DELETE", 5)); + + // Задания получают менеджер параметром, а не читают переменную модуля: задание, пережившее + // свой тест, не увидит менеджер следующего. Упавший запуск не оставляет уже запущенные + // задания работать дальше: они дожидаются, и только потом ошибка уходит дальше. + Задания = Новый Массив; + Попытка + Задания.Добавить(ЗапуститьЗадание("СохранитьСущностьВФоне", "Первый")); + Задания.Добавить(ЗапуститьЗадание("СохранитьСущностьВФоне", "Второй")); + Читатель = ЗапуститьЗадание("ПрочитатьКоллекцииВФоне"); + Задания.Добавить(Читатель); + Исключение + Ошибка = ИнформацияОбОшибке(); + ТестовыеУтилиты.СообщитьОЗаданиях(ТестовыеУтилиты.ДождатьсяЗаданий(Задания, СрокЗаданий())); + ВызватьИсключение; + КонецПопытки; + + ТестовыеУтилиты.ПроверитьЗадания(Задания, СрокЗаданий()); + + Ожидаем.Что(Читатель.Результат, "Каждое чтение видело коллекцию целиком").ИмеетДлину(0); + + Итог = МенеджерСущностей.ПолучитьОдно(Тип("СущностьСКоллекцией"), 1); + Ожидаем.Что(Итог.Элементы, "Итоговая коллекция - элементы одного сохранения").ИмеетДлину(ЭлементовВКоллекции()); + Для Каждого Элемент Из Итог.Элементы Цикл + Ожидаем.Что(СтрНачинаетсяС(Элемент, Итог.Метка + "-"), "Элемент сохранен вместе со строкой сущности") + .ЭтоИстина(); + КонецЦикла; + +КонецПроцедуры + +// Запускает фоновое задание с менеджером теста первым параметром. +// +// Параметры: +// ИмяМетода - Строка - Метод, выполняемый заданием. +// Метка - Строка, Неопределено - Второй параметр метода; Неопределено - второго параметра нет. +// +// Возвращаемое значение: +// ФоновоеЗадание - Запущенное задание. +// +Функция ЗапуститьЗадание(Знач ИмяМетода, Знач Метка = Неопределено) + + Параметры = Новый Массив; + Параметры.Добавить(МенеджерСущностей); + Если Метка <> Неопределено Тогда + Параметры.Добавить(Метка); + КонецЕсли; + + Возврат ФоновыеЗадания.Выполнить(ЭтотОбъект, ИмяМетода, Параметры, Истина); + +КонецФункции + +// Сохраняет одну и ту же сущность в цикле. Запускается в фоновом задании. +// +// Параметры: +// Менеджер - МенеджерСущностей - Менеджер теста. +// Метка - Строка - Метка задания: ею помечены строка сущности и элементы коллекции. +// +Процедура СохранитьСущностьВФоне(Менеджер, Знач Метка) Экспорт + + Сохранено = 0; + Пока Сохранено < СохраненийВЗадании() Цикл + Менеджер.Сохранить(НоваяСущность(Метка + Сохранено)); + Сохранено = Сохранено + 1; + КонецЦикла; + +КонецПроцедуры + +// Читает сущность в цикле и запоминает каждое чтение, увидевшее коллекцию не целиком. +// Запускается в фоновом задании. +// +// Параметры: +// Менеджер - МенеджерСущностей - Менеджер теста. +// +// Возвращаемое значение: +// Массив из Число - Размеры коллекции в неверных чтениях; пустой, если все чтения верны. +// +Функция ПрочитатьКоллекцииВФоне(Менеджер) Экспорт + + НеверныеЧтения = Новый Массив; + + Прочитано = 0; + Пока Прочитано < СохраненийВЗадании() Цикл + Сущность = Менеджер.ПолучитьОдно(Тип("СущностьСКоллекцией"), 1); + Размер = Сущность.Элементы.Количество(); + Если Размер <> ЭлементовВКоллекции() Тогда + НеверныеЧтения.Добавить(Размер); + КонецЕсли; + Прочитано = Прочитано + 1; + КонецЦикла; + + Возврат НеверныеЧтения; + +КонецФункции + +// Создает сущность с коллекцией, элементы которой помечены меткой сущности. +// +// Параметры: +// Метка - Строка - Метка сущности. +// +// Возвращаемое значение: +// СущностьСКоллекцией - Новая сущность. +// +Функция НоваяСущность(Знач Метка) + + Сущность = Новый СущностьСКоллекцией; + Сущность.Код = 1; + Сущность.Метка = Метка; + Сущность.Элементы = Новый Массив; + + Номер = 0; + Пока Номер < ЭлементовВКоллекции() Цикл + Сущность.Элементы.Добавить(Метка + "-" + Номер); + Номер = Номер + 1; + КонецЦикла; + + Возврат Сущность; + +КонецФункции + +// Число элементов в коллекции каждого сохранения. +// +// Возвращаемое значение: +// Число - Число элементов. +// +Функция ЭлементовВКоллекции() + Возврат 5; +КонецФункции + +// Число сохранений в каждом пишущем задании и чтений в читающем. +// +// Возвращаемое значение: +// Число - Число повторов. +// +Функция СохраненийВЗадании() + Возврат 40; +КонецФункции + +// Общий срок ожидания заданий теста. +// +// Возвращаемое значение: +// Число - Срок в миллисекундах. +// +Функция СрокЗаданий() + Возврат 120000; +КонецФункции + +// Строка соединения с тестовой базой PostgreSQL. +// +// Возвращаемое значение: +// Строка - Строка соединения. +// +Функция СтрокаСоединенияPostgres() + + Хост = ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("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;", + Хост, + Пользователь, + Пароль, + ИмяБД, + Порт + ); + +КонецФункции diff --git "a/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\321\213\320\265\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270SQLite.os" "b/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\321\213\320\265\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270SQLite.os" new file mode 100644 index 00000000..d463eb71 --- /dev/null +++ "b/tests/\320\237\320\260\321\200\320\260\320\273\320\273\320\265\320\273\321\214\320\275\321\213\320\265\320\242\321\200\320\260\320\275\320\267\320\260\320\272\321\206\320\270\320\270SQLite.os" @@ -0,0 +1,150 @@ +// Параллельная монопольная работа с SQLite из разных потоков исполнения: каждое действие сначала +// читает, потом пишет. +// +// Отложенная транзакция SQLite берет блокировку записи только на первой записи. Две такие +// транзакции, обе успевшие прочитать, ждут друг друга: первая не может зафиксироваться, пока вторая +// держит блокировку чтения, вторая не может писать, пока первая держит блокировку записи. Ожидание +// кончается ошибкой "database is locked". Монопольное действие начинает транзакцию сразу с +// блокировкой записи и ждет предшественницу на начале, до чтения: взаимной блокировки нет. +// +// База - файл: база в памяти с общим кэшем ведет блокировки иначе. +// +// BSLLS:MagicNumber-off +// BSLLS:LatinAndCyrillicSymbolInWord-off +// BSLLS:DuplicateStringLiteral-off + +#Использовать ".." +#Использовать "utils" + +// Массив из ИсточникДанных - Источники, созданные тестом. +Перем СозданныеИсточники; +// МенеджерСущностей - Менеджер теста; закрывается после теста. +Перем МенеджерСущностей; +// Строка - Файл базы теста; удаляется после теста. +Перем ФайлБазы; +// Строка - Строка соединения с базой теста. +Перем СтрокаСоединения; + +&ВключенЕсли("() -> ЗапускатьТестыSQLite()") +&ТестовыйНабор +Процедура ПриСозданииОбъекта() +КонецПроцедуры + +// Определяет, доступна ли в текущем окружении база SQLite. +// +// Возвращаемое значение: +// Булево - Истина, если набор нужно выполнять. +// +Функция ЗапускатьТестыSQLite() Экспорт + Возврат ТестовыеУтилиты.ПолучитьПеременнуюСредыИлиЗначение("TESTRUNNER_RUN_SQLITE_TESTS", "true") = "true"; +КонецФункции + +&ПередКаждым +Процедура ПередЗапускомТеста() Экспорт + + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "СигналСобытия.os"), + "СигналСобытия" + ); + ПодключитьСценарий( + ОбъединитьПути(ТекущийКаталог(), "tests", "fixtures", "ЧтениеИЗаписьАвтора.os"), + "ЧтениеИЗаписьАвтора" + ); + + СозданныеИсточники = Новый Массив; + ФайлБазы = ПолучитьИмяВременногоФайла("sqlite"); + + // Ожидание занятой базы - 3 секунды вместо 30 по умолчанию: взаимная блокировка кончается + // ошибкой быстро. + СтрокаСоединения = "Data Source=" + ФайлБазы + ";Default Timeout=3"; + + Источник = Новый ИсточникДанных("Основной", Тип("КоннекторSQLite"), СтрокаСоединения); + СозданныеИсточники.Добавить(Источник); + // oneunit исполняет подготовку и тест в разных потоках: источник живет до ПослеКаждого. + Источник.УстановитьАвтоЗакрытие(Ложь); + + МенеджерСущностей = ТестовыеУтилиты.МенеджерАвторов(Источник); + +КонецПроцедуры + +&ПослеКаждого +Процедура ПослеЗапускаТеста() Экспорт + МенеджерСущностей.Закрыть(); + ТестовыеУтилиты.ЗакрытьИсточники(СозданныеИсточники); + УдалитьФайлы(ФайлБазы); +КонецПроцедуры + +// Два монопольных действия читают таблицу, дожидаются друг друга и пишут в нее. Оба должны +// завершиться. +// +&Тест +Процедура МонопольныеДействияСЧтениемИЗаписьюНеБлокируютДругДруга() Экспорт + + ПервыйПрочитал = Новый СигналСобытия; + ВторойПрочитал = Новый СигналСобытия; + + Задания = Новый Массив; + Попытка + Задания.Добавить(ЗапуститьЗадание("Первый", ПервыйПрочитал, ВторойПрочитал)); + Задания.Добавить(ЗапуститьЗадание("Второй", ВторойПрочитал, ПервыйПрочитал)); + Исключение + Ошибка = ИнформацияОбОшибке(); + ТестовыеУтилиты.СообщитьОЗаданиях(ТестовыеУтилиты.ДождатьсяЗаданий(Задания)); + ВызватьИсключение; + КонецПопытки; + + ТестовыеУтилиты.ПроверитьЗадания(Задания); + + Строки = ТестовыеУтилиты.ВыполнитьЗапрос(МенеджерСущностей, "SELECT Имя FROM Авторы"); + Ожидаем.Что(Строки, "Оба действия записали").ИмеетДлину(2); + +КонецПроцедуры + +// Запускает задание, выполняющее монопольное действие своим коннектором. +// +// Параметры: +// Имя - Строка - Имя записываемого автора. +// СвойСигнал - СигналСобытия - Отмечается заданием после чтения. +// ЧужойСигнал - СигналСобытия - Чтение другого задания. +// +// Возвращаемое значение: +// ФоновоеЗадание - Запущенное задание. +// +Функция ЗапуститьЗадание(Знач Имя, СвойСигнал, ЧужойСигнал) + + Параметры = Новый Массив; + Параметры.Добавить(СтрокаСоединения); + Параметры.Добавить(Имя); + Параметры.Добавить(СвойСигнал); + Параметры.Добавить(ЧужойСигнал); + + Возврат ФоновыеЗадания.Выполнить(ЭтотОбъект, "ПрочитатьИЗаписатьМонопольноВФоне", Параметры, Истина); + +КонецФункции + +// Открывает свой коннектор и выполняет им монопольно чтение и запись. Выполняется фоновым +// заданием. +// +// Параметры: +// СтрокаСоединенияБазы - Строка - Строка соединения с базой теста. +// Имя - Строка - Имя записываемого автора. +// СвойСигнал - СигналСобытия - Отмечается после чтения. +// ЧужойСигнал - СигналСобытия - Чтение другого задания. +// +Процедура ПрочитатьИЗаписатьМонопольноВФоне(Знач СтрокаСоединенияБазы, Знач Имя, СвойСигнал, ЧужойСигнал) Экспорт + + Коннектор = Новый КоннекторSQLite; + Коннектор.Открыть(СтрокаСоединенияБазы, Новый Массив); + + Попытка + Действие = Новый ЧтениеИЗаписьАвтора(Коннектор, Имя, СвойСигнал, ЧужойСигнал); + Коннектор.ВыполнитьМонопольно(Новый Действие(Действие, "Записать")); + Исключение + Ошибка = ИнформацияОбОшибке(); + Коннектор.Закрыть(); + ВызватьИсключение; + КонецПопытки; + + Коннектор.Закрыть(); + +КонецПроцедуры From 0593a7282b3fb52252acb06be7862559ff788c8b Mon Sep 17 00:00:00 2001 From: sfaqer Date: Fri, 2 Oct 2026 21:41:22 +1000 Subject: [PATCH 2/2] =?UTF-8?q?docs(sqlite):=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=9D=D0=B0=D1=87=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=D0=A2=D1=80=D0=B0=D0=BD=D0=B7=D0=B0=D0=BA=D1=86=D0=B8=D1=8E=20?= =?UTF-8?q?=D0=B8=20=D0=92=D1=8B=D0=BF=D0=BE=D0=BB=D0=BD=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=D0=9C=D0=BE=D0=BD=D0=BE=D0=BF=D0=BE=D0=BB=D1=8C=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=20=D0=BA=D0=BE=D0=B4=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit НачатьТранзакцию отложенная, BEGIN IMMEDIATE берет только своя транзакция ВыполнитьМонопольно. Документация повторяла первый вариант Е.4. Co-Authored-By: Claude Opus 5.5 --- ...20\265\320\272\321\202\320\276\321\200SQLite.md" | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" index f9e202e8..b8b4b962 100644 --- "a/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" +++ "b/docs/\320\232\320\276\320\275\320\275\320\265\320\272\321\202\320\276\321\200SQLite.md" @@ -45,10 +45,10 @@ ```bsl // Начинает новую транзакцию в БД. // -// Транзакция сразу берет блокировку записи (BEGIN IMMEDIATE). Отложенная взяла бы ее только на -// первой записи, и две транзакции, обе успевшие прочитать, ждали бы друг друга до ошибки -// "database is locked". Немедленная ждет предшественницу на начале - не дольше срока ожидания -// соединения, параметр строки соединения "Default Timeout", по умолчанию 30 секунд. +// Транзакция отложенная: блокировку записи она берет на первой записи, поэтому параллельные +// читающие транзакции друг другу не мешают. Две транзакции, обе успевшие прочитать и затем +// пишущие, ждут друг друга до ошибки "database is locked" - такую работу следует выполнять +// монопольно, см. ВыполнитьМонопольно. // Процедура НачатьТранзакцию() Экспорт ``` @@ -87,6 +87,11 @@ // Выполняет действие в транзакции: в своей, если транзакция еще не начата, иначе - в уже // начатой. // +// Своя транзакция сразу берет блокировку записи (BEGIN IMMEDIATE): действие, читающее перед +// записью, в отложенной транзакции ждало бы такое же параллельное действие до ошибки +// "database is locked". Немедленная ждет предшественницу на начале - не дольше срока ожидания +// соединения, параметр строки соединения "Default Timeout", по умолчанию 30 секунд. +// // Параметры: // Действие - Действие - Действие без параметров. //