ข้ามไปยังเนื้อหา
การตั้งค่า

search and replace URL ในฐานข้อมูล WordPress โดยไม่ทำให้ serialized data พัง

เปลี่ยน URL ของเว็บ WordPress ในฐานข้อมูลโดยที่วิดเจ็ต เมนู และค่าตั้งของธีมไม่หายเกลี้ยง ด้วยคำสั่ง WP-CLI ที่รักษาความถูกต้องของ serialized data ไว้ได้

เผยแพร่แล้ว

อย่ารันคำสั่ง UPDATE ... SET column = REPLACE(column, 'oldurl', 'newurl') แบบดิบ ๆ ทั่วทั้งฐานข้อมูล WordPress มันจะเปลี่ยนข้อความได้ถูกต้องและทำลายค่าตั้งของคุณไปพร้อมกัน เพราะ WordPress เก็บ array และ object เป็นสตริงแบบ PHP-serialized และรูปแบบนั้นบันทึกความยาวเป็นไบต์ของทุกสตริงที่อยู่ข้างในไว้ด้วย การแทนที่ด้วย SQL ดิบ ๆ จะเขียนทับข้อความแต่ไม่เคยแตะตัวเลขความยาวที่บันทึกไว้ ค่านั้นจึงอ่านไม่ออกอีกต่อไป

ให้ใช้เครื่องมือที่เข้าใจ serialization แทน บน command line ก็คือ wp search-replace สำรองฐานข้อมูลก่อน รันแบบ dry run แล้วค่อยรันของจริง

สิ่งที่พังจริง ๆ

สตริงแบบ PHP-serialized หน้าตาเป็นแบบนี้:

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

เลข 18 คือความยาวเป็นไบต์ของข้อความที่ตามมา ลองนับดู — http://example.com ยาว 18 ตัวอักษรพอดี

ทีนี้ลองนึกถึงค่าตั้งของธีมที่เก็บไว้แบบนี้:

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

รันคำสั่งแทนที่ http:// เป็น https:// ด้วย SQL ธรรมดา แล้วคุณจะได้:

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

แต่ความจริงคุณไม่ได้แบบนั้น คุณได้แบบนี้:

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

สตริงยาว 19 ไบต์แล้ว แต่ยังอ้างว่าตัวเองยาว 18 อยู่ PHP อ่านความยาวที่ประกาศไว้ เดินหน้าไป 18 ไบต์ แล้วไม่เจอเครื่องหมายคำพูดปิดกับเซมิโคลอนตรงตำแหน่งที่ควรจะเป็น มันก็ยอมแพ้ ทั้ง array นั้น unserialize ไม่ผ่าน

ทำไมมันถึงพังแบบเงียบ ๆ

นี่คือส่วนที่ทำให้บั๊กนี้แพงมาก WordPress ไม่ได้โยน error แล้วหยุดทำงาน เวลาอ่านค่า option มันจะส่งค่านั้นผ่านตัวช่วย unserialize PHP คืน false กลับมาสำหรับข้อมูลที่ผิดรูป แล้ว WordPress ก็ส่งค่าที่เหมือนกับว่าไม่เคยตั้ง option นี้ไว้เลยให้โค้ดของคุณ ปลั๊กอินหรือธีมจึงถอยไปใช้ค่าเริ่มต้นของตัวเอง

อาการจึงไม่ใช่หน้า error แต่เป็นแบบนี้:

  • วิดเจ็ตหายไปจาก sidebar
  • ค่าใน customizer รีเซ็ตกลับไปเป็นค่าเริ่มต้นของธีม
  • ข้อมูลเลย์เอาต์ของ page builder กลับมาว่างเปล่า หน้าเว็บเลยแสดงผลเป็นหน้าโล่ง
  • หน้าตั้งค่าของปลั๊กอินดูเหมือนเพิ่งติดตั้งใหม่
  • เมนูหลุดจากตำแหน่งที่กำหนดไว้

ไม่มีอะไรในหน้าแอดมินบอกว่ามีอะไรผิดพลาด แถวข้อมูลยังอยู่ในฐานข้อมูล ยังเต็มไปด้วยข้อความที่ดูถูกต้อง ผิดแค่ตัวเลขความยาวข้างหน้าที่เพี้ยนไปหนึ่งหรือสองไบต์เท่านั้น PHP มีการส่ง notice ออกมาเวลา unserialize ไม่ผ่าน แต่บนเว็บจริงที่ปิดการแสดง error ไว้ ก็ไม่มีใครเห็น การเปิด WP_DEBUG พร้อมกับเปิด WP_DEBUG_LOG และตั้ง WP_DEBUG_DISPLAY เป็น false จะทำให้ notice พวกนั้นไปอยู่ในไฟล์ log ที่คุณเปิดอ่านได้

ความเสียหายก็ไม่ได้กระจายเท่ากันด้วย ค่าธรรมดา ๆ อย่าง siteurl, home, เนื้อหาโพสต์ล้วน ๆ หรือ URL ที่อยู่ในคอลัมน์ของตัวเอง รอดจากการแทนที่แบบซื่อ ๆ ได้สบาย มีแต่ค่าที่ serialize ไว้เท่านั้นที่พัง นี่แหละคือเหตุผลว่าทำไมการแทนที่ผิด ๆ ถึงดูเหมือนสำเร็จเวลามองผ่าน ๆ

เช็ค wp-config.php ก่อนแตะอะไรทั้งนั้น

ถ้า wp-config.php กำหนด WP_HOME หรือ WP_SITEURL ไว้ ค่าคงที่พวกนั้นจะชนะสิ่งที่อยู่ในฐานข้อมูล การแก้ wp_options จะดูเหมือนไม่เกิดอะไรขึ้นเลย มองหาบรรทัดแบบนี้ก่อนจะสรุปว่าปัญหาอยู่ที่ฐานข้อมูล:

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

จะแก้เป็น URL ใหม่ หรือลบทิ้งแล้วปล่อยให้ฐานข้อมูลเป็นตัวกำหนด ก็ได้ทั้งคู่

สำรองข้อมูลก่อน และเอาจริง

การพังของ serialization ไม่ได้ย้อนกลับง่ายเสมอไป เมื่อตัวเลขความยาวกับเนื้อหาไม่ตรงกันแล้ว ค่าต้นฉบับกู้กลับจากค่าที่พังไม่ได้ — คุณไม่มีทางรู้ว่าความยาวที่ถูกต้องควรเป็นเท่าไหร่ ถ้าไม่รู้สตริงต้นฉบับ ไฟล์สำรองคือปุ่ม undo อันเดียวที่มีจริง

wp db export backup-before-replace.sql

หรือใช้ mysqldump ถ้าไม่มี WP-CLI:

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

เก็บไฟล์ไว้นอกเซิร์ฟเวอร์ถ้าทำได้ ไฟล์สำรองที่วางอยู่ใน web root เป็นทั้งจุดกู้คืนและปัญหาความปลอดภัยในตัวมันเอง

คำสั่งที่ปลอดภัย

search-replace ของ WP-CLI จะ unserialize ค่าแต่ละตัว เดินเข้าไปในโครงสร้างที่ถอดออกมา แทนที่ข้อความข้างใน แล้ว serialize กลับพร้อมความยาวที่ถูกต้อง ทำ dry run ก่อน:

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 คือตัวระบุถาวรสำหรับโปรแกรมอ่านฟีด ไม่ใช่ URL ที่ใช้งานอยู่จริง การเขียนทับมันทำให้ผู้ติดตามฟีดเห็นคลังโพสต์ทั้งหมดของคุณเป็นโพสต์ใหม่ได้
  • ค้นหาโดยไม่ใส่สแลชปิดท้าย และไม่ใส่รูปแบบโปรโตคอลที่คุณไม่อยากไปแตะ การแทนที่ example.com เปล่า ๆ จะโดนทั้งอีเมลและทุกที่ที่มีการเอ่ยถึงในเนื้อหาโพสต์ด้วย

ถ้าใช้ command line ไม่ได้ ปลั๊กอินย้ายเว็บที่รองรับ serialized data ก็ทำงานถอดรหัสแบบเดียวกันผ่านหน้าแอดมิน สิ่งที่ต้องมองหาคือเครื่องมือที่ระบุชัดเจนว่ารองรับ serialized data ถ้าเครื่องมือไหนไม่ได้บอกไว้ ให้ถือว่ามันทำไม่ได้

ตารางและคอลัมน์ไหนบ้างที่เกี่ยวข้อง

ตารางคอลัมน์ความเสี่ยงเรื่อง serialized
wp_optionsoption_valueสูง — วิดเจ็ต ค่าปรับแต่งธีม transient และค่าตั้งของปลั๊กอินส่วนใหญ่
wp_postmetameta_valueสูง — เลย์เอาต์ของ page builder และ array ของ custom field
wp_usermetameta_valueสูง — สิทธิ์การใช้งาน ค่าหน้าจอ และค่าแดชบอร์ด
wp_termmetameta_valueปานกลาง — ขึ้นอยู่กับปลั๊กอินที่ใช้
wp_postspost_content, guidต่ำสำหรับเนื้อหา แต่ค่า attribute ของ shortcode และมาร์กอัปของบล็อกมี URL ปนอยู่
wp_commentscomment_content, comment_author_urlต่ำ
wp_linkslink_url, link_imageต่ำ และมักไม่ได้ใช้งานแล้ว

ถ้าเป็น multisite ให้เพิ่ม wp_blogs, wp_site, wp_sitemeta และตารางรายเว็บอย่าง wp_2_options, wp_3_options ไปด้วย การเปลี่ยน URL บน multisite มีกฎเพิ่มเติมนอกเหนือจากการ search and replace อยู่อีก ให้มองว่าเป็นงานคนละชิ้นไปเลย

กรณีที่ search-replace แก้ให้ไม่ได้ทั้งหมด

พูดกันตรง ๆ เรื่องข้อจำกัด:

JSON ที่เก็บอยู่ในฐานข้อมูล page builder บางตัวเก็บข้อมูลเลย์เอาต์เป็น JSON ที่มีการ escape เครื่องหมายสแลช URL ของคุณจึงปรากฏเป็น https:\/\/example.com ไม่ใช่ https://example.com การค้นหาด้วยรูปแบบปกติจะพลาดตรงนี้ไป คุณอาจต้องรันรอบสองสำหรับสตริงที่ escape ไว้ ทดสอบบนสำเนาก่อน

ค่าที่เข้ารหัสแบบ Base64 ถ้าปลั๊กอินเข้ารหัสค่าตั้งก่อนบันทึก ไม่มีการแทนที่แบบข้อความไหนมองเห็น URL ได้เลย ค่าพวกนั้นมักต้องกรอกใหม่ผ่านหน้าตั้งค่าของปลั๊กอินเอง

URL ที่ hardcode ไว้ในไฟล์ อะไรก็ตามที่เขียนไว้ใน functions.php เทมเพลตของ child theme หรือพาธของไฟล์ที่ hardcode ไว้ ไม่ได้อยู่ในฐานข้อมูลและจะไม่ถูกแตะ ต้อง grep ในโฟลเดอร์ธีมแยกต่างหาก

ผลลัพธ์ที่แคชหรือถูกสร้างไว้แล้ว object cache, page cache และสำเนาบน CDN ยังคงเสิร์ฟ URL เก่าต่อไปแม้แทนที่สำเร็จแล้ว ล้างมันทิ้งก่อน แล้วค่อยเช็คอีกที ก่อนจะสรุปว่าการแทนที่ล้มเหลว

ถ้าคุณรันคำสั่งผิดไปแล้ว

กู้ไฟล์สำรองกลับมา นั่นแหละคือคำตอบ และควรพูดกันตรง ๆ แบบนี้ ดีกว่าเสนอวิธีซ่อมที่ส่วนใหญ่แล้วใช้ไม่ได้ผล

ถ้าไม่มีไฟล์สำรอง ทางรอดก็แคบมาก ค่าที่ไม่เคยถูก serialize จะไม่เป็นไร ส่วนค่า serialized ที่พังไปแล้วต้องประกอบขึ้นใหม่ด้วยมือ หรือรีเซ็ตด้วยการบันทึกหน้าตั้งค่านั้นซ้ำ การบันทึกพื้นที่วิดเจ็ตใหม่ เลือกตำแหน่งเมนูใหม่ หรือกรอกค่าตั้งของปลั๊กอินใหม่ จะเขียนค่าที่ serialize ถูกต้องทับค่าที่พังอยู่ น่าเบื่อ แต่ได้ผล และดีกว่าปล่อยแถวข้อมูลที่เสียหายทิ้งไว้ให้คนที่มาดูเว็บนี้ต่อจากคุณต้องมางง

ถ้าอยากสร้างคำสั่งที่ถูกต้องก่อนจะรันอะไรจริง ๆ ลองประกอบมันด้วย เครื่องมือสร้าง SQL สำหรับ search and replace ของ WordPress — มันจะบอกว่าคอลัมน์ไหนแตะด้วย SQL ธรรมดาได้อย่างปลอดภัย และคอลัมน์ไหนต้องใช้วิธีที่รองรับ serialization คุณจะเห็นเส้นแบ่งชัดเจนก่อนตัดสินใจรันคำสั่ง

FAQ

คำถาม

ทำไมคำสั่ง REPLACE ของ SQL ธรรมดาถึงทำให้เว็บ WordPress พัง

เพราะ WordPress เก็บ array และ object เป็นข้อความแบบ PHP-serialized และรูปแบบนั้นจะบันทึกความยาวเป็นไบต์ของทุกสตริงที่อยู่ข้างในไว้ด้วย คำสั่ง REPLACE ดิบ ๆ จะเปลี่ยนแค่ตัวข้อความ แต่ปล่อยตัวเลขความยาวเดิมทิ้งไว้ PHP จึงปฏิเสธที่จะ unserialize ค่านั้น WordPress ก็ถอยไปใช้ค่าเริ่มต้นแทน ผลคือค่าที่ตั้งไว้ไม่ได้ผิด แต่กลายเป็นว่างเปล่า

วิธีที่ปลอดภัยที่สุดในการเปลี่ยน URL ของเว็บ WordPress ในฐานข้อมูลคืออะไร

สำรองฐานข้อมูลให้ครบก่อน แล้วรันคำสั่ง WP-CLI คือ wp search-replace โดยทำ dry run ก่อนเสมอ WP-CLI จะ unserialize ค่าแต่ละตัว แทนที่ข้อความข้างในโครงสร้างที่ถอดออกมาแล้ว จากนั้นจึง serialize กลับพร้อมแก้ตัวเลขความยาวให้ถูกต้อง ถ้าเข้าถึง command line ไม่ได้ ปลั๊กอินย้ายเว็บที่รองรับ serialized data ก็ทำงานแบบเดียวกันผ่านหน้าแอดมิน

ตารางไหนบ้างที่เก็บ URL เก่าเอาไว้

หลัก ๆ คือ wp_options, wp_posts, wp_postmeta, wp_usermeta, wp_termmeta และ wp_comments จุดอันตรายเรื่อง serialized อยู่ที่ wp_options และคอลัมน์ meta_value ทุกตัว เพราะวิดเจ็ต ค่าปรับแต่งธีม transient และข้อมูลของ page builder ถูกเก็บเป็น array ถ้าเป็น multisite ก็ต้องรวม wp_blogs, wp_site และ wp_sitemeta ที่เก็บชื่อโดเมนไว้ด้วย

ถ้า wp-config.php กำหนด URL ไว้ การแก้ฐานข้อมูลจะช่วยได้ไหม

ไม่ได้ ถ้า wp-config.php กำหนด WP_HOME หรือ WP_SITEURL ไว้ ค่าคงที่พวกนั้นจะเหนือกว่าค่า siteurl และ home ใน wp_options เสมอ แก้ฐานข้อมูลไปก็ไม่เห็นอะไรเปลี่ยน จนกว่าจะแก้หรือลบค่าคงที่นั้นออก ดังนั้นเช็ค wp-config.php เป็นอย่างแรก เพราะบรรทัดเดียวนี้อธิบายการย้ายเว็บที่ล้มเหลวได้เป็นจำนวนมาก

ควรแทนที่ในคอลัมน์ guid ด้วยหรือเปล่า

ไม่ควร guid คือตัวระบุถาวรสำหรับโปรแกรมอ่านฟีด ไม่ใช่ URL ที่ใช้งานจริง การเขียนทับมันอาจทำให้คนที่ติดตามฟีดเห็นโพสต์เก่าทั้งหมดโผล่ขึ้นมาเป็นโพสต์ใหม่ ให้ใส่ skip-columns พร้อม guid ตอนรัน WP-CLI ข้อยกเว้นที่พบบ่อยมีอย่างเดียวคือเว็บที่ไม่เคยมีฟีดสาธารณะหรือผู้ติดตามเลย

จะรู้ได้อย่างไรว่าค่าไหนพังไปแล้วหลังแทนที่ผิดวิธี

ค่า serialized ที่พังจะดูปกติดีในฐานข้อมูล แต่กลับมาเป็นค่าว่างใน WordPress วิดเจ็ตหายไป ค่าใน customizer ของธีมรีเซ็ต และค่าตั้งของปลั๊กอินกลับไปเป็นค่าเริ่มต้นโดยไม่มีข้อความแจ้งเตือนใด ๆ การเปิด WP_DEBUG และ WP_DEBUG_LOG จะทำให้เห็น notice ของ unserialize ส่วนการเทียบตัวเลขความยาวที่ประกาศไว้กับจำนวนไบต์จริงจะยืนยันได้ตรง ๆ