План обмена 1С — объект конфигурации, который помнит, какие данные изменились и какому узлу их ещё нужно отправить. На нём построены РИБ, синхронизация в типовых конфигурациях и самописные обмены. Выбор способа обмена разобран в обзоре способов обмена данными 1С, здесь — только план обмена данными: узлы, состав, регистрация изменений, сообщения обмена, квитирование и РИБ.
Код — для платформы 8.3, имена условные: план обмена Магазины с реквизитом узла Склад, справочник Номенклатура, документ ПеремещениеТоваров. Процедуры обмена лежат в общем модуле с флажком «Сервер».
План обмена устроен как справочник: его элементы — узлы, участники обмена (центральная база, магазин, сайт). У узла есть код, наименование, свои реквизиты и стандартные НомерОтправленного и НомерПринятого. Один узел платформа создаёт сама — ЭтотУзел, он обозначает текущую базу. Его возвращает ПланыОбмена.Магазины.ЭтотУзел(), удалить его нельзя.
Как создать план обмена с нуля:
Магазины.Склад.Код ЭтотУзел — подпись отправителя в заголовке сообщения: база-получатель ищет у себя узел с таким кодом. Поэтому коды настраивают крест-накрест:
| База | Код ЭтотУзел | Узел-корреспондент |
|---|---|---|
| Центральный офис | ЦО | М01 — магазин |
| Магазин | М01 | ЦО — центральный офис |
// Общий модуль ОбменМагазины, флажок «Сервер». Имена условные: план обмена
// Магазины с реквизитом узла Склад. В ЦО: НастроитьЭтотУзел("ЦО", "Центральный офис")
// и УзелКорреспондента("М01", "Магазин 1", Склад); в базе магазина — зеркально.
// Код этого узла попадает в заголовок каждого сообщения как код отправителя.
Процедура НастроитьЭтотУзел(Код, Наименование) Экспорт
УзелОбъект = ПланыОбмена.Магазины.ЭтотУзел().ПолучитьОбъект();
Если УзелОбъект.Код = Код И УзелОбъект.Наименование = Наименование Тогда
Возврат;
КонецЕсли;
УзелОбъект.Код = Код;
УзелОбъект.Наименование = Наименование;
УзелОбъект.Записать();
КонецПроцедуры
// Возвращает узел другой базы, при первом вызове создаёт его.
Функция УзелКорреспондента(Код, Наименование, Склад = Неопределено) Экспорт
Узел = ПланыОбмена.Магазины.НайтиПоКоду(Код);
Если Узел = ПланыОбмена.Магазины.ЭтотУзел() Тогда
ВызватьИсключение СтрШаблон("Код %1 уже занят узлом этой базы", Код);
ИначеЕсли ЗначениеЗаполнено(Узел) Тогда
Возврат Узел;
КонецЕсли;
УзелОбъект = ПланыОбмена.Магазины.СоздатьУзел();
УзелОбъект.Код = Код;
УзелОбъект.Наименование = Наименование;
Если Склад <> Неопределено Тогда
УзелОбъект.Склад = Склад;
КонецЕсли;
УзелОбъект.Записать();
Возврат УзелОбъект.Ссылка;
КонецФункции
// Узлы для регламентного обмена: все, кроме этого и помеченных на удаление.
Функция УзлыОбмена() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Магазины.Ссылка КАК Узел
|ИЗ
| ПланОбмена.Магазины КАК Магазины
|ГДЕ
| Магазины.Ссылка <> &ЭтотУзел
| И НЕ Магазины.ПометкаУдаления";
Запрос.УстановитьПараметр("ЭтотУзел", ПланыОбмена.Магазины.ЭтотУзел());
Возврат Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Узел");
КонецФункцииСоздатьУзел() работает как СоздатьЭлемент() у справочника: объект живёт в памяти до Записать(). Проверка на ЭтотУзел не даёт завести корреспондента с кодом своей базы — иначе ошибка всплывёт только при первой загрузке. С версии 8.3.9 у ссылки на узел есть и свойство ЭтотУзел (Булево).
В состав включают объекты, изменения которых надо отслеживать: справочники, документы, регистры, константы. У каждого — признак Авторегистрация. При «Разрешить» каждая запись или удаление объекта регистрирует изменение для всех узлов плана, кроме ЭтотУзел и узла из ОбменДанными.Отправитель, — даже если данные не менялись. При «Запретить» очередь наполняет только код.
Регистрируется факт, а не версия: узел, ключ данных и номер сообщения. Записи лежат в таблицах изменений, по одной на объект для всех планов обмена. В запросе это Справочник.Номенклатура.Изменения с полями Узел, НомерСообщения, Ссылка.
До вызова ПередЗаписью платформа заполняет набор ОбменДанными.Получатели узлами, для которых будет регистрация. Его можно сократить или дополнить — так делают отбор по складу или организации.
// Модуль объекта документа ПеремещениеТоваров. Документ входит в состав плана
// обмена Магазины с авторегистрацией «Разрешить». Перед вызовом ПередЗаписью
// платформа заполняет ОбменДанными.Получатели узлами, которым уйдёт изменение.
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Фильтр работает и при загрузке: документ, полученный ЦО от одного
// магазина, не должен разойтись по остальным магазинам.
ОставитьНужныхПолучателей();
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Здесь обычная логика документа: заполнение, проверки, пересчёт сумм.
КонецПроцедуры
// Убирает магазины, склады которых не участвуют в перемещении.
// Узел ЦО (Склад не заполнен) и узлы других планов обмена не трогаем.
Процедура ОставитьНужныхПолучателей()
Лишние = Новый Массив;
Для Каждого Узел Из ОбменДанными.Получатели Цикл
Если ТипЗнч(Узел) <> Тип("ПланОбменаСсылка.Магазины") Тогда
Продолжить;
КонецЕсли;
СкладУзла = Узел.Склад;
Если ЗначениеЗаполнено(СкладУзла)
И СкладУзла <> СкладОтправитель
И СкладУзла <> СкладПолучатель Тогда
Лишние.Добавить(Узел);
КонецЕсли;
КонецЦикла;
Для Каждого Узел Из Лишние Цикл
ОбменДанными.Получатели.Удалить(Узел);
КонецЦикла;
КонецПроцедурыОбменДанными.Загрузка: при загрузке авторегистрация тоже работает, и без фильтра документ одного магазина ушёл бы через ЦО во все остальные.ОбменДанными.Получатели.Добавить(Узел).ПланыОбмена.ЗарегистрироватьИзменения(Узлы, Данные) ставит данные в очередь узла или массива узлов, ПланыОбмена.УдалитьРегистрациюИзменений снимает. Что передают вторым параметром:
| Данные | Что регистрируется |
|---|---|
| Объект метаданных | все данные этого объекта |
| Ссылка или объект | один элемент |
| Набор записей | записи регистра по отбору набора |
| Массив ссылок или наборов | то же списком, в одной транзакции |
| Не указано | весь состав плана, только для одного узла |
Объект должен входить в состав плана. УдалитьРегистрациюИзменений принимает ещё и число: удаляются записи узла с номером сообщения не больше него — это и есть квитирование.
// Общий модуль ОбменМагазины (продолжение).
// Узел подключили к работающей базе: авторегистрация ловит только изменения
// после его создания, поэтому стартовый набор данных регистрируем сами.
Процедура ЗарегистрироватьСтартовыеДанные(Узел) Экспорт
// Справочник целиком: передаём объект метаданных.
ПланыОбмена.ЗарегистрироватьИзменения(Узел, Метаданные.Справочники.Номенклатура);
// Документы — только склада узла и за 30 дней. Массив ссылок
// регистрируется одним вызовом в одной транзакции.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Док.Ссылка КАК Ссылка
|ИЗ
| Документ.ПеремещениеТоваров КАК Док
|ГДЕ
| Док.Дата >= &Начало
| И (Док.СкладОтправитель = &Склад
| ИЛИ Док.СкладПолучатель = &Склад)";
Запрос.УстановитьПараметр("Начало", НачалоДня(ТекущаяДатаСеанса()) - 30 * 86400);
Запрос.УстановитьПараметр("Склад", Узел.Склад);
Ссылки = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
Если Ссылки.Количество() > 0 Тогда
ПланыОбмена.ЗарегистрироватьИзменения(Узел, Ссылки);
КонецЕсли;
КонецПроцедуры
// После перепроведения закрытого периода в очереди тысячи старых документов.
// Снимаем с отправки те, что ещё не выгружены и датированы раньше Граница.
Процедура СнятьСОтправкиСтарыеДокументы(Узел, Граница) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Изм.Ссылка КАК Ссылка
|ИЗ
| Документ.ПеремещениеТоваров.Изменения КАК Изм
|ГДЕ
| Изм.Узел = &Узел
| И Изм.НомерСообщения ЕСТЬ NULL
| И Изм.Ссылка.Дата < &Граница";
Запрос.УстановитьПараметр("Узел", Узел);
Запрос.УстановитьПараметр("Граница", Граница);
Ссылки = Запрос.Выполнить().Выгрузить().ВыгрузитьКолонку("Ссылка");
Если Ссылки.Количество() > 0 Тогда
ПланыОбмена.УдалитьРегистрациюИзменений(Узел, Ссылки);
КонецЕсли;
КонецПроцедурыПервая процедура нужна при подключении узла к работающей базе: авторегистрация не знает о данных, созданных до него. Во второй условие НомерСообщения ЕСТЬ NULL оставляет только невыгруженное — выгруженное ждёт квитанции. Удаления остаются в очереди: у ссылки на удалённый документ Ссылка.Дата равна NULL.
Сообщение обмена — XML с заголовком (план обмена, коды отправителя и получателя, номер сообщения, номер последнего принятого) и телом с данными. Заголовок пишет ЗаписьСообщенияОбмена, тело — ваш код; строку из примера удобно отдавать через HTTP-сервис.
// Общий модуль ОбменМагазины (продолжение).
// Собирает сообщение для узла в строку XML: её можно вернуть из HTTP-сервиса,
// записать в файл или отправить почтой.
Функция СообщениеДляУзла(Узел) Экспорт
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.УстановитьСтроку("UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
ЗаписьСообщения.НачатьЗапись(ЗаписьXML, Узел); // номер = НомерОтправленного + 1
Попытка
Выборка = ПланыОбмена.ВыбратьИзменения(Узел, ЗаписьСообщения.НомерСообщения);
Пока Выборка.Следующий() Цикл
Данные = Выборка.Получить(); // объект, набор записей или УдалениеОбъекта
Если ТипЗнч(Данные) = Тип("ДокументОбъект.ПеремещениеТоваров") Тогда
// Объект только прочитан в память, в базу он не пишется:
// внутренний комментарий ЦО магазину не передаём.
Данные.Комментарий = "";
КонецЕсли;
ЗаписатьXML(ЗаписьXML, Данные);
КонецЦикла;
ЗаписьСообщения.ЗакончитьЗапись();
Исключение
ЗаписьСообщения.ПрерватьЗапись();
ВызватьИсключение;
КонецПопытки;
Возврат ЗаписьXML.Закрыть();
КонецФункцииНачатьЗапись присваивает номер «НомерОтправленного + 1» и блокирует узел от параллельного обмена; получателем не может быть ЭтотУзел.ВыбратьИзменения ничего не удаляет, а проставляет номер сообщения в записях регистрации. Удалит их квитанция из ответа.Получить() возвращает объект, набор записей, менеджер значения константы или УдалениеОбъекта. Объект можно поправить в памяти перед сериализацией.ЗакончитьЗапись сообщение не считается отправленным.ВыбратьИзменения — фильтр для выгрузки частями. Записи вне фильтра сохраняют старый номер, и квитанция на новое сообщение удалит их, даже если старое потерялось.// Общий модуль ОбменМагазины (продолжение), выполняется в ЦО.
// Загружает сообщение магазина. Номенклатуру ведут только в ЦО: её изменения
// из магазина не принимаем и ставим к отправке версию ЦО.
Процедура ЗагрузитьСообщение(ТекстXML) Экспорт
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.УстановитьСтроку(ТекстXML);
ЧтениеСообщения = ПланыОбмена.СоздатьЧтениеСообщения();
// Проверка заголовка: план обмена, узлы, номер сообщения.
ЧтениеСообщения.НачатьЧтение(ЧтениеXML, ДопустимыйНомерСообщения.Больший);
Отправитель = ЧтениеСообщения.Отправитель;
НачатьТранзакцию();
Попытка
// Квитанция: магазин получил наши сообщения до НомерПринятого включительно.
ПланыОбмена.УдалитьРегистрациюИзменений(Отправитель, ЧтениеСообщения.НомерПринятого);
Пока ВозможностьЧтенияXML(ЧтениеXML) Цикл
Данные = ПрочитатьXML(ЧтениеXML);
Если ЭтоНоменклатура(Данные) Тогда
ПланыОбмена.ЗарегистрироватьИзменения(Отправитель, Данные.Ссылка);
Продолжить;
КонецЕсли;
Данные.ОбменДанными.Отправитель = Отправитель; // не регистрировать обратно
Данные.ОбменДанными.Загрузка = Истина; // без бизнес-логики
Данные.Записать();
КонецЦикла;
ЧтениеСообщения.ЗакончитьЧтение(); // запоминает НомерПринятого
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЧтениеСообщения.ПрерватьЧтение();
ВызватьИсключение;
КонецПопытки;
ЧтениеXML.Закрыть();
КонецПроцедуры
// Изменение или удаление элемента справочника Номенклатура.
Функция ЭтоНоменклатура(Данные)
Если ТипЗнч(Данные) = Тип("УдалениеОбъекта") Тогда
Возврат ТипЗнч(Данные.Ссылка) = Тип("СправочникСсылка.Номенклатура");
КонецЕсли;
Возврат ТипЗнч(Данные) = Тип("СправочникОбъект.Номенклатура");
КонецФункцииНачатьЧтение проверяет заголовок — план обмена, узлы, номер — и при расхождении вызывает исключение, поэтому стоит до транзакции.УдалитьРегистрациюИзменений(Отправитель, НомерПринятого) — квитанция за наши сообщения.ОбменДанными.Отправитель отключает регистрацию обратно отправителю; для остальных узлов авторегистрация работает с фильтром из модуля документа.ОбменДанными.Загрузка = Истина — запись с минимумом проверок. ПередЗаписью, ПриЗаписи и подписки всё равно вызываются, поэтому в них первой строкой проверяют флаг.УдалениеОбъекта.ИзменениеЗарегистрировано — в обзорной статье.Цикл обмена между ЦО и магазином М01:
НомерПринятого = 9 — последним, что ЦО принял от М01. Выбранные записи регистрации получают номер 15, НомерОтправленного узла М01 становится 15.НомерПринятого от ЦО, удаляет свои регистрации для ЦО с номерами до 9 и запоминает НомерПринятого = 15.НомерПринятого = 15, и ЦО удаляет регистрации для М01 с номерами до 15.Если сообщение № 15 потерялось, ВыбратьИзменения заберёт неподтверждённые записи в № 16 — переотправлять вручную ничего не нужно. Объект, изменённый после выгрузки, снова встаёт в очередь без номера, и квитанция за старое сообщение его не удалит.
Допустимый номер задаёт второй параметр НачатьЧтение:
ДопустимыйНомерСообщения.Больший — по умолчанию: номер больше последнего принятого, пропуски разрешены;ДопустимыйНомерСообщения.Очередной — строго следующий номер;ДопустимыйНомерСообщения.Любой — без проверки, для осознанной повторной загрузки.Функция обходит состав плана и считает записи в таблицах изменений по узлу, процедура очищает очередь одного объекта.
// Общий модуль ОбменМагазины (продолжение).
// Очередь узла по каждому объекту состава: сколько изменений ещё не выгружено
// и сколько выгружено, но не подтверждено квитанцией.
Функция ОчередьУзла(Узел) Экспорт
Результат = Новый ТаблицаЗначений;
Результат.Колонки.Добавить("Объект", Новый ОписаниеТипов("Строка"));
Результат.Колонки.Добавить("Авторегистрация", Новый ОписаниеТипов("Булево"));
Результат.Колонки.Добавить("НеВыгружено", Новый ОписаниеТипов("Число"));
Результат.Колонки.Добавить("ЖдетКвитанции", Новый ОписаниеТипов("Число"));
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("Узел", Узел);
Для Каждого ЭлементСостава Из Метаданные.ПланыОбмена.Магазины.Состав Цикл
ПолноеИмя = ЭлементСостава.Метаданные.ПолноеИмя();
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК Всего,
| КОЛИЧЕСТВО(Изм.НомерСообщения) КАК Выгружено
|ИЗ
| " + ПолноеИмя + ".Изменения КАК Изм
|ГДЕ
| Изм.Узел = &Узел";
Выборка = Запрос.Выполнить().Выбрать();
Если Не Выборка.Следующий() Или Выборка.Всего = 0 Тогда
Продолжить;
КонецЕсли;
СтрокаОчереди = Результат.Добавить();
СтрокаОчереди.Объект = ПолноеИмя;
СтрокаОчереди.Авторегистрация =
(ЭлементСостава.АвтоРегистрация = АвтоРегистрацияИзменений.Разрешить);
СтрокаОчереди.НеВыгружено = Выборка.Всего - Выборка.Выгружено;
СтрокаОчереди.ЖдетКвитанции = Выборка.Выгружено;
КонецЦикла;
Результат.Сортировать("НеВыгружено Убыв");
Возврат Результат;
КонецФункции
// Снимает с отправки все изменения одного объекта состава,
// например "РегистрСведений.ЦеныМагазинов".
Процедура ОчиститьОчередьОбъекта(Узел, ПолноеИмя) Экспорт
ОбъектМетаданных = Метаданные.НайтиПоПолномуИмени(ПолноеИмя);
Если ОбъектМетаданных = Неопределено
Или Не Метаданные.ПланыОбмена.Магазины.Состав.Содержит(ОбъектМетаданных) Тогда
ВызватьИсключение СтрШаблон("%1 не входит в состав плана обмена", ПолноеИмя);
КонецЕсли;
ПланыОбмена.УдалитьРегистрациюИзменений(Узел, ОбъектМетаданных);
КонецПроцедурыКОЛИЧЕСТВО(Изм.НомерСообщения) не считает NULL, поэтому разность с КОЛИЧЕСТВО(*) — записи, ещё не попавшие в сообщения.ПланыОбмена.УдалитьРегистрациюИзменений(Узел). Делайте это, только если узел получит данные другим путём.С флажком «Распределенная информационная база» все узлы работают на одной конфигурации, а её изменения идут из главного узла в подчинённые вместе с данными. Сериализацию, запись данных и коллизии платформа берёт на себя (побеждает главный узел), поэтому вместо цикла с ЗаписатьXML вызывают ПланыОбмена.ЗаписатьИзменения и ПланыОбмена.ПрочитатьИзменения. Подчинённый узел создаёт ПланыОбмена.СоздатьНачальныйОбраз, а ПланыОбмена.ГлавныйУзел() возвращает главный узел или Неопределено в корневой базе.
// Общий модуль ОбменРИБ, флажок «Сервер». План обмена Филиалы (имя условное)
// с флажком «Распределенная информационная база». Данные и изменения
// конфигурации платформа сериализует сама: ЗаписатьXML и ПрочитатьXML не нужны.
Процедура ВыгрузитьДляУзла(Узел, ИмяФайла) Экспорт
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ИмяФайла);
ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
ЗаписьСообщения.НачатьЗапись(ЗаписьXML, Узел);
Попытка
ПланыОбмена.ЗаписатьИзменения(ЗаписьСообщения, 1000); // по 1000 в транзакции
ЗаписьСообщения.ЗакончитьЗапись();
Исключение
ЗаписьСообщения.ПрерватьЗапись();
ЗаписьXML.Закрыть();
ВызватьИсключение;
КонецПопытки;
ЗаписьXML.Закрыть();
КонецПроцедуры
Процедура ЗагрузитьСообщение(ИмяФайла) Экспорт
ЧтениеXML = Новый ЧтениеXML;
ЧтениеXML.ОткрытьФайл(ИмяФайла);
ЧтениеСообщения = ПланыОбмена.СоздатьЧтениеСообщения();
Попытка
ЧтениеСообщения.НачатьЧтение(ЧтениеXML);
ПланыОбмена.ПрочитатьИзменения(ЧтениеСообщения, 1000);
ЧтениеСообщения.ЗакончитьЧтение();
Исключение
// Отправитель заполнен, только пока чтение начато и не завершено.
Если ЧтениеСообщения.Отправитель <> Неопределено Тогда
ЧтениеСообщения.ПрерватьЧтение();
КонецЕсли;
ЧтениеXML.Закрыть();
ВызватьИсключение;
КонецПопытки;
ЧтениеXML.Закрыть();
КонецПроцедурыВторой параметр — сколько элементов обрабатывать в одной транзакции, 0 — все сразу. Если главный узел прислал изменения конфигурации, ПрочитатьИзменения в подчинённом узле остановится с ошибкой «Из главного узла распределенной информационной базы получены изменения конфигурации. Необходимо выполнить обновление конфигурации базы данных.» — обновите конфигурацию и повторите загрузку.
// Модуль объекта справочника Номенклатура в конфигурации с РИБ:
// справочник правят в главном узле, в подчинённых он только читается.
Процедура ПередЗаписью(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат; // элемент пришёл из главного узла через ПрочитатьИзменения
КонецЕсли;
Если ПланыОбмена.ГлавныйУзел() <> Неопределено Тогда
ВызватьИсключение "Номенклатуру изменяют только в главном узле";
КонецЕсли;
КонецПроцедурыДанные из главного узла платформа пишет в режиме загрузки, поэтому проверка флага стоит первой — иначе подчинённый узел отверг бы изменения из центра.
НомерОтправленного у узла-получателя в восстановленной базе (у объекта узла номера доступны на запись) или один раз читают с ДопустимыйНомерСообщения.Любой.Метаданные.ПланыОбмена.Магазины.Состав.Содержит(...).НачатьЧтение не находит узел из заголовка. Сверьте коды по таблице выше.НачатьЧтение или НачатьЗапись.ОбменДанными.Отправитель: принятый объект регистрируется обратно, и базы пересылают его друг другу каждый сеанс.ОбменДанными.Загрузка: документы перенумеровываются, реквизиты перезаполняются, движения пишутся дважды.Да, как журнал изменений: план с одним узлом «Сайт». Выгрузка берёт ссылки из Справочник.Номенклатура.Изменения по этому узлу и после ответа сайта снимает регистрацию методом УдалитьРегистрациюИзменений. Если товары правят часто, выбирайте через ВыбратьИзменения и снимайте по номеру — правки во время выгрузки не потеряются.
План обмена решает, что и кому отправить: узлы, очередь, номера. Как превратить объект одной конфигурации в объект другой, решают правила конвертации данных или формат EnterpriseData. ЗаписатьXML и ПрочитатьXML из примеров переносят объекты один к одному и годятся, только когда объекты обмена в обеих базах одинаковые.
Передать в НачатьЧтение второй параметр ДопустимыйНомерСообщения.Любой. Объекты перезапишутся, НомерПринятого не уменьшится.
Если обмен теряет или дублирует данные, очередь узлов разрослась или нужны свои правила отбора и коллизий, быстрее поручить диагностику и доработку исполнителю: услуги по доработке 1С. Опишите базы, план обмена и симптомы в форме задания, оценка бесплатная.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.