シリアライズが壊れる「Error at offset」|SQLのREPLACE一発で、記事が丸ごと消えた話
SQLのREPLACE()を1本流しただけで、記事の本文が丸ごと消えた。
原因はシリアライズのバイト長のズレ。unserializeが静かに失敗する仕組みと、直し方をまとめた。
記事が消えた日のこと
自社のCMSで投稿データを一括で直したいときがある。
タグの表記を統一したいとか、本文中の古い表記を新しいものに変えたいとか。
SQLの REPLACE() を1本流せば済む、そう思っていた時期が僕にもあった。
ある夜、投稿本文の中の古い表記を一括置換した。
数十件、一瞬で終わった。
ところが翌朝、そのうちの何本かが真っ白になっていた。
管理画面を開いても本文欄が空っぽで、フロントも表示が崩れている。
正体は「serialize」というブラックボックス
原因は、このCMSが投稿本文を保存するとき、配列や設定値をまとめて「シリアライズ」という形式で文字列化していたことだった。
中身を覗くと s:44:"本文の中身…"; のような書き方になっている。
この 44 は見た目の文字数ではなく、あとに続く文字列のバイト数そのものを指す数字だった。
REPLACE()は文字列の中身しか見ていない
SQLの REPLACE() は「この文字列をあの文字列に置き換える」という作業しかしない。
置換した結果、文字列の長さが1バイトでも変われば、さっきの 44 という数字と実際の長さが食い違う。
PHPの unserialize() はこのズレを見つけた瞬間に読み込みをやめてしまう。
「Error at offset」という、あのそっけないエラーの正体がこれだった。
本文がまるごと読めなくなり、画面上は「消えた」ように見えていたわけだ。
直したのは、置換ではなく「読んで、直して、また包む」
それからは、シリアライズされた列を文字列として直接いじる書き換えを一切やめた。
やることは3段階に分けている。
まず unserialize() で中身をいったんただの配列に戻す。
次にその中身だけを普通の文字列操作で直す。
最後に serialize() でもう一度、正しいバイト数付きの形式に包み直す。
この順番なら、長さのつじつまは自動的に合う。
念のため、書き換えスクリプトには unserialize() が失敗したら即座に処理を止めるガードも入れた。
静かに壊れたデータを保存されるより、途中で止まってくれたほうがずっとありがたい。
自分のCMSじゃなくても起きる話
これはうちの自社CMSに限った話ではないと思う。
WordPressのオプションテーブルやウィジェットの設定も、同じ仕組みでシリアライズされている。
「本文中の表記を一括で直したい」というよくある作業ほど、SQLで一発置換したくなる。
そのときは一度、対象のカラムがシリアライズされていないか確かめてから手を動かしたい。
僕は一晩、記事が消える経験をしてようやく覚えた話だ。
ほかにも書いています
note・スタンプ※noteは運営者が個人で書いているものです。
