Share:

Копия базы 1С для отчетов. Как работать с репликой только для чтения

YPermitinвSQL Server

2022-04-30

#SQL Server

#онлайн-копия

#репликация

#масштабирование

#1С:Предприятие

Старая проблема

Любовь к монолитам идет от самой фирмы “1С” и простирается на большинство ее партнеров и клиентов. Многие информационные системы на базе платформы 1С построены в виде единого монолита, где ведется как оперативная работа и выполнение различных регламентных операций, так и сбор разного рода отчетности.

У многих архитекторов и разработчиков, имеющих дело с платформой 1С (по крайней мере которых я знаю) есть стойкое видение, что монолит лучше разрозненных баз, т.к. исключает проблемы интеграции, проще идет разработка, нет проблем согласованности изменений между частями системы, меньше проблем с администрированием, да и вообще разделение на сервисы это “модное” зло и так далее.

Для небольших систем это не критично, но если в базе работают сотня и больше пользователей, то монолит может начать сбоить, т.к. OLTP и OLAP нагрузки начинают конкурировать друг с другом за ресурсы. Обычно не в пользу первой.

Отсюда и появляется проблема с производительностью и надежностью. Особенно в периоды закрытия месяца и подобных “особых” периодов. Тормозит ли ваша база в период закрытия или годовой отчетности? Если да, то сочувствую. Думаю, у Вас есть архитектурные проблемы. Или проблемы с железом и пора добавить 6 ТБ оперативной памяти! (это шутка, пощадите ваших админов!).

Ниже мы поговорим о путях решения подобных проблем, рассмотрим некоторые варианты. Основное направление - это вынос отчетной, OLAP нагрузки из основной базы, чтобы никакая отчетность больше не влияла на оперативную работу. Альтернативный путь разбивания монолита на сервисы рассматривать не будем, оставим это для другой истории.

Первое, что приходит в голову

Разработчики 1С тут же предлагают сделать копию информационной базы и отправлять в нее данные основной базы с помощью стандартных или доработанных обменов данными с некоторой периодичностью (это и планы обмена, и регистрация изменений в регистре сведений и так далее). Эту копию использовать как обычными клиентскими сеансами (тонкий и толстый клиенты), так и опубликовать из нее веб-сервис и запрашивать данные через него.

Этот вариант мы рассматривать не будем, так как он имеет существенные недостатки, с которыми придется героически бороться:

  • Снижение производительности и параллельности работы пользователей, особенно при использовании планов обмена.
  • Повышенная нагрузка на сервер 1С как основной базы, так и копии, ведь все операции нужно более-менее дублировать.
  • Сложность в сопровождении из-за потенциальных ошибок в обмене данными, ошибок в конфигурации и так далее.

Для небольших баз это все может быть не критичным, но нужна ли для них копия:)

Если вы считаете, что обмен средствами 1С все же хороший вариант для больших баз, то рекомендую здесь на ИС ознакомиться с опытом коллег.

В общем, этот вариант только для фанатов 1С. Бесстрашных и упрямых.

Какие еще варианты

В статье “Копия базы 1С для отчетов. Или как выжить с тяжелой отчетностью” мы рассматривали разные способы создания копии базы данных для отчетности:

  • Создание копии через обычное бэкапирование.
  • Через репликацию средствами СУБД, в примере был SQL Server.
  • Онлайн-копии через механизм групп высокой доступности AlwaysOn, реализованном также в SQL Server. Для PostgreSQL такое также возможно через потоковую репликацию.

Кроме этого можно создавать копию через механизм доставки жарнала транзакций с учетом всех ограничений этого механизма.

Бэкапирование создает копию, которую платформа 1С может использовать без ограничений, т.к. в базу все также возможна запись данных, но актуальность данных будет сильно страдать в такой базе.

Репликация и AlwaysOn позволяют создать копию базы данных практически в онлайн режиме, но она будет доступна только для чтения. По умолчанию платформа 1С с такими базами работать нормально не сможет. Особенности работы платформы 1С с базами только для чтения описаны в этой инструкции. Самые простые способы подружить платформу 1С с такими базами это:

  • Использовать толстый клиент обычного приложения, предварительно убрав из конфигурации выполнение операций записи при запуске клиента.
  • Опубликовать веб-сервисы и через них обращаться к этой копии.

Также есть более “правильные”, но дорогие способы работы с копиями базы в режиме только для чтения, при этом в более удобном виде:

  • Компания Softpoint реализовала готовый продукт Data Cluster, которая позволяет выполнять балансировку запросов между серверами. Другими словами, это промежуточное ПО позволяет отправлять читающие запросы на онлайн-копии базы данных, причем это происходит незаметно для клиента базы данных. В том числе и для платформы 1С.
  • Сама фирма “1С” реализовала механизм копий баз данных, доступный с лицензией КОРП. Также позволяет создавать копии баз данных и отправлять выполнением запросов на различных серверах.

Но можно пойти и другим путем, который даст больший контроль над процессом создания и использования копий, а также не требует покупки лицензий стороннего ПО или дополнительных лицензий.

Выбираем способ создания копии базы

Первое, что мы выберем - это способ создания копии базы данных. Тут исходим из того, что доступно.

Для SQL Server:

Для PostgreSQL:

В нашем случае выберем механизм групп высокой доступности AlwaysOn из SQL Server. Будем создавать онлайн-копию базы только для чтения именно этим способом. Информацию по настройке AlwaysOn можете посмотреть здесь.

Вы можете выбрать любой другой подходящий способ. Главное создать копию в режиме “только для чтения”.

Почему платформа не работает

Прежде чем что-то делать нужно ответить на вопрос: “Почему платформа 1С не работает с базами в режиме только для чтения?”.

Если попытаться добавить такую базы в кластер 1С и запустить клиентское приложение, то с высокой вероятностью Вы получите такую ошибку.

Невосстановимая ошибка Ошибка при выполнении запроса POST к ресурсу /e1cib/modules/call: по причине: Соединение с сервером баз данных непригодно для использования после разрыва соединения администратором и будет переустановлено. Microsoft SQL Server Native Client 11.0: Failed to update database “YourDatabaseName” because the database is read-only. HRESULT=80004005, SQLSrvr: SQLSTATE=25000, state=2, Severity=10, native=3906, line=1

Причина проста - платформа пытается записать изменения в служебные таблицы, например в “_SystemSettings” таким запросом:

INSERT INTO dbo._SystemSettings (
_UserId,
_ObjectKey,
_SettingsKey,
_Version,
_SettingsPresentation,
_SettingsData,
_DataSeparationUse16249,
_DataSeparationUse16250,
_Fld2293,
_Fld774)
VALUES(@P1,@P2,@P3,@P4,@P5,@P6,@P7,@P8,@P9,@P10)

Такую же ошибку можно получить и при явных операциях изменения данных над объектами базы (справочников, документов и так далее).

Больше всего шансов поймать такую ошибку при использовании клиента в управляемом приложении, т.к. операций записи изменений там происходят гораздо чаще. Например, в таблицу историй действий пользователей “_UsersWorkHistory”, в которой сохраняется информация об использовании форм пользователем. В обычном приложении эта таблица не используется.

Вот список служебных таблиц, в которые платформа 1С может попытаться выполнить запись, даже если никаких действий в базе 1С не выполнять:

  • V8Users
  • DBSchema
  • SchemaStorage
  • Params
  • Files
  • _FrmDtSettings
  • DepotFiles
  • IBVersion
  • Config
  • ConfigCAS
  • ConfigCASSave
  • ConfigSave
  • _UsersWorkHistory
  • _SystemSettings
  • _DynListSettings
  • _CommonSettings
  • _ExtensionsInfo
  • _ExtensionsInfoNGS
  • _ExtensionsRestruct
  • _ExtensionsRestructNGS
  • _YearOffset

Получается, что если мы сможем показать платформе 1С, что запись в эти таблицы возможна без каких-либо ошибок, то проблем с работой в базах с режимом “только для чтения” не будет.

Хитрый ход

Но как можно “обмануть” платформы 1С? Ведь копия базы недоступна для изменения. Но выход есть всегда!

Мы можем создать новую базу данных на том же сервере, рядом с базой реплики, и создать копии перечисленных выше таблиц, перенеся в них данные из реплики. Остальные таблицы мы создавать не будем, вместо этого создадим одноименные представления, которые будут ссылаться на настоящие таблицы в базе данных реплики.

Например, у нас есть база “BSL-ORIG” на основном сервере SRV-SQL-1. Подключена к серверу 1С обычным образом, все работает штатно. Через механизм групп высокой доступности AlwaysOn сделана копия этой базы на SRV-SQL-2. Копия, конечно же, доступна только для чтения.

Настраиваем репликацию

Создаем пустую базу на SRV-SQL-2, назовем ее “BSL_COPY”. Она и будет располагать все необходимые объекты базы. И именно к ней будет обращаться сервер 1С для работы.

Но прежде, чем пустить платформу 1С в огород базу нужно выполнить подготовительные действия.

1. Удаляем все существующие объекты в базе

На всякий случай удалим все существующие объекты, т.к. мы их будем создавать заново. Действие имеет смысл при обновлении объектов, например, если была обновлена конфигурация 1С в оригинальной базе или изменилась структура базы данных после реструктуризации. Обычно эти два шага идут рядом.

-- Удаление всех представлений
DECLARE @name VARCHAR(128)
DECLARE @SQL VARCHAR(254)
SELECT @name = (SELECT TOP 1 [name] FROM sysobjects WHERE [type] = 'V' AND category = 0 ORDER BY [name])
WHILE @name IS NOT NULL
BEGIN
SELECT @SQL = 'DROP VIEW [dbo].[' + RTRIM(@name) +']'
EXEC (@SQL)
SELECT @name = (SELECT TOP 1 [name] FROM sysobjects WHERE [type] = 'V' AND category = 0 AND [name] > @name ORDER BY [name])
END
GO
-- Удаление всех таблиц
DECLARE @name VARCHAR(128)
DECLARE @SQL VARCHAR(254)
SELECT @name = (SELECT TOP 1 [name] FROM sysobjects WHERE [type] = 'U' AND category = 0 ORDER BY [name])
WHILE @name IS NOT NULL
BEGIN
SELECT @SQL = 'DROP TABLE [dbo].[' + RTRIM(@name) +']'
EXEC (@SQL)
SELECT @name = (SELECT TOP 1 [name] FROM sysobjects WHERE [type] = 'U' AND category = 0 AND [name] > @name ORDER BY [name])
END
GO

Не запустите этот скрипт на другой базе, он безвозвратно удалит все объекты :)

2. Создаем таблицы

Как уже говорилось выше, нужно “объяснить” платформе 1С, что эта база доступна для работы. Именно поэтому некоторые объекты из исходной базы нужно перенести как есть, то есть в виде таблиц. При этом мы сохраним возможность записи в эти таблицы.

Некоторые объекты, например, таблицы с конфигурацией базы данных, по факту не будут использоваться для записи. Но если сделать их представлениями платформа все равно не сможет с ними работать из-за своих “особых” внутренностей. Поэтому некоторые таблицы переносятся как есть.

Ну и не забываем, что нужно не только создать таблицы, но и заполнить их из реплики.

-- !!! Эти параметры настраиваются перед запуском !!!
-- Имя оригинальной базы данных (реплики)
DECLARE @originalDatabaseName sysname = 'BSL-ORIG';
-- Имя адаптированной базы данных, где нужно создать объекты
DECLARE @adaptiveDatabaseName sysname = 'BSL-COPY';
DECLARE @tableName sysname;
DECLARE tables_cursor CURSOR
FOR SELECT
[name]
FROM SYSOBJECTS
WHERE xtype = 'U'
-- Список обрабатываемых таблиц
AND [name] IN (
'V8Users',
'DBSchema',
'SchemaStorage',
'Params',
'Files',
'_FrmDtSettings',
'DepotFiles',
'IBVersion',
'Config',
'ConfigCAS',
'ConfigCASSave',
'ConfigSave',
'_UsersWorkHistory',
'_SystemSettings',
'_DynListSettings',
'_CommonSettings',
'_ExtensionsInfo',
'_ExtensionsInfoNGS',
'_ExtensionsRestruct',
'_ExtensionsRestructNGS',
'_YearOffset'
);
OPEN tables_cursor;
FETCH NEXT FROM tables_cursor INTO @tableName;
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @tableNameFull SYSNAME
SELECT @tableNameFull = 'dbo.' + @tableName
DECLARE
@object_name SYSNAME
, @object_id INT
SELECT
@object_name = '[' + s.name + '].[' + o.name + ']'
, @object_id = o.[object_id]
FROM sys.objects o WITH (NOWAIT)
JOIN sys.schemas s WITH (NOWAIT) ON o.[schema_id] = s.[schema_id]
WHERE s.name + '.' + o.name = @tableNameFull
AND o.[type] = 'U'
AND o.is_ms_shipped = 0;
DECLARE @SQL NVARCHAR(MAX) = '';
DECLARE @SQLTRANSFER NVARCHAR(MAX) = '';
WITH index_column AS
(
SELECT
ic.[object_id]
, ic.index_id
, ic.is_descending_key
, ic.is_included_column
, c.name
FROM sys.index_columns ic WITH (NOWAIT)
JOIN sys.columns c WITH (NOWAIT) ON ic.[object_id] = c.[object_id] AND ic.column_id = c.column_id
WHERE ic.[object_id] = @object_id
),
fk_columns AS
(
SELECT
k.constraint_object_id
, cname = c.name
, rcname = rc.name
FROM sys.foreign_key_columns k WITH (NOWAIT)
JOIN sys.columns rc WITH (NOWAIT) ON rc.[object_id] = k.referenced_object_id AND rc.column_id = k.referenced_column_id
JOIN sys.columns c WITH (NOWAIT) ON c.[object_id] = k.parent_object_id AND c.column_id = k.parent_column_id
WHERE k.parent_object_id = @object_id
)
SELECT @SQL = 'USE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '];
IF EXISTS (SELECT * FROM SYSOBJECTS WHERE name=''' + CAST(@tableNameFull as nvarchar(max)) + ''' AND xtype=''U'')
BEGIN
DROP TABLE [dbo].[' + CAST(@tablename as nvarchar(max)) + '];
END
CREATE TABLE ' + @object_name + CHAR(13) + '(' + CHAR(13) + STUFF((
SELECT CHAR(9) + ', [' + c.name + '] ' +
CASE WHEN c.is_computed = 1
THEN 'AS ' + cc.[definition]
ELSE UPPER(tp.name) +
CASE WHEN tp.name IN ('varchar', 'char', 'varbinary', 'binary', 'text')
THEN '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR(5)) END + ')'
WHEN tp.name IN ('nvarchar', 'nchar', 'ntext')
THEN '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS VARCHAR(5)) END + ')'
WHEN tp.name IN ('datetime2', 'time2', 'datetimeoffset')
THEN '(' + CAST(c.scale AS VARCHAR(5)) + ')'
WHEN tp.name = 'decimal'
THEN '(' + CAST(c.[precision] AS VARCHAR(5)) + ',' + CAST(c.scale AS VARCHAR(5)) + ')'
ELSE ''
END +
CASE WHEN c.collation_name IS NOT NULL THEN ' COLLATE ' + c.collation_name ELSE '' END +
CASE WHEN c.is_nullable = 1 THEN ' NULL' ELSE ' NOT NULL' END +
CASE WHEN dc.[definition] IS NOT NULL THEN ' DEFAULT' + dc.[definition] ELSE '' END +
CASE WHEN ic.is_identity = 1 THEN ' IDENTITY(' + CAST(ISNULL(ic.seed_value, '0') AS CHAR(1)) + ',' + CAST(ISNULL(ic.increment_value, '1') AS CHAR(1)) + ')' ELSE '' END
END + CHAR(13)
FROM sys.columns c WITH (NOWAIT)
JOIN sys.types tp WITH (NOWAIT) ON c.user_type_id = tp.user_type_id
LEFT JOIN sys.computed_columns cc WITH (NOWAIT) ON c.[object_id] = cc.[object_id] AND c.column_id = cc.column_id
LEFT JOIN sys.default_constraints dc WITH (NOWAIT) ON c.default_object_id != 0 AND c.[object_id] = dc.parent_object_id AND c.column_id = dc.parent_column_id
LEFT JOIN sys.identity_columns ic WITH (NOWAIT) ON c.is_identity = 1 AND c.[object_id] = ic.[object_id] AND c.column_id = ic.column_id
WHERE c.[object_id] = @object_id
ORDER BY c.column_id
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, CHAR(9) + ' ')
+ ISNULL((SELECT CHAR(9) + ', CONSTRAINT [' + k.name + '] PRIMARY KEY (' +
(SELECT STUFF((
SELECT ', [' + c.name + '] ' + CASE WHEN ic.is_descending_key = 1 THEN 'DESC' ELSE 'ASC' END
FROM sys.index_columns ic WITH (NOWAIT)
JOIN sys.columns c WITH (NOWAIT) ON c.[object_id] = ic.[object_id] AND c.column_id = ic.column_id
WHERE ic.is_included_column = 0
AND ic.[object_id] = k.parent_object_id
AND ic.index_id = k.unique_index_id
FOR XML PATH(N''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, ''))
+ ')' + CHAR(13)
FROM sys.key_constraints k WITH (NOWAIT)
WHERE k.parent_object_id = @object_id
AND k.[type] = 'PK'), '') + ')' + CHAR(13)
+ ISNULL((SELECT (
SELECT CHAR(13) +
'ALTER TABLE ' + @object_name + ' WITH'
+ CASE WHEN fk.is_not_trusted = 1
THEN ' NOCHECK'
ELSE ' CHECK'
END +
' ADD CONSTRAINT [' + fk.name + '] FOREIGN KEY('
+ STUFF((
SELECT ', [' + k.cname + ']'
FROM fk_columns k
WHERE k.constraint_object_id = fk.[object_id]
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '')
+ ')' +
' REFERENCES [' + SCHEMA_NAME(ro.[schema_id]) + '].[' + ro.name + '] ('
+ STUFF((
SELECT ', [' + k.rcname + ']'
FROM fk_columns k
WHERE k.constraint_object_id = fk.[object_id]
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '')
+ ')'
+ CASE
WHEN fk.delete_referential_action = 1 THEN ' ON DELETE CASCADE'
WHEN fk.delete_referential_action = 2 THEN ' ON DELETE SET NULL'
WHEN fk.delete_referential_action = 3 THEN ' ON DELETE SET DEFAULT'
ELSE ''
END
+ CASE
WHEN fk.update_referential_action = 1 THEN ' ON UPDATE CASCADE'
WHEN fk.update_referential_action = 2 THEN ' ON UPDATE SET NULL'
WHEN fk.update_referential_action = 3 THEN ' ON UPDATE SET DEFAULT'
ELSE ''
END
+ CHAR(13) + 'ALTER TABLE ' + @object_name + ' CHECK CONSTRAINT [' + fk.name + ']' + CHAR(13)
FROM sys.foreign_keys fk WITH (NOWAIT)
JOIN sys.objects ro WITH (NOWAIT) ON ro.[object_id] = fk.referenced_object_id
WHERE fk.parent_object_id = @object_id
FOR XML PATH(N''), TYPE).value('.', 'NVARCHAR(MAX)')), '')
+ ISNULL(((SELECT
CHAR(13) + 'CREATE' + CASE WHEN i.is_unique = 1 THEN ' UNIQUE' ELSE '' END
+ ' NONCLUSTERED INDEX [' + i.name + '] ON ' + @object_name + ' (' +
STUFF((
SELECT ', [' + c.name + ']' + CASE WHEN c.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END
FROM index_column c
WHERE c.is_included_column = 0
AND c.index_id = i.index_id
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')'
+ ISNULL(CHAR(13) + 'INCLUDE (' +
STUFF((
SELECT ', [' + c.name + ']'
FROM index_column c
WHERE c.is_included_column = 1
AND c.index_id = i.index_id
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')', '') + CHAR(13)
FROM sys.indexes i WITH (NOWAIT)
WHERE i.[object_id] = @object_id
AND i.is_primary_key = 0
AND i.[type] = 2
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)')
), '')
SELECT
@SQLTRANSFER =
N'USE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '];
DELETE FROM [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[dbo].[' + CAST(@tablename as nvarchar(max)) + '];
INSERT INTO [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[dbo].[' + CAST(@tablename as nvarchar(max)) + ']
(' +
STUFF(
(SELECT N',' + c.name
FROM
sys.columns AS c
INNER JOIN sys.types tp ON tp.system_type_id = c.system_type_id AND tp.user_type_id = c.user_type_id
WHERE
c.OBJECT_ID = OBJECT_ID(@tableNameFull)
AND NOT tp.name = 'timestamp'
ORDER BY
column_id
FOR XML PATH(''), TYPE).value('.',N'nvarchar(max)')
,1,1,N'')
+ N')
SELECT
' +
STUFF(
(SELECT N',' + c.name
FROM
sys.columns AS c
INNER JOIN sys.types tp ON tp.system_type_id = c.system_type_id AND tp.user_type_id = c.user_type_id
WHERE
c.OBJECT_ID = OBJECT_ID(@tableNameFull)
AND NOT tp.name = 'timestamp'
ORDER BY
column_id
FOR XML PATH(''), TYPE).value('.',N'nvarchar(max)')
,1,1,N'')
+ N'
FROM [' + CAST(@originalDatabaseName as nvarchar(max)) + '].[dbo].[' + CAST(@tablename as nvarchar(max)) + '];
';
-- Выполняем команды создания объектов в контексте адаптированной базы данных
EXECUTE sp_executesql @SQL
-- Затем переносим данные из исходной таблицы в созданную
EXECUTE sp_executesql @SQLTRANSFER
FETCH NEXT FROM tables_cursor INTO @tableName;
END
CLOSE tables_cursor;
DEALLOCATE tables_cursor;

По факту эта команда создает таблицы из оригинальной реплики (по структуре), переносит туда данные. Индексы, конечно, также переносятся.

3. Создаем представления, ссылающиеся на оригинальные таблицы из реплики

Этот шаг может казаться простым, но на самом деле нет!

Создать представление, которое ссылается на оригинальную таблицу базы не сложно. Но мы пойдем дальше и сделаем защиту, чтобы при вставке новых значений, удалении или обновлении существующих - SQL Server ничего не делал. Да, он сделает вид, что выполнил операцию и вернет платформе сигнал об успехе, но по факту ничего не произойдет.

Это позволит избежать множества ошибок при запуске клиента 1С, т.к. современные конфигурации при запуске выполняют множество операций записи в базу данных, обновления каких-либо наборов регистра сведений и так далее. Иначе пришлось бы вносить изменения в программный код 1С, а мы этого попытаемся максимально избежать.

Да, последнее не всегда сработает! Например, если с такой защитой попытаться записать НОВЫЙ документ, то возникнет ошибка SDBL. Но это уже мелочи, потому что вряд ли кто-то записывает документы при запуске клиента или при формировании отчетов. Правда ведь, Вы не из этих людей? :)

Сам скрипт нужно запускать в контексте оригинальной реплики. Не забудьте обновить параметры.

-- !!! Эти параметры настраиваются перед запуском !!!
-- Имя оригинальной базы данных (реплики)
DECLARE @originalDatabaseName sysname = 'BSL-ORIG';
-- Имя адаптированной базы данных, где нужно создать объекты
DECLARE @adaptiveDatabaseName sysname = 'BSL-COPY';
DECLARE @dropView nvarchar(max);
DECLARE @createView nvarchar(max);
DECLARE @createTriggerInsteadOfInsert nvarchar(max);
DECLARE @createTriggerInsteadOfUpdate nvarchar(max);
DECLARE @createTriggerInsteadOfDelete nvarchar(max);
DECLARE @dropTriggerInsteadOfInsert nvarchar(max);
DECLARE @dropTriggerInsteadOfUpdate nvarchar(max);
DECLARE @dropTriggerInsteadOfDelete nvarchar(max);
DECLARE cmds CURSOR FOR
SELECT
-- Удаление существующих представлений
N'USE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '];
IF EXISTS(
SELECT 1 FROM sys.objects
WHERE name = N''' + t.name + N''' AND type = N''V'')
BEGIN
DROP VIEW [' + t.name + ']
END' AS [DropViewIfExistCommand],
-- Создание представления
N'CREATE VIEW [' + t.name + N']
AS
SELECT
' +
STUFF(
(SELECT N',' + c.name
FROM
sys.columns AS c
WHERE
c.OBJECT_ID = t.OBJECT_ID
ORDER BY
column_id
FOR XML PATH(''), TYPE).value('.',N'nvarchar(max)')
,1,1,N'')
+ N'
FROM [' + CAST(@originalDatabaseName as nvarchar(max)) + '].[dbo].[' + t.name + N'];
' AS [CreateViewCommand],
-- Создание триггера запрета вставки значений
N'CREATE TRIGGER [dbo].[read_only_' + t.name + N'_insert]
ON [dbo].[' + t.name + N']
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON;
-- При добавлении ничего не выполняем
END
' AS [CreateTriggerBreakInsertCommand],
-- Удалить триггер запрета вставки значений, если существует
N'DROP TRIGGER IF EXISTS [read_only_' + t.name + N'_insert];' AS [DropTriggerIfExistBreakInsertCommand],
-- Создание триггера запрета обновления значений
N'CREATE TRIGGER [dbo].[read_only_' + t.name + N'_update]
ON [dbo].[' + t.name + N']
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- При обновлении ничего не выполняем
END
' AS [CreateTriggerBreakUpdateCommand],
-- Удалить триггер запрета вставки значений, если существует
N'USE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '];
DROP TRIGGER IF EXISTS [read_only_' + t.name + N'_update];' AS [DropTriggerIfExistBreakUpdateCommand],
-- Создание триггера запрета удаления значений
N'CREATE TRIGGER [dbo].[read_only_' + t.name + N'_delete]
ON [dbo].[' + t.name + N']
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
-- При удалении ничего не выполняем
END
' AS [CreateTriggerBreakDeleteCommand],
-- Удалить триггер запрета вставки значений, если существует
N'DROP TRIGGER IF EXISTS [read_only_' + t.name + N'_delete];' AS [DropTriggerIfExistBreakDeleteCommand]
FROM sys.tables AS t
-- Исключаем список таблиц, которые были перенесены "как есть"
WHERE NOT t.[name] IN (
'V8Users',
'DBSchema',
'SchemaStorage',
'Params',
'Files',
'_FrmDtSettings',
'DepotFiles',
'IBVersion',
'Config',
'ConfigCAS',
'ConfigCASSave',
'ConfigSave',
'_UsersWorkHistory',
'_SystemSettings',
'_DynListSettings',
'_CommonSettings',
'_ExtensionsInfo',
'_ExtensionsInfoNGS',
'_ExtensionsRestruct',
'_ExtensionsRestructNGS',
'_YearOffset'
);
OPEN cmds;
FETCH NEXT FROM cmds INTO @dropView, @createView,
@createTriggerInsteadOfInsert, @dropTriggerInsteadOfInsert,
@createTriggerInsteadOfUpdate, @dropTriggerInsteadOfUpdate,
@createTriggerInsteadOfDelete, @dropTriggerInsteadOfDelete;
WHILE @@FETCH_STATUS = 0
BEGIN
declare @currentCmd nvarchar(max);
-- Удаление существующего представления
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@dropView,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
-- Создание представления
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@createView,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
-- Создание защиты от вставки новых значений
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@dropTriggerInsteadOfInsert,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@createTriggerInsteadOfInsert,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
-- Создание защиты от изменения данных
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@dropTriggerInsteadOfUpdate,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@createTriggerInsteadOfUpdate,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
-- Создание защиты от удаления данных
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@dropTriggerInsteadOfDelete,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
SET @currentCmd =
'declare @textCmd nvarchar(max) = CAST('' ' + CAST(REPLACE(@createTriggerInsteadOfDelete,'''','''''') as nvarchar(max)) + ' '' as nvarchar(max));
EXECUTE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '].[sys].sp_executesql @textCmd'
EXECUTE sp_executesql @currentCmd
FETCH NEXT FROM cmds INTO @dropView, @createView,
@createTriggerInsteadOfInsert, @dropTriggerInsteadOfInsert,
@createTriggerInsteadOfUpdate, @dropTriggerInsteadOfUpdate,
@createTriggerInsteadOfDelete, @dropTriggerInsteadOfDelete;
END
CLOSE cmds;
DEALLOCATE cmds;

Готовность №1

Готово! Наша “заглушка” реплики готова для использования.

Реплика настроена

Все аналогичные действия как-раз выполняются инструментом “Мастер создания копии информационной базы для отчетности”, который Вы можете использовать в качестве шаблона для своей разработки.

Попробуем в ней поработать.

Тестовый запуск

Добавим в кластер 1С информационную базу, которая будет ссылаться на нашу “базу-заглушку”. Запускаем тонкий клиент и … барабанная дробь!

Тестовый запуск!

Клиентское приложение запустилось без ошибок! Мы даже “походили” по разделам и некоторым объектам. Попробуем изменить какие-нибудь данные.

Работа с записью в базу

И ничего не произошло, наша защита изменений отработала! Сформируем какой-нибудь отчет для очистки совести.

Формируем отчет на копии

Дело сделано!

Вместо заключения

В самом начале мы говорили про монолиты и решения проблем производительности в них. С помощью таких копий баз данных мы можем выполнять горизонтальное масштабирование базы за счет новых серверов. И это прекрасно! Наверное.

Теперь мы можем использовать базы 1С в режиме “только для чтения” в привычном режиме, запускать клиентов 1С туда. Что это значит:

  • Можно создавать копии баз на других серверах и переносить туда работу пользователей, которые не изменяют данные в базе. Как побочный эффект мы получим защиту от изменения данных. Для аудиторов, например, отличный вариант.
  • Можно опубликовать такую базу на веб-сервере, добавить необходимые веб-сервисы или HTTP-сервисы и через них получать данные для использования в основной базе. Таким образом можно организовать масштабирование системы.
  • Данный подход может работать на любой версии платформы 1С от версии 8.0 до последних релизов. На скриншотах выше все примеры сделаны на 8.3.20.1710.
  • Срабатывание защиты от изменений может вызвать непонимание пользователей, ведь никаких сообщений им не выводится. Поэтому рекомендуется сделать изменение прав доступа пользователям в копии так, чтобы само редактирование не было доступно срадствами платформы 1С. Плюс изменить заголовок приложения, чтобы было понятно, что это копия.
  • Не забудьте отключить регламентные задания в копии базы. Если они там не нужны явно :)
Пример распределенных реплик

Можно придумать также множество нестандартных применений для такой работы с базой:

  • В базе-заглушке можно создавать представления СУБД с понятными наименованиями, которые будут ссылаться на таблицы реплики. Интеграция таких инструментов как PowerBI теперь не потребуют обращения к оперативной базе данных.
  • Так как таблицы конфигурации в базе-заглушке доступны для изменения, то можно в такой базе обновить конфигурацию, если изменения не влияют на структуру данных. То есть можно обновлять конфигурацию и проверять работу еще до применения изменений на основной базе.
  • Если используется AlwaysOn, то можно использовать снапшоты в реплике на определенные моменты времени. Тогда можно получить копии базы на утро, например, и работать с ними. Может быть полезно, если людям нужна стабильная версия данных на момент времени для сбора отчетности. А пересоздавая снапшот в режиме скользящего окна, можно каждый день иметь актуальный срез базы данных на определенный момент времени.
  • Если стоит задача создания копий распределенных, например в разных городах, то можно создать реплику с помощью распределенной группы доступности AlwaysOn в отделенной точке и сделать для нее базу-заглушку. Так люди географически отдаленные от основной базы смогут работать с ней максимально эффективно. Смотрите схему выше, теперь это возможно и с информационными базами 1С.
  • В примерах мы используем SQL Server, но все справедливо и для PostgreSQL со своими нюансами. О последнем примеров не планирую. У кого “красные глаза”, тот сам знает, как это все организовать по аналогии :)
  • И много других применений.

Есть ли какие-то минусы у этого подхода? Конечно! Усложнение сопровождения и необходимость в понимании работы СУБД. Если Вы не хотите выходить из зоны комфорта 1С, то это явно не для Вас.

Почему не сделать платный / готовый инструмент, который упростит настройку и создание таких копий баз? Мне это не интересно. Организация поддержки и консультаций - это не мое. Но вся информация теперь есть у Вас, действуйте!

Спасибо, что дочитали до конца. Удачи в делах и хорошего настроения! Вперед к свершениям!

Y

YPermitin

.NET, TSQL, DevOps, 1C:Enterprise

Developer, just developer.

Поделиться

Другие статьи

Развертывание ASP.NET Core приложений на Ubuntu Linux
Развертывание ASP.NET Core приложений на Ubuntu Linux
Настройка обслуживания PostgreSQL. Основное и простое
Настройка обслуживания PostgreSQL. Основное и простое
Счетчики производительности Windows + PowerShell
Счетчики производительности Windows + PowerShell

Все статьи от автора: YPermitin

Copyright © 2024 Убежище инженера