Bỏ qua tới nội dung
Cấu hình

Search và Replace URL trong database WordPress mà không làm hỏng dữ liệu serialized

Đổi URL của site WordPress ngay trong database mà không làm trắng widget, menu hay cài đặt theme. Dùng lệnh WP-CLI giữ nguyên dữ liệu serialized.

Đã đăng

Đừng chạy một câu UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') thuần trên database WordPress. Nó sẽ đổi phần chữ hoàn toàn chính xác và phá huỷ cài đặt của bạn cùng lúc, bởi vì WordPress lưu mảng và đối tượng dưới dạng chuỗi PHP-serialized, và định dạng đó ghi lại độ dài tính bằng byte của từng chuỗi mà nó chứa. Một câu SQL replace thô viết lại phần chữ nhưng không bao giờ đụng tới con số độ dài đã ghi, và giá trị đó thành ra không đọc được nữa.

Hãy dùng công cụ hiểu được dữ liệu serialized. Trên dòng lệnh, đó là wp search-replace. Sao lưu database trước, chạy dry run, rồi mới chạy thật.

Chuyện gì thực sự hỏng

Một chuỗi PHP-serialized trông như thế này:

s:18:"http://example.com"

Số 18 là độ dài byte của đoạn text đứng sau. Đếm thử đi — http://example.com đúng 18 ký tự.

Giờ hình dung một cài đặt theme đang được lưu:

a:1:{s:4:"logo";s:18:"http://example.com";}

Chạy một câu SQL replace thuần từ http:// sang https:// và bạn nhận được:

a:1:{s:4:"logo";s:19:"https://example.com";}

Chỉ có điều là không. Cái bạn nhận được là thế này:

a:1:{s:4:"logo";s:18:"https://example.com";}

Chuỗi giờ dài 19 byte nhưng vẫn khai là 18. PHP đọc độ dài được khai báo, đi tới 18 byte, không tìm thấy dấu nháy đóng và dấu chấm phẩy ở chỗ nó mong đợi, và bỏ cuộc. Cả mảng đó unserialize thất bại.

Vì sao nó hỏng trong im lặng

Đây mới là phần khiến lỗi này đắt đỏ đến vậy. WordPress không ném ra lỗi rồi dừng lại. Khi đọc một option, nó đưa giá trị qua hàm unserialize của mình, PHP trả về false cho dữ liệu hỏng, và WordPress đưa cho code của bạn một giá trị hành xử như thể option đó chưa từng được đặt. Plugin hoặc theme sau đó quay về giá trị mặc định của nó.

Nên triệu chứng không phải là trang báo lỗi. Triệu chứng là:

  • Widget biến mất khỏi sidebar
  • Cài đặt trong Customizer reset về mặc định của theme
  • Dữ liệu layout của page builder trả về rỗng và trang hiển thị trống trơn
  • Trang cài đặt plugin trông như vừa mới cài xong
  • Menu mất vị trí đã gán

Không có gì trong admin nói rằng có chuyện gì sai. Dòng dữ liệu vẫn nằm đó, vẫn đầy những gì trông như text đúng. Chỉ có phần tiền tố độ dài là lệch một hai byte. PHP có phát ra notice khi unserialize thất bại, nhưng trên site production với display errors đã tắt, không ai thấy nó cả. Bật WP_DEBUG cùng với WP_DEBUG_LOG và đặt WP_DEBUG_DISPLAY thành false sẽ đẩy các notice đó vào file log để bạn đọc.

Thiệt hại cũng không rải đều. Các giá trị vô hướng đơn giản — siteurl, home, nội dung bài viết thuần, một URL nằm riêng trong cột của nó — sống sót hoàn hảo qua một lần replace ngây thơ. Chỉ có giá trị serialized là hỏng. Đó là lý do một lần replace sai thường trông như đã thành công khi nhìn thoáng qua.

Kiểm tra wp-config.php trước khi đụng vào bất cứ thứ gì

Nếu wp-config.php khai báo WP_HOME hoặc WP_SITEURL, các hằng số đó thắng bất cứ thứ gì nằm trong database. Sửa wp_options sẽ trông như chẳng làm được gì cả. Hãy tìm những dòng như dưới đây trước khi kết luận rằng database là vấn đề:

define( 'WP_HOME', 'http://example.com' );
define( 'WP_SITEURL', 'http://example.com' );

Hoặc cập nhật chúng sang URL mới, hoặc xoá chúng đi và để database quyết định.

Sao lưu trước, và nói thật lòng đấy

Một lỗi serialization không phải lúc nào cũng dễ đảo ngược. Khi tiền tố độ dài và nội dung đã lệch nhau, giá trị gốc không thể khôi phục từ giá trị hỏng — bạn không thể biết độ dài đúng lẽ ra là bao nhiêu nếu không biết chuỗi ban đầu. Bản sao lưu là cách undo thật sự duy nhất.

wp db export backup-before-replace.sql

Hoặc dùng mysqldump nếu không có WP-CLI:

mysqldump -u USER -p DBNAME > backup-before-replace.sql

Nếu được, hãy giữ nó ngoài server. Một bản sao lưu nằm trong web root vừa là điểm khôi phục vừa là một lỗ hổng bảo mật.

Câu lệnh an toàn

search-replace của WP-CLI unserialize từng giá trị, đi qua cấu trúc đã giải mã, thay thế bên trong nó, rồi serialize lại với độ dài đúng. Dry run trước đã:

wp search-replace 'http://example.com' 'https://example.com' --dry-run --all-tables --skip-columns=guid

Hãy đọc báo cáo. Nó cho biết sẽ có bao nhiêu lượt thay thế trên mỗi bảng và mỗi cột. Nếu danh sách bảng trông sai, hoặc một bảng bạn không nhận ra lại có hàng nghìn lượt khớp, dừng lại và xem kỹ trước khi chạy thật.

Rồi chạy nó:

wp search-replace 'http://example.com' 'https://example.com' --all-tables --skip-columns=guid --report-changed-only

Nói thật về mấy cờ này:

  • --all-tables bao gồm cả những bảng mà WordPress không tự đăng ký, và đó thường là nơi dữ liệu plugin nằm. Không có nó, một số bảng plugin bị bỏ qua hoàn toàn.
  • --skip-columns=guid rất quan trọng. guid là định danh cố định cho trình đọc feed, không phải URL đang dùng. Viết lại nó có thể khiến người theo dõi feed thấy toàn bộ kho bài của bạn như bài mới.
  • Hãy tìm kiếm không kèm dấu gạch chéo cuối và không kèm các biến thể giao thức mà bạn không muốn đụng tới. Thay thế chuỗi trần example.com cũng sẽ trúng cả địa chỉ email và mọi lần nhắc tới trong nội dung bài viết.

Nếu bạn không dùng được dòng lệnh, các plugin migration hiểu dữ liệu serialized làm đúng công việc giải mã đó qua giao diện admin. Hành vi bạn cần tìm được mô tả rõ ràng là xử lý được dữ liệu serialized; nếu một công cụ không nói vậy, hãy mặc định là nó không làm được.

Những bảng và cột có liên quan

BảngCộtRủi ro serialized
wp_optionsoption_valueCao — widget, theme mods, transient, hầu hết cài đặt plugin
wp_postmetameta_valueCao — layout page builder, mảng custom field
wp_usermetameta_valueCao — capability, tuỳ chọn màn hình và dashboard
wp_termmetameta_valueTrung bình — tuỳ vào plugin
wp_postspost_content, guidThấp với nội dung, nhưng thuộc tính shortcode và markup block có chứa URL
wp_commentscomment_content, comment_author_urlThấp
wp_linkslink_url, link_imageThấp, và thường không dùng tới

Với multisite, thêm wp_blogs, wp_site, wp_sitemeta, và các bảng riêng của từng site như wp_2_options, wp_3_options và cứ thế. Đổi URL trên multisite có thêm những quy tắc riêng vượt ngoài phạm vi một lần search và replace — hãy coi đó là một việc riêng.

Những trường hợp search-replace không giải quyết trọn vẹn

Nói thẳng về giới hạn:

JSON lưu bên trong database. Một số page builder lưu dữ liệu layout dưới dạng JSON với dấu gạch chéo bị escape, nên URL của bạn xuất hiện dưới dạng https:\/\/example.com thay vì https://example.com. Tìm theo dạng bình thường sẽ bỏ sót nó. Bạn có thể cần một lượt chạy thứ hai cho chuỗi đã escape. Hãy thử trên bản sao.

Giá trị mã hoá Base64. Nếu một plugin mã hoá cài đặt trước khi lưu, không có phép thay thế dựa trên text nào nhìn thấy được URL đó cả. Những cài đặt kiểu này thường phải nhập lại qua chính màn hình của plugin.

URL viết cứng trong file. Bất cứ thứ gì được viết vào functions.php, một template của child theme, hay một đường dẫn tài nguyên viết cứng đều không nằm trong database và sẽ không bị đụng tới. Hãy grep thư mục theme riêng.

Nội dung đã cache và đã sinh sẵn. Object cache, page cache và bản sao trên CDN vẫn tiếp tục phục vụ URL cũ sau một lần replace đúng. Hãy xoá chúng, rồi kiểm tra lại trước khi kết luận rằng lần replace đã thất bại.

Nếu bạn đã lỡ chạy câu lệnh sai

Khôi phục bản sao lưu. Đó là câu trả lời, và nó đáng được nói thẳng thay vì đưa ra một quy trình sửa chữa mà phần lớn là không hiệu quả.

Nếu không có bản sao lưu, con đường cứu vãn rất hẹp: những giá trị chưa từng được serialize thì vẫn ổn, còn những giá trị serialized đã hỏng thì phải dựng lại bằng tay hoặc reset bằng cách lưu lại màn hình cài đặt tương ứng. Lưu lại một khu vực widget, chọn lại vị trí menu, hay nhập lại cài đặt của một plugin sẽ ghi đè một giá trị mới, được serialize đúng, lên giá trị hỏng. Cực nhọc, nhưng có tác dụng, và vẫn hơn là để lại những dòng dữ liệu hỏng nằm đó chờ làm rối người tiếp theo mở site ra xem.

Để dựng sẵn các câu lệnh đúng trước khi chạy bất cứ thứ gì, hãy tạo chúng bằng công cụ tạo SQL search và replace cho WordPress — nó chỉ ra cột nào an toàn để đụng bằng SQL thuần và cột nào cần cách xử lý hiểu được dữ liệu serialized, để bạn thấy rõ ranh giới trước khi quyết định chạy lệnh.

FAQ

Câu hỏi

Vì sao một câu lệnh SQL REPLACE thuần lại làm hỏng site WordPress?

Vì WordPress lưu mảng và đối tượng dưới dạng chuỗi PHP-serialized, và định dạng đó ghi lại độ dài tính bằng byte của từng chuỗi bên trong. Một câu REPLACE thô sẽ đổi phần chữ nhưng giữ nguyên con số độ dài cũ. PHP sau đó từ chối unserialize giá trị đó, WordPress quay về giá trị mặc định, và các cài đặt hiện ra trống rỗng chứ không phải sai.

Cách an toàn nhất để đổi URL của site WordPress trong database là gì?

Sao lưu toàn bộ database trước, rồi chạy lệnh WP-CLI wp search-replace ở chế độ dry run trước tiên. WP-CLI sẽ unserialize từng giá trị, thay thế bên trong cấu trúc đã giải mã, rồi serialize lại với độ dài đã được tính đúng. Các plugin migration hiểu dữ liệu serialized cũng làm đúng công việc đó qua giao diện admin nếu bạn không có quyền truy cập dòng lệnh.

Những bảng nào chứa URL cũ?

Chủ yếu là wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta và wp_comments. Nguy cơ về serialized nằm ở wp_options và ở mọi cột meta_value, vì widget, theme mods, transient và dữ liệu page builder đều được lưu dưới dạng mảng. Với multisite, wp_blogs, wp_site và wp_sitemeta cũng chứa tên miền.

Sửa database có đổi được URL không nếu wp-config.php đã khai báo nó?

Không. Nếu wp-config.php khai báo WP_HOME hoặc WP_SITEURL, các hằng số đó đè lên bất cứ giá trị siteurl và home nào nằm trong wp_options. Sửa database sẽ không tạo ra thay đổi nào nhìn thấy được cho tới khi các hằng số đó được cập nhật hoặc xoá đi. Hãy kiểm tra wp-config.php trước, vì đúng một dòng đó giải thích rất nhiều ca migration thất bại.

Tôi có nên thay cả cột guid không?

Không. guid là một định danh cố định dành cho trình đọc feed, không phải một URL đang hoạt động, và viết lại nó có thể khiến người theo dõi feed thấy lại toàn bộ bài cũ như bài mới. Hãy truyền skip-columns kèm guid khi chạy WP-CLI. Ngoại lệ phổ biến duy nhất là site chưa từng có feed công khai hay người đăng ký nào.

Làm sao biết một giá trị đã hỏng sau khi replace sai?

Giá trị serialized bị hỏng nhìn vẫn bình thường trong database nhưng trả về rỗng trong WordPress. Widget biến mất, cài đặt trong Customizer bị reset, tuỳ chọn plugin quay về mặc định mà không có thông báo lỗi nào. Bật WP_DEBUG và WP_DEBUG_LOG sẽ làm lộ ra notice của unserialize. So sánh độ dài chuỗi được khai báo với số byte thật là cách xác nhận trực tiếp.