Баг или фича?
Длительное время работая с платформой 1С и решениями на ее основе? приходится встречаться с ее неожиданным поведением в самых разных ситуациях. Странные результаты алгоритмов, непонятные ошибки, вылеты приложения, невероятное поведение клиентского интерфейса, некорректные данные в регистрах, странные результаты при выгрузке данных и многое другое. Одним словом - "магия".
Ниже будут несколько примеров с подробным описанием, чтобы Вы могли попробовать подобное у себя. Все что здесь описано будет актуальным для версии 8.3.13.1690 и, почти уверен, для версий ниже.
По каждому примеру Вам и только Вам решать баг это или фича.
Сталкивались с другими странностями или есть что сказать по текущему материалу? Добро пожаловать в комментарии!
Отличная математика
Начнем с простого случая - округлением дробных чисел при работе с ними во временных таблицах. Звучит странно. Меньше слов - больше кода!
Число1 = 0.000102410434234235348573456378465353465;
Число2 = 0.564004435745654732352330006506040060406;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| &Число1 КАК Число1,
| &Число2 КАК Число2
|ПОМЕСТИТЬ ВТ
|;
|
|ВЫБРАТЬ
| ВТ.Число1 КАК Число1,
| ВТ.Число2 КАК Число2
|ИЗ
| ВТ КАК ВТ";
Запрос.УстановитьПараметр("Число1", Число1);
Запрос.УстановитьПараметр("Число2", Число2);
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Число1Корректное = Выборка.Число1 = Число1;
Сообщить("Проверка числа 1: " + Число1Корректное);
Число2Корректное = Выборка.Число2 = Число2;
Сообщить("Проверка числа 2: " + Число2Корректное);
Как думаете, какой результат мы получим? Хотя что спрашивать, если бы числа после передачи их в запрос не изменились бы, то в примеры бы это не попало. В обоих случаях результат проверки будет отрицательным. Так происходит потому что при создании временных таблиц платформа (Внимание!) почти всегда создает числовое поле с типом "NUMERIC(37, 37)" для чисел. Вот так это выглядит на стороне SQL-сервера.
CREATE TABLE #tt1 (
_Q_000_F_000 NUMERIC(37, 37),
_Q_000_F_001 NUMERIC(37, 37)
)
INSERT INTO #tt1 WITH(TABLOCK)
(_Q_000_F_000, _Q_000_F_001)
SELECT
0.0001024104342342353485734563784653534,
0.5640044357456547323523300065060400604
;
SELECT
T1._Q_000_F_000,
T1._Q_000_F_001
FROM #tt1 T1 WITH(NOLOCK)
В нашем случае количество цифр в дробной части - 39, то есть число "обрезается" до 37 цифр в дробной части. В более ранних версиях платформы, например 8.3.6, количество цифр в дробной части было вообще 8 и можно было очень долго разбираться, почему запрос отрабатывает не так как надо. Подобное поведение также замечено и в файловом режиме работы. На PostgreSQL не пробовал.
Зачем нужно оперировать такими числами? На практике встречался с задачами расчета доли в какой-либо сумме и в некоторых ситуациях такая точность была очень важна. Также могут быть задачи установки приоритета для операций, в которых округление может сыграть злую шутку.
Обходные решения тут также простые:
- Если нужна высокая точность в расчете, то эту бизнес-логику придется перенести на сторону платформы, а не использовать для этого сервер СУБД.
- Если задача позволяет, то можно попытаться перенести часть цифр из дробной части в целую, умножив к примеру на 100000.
В любом случае, с такими ситуациями встречаешься не каждый день.
Дата с миллисекундами
Все мы знаем, что к дате можно прибавлять целое число секунд и пользуемся этим довольно часто. Например, вот так.
ТекДата = ТекущаяДата();
НачалоСледующегоМесяца = КонецМесяца(ТекДата) + 1;
Сообщить("Начало следующего месяца: " + НачалоСледующегоМесяца);
Отлично, но что будет если мы прибавим дробное число к дате. Вот интересный пример.
ДатаОбычная = Дата(2019,2,1);
ДатаСтранная = ДатаОбычная + 0.001;
СравнениеДат1 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 1: " + СравнениеДат1);
ДатаСтранная = ДатаСтранная - 0.001;
СравнениеДат2 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 2: " + СравнениеДат2);
В первом случае даты будут не равны, а во втором равны. То есть фактически даты в 1С поддерживают миллисекунды! Если оперировать не миллисекундами, а микросекундами, то мы сразу увидим, что даты их не поддерживают.
ДатаОбычная = Дата(2019,2,1);
ДатаСтранная = ДатаОбычная + 0.000001;
СравнениеДат1 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 1: " + СравнениеДат1);
ДатаСтранная = ДатаСтранная - 0.000001;
СравнениеДат2 = ДатаСтранная = ДатаОбычная;
Сообщить("Вариант с датами 2: " + СравнениеДат2);
В обоих случаях результат сравнения будет Истина.
На сколько мне известно, подобное поведение никак не документировано, а при разработке в программном коде невозможно определить содержит ли дата миллисекунды. В итоге можно нарваться на ситуацию, когда в алгоритме сравниваются две даты, но они не равны, хотя в отладке "на глаз" ничем не отличаются.
Поэтому, берегите время. И будьте с ним осторожны!
P.S. Определить имеются ли в дате миллисекунды все же можно, но способ не очевидный. Можно из даты вычесть в точности такую же новую дату. Там можно получить дробное число, которое покажет наличие миллисекунд.
Регистр не имеет значения
Платформа по умолчанию для информационных баз использует регистронезависимую проверку паролей пользователей. Трудно назвать это поведение неожиданным, т.к. этот момент прописан в справке, но кто ее читает. :)
Только представьте, служба безопасности требует по регламенту установки сложных паролей вида "aQfxFA352Ga", а по факту регистр букв в нем не будет иметь значение. Пароль "1ABC" эквивалентен паролям "1AbC", "1abc" и т.д.
На Инфостарте об этом уже ни раз говорилось:
- 1С и чувствительность к регистру [поход на грабли] от Дмитрия Кинаша.
-
"Скажи пароль" или как работать со свойством СохраняемоеЗначениеПароля объекта типа ПользовательИнформационнойБазы от Константина Хрипкова.
Исправить эту ситуацию просто, нужно лишь поставить проверку сложности паролей через конфигуратор: "Администрирование" -> "Параметры информационной базы" -> "Проверка сложности паролей пользователей". В этом случае будет задействована регистрозависимая проверка паролей.
Невидимая команда
Еще одно очень интересное поведение платформы - это выполнение глобальной команды без параметров, даже если связанный с ней элемент формы невидим. На Инфостарте его уже даже использовали, причем очень полезным способом. В статье "Открывашка ячеек таблиц" Сергей Старых использует это поведение для открытия значений в любых таблицах, ну или почти.
Суть этого поведения заключается в том, что в конфигурацию можно добавить глобальную непараметризированную команду, назначить ей сочетание клавиш и.... ее можно будет выполнять, даже если на форме она будет невидима.
Демонстрацию такой возможности Вы можете посмотреть в публикации "Открывашка ячеек таблиц", либо в прикрепленной к статье конфигурации со всеми примерами из статьи.
Рекурсивный беспредел
Еще одна особенность - это контроль рекурсивного вызова процедур и функций. Например, попытайтесь выполнить такой код на сервере.
&НаКлиенте
Процедура НачатьРекурсиюНаСервере(Команда)
// Вызываем сервер
НачатьРекурсиюНаСервереНаСервере();
КонецПроцедуры
&НаСервере
Процедура НачатьРекурсиюНаСервереНаСервере()
// И начинаем рекурсию
НачатьРекурсиюНаСервереНаСервере();
КонецПроцедуры
В этом случае платформа обязательно отловит ошибку и выведет исключение.
Если же вы попытаетесь запустить бесконечную рекурсию на клиенте подобным образом.
&НаКлиенте
Процедура НачатьРекурсию(Команда)
// Точка входа в рекунсию
НачатьРекурсиюНаКлиенте();
КонецПроцедуры
&НаКлиенте
Процедура НачатьРекурсиюНаКлиенте()
// И продолжаем вызов, еще и еще
НачатьРекурсиюНаКлиенте();
КонецПроцедуры
То получите аварийное завершение работы клиентского приложения без объяснения причины.
Не берусь судить нормально ли это, но было бы удобнее получить более внятную информацию.
P.S. В технологическом журнале можно найти более подробную информацию, но это не всегда можно сделать быстро на клиентских машинах.
Внезапная пропажа колонок
Занимательная ситуация - Вы обновляете модифицированную конфигурацию, все прошло хорошо. На Ваш взгляд ошибок быть не может, конфигурацию знаете как свои пять пальцев, а релиз от поставщика изменил всего несколько объектов в базе и они никак не связаны с доработками. Применяете изменения и идете спать (ну, базу же обновляют обычно в нерабочее время).
На следующее утро Вам звонок, что в некотором документе пропала колонка "Артикул" в табличной части документа. Вы с хорошим настроением (что может быть лучше раннего начала дня после вечерней работы) начинаете перепроверять все ли правильно перенесено при обновлении и ... ничего не находите.
После более подробного анализа выяснилось следующее:
- В документе была выведена колонка "Артикул" от другого реквизита "Объект" по ссылке, как это возможно сделать в управляемой форме.
- До обновления тип реквизита "Объект" был "СправочникСсылка.Номенклатура".
- После обновления тип стал составным, т.к. к нему добавился тип "СправочникСсылка.ГруппаНоменклатуры".
И вот в этом то и проблема, что от составных типов данных вывод реквизитов через ссылку недоступен. Причем сравнение в конфигураторе такое не покажет, т.к. этот объект вообще может быть без изменений, а если открыть форму в конфигураторе, то тоже не сразу будет заметно, что эта колонка уже не рабочая. Среди элементов формы она присутствует, и идентифицировать ошибку можно только посмотреть ее свойства - там не будет связи с данными.
Но кто будет проверять все колонки вручную, да еще и в не измененных при обновлении объектах. И вот приходится искать альтернативные решения, делать костыли, ведь пропавшая колонка так важна...
Индексированный ад
В одной из прошлых статей уже шла речь о недостатках структуры индексов баз 1С и некоторые способы их решения. Но там речь не шла об избыточных индексах, которые создает платформа. Вот тут то мы пример и приведем.
Во многих конфигурациях есть справочник "Контрагенты" и подчиненный владельцу "ДоговорыКонтрагентов". У обоих справочников включена иерархия. Проблема заключается в том, что при такой комбинации настроек платформа создает множество избыточных индексов. Далее пример.
Справочник "ДоговорыКонтрагентов" подчиненный справочнику "Контрагенты", имеет множество реквизитов, 4 из которых проиндексированы:
- Организация - индексирован с доп. упорядочиванием для списков.
- ДатаНачалаДействия - индексирован
- ДатаОкончанияДействия - индексирован
- Менеджер - индексирован с доп. упорядочиванием для списков.
Из-за включенного подчинения по владельцу и иерархии платформа создает настоящую коллекцию индексов, состав которых схожий.
Имя индекса | Состав полей |
_Reference27_ByPredefinedIDNotUniq | ИмяПредопределенныхДанных |
_Reference27_OwnerCode | Владелец + Код + Ссылка |
_Reference27_OwnerDescr | Владелец + Наименование + Ссылка |
_Reference27_ParentCode | Родитель + ЭтоГруппа + Код + Ссылка + Владелец |
_Reference27_ParentDescr | Родитель + Владелец + ЭтоГруппа + Наименование + Ссылка |
_Reference27_Code | Код + Ссылка |
_Reference27_Descr | Наименование + Ссылка |
_Reference27_ByOwnerFieldFld28 | Владелец + Организация + Наименование + Ссылка + ПометкаУдаления + Менеджер |
_Reference27_ByParentFieldFld28 | Владелец + Родитель + ЭтоГруппа + Организация + Наименование + Ссылка + ПометкаУдаления + Менеджер |
_Reference27_ByFieldFld28 | Организация + Наименование + Ссылка + ПометкаУдаления + Менеджер |
_Reference27_ByOwnerFieldFld55 | Владелец + ДатаНачалаДействия + Ссылка |
_Reference27_ByParentFieldFld55 | Владелец + Родитель + ЭтоГруппа + ДатаНачалаДействия + Ссылка |
_Reference27_ByFieldFld55 | ДатаНачалаДействия + Ссылка |
_Reference27_ByOwnerFieldFld56 | Владелец + ДатаОкончанияДействия + Ссылка |
_Reference27_ByParentFieldFld56 | Владелец + Родитель + ЭтоГруппа + ДатаОкончанияДействия + Ссылка |
_Reference27_ByFieldFld56 | ДатаОкончанияДействия + Ссылка |
_Reference27_ByOwnerFieldFld57 | Владелец + Менеджер + Наименование + Ссылка + ПометкаУдаления + Организация |
_Reference27_ByParentFieldFld57 | Владелец + Родитель + ЭтоГруппа + Менеджер + Наименование + Ссылка + ПометкаУдаления + Организация |
_Reference27_ByFieldFld57 | Менеджер + Наименование + Ссылка + ПометкаУдаления + Организация |
PK___Referen__AC8ED0C46C28FEE5 | Ссылка |
Вот тут то и начинается самое интересное. Индексы очень тяжелые, а большинство из них избыточно. Например, индексы "_Reference27_ByOwnerFieldFld28", "_Reference27_ByParentFieldFld28" избыточны. В большинстве случаев достаточно индекса только по полю "Владелец", которое ускорит поиск договоров по конкретному контрагенту. Конечно, бывает у одно контрагента огромное количество договоров, но это отдельная ситуация. То же самое можно сказать про индексы "_Reference27_ByOwnerFieldFld56", "_Reference27_ByParentFieldFld56" и некоторых других. Все усугубляется еще и тем, что поля с индексированием с доп. упорядочиванием добавляются в другие индексы, что во многих случаях тоже избыточно. В идеале было бы не плохо иметь возможность настраивать необходимость индексов для таких ситуаций, но пока эта возможность отсутствует.
Сейчас это выглядит как создание индексов "на всякий случай", причем индексов тяжелых как в обслуживании, так и по занимаемому месту на диске.
Есть случаи, когда на проектах приходилось либо удалять избыточные индексы бессовестным нерекомендуемым фирмой "1С" способом через SQL, чтобы производительность работы со справочником находилась на приемлемом уровне. Вы можете также взглянуть на современные конфигурации, например ERP - справочник "ДоговорыКонтрагентов" больше не подчинен справочнику "Контрагенты" через владельца, вместо этого в справочнике есть реквизит "Контрагент" с ссылкой на владельца. Иерархия у справочника также отключена.
NULL или Неопределено или ПустаяСсылка
Есть такая особенность у регистров бухгалтерии - значениями субконто, если там составной тип, могут быть конкретная ссылка (в т.ч. и пустая), неопределено и NULL.
Вот немного информации:
-
Исправление ошибки отсутствия субконто в проводках после редактирования плана счетов от Александра Серова
Все это может привести к некоторым странностями при формировании отчетов, когда вроде бы сгруппированные записи двоятся, хотя аналитика одинаковая. Или может быть случай, когда и месяц может не закрыться. Вообщем, настоящее "веселье".
Согласно официальной документации, значение Неопределено может появиться в тех случаях, когда значение составного типа субконто не было установлено явно. В типовых конфигурациях для исправления подобных случаев в регистрах бухгалтерии используют такой алгоритм перед записью.
Процедура ПривестиПустыеЗначенияСубконтоСоставногоТипа(Проводки)
КэшВидыСоставныхСубконто = Новый Соответствие;
Для Каждого Проводка Из Проводки Цикл
Для Каждого Субконто Из Проводка.СубконтоДт Цикл
Если НЕ ЗначениеЗаполнено(Субконто.Значение)
И Субконто.Значение <> Неопределено
И СоставнойТипСубконто(Субконто.Ключ, КэшВидыСоставныхСубконто) Тогда
Проводка.СубконтоДт.Вставить(Субконто.Ключ, Неопределено);
КонецЕсли;
КонецЦикла;
Для Каждого Субконто Из Проводка.СубконтоКт Цикл
Если НЕ ЗначениеЗаполнено(Субконто.Значение)
И Субконто.Значение <> Неопределено
И СоставнойТипСубконто(Субконто.Ключ, КэшВидыСоставныхСубконто) Тогда
Проводка.СубконтоКт.Вставить(Субконто.Ключ, Неопределено);
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Все пустые значения приводятся к Неопределено. Поэтому если Вы не записываете набор в режиме "ОбменДанными.Загрузка", то ошибиться теперь в аналитике будет сложнее. С другой стороны есть еще обмены данными, которые до сих пор создают очень интересные ошибки при заполнении субконто. А про кастомные обработки с инфостарта вообще молчу :)
Что касается значения NULL - то это интересный случай, когда у счетов "на лету" меняют состав субконто. Например, добавляя субконто к счету, по которому учет ведется уже много лет, платформа заполняет значение этого субконто для проводок прошлых периодов значением NULL. Это нужно учитывать при проведении таких опасных операций.
Будьте осторожны при изменении счетов, это может привести к новым приключениям. В любом случае, эти проблемы решаются не сложно, путем написания корректировочных обработок для таких движений. Подобные случаи часто всплывают в период закрытия месяца, когда исправления нужно было внести еще вчера.
Это норма
На этом все. Многие подобные пасхалки давно известны сообществу, а многие и вовсе задокументированы на ИТС. Но если баг / фича задокументированы, это не значит что они перестали ими быть.
В абсолютном большинстве случаев возникающие проблемы из-за описанных выше особенностей платформы легко решить. Сложность только в том, что вопросы возникают не сразу во время разработки или тестирования, а спустя время. Иногда даже годы.
В публикации описаны не все возможные пасхалки платформы, их гораздо больше. Например, если обратиться к механизму СКД или начать обсуждение обширной темы работы через веб-клиент, то можно сделать десятки публикаций.
Есть что добавить? Отлично! Пишите в комментариях. Чем больше пасхалок, тем интересней!
P.S. К публикации прикреплена простая конфигурация с примерами из статьи.