Копия базы 1С для отчетов. Как работать с репликой только для чтения
#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:
- Доставка логов транзакций (доступен в редакциях Standard и Enterprise)
- Репликация данных (различные способы) (обычно все варианты доступны в Standard и Enterprise, но есть исключения)
- Группы высокой доступности AlwaysOn (полный функционал доступен в Enterprise, но ограниченный функционал доступен и в Standard)
Для 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 NULLBEGINSELECT @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])ENDGO-- Удаление всех таблиц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 NULLBEGINSELECT @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])ENDGO
Не запустите этот скрипт на другой базе, он безвозвратно удалит все объекты :)
2. Создаем таблицы
Как уже говорилось выше, нужно “объяснить” платформе 1С, что эта база доступна для работы. Именно поэтому некоторые объекты из исходной базы нужно перенести как есть, то есть в виде таблиц. При этом мы сохраним возможность записи в эти таблицы.
Некоторые объекты, например, таблицы с конфигурацией базы данных, по факту не будут использоваться для записи. Но если сделать их представлениями платформа все равно не сможет с ними работать из-за своих “особых” внутренностей. Поэтому некоторые таблицы переносятся как есть.
Ну и не забываем, что нужно не только создать таблицы, но и заполнить их из реплики.
-- !!! Эти параметры настраиваются перед запуском !!!-- Имя оригинальной базы данных (реплики)DECLARE @originalDatabaseName sysname = 'BSL-ORIG';-- Имя адаптированной базы данных, где нужно создать объектыDECLARE @adaptiveDatabaseName sysname = 'BSL-COPY';DECLARE @tableName sysname;DECLARE tables_cursor CURSORFOR SELECT[name]FROM SYSOBJECTSWHERE 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 = 0BEGINDECLARE @tableNameFull SYSNAMESELECT @tableNameFull = 'dbo.' + @tableNameDECLARE@object_name SYSNAME, @object_id INTSELECT@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 = @tableNameFullAND o.[type] = 'U'AND o.is_ms_shipped = 0;DECLARE @SQL NVARCHAR(MAX) = '';DECLARE @SQLTRANSFER NVARCHAR(MAX) = '';WITH index_column AS(SELECTic.[object_id], ic.index_id, ic.is_descending_key, ic.is_included_column, c.nameFROM 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_idWHERE ic.[object_id] = @object_id),fk_columns AS(SELECTk.constraint_object_id, cname = c.name, rcname = rc.nameFROM 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_idJOIN sys.columns c WITH (NOWAIT) ON c.[object_id] = k.parent_object_id AND c.column_id = k.parent_column_idWHERE 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'')BEGINDROP TABLE [dbo].[' + CAST(@tablename as nvarchar(max)) + '];ENDCREATE TABLE ' + @object_name + CHAR(13) + '(' + CHAR(13) + STUFF((SELECT CHAR(9) + ', [' + c.name + '] ' +CASE WHEN c.is_computed = 1THEN '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 '' ENDEND + CHAR(13)FROM sys.columns c WITH (NOWAIT)JOIN sys.types tp WITH (NOWAIT) ON c.user_type_id = tp.user_type_idLEFT JOIN sys.computed_columns cc WITH (NOWAIT) ON c.[object_id] = cc.[object_id] AND c.column_id = cc.column_idLEFT 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_idLEFT 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_idWHERE c.[object_id] = @object_idORDER BY c.column_idFOR 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' ENDFROM 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_idWHERE ic.is_included_column = 0AND ic.[object_id] = k.parent_object_idAND ic.index_id = k.unique_index_idFOR XML PATH(N''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, ''))+ ')' + CHAR(13)FROM sys.key_constraints k WITH (NOWAIT)WHERE k.parent_object_id = @object_idAND k.[type] = 'PK'), '') + ')' + CHAR(13)+ ISNULL((SELECT (SELECT CHAR(13) +'ALTER TABLE ' + @object_name + ' WITH'+ CASE WHEN fk.is_not_trusted = 1THEN ' NOCHECK'ELSE ' CHECK'END +' ADD CONSTRAINT [' + fk.name + '] FOREIGN KEY('+ STUFF((SELECT ', [' + k.cname + ']'FROM fk_columns kWHERE 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 kWHERE k.constraint_object_id = fk.[object_id]FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '')+ ')'+ CASEWHEN 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+ CASEWHEN 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_idWHERE fk.parent_object_id = @object_idFOR XML PATH(N''), TYPE).value('.', 'NVARCHAR(MAX)')), '')+ ISNULL(((SELECTCHAR(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' ENDFROM index_column cWHERE c.is_included_column = 0AND c.index_id = i.index_idFOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')'+ ISNULL(CHAR(13) + 'INCLUDE (' +STUFF((SELECT ', [' + c.name + ']'FROM index_column cWHERE c.is_included_column = 1AND c.index_id = i.index_idFOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, '') + ')', '') + CHAR(13)FROM sys.indexes i WITH (NOWAIT)WHERE i.[object_id] = @object_idAND i.is_primary_key = 0AND i.[type] = 2FOR 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.nameFROMsys.columns AS cINNER JOIN sys.types tp ON tp.system_type_id = c.system_type_id AND tp.user_type_id = c.user_type_idWHEREc.OBJECT_ID = OBJECT_ID(@tableNameFull)AND NOT tp.name = 'timestamp'ORDER BYcolumn_idFOR XML PATH(''), TYPE).value('.',N'nvarchar(max)'),1,1,N'')+ N')SELECT' +STUFF((SELECT N',' + c.nameFROMsys.columns AS cINNER JOIN sys.types tp ON tp.system_type_id = c.system_type_id AND tp.user_type_id = c.user_type_idWHEREc.OBJECT_ID = OBJECT_ID(@tableNameFull)AND NOT tp.name = 'timestamp'ORDER BYcolumn_idFOR 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 @SQLTRANSFERFETCH NEXT FROM tables_cursor INTO @tableName;ENDCLOSE 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 FORSELECT-- Удаление существующих представленийN'USE [' + CAST(@adaptiveDatabaseName as nvarchar(max)) + '];IF EXISTS(SELECT 1 FROM sys.objectsWHERE name = N''' + t.name + N''' AND type = N''V'')BEGINDROP VIEW [' + t.name + ']END' AS [DropViewIfExistCommand],-- Создание представленияN'CREATE VIEW [' + t.name + N']ASSELECT' +STUFF((SELECT N',' + c.nameFROMsys.columns AS cWHEREc.OBJECT_ID = t.OBJECT_IDORDER BYcolumn_idFOR 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 INSERTASBEGINSET 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 UPDATEASBEGINSET 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 DELETEASBEGINSET 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 = 0BEGINdeclare @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 @currentCmdSET @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 @currentCmdSET @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 @currentCmdSET @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 @currentCmdFETCH NEXT FROM cmds INTO @dropView, @createView,@createTriggerInsteadOfInsert, @dropTriggerInsteadOfInsert,@createTriggerInsteadOfUpdate, @dropTriggerInsteadOfUpdate,@createTriggerInsteadOfDelete, @dropTriggerInsteadOfDelete;ENDCLOSE cmds;DEALLOCATE cmds;
Готовность №1
Готово! Наша “заглушка” реплики готова для использования.
Все аналогичные действия как-раз выполняются инструментом “Мастер создания копии информационной базы для отчетности”, который Вы можете использовать в качестве шаблона для своей разработки.
Попробуем в ней поработать.
Тестовый запуск
Добавим в кластер 1С информационную базу, которая будет ссылаться на нашу “базу-заглушку”. Запускаем тонкий клиент и … барабанная дробь!
Клиентское приложение запустилось без ошибок! Мы даже “походили” по разделам и некоторым объектам. Попробуем изменить какие-нибудь данные.
И ничего не произошло, наша защита изменений отработала! Сформируем какой-нибудь отчет для очистки совести.
Дело сделано!
Вместо заключения
В самом начале мы говорили про монолиты и решения проблем производительности в них. С помощью таких копий баз данных мы можем выполнять горизонтальное масштабирование базы за счет новых серверов. И это прекрасно! Наверное.
Теперь мы можем использовать базы 1С в режиме “только для чтения” в привычном режиме, запускать клиентов 1С туда. Что это значит:
- Можно создавать копии баз на других серверах и переносить туда работу пользователей, которые не изменяют данные в базе. Как побочный эффект мы получим защиту от изменения данных. Для аудиторов, например, отличный вариант.
- Можно опубликовать такую базу на веб-сервере, добавить необходимые веб-сервисы или HTTP-сервисы и через них получать данные для использования в основной базе. Таким образом можно организовать масштабирование системы.
- Данный подход может работать на любой версии платформы 1С от версии 8.0 до последних релизов. На скриншотах выше все примеры сделаны на 8.3.20.1710.
- Срабатывание защиты от изменений может вызвать непонимание пользователей, ведь никаких сообщений им не выводится. Поэтому рекомендуется сделать изменение прав доступа пользователям в копии так, чтобы само редактирование не было доступно срадствами платформы 1С. Плюс изменить заголовок приложения, чтобы было понятно, что это копия.
- Не забудьте отключить регламентные задания в копии базы. Если они там не нужны явно :)
Можно придумать также множество нестандартных применений для такой работы с базой:
- В базе-заглушке можно создавать представления СУБД с понятными наименованиями, которые будут ссылаться на таблицы реплики. Интеграция таких инструментов как PowerBI теперь не потребуют обращения к оперативной базе данных.
- Так как таблицы конфигурации в базе-заглушке доступны для изменения, то можно в такой базе обновить конфигурацию, если изменения не влияют на структуру данных. То есть можно обновлять конфигурацию и проверять работу еще до применения изменений на основной базе.
- Если используется AlwaysOn, то можно использовать снапшоты в реплике на определенные моменты времени. Тогда можно получить копии базы на утро, например, и работать с ними. Может быть полезно, если людям нужна стабильная версия данных на момент времени для сбора отчетности. А пересоздавая снапшот в режиме скользящего окна, можно каждый день иметь актуальный срез базы данных на определенный момент времени.
- Если стоит задача создания копий распределенных, например в разных городах, то можно создать реплику с помощью распределенной группы доступности AlwaysOn в отделенной точке и сделать для нее базу-заглушку. Так люди географически отдаленные от основной базы смогут работать с ней максимально эффективно. Смотрите схему выше, теперь это возможно и с информационными базами 1С.
- В примерах мы используем SQL Server, но все справедливо и для PostgreSQL со своими нюансами. О последнем примеров не планирую. У кого “красные глаза”, тот сам знает, как это все организовать по аналогии :)
- И много других применений.
Есть ли какие-то минусы у этого подхода? Конечно! Усложнение сопровождения и необходимость в понимании работы СУБД. Если Вы не хотите выходить из зоны комфорта 1С, то это явно не для Вас.
Почему не сделать платный / готовый инструмент, который упростит настройку и создание таких копий баз? Мне это не интересно. Организация поддержки и консультаций - это не мое. Но вся информация теперь есть у Вас, действуйте!
Спасибо, что дочитали до конца. Удачи в делах и хорошего настроения! Вперед к свершениям!