Перейти к содержимому
Конфигурация

Замена 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_optionsoption_valueВысокий — виджеты, настройки темы, транзиенты, большинство настроек плагинов
wp_postmetameta_valueВысокий — раскладки конструкторов страниц, массивы произвольных полей
wp_usermetameta_valueВысокий — права доступа, настройки экранов и консоли
wp_termmetameta_valueСредний — зависит от плагинов
wp_postspost_content, guidНизкий для контента, но URL встречаются в атрибутах шорткодов и разметке блоков
wp_commentscomment_content, comment_author_urlНизкий
wp_linkslink_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 покажут уведомление об ошибке разбора. Сравнение объявленной длины строки с реальным числом байт подтверждает это напрямую.