8 lines
21 KiB
JSON
8 lines
21 KiB
JSON
{
|
||
"index": 31,
|
||
"slug": "editorial-2027-02-field-bitrix-lessons",
|
||
"title": "Миграция данных пользователя Bitrix: сохранить смысл, а не только значение",
|
||
"excerpt": "Как перенести данные пользователя из legacy-контракта Bitrix без тихой потери: различить стандартные и UF-поля, сохранить пустые состояния, сделать read-back и безопасно повторить операцию.",
|
||
"contentHtml": "<p>Сбой обнаруживается после внешне успешной миграции. Bitrix вернул <code>true</code>, но телефон оказался пустым, email изменил регистр, а повторный запуск создал вторую связь. Такая ошибка стоит дороже одной неверной строки: поиск пользователя перестаёт работать, уведомление уходит не туда, а восстановление исходного значения требует отдельного источника данных.</p><p>Первое действие — перестать считать ответ <code>CUser::Update</code> доказательством результата. Метод сообщает, что вызов прошёл без собственной ошибки, но не подтверждает смысл сохранённых данных, работу локальных обработчиков или результат последующего чтения. Миграция готова только тогда, когда mapping понятен, запись прочитана обратно, а повтор того же входа не меняет результат неожиданно.</p><h2>Что именно переносится</h2><p>В Bitrix нужно разделить стандартные поля пользователя и пользовательские поля. К первым относятся, например, <code>EMAIL</code> и <code>PERSONAL_PHONE</code>. Пользовательские поля обычно имеют имена <code>UF_*</code>, а их конкретные коды, типы и множественные значения задаёт сама установка. Поэтому имя из одной базы нельзя переносить в другую только по совпадению текста.</p><p>Есть и третье различие — действие над полем. Отсутствующий ключ означает «не менять», явное пустое значение может означать «очистить», а некорректное значение должно остановить операцию до записи. Если adapter превращает все три состояния в <code>null</code>, он теряет часть команды источника и может стереть данные при частичном обновлении.</p><figure><img src=\"/assets/editorial/2027/bitrix-lessons-2027-migration-evidence-loop.svg\" alt=\"Цикл миграции данных пользователя Bitrix: legacy-вход проходит mapping и запись, затем read-back и retry; missing отделён от clear\" loading=\"lazy\"/><figcaption>Запись — только середина цикла. Доказательство сохранённого смысла появляется после read-back и проверки повторного запуска.</figcaption></figure><h2>Mapping должен описывать контракт</h2><p>Сначала составьте карту полей для одной целевой записи. В ней зафиксируйте не только имена, но и тип, действие при пустом значении, правило нормализации и способ проверки. Имя <code>UF_LEGACY_ID</code> ниже — пример строкового пользовательского поля; в реальном проекте его нужно заменить кодом, который существует в целевой установке.</p><table><caption>Пример карты переноса одной записи</caption><thead><tr><th scope=\"col\">Источник</th><th scope=\"col\">Цель Bitrix</th><th scope=\"col\">Правило</th><th scope=\"col\">Доказательство</th></tr></thead><tbody><tr><td><code>phone</code></td><td><code>PERSONAL_PHONE</code></td><td>обрезать внешние пробелы; пустое значение не скрывать</td><td>read-back и проверка формата</td></tr><tr><td><code>email</code></td><td><code>EMAIL</code></td><td>применить только правило доменного контракта</td><td>точное чтение и отрицательный тест</td></tr><tr><td><code>legacyId</code></td><td><code>UF_LEGACY_ID</code></td><td>проверить тип и уникальность в этой установке</td><td>поиск по стабильному ключу</td></tr><tr><td>ключ отсутствует</td><td>поле не включать в update</td><td>старое значение сохраняется</td><td>тест partial update</td></tr><tr><td>ключ есть, значение пустое</td><td>явная очистка по контракту</td><td>не смешивать с missing</td><td>отдельный тест clear</td></tr></tbody></table><p>Такая карта отвечает на вопрос, который обычно теряется в legacy-коде: что должен сделать adapter, если поле пришло не в идеальной форме. Для списка, файла или множественного пользовательского поля нельзя автоматически использовать правило обычной строки. Сначала прочитайте тип поля в целевой установке и зафиксируйте ожидаемое представление.</p><h2>Нормализация не должна скрывать ошибку</h2><p>Нормализуйте данные до вызова API и возвращайте не только значение, но и действие. В учебном JavaScript-примере ниже отсутствующий ключ вообще не попадает в результат, пустая строка становится явным <code>clear</code>, а очевидно неверный email отклоняется. Это граница примера, а не универсальный валидатор почты.</p><pre><code>function toCanonical(input) { const result = {}; if (Object.hasOwn(input, 'phone')) { if (input.phone === '') { result.phone = { action: 'clear' }; } else if (typeof input.phone !== 'string') { throw new Error('phone-type'); } else { const phone = input.phone.trim(); if (!phone) throw new Error('phone-invalid'); result.phone = { action: 'set', value: phone }; } } if (Object.hasOwn(input, 'email')) { if (input.email === '') { result.email = { action: 'clear' }; } else if (typeof input.email !== 'string') { throw new Error('email-type'); } else { const email = input.email.trim().toLowerCase(); if (!/^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$/.test(email)) throw new Error('email-invalid'); result.email = { action: 'set', value: email }; } } if (Object.hasOwn(input, 'legacyId')) { if (input.legacyId === '') { result.legacyId = { action: 'clear' }; } else { const legacyId = String(input.legacyId).trim(); if (!legacyId) throw new Error('legacy-id-invalid'); result.legacyId = { action: 'set', value: legacyId }; } } return result; } const canonical = toCanonical({ phone: ' +7 900 000-00-00 ', email: 'User@Example.TEST', legacyId: 'crm-17' }); // phone: set '+7 900 000-00-00' // email: set 'user@example.test' // legacyId: set 'crm-17'</code></pre><p>Пример намеренно не угадывает формат телефона и не объявляет проверку email полной. Нижний регистр email может быть правилом вашего домена, но не универсальной гарантией для всех систем. Если бизнес-контракт требует E.164, разрешённый список доменов или сохранение исходного регистра, эти правила должны появиться в отдельной функции и тестах. Нельзя превращать ошибку в пустое значение ради того, чтобы запись прошла.</p><h2>Запись и read-back — разные операции</h2><p>После нормализации соберите только те поля, для которых есть действие. Для пользовательского поля подставьте фактический <code>UF_*</code>-код и представление, подтверждённое в целевой установке. Метод <code>CUser::Update</code> принимает ID и массив полей; при ошибке он возвращает <code>false</code> и оставляет текст в <code>LAST_ERROR</code>.</p><pre><code>$fields = []; if (($canonical['phone']['action'] ?? null) === 'set') { $fields['PERSONAL_PHONE'] = $canonical['phone']['value']; } if (($canonical['phone']['action'] ?? null) === 'clear') { $fields['PERSONAL_PHONE'] = ''; } if (($canonical['email']['action'] ?? null) === 'set') { $fields['EMAIL'] = $canonical['email']['value']; } if (($canonical['legacyId']['action'] ?? null) === 'set') { $fields['UF_LEGACY_ID'] = $canonical['legacyId']['value']; } $user = new CUser; if (!$user->Update($userId, $fields)) { throw new RuntimeException($user->LAST_ERROR); } // Следующий шаг: получить эту же запись приложенческим способом // и сравнить поля, пустые состояния и внешний ключ.</code></pre><p>Вызов не должен быть последней строкой мигратора. Сохраните безопасный идентификатор операции и перечень изменённых полей, но не кладите телефон, email и другие персональные данные в обычный лог. Затем прочитайте запись тем же слоем, которым её использует приложение. Сравнивайте смысловые значения, а не только ID и флаг успеха.</p><p>У официального описания есть важная граница: если пользователя с указанным ID нет, ошибка может не возникнуть. Поэтому read-back должен подтвердить существование целевой записи. Для пользовательского поля также нужно убедиться, что код поля, тип и права относятся именно к целевой установке.</p><h2>Симптомы отделяют гипотезы</h2><table><caption>Диагностика после одного запуска</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Гипотеза</th><th scope=\"col\">Проверка</th><th scope=\"col\">Решение</th></tr></thead><tbody><tr><td>API вернул успех, значение не читается</td><td>неверное имя, тип или локальный обработчик</td><td>сравнить mapping, вход и read-back</td><td>остановить batch и проверить одну запись</td></tr><tr><td>Пропуск поля очистил старое значение</td><td>missing сведён к clear</td><td>повторить partial update без ключа</td><td>не включать отсутствующий ключ в <code>$fields</code></td></tr><tr><td>Повтор создал дубль</td><td>операция создаёт запись вместо поиска по ключу</td><td>найти запись по стабильному <code>legacyId</code></td><td>разделить upsert, update и создание</td></tr><tr><td>В одном окружении работает, в другом нет</td><td>разный набор модулей, UF-кодов или прав</td><td>проверить поверхность API и схему поля в каждой среде</td><td>не расширять запуск до устранения расхождения</td></tr><tr><td>Email стал пустым после ошибки</td><td>валидатор заменил invalid на fallback</td><td>проверить отрицательный тест до записи</td><td>вернуть ошибку и оставить исходное значение</td></tr></tbody></table><p>Таблица нужна не для классификации задним числом. Она задаёт следующий fetch или тест. Если read-back показывает другой телефон, это ещё не доказывает потерю: сервер мог применить согласованную нормализацию. Если поле исчезло, ищите точку расхождения между исходным ключом, mapping, обработчиком и способом чтения.</p><h2>Безопасный порядок миграции</h2><ol><li><strong>Выберите одну запись.</strong> Зафиксируйте стабильный ID источника и целевой ID, если он уже известен. Не начинайте с массового запуска.</li><li><strong>Снимите вход.</strong> Сохраните имена ключей, типы и факт пустоты; персональные значения в диагностике обезличьте.</li><li><strong>Проверьте схему.</strong> Разделите стандартные поля и <code>UF_*</code>, подтвердите тип, множественность, обязательность и права.</li><li><strong>Составьте mapping.</strong> Для каждого ключа запишите <code>set</code>, <code>clear</code>, <code>unchanged</code> или <code>invalid</code>.</li><li><strong>Прогоните нормализацию отдельно.</strong> Проверьте пробелы, регистр, неверный тип, неизвестное поле и пустое значение.</li><li><strong>Запишите одну запись.</strong> Передайте только разрешённые изменения. При <code>false</code> сохраните <code>LAST_ERROR</code> и прекратите расширение.</li><li><strong>Сделайте read-back.</strong> Сравните значения, пустые состояния, внешний ключ и существование записи через прикладной способ чтения.</li><li><strong>Повторите тот же вход.</strong> Убедитесь, что результат не меняется, новая связь не дублируется, а операция остаётся наблюдаемой.</li><li><strong>Расширяйте выборку порциями.</strong> На каждом шаге проверяйте количество обновлённых записей, ошибки и расхождения между входом и read-back.</li></ol><h2>Retry и откат требуют отдельного решения</h2><p><code>CUser::Update</code> обновляет запись по ID, но сам по себе не решает задачу поиска записи по ключу источника и не делает весь процесс миграции идемпотентным. Сначала найдите существующую связь по стабильному ключу, затем решите, допустим ли update. Создание новой записи без проверки ключа — отдельная операция с отдельными правилами и риском дубля.</p><p>Перед batch-запуском определите, что делать при частичном успехе. Остановка на первой ошибке проще для контроля, очередь с повтором лучше для большого объёма, а обратная миграция возможна только при сохранённом старом значении. Не называйте процесс обратимым, если вы не храните исходный снимок и не проверили восстановление на тестовой записи.</p><h2>Ограничения применимости</h2><p>Документация Bitrix описывает публичный метод и базовые поля, но не знает локальные обработчики событий, права, пользовательские поля, индексы, настройки валидации и формат обмена конкретного проекта. Коды <code>UF_*</code> и их типы нельзя переносить между установками без проверки схемы.</p><p>Нормализация телефона и email в статье учебная. Нижний регистр email может быть правилом вашего домена, но не универсальной гарантией для всех систем. Проверка через регулярное выражение не заменяет бизнес-ограничения, подтверждение адреса или проверку уникальности.</p><p>Если приложение читает данные из кэша, поискового индекса или внешней копии, немедленный read-back из одного слоя не доказывает, что все потребители уже увидели изменение. Для такой архитектуры добавьте проверку задержки распространения и критерий согласованности. Если read-back невозможен, массовую миграцию продолжать нельзя.</p><h2>Проверяемый критерий готовности</h2><p>Одна запись считается перенесённой, когда другой инженер может восстановить вход и объяснить каждое изменение: mapping различает стандартное поле и <code>UF_*</code>, missing не очищает значение, invalid останавливает запись, read-back подтверждает смысл, а повтор не создаёт дубль.</p><p>Для расширения на batch нужны те же доказательства на отрицательных ветках: неизвестный код поля останавливает запуск, недоступная схема не приводит к частичному обновлению, ошибка API не маскируется пустым fallback, а расхождение read-back классифицируется как ожидаемая нормализация или потеря данных. Если хотя бы один сигнал отсутствует, следующий шаг — собрать его, а не объявлять миграцию успешной.</p><h2>Проверяемые источники</h2><ul><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cuser/update.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CUser::Update</a> — сигнатура метода, массив полей, значения <code>true</code>/<code>false</code>, <code>LAST_ERROR</code> и оговорка о несуществующем ID.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cuser/index.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: класс CUser</a> — базовые поля пользователя, включая <code>EMAIL</code>, <code>PERSONAL_PHONE</code>, <code>XML_ID</code>, и соответствие классу D7.</li><li><a href=\"https://dev.1c-bitrix.ru/api_help/main/reference/cmodule/includemodule.php\" target=\"_blank\" rel=\"noopener noreferrer\">1С-Битрикс: CModule::IncludeModule</a> — проверка доступности установленного модуля перед вызовом его классов в конкретной среде.</li></ul>"
|
||
}
|