Замена URL в базе данных WordPress без порчи сериализованных данных
Как сменить адрес сайта WordPress в базе данных и не обнулить виджеты, меню и настройки темы. Команда WP-CLI, которая сохраняет сериализованные данные целыми.
Опубликовано
Не запускайте по базе WordPress обычный UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl'). Он корректно заменит текст и одновременно уничтожит ваши настройки, потому что WordPress хранит массивы и объекты как PHP-сериализованные строки, а этот формат записывает длину каждой строки в байтах. Прямая SQL-замена переписывает текст, но не трогает записанную длину — и значение перестаёт читаться.
Используйте инструмент, который понимает сериализацию. В командной строке это wp search-replace. Сначала бэкап базы, потом пробный прогон, и только потом реальный запуск.
Что именно ломается
PHP-сериализованная строка выглядит так:
s:18:"http://example.com"
18 — это длина следующего за ней текста в байтах. Посчитайте сами: в http://example.com ровно 18 символов.
Теперь представьте сохранённую настройку темы:
a:1:{s:4:"logo";s:18:"http://example.com";}
Делаем обычную SQL-замену http:// на https:// и получаем:
a:1:{s:4:"logo";s:19:"https://example.com";}
Вот только нет. Получаете вы вот это:
a:1:{s:4:"logo";s:18:"https://example.com";}
В строке теперь 19 байт, а заявлено по-прежнему 18. PHP читает объявленную длину, отсчитывает 18 байт вперёд, не находит на ожидаемом месте закрывающую кавычку с точкой с запятой и сдаётся. Весь массив перестаёт разбираться.
Почему сбой происходит молча
Именно эта часть делает ошибку такой дорогой. WordPress не выбрасывает ошибку и не останавливается. Читая опцию, он прогоняет значение через свой хелпер разбора, PHP возвращает false для битых данных, и WordPress отдаёт вашему коду значение, которое ведёт себя так, будто опции никогда не существовало. Плагин или тема после этого откатываются к своим значениям по умолчанию.
Так что симптом — это не страница с ошибкой. Симптом такой:
- Виджеты пропадают из сайдбаров
- Настройки кастомайзера сбрасываются к дефолтам темы
- Данные раскладки конструктора страниц возвращаются пустыми, и страница рендерится пустой
- Страницы настроек плагинов выглядят так, будто их только что установили
- Меню теряют назначенные позиции
Ни одно место в админке не сообщает, что что-то не так. Строка по-прежнему в базе, по-прежнему заполнена текстом, который выглядит верным. Не сходится только префикс длины — на байт-другой. PHP действительно выдаёт уведомление, когда разбор не удаётся, но на боевом сайте с отключённым выводом ошибок его никто не видит. Включите WP_DEBUG вместе с WP_DEBUG_LOG и WP_DEBUG_DISPLAY в false — уведомления лягут в лог-файл, где их можно прочитать.
Урон распределяется неравномерно. Простые скалярные значения — siteurl, home, обычный текст записи, URL в собственном столбце — переживают наивную замену без единой царапины. Ломаются только сериализованные значения. Именно поэтому неудачная замена на первый взгляд часто выглядит удачной.
Проверьте wp-config.php до того, как что-либо трогать
Если в wp-config.php объявлены WP_HOME или WP_SITEURL, эти константы сильнее всего, что лежит в базе. Правка wp_options будет выглядеть как полное отсутствие эффекта. Поищите такие строки, прежде чем решать, что проблема в базе:
define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );
Либо обновите их на новый адрес, либо удалите и отдайте управление базе.
Сначала бэкап — и это всерьёз
Сломанную сериализацию не всегда легко откатить. Как только префикс длины и содержимое разошлись, исходное значение из сломанного не восстанавливается: невозможно узнать, какой должна была быть правильная длина, не зная исходной строки. Бэкап — единственная настоящая кнопка «отменить».
wp db export backup-before-replace.sql
Или через mysqldump, если WP-CLI недоступен:
mysqldump -u USER -p DBNAME > backup-before-replace.sql
По возможности храните дамп вне сервера. Бэкап, лежащий в корне сайта, — это одновременно точка восстановления и дыра в безопасности.
Безопасная команда
search-replace в WP-CLI разбирает каждое значение, обходит распакованную структуру, заменяет текст внутри неё и собирает обратно с верными длинами. Сначала пробный прогон:
wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid
Прочитайте отчёт. В нём видно, сколько замен произойдёт в каждой таблице и каждом столбце. Если список таблиц выглядит странно или в незнакомой таблице тысячи совпадений — остановитесь и разберитесь, прежде чем запускать всерьёз.
Потом запускайте:
wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only
Честно о флагах:
--all-tablesзахватывает таблицы, которые WordPress не регистрирует сам, — а именно там часто и живут данные плагинов. Без этого флага часть таблиц плагинов пропускается целиком.--skip-columns=guidважен.guid— постоянный идентификатор для читалок лент, а не живая ссылка. Его переписывание может показать подписчикам весь ваш архив как новые записи.- Ищите без завершающего слеша и без тех вариантов протокола, которые трогать не собираетесь. Замена голого
example.comзаденет и адреса электронной почты, и любые упоминания в тексте записей.
Если командная строка недоступна, ту же работу по распаковке делают плагины миграции, умеющие обращаться с сериализацией, — через админку. Нужное поведение прямо описывается как поддержка сериализованных данных; если инструмент об этом не говорит, считайте, что он этого не умеет.
Какие таблицы и столбцы задействованы
| Таблица | Столбец | Риск сериализации |
|---|---|---|
| wp_options | option_value | Высокий — виджеты, настройки темы, транзиенты, большинство настроек плагинов |
| wp_postmeta | meta_value | Высокий — раскладки конструкторов страниц, массивы произвольных полей |
| wp_usermeta | meta_value | Высокий — права доступа, настройки экранов и консоли |
| wp_termmeta | meta_value | Средний — зависит от плагинов |
| wp_posts | post_content, guid | Низкий для контента, но URL встречаются в атрибутах шорткодов и разметке блоков |
| wp_comments | comment_content, comment_author_url | Низкий |
| wp_links | link_url, link_image | Низкий, и чаще всего не используется |
На мультисайте добавьте wp_blogs, wp_site, wp_sitemeta и посайтовые wp_2_options, wp_3_options и так далее. Смена URL на мультисайте подчиняется дополнительным правилам сверх поиска и замены — считайте это отдельной задачей.
Случаи, которые search-replace решает не до конца
Честно о границах применимости:
JSON внутри базы. Некоторые конструкторы страниц хранят раскладку в JSON с экранированными слешами, поэтому ваш адрес выглядит как https:\/\/example.com, а не https://example.com. Поиск обычной формы его не найдёт. Может понадобиться второй проход по экранированной строке. Проверяйте на копии.
Значения в base64. Если плагин кодирует настройки перед сохранением, никакая текстовая замена URL там попросту не увидит. Такие настройки обычно приходится вводить заново через экран самого плагина.
Жёстко прописанные URL в файлах. Всё, что записано в functions.php, в шаблон дочерней темы или в захардкоженный путь к ресурсу, лежит не в базе и затронуто не будет. Каталог темы проверяйте отдельно через grep.
Кеш и сгенерированный вывод. Объектный кеш, кеш страниц и копии в CDN продолжают отдавать старые адреса даже после корректной замены. Сбросьте их и проверьте ещё раз, прежде чем решать, что замена не сработала.
Если плохой запрос уже выполнен
Восстанавливайте бэкап. Это и есть ответ, и его стоит сказать прямо, а не предлагать процедуру починки, которая в большинстве случаев не работает.
Если бэкапа нет, путь восстановления узкий: значения, которые никогда не были сериализованы, в порядке, а сломанные сериализованные придётся собирать руками или сбрасывать повторным сохранением соответствующего экрана настроек. Пересохранение области виджетов, повторный выбор позиции меню, повторный ввод настроек плагина записывают поверх сломанного значения свежее и корректно сериализованное. Занудно, но работает — и это лучше, чем оставлять в базе битые строки, которые собьют с толку следующего человека, открывшего сайт.
Чтобы подготовить правильные запросы до того, как что-то запускать, соберите их в инструменте поиска и замены SQL для WordPress — он показывает, какие столбцы можно править обычным SQL, а какие требуют обращения с учётом сериализации, так что разделение видно ещё до выбора команды.
FAQ
Вопросы
Почему обычный SQL-запрос REPLACE ломает сайт на WordPress?
Потому что WordPress хранит массивы и объекты как PHP-сериализованный текст, а этот формат записывает длину каждой строки в байтах. Прямой REPLACE меняет текст, но оставляет старое число длины на месте. PHP отказывается разбирать такое значение, WordPress подставляет значение по умолчанию — и настройки выглядят пустыми, а не ошибочными.
Как безопаснее всего сменить адрес сайта WordPress в базе данных?
Сделайте полный бэкап базы, затем выполните команду WP-CLI wp search-replace, начав с пробного прогона. WP-CLI разбирает каждое значение, заменяет текст уже внутри распакованной структуры и собирает её обратно с правильными длинами. Плагины миграции, умеющие работать с сериализацией, делают то же самое через админку, если доступа к командной строке нет.
В каких таблицах лежит старый URL?
В основном это wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta и wp_comments. Опасность с сериализацией живёт в wp_options и в любом столбце meta_value, потому что виджеты, настройки темы, транзиенты и данные конструкторов страниц хранятся массивами. На мультисайте домен встречается ещё и в wp_blogs, wp_site и wp_sitemeta.
Поможет ли правка базы, если URL задан в wp-config.php?
Нет. Если в wp-config.php объявлены WP_HOME или WP_SITEURL, эти константы перебивают значения siteurl и home из wp_options. Правка базы ничего не изменит на экране, пока константы не будут обновлены или удалены. Проверяйте wp-config.php первым делом — эта одна строка объясняет добрую половину неудавшихся переездов.
Нужно ли менять столбец guid?
Нет. guid — это постоянный идентификатор для читалок RSS, а не рабочая ссылка, и его переписывание может привести к тому, что подписчики увидят весь архив как новые записи. При запуске WP-CLI передавайте skip-columns со значением guid. Единственное частое исключение — сайт, у которого никогда не было публичных лент и подписчиков.
Как понять, что значение сломалось после неудачной замены?
Сломанные сериализованные значения в базе выглядят нормально, но в WordPress возвращаются пустыми. Виджеты исчезают, настройки кастомайзера сбрасываются, опции плагинов откатываются к значениям по умолчанию — и ни одного сообщения об ошибке. Включённые WP_DEBUG и WP_DEBUG_LOG покажут уведомление об ошибке разбора. Сравнение объявленной длины строки с реальным числом байт подтверждает это напрямую.