بار اول که این خطا را در یک پروژه وردپرسی دیدم، در مرحله ذخیره نظر کاربر بود؛ کاربر یک ایموجی در متن گذاشته بود و MySQL با پیام Incorrect string value: '\xF0\x9F...' for column 'comment_content' جلوی درج را گرفته بود. آن روز فکر کردم مسئله از خود ایموجی است، ولی وقتی همان مشکل با حرف «ی» فارسی در یک فیلد تازه تکرار شد، فهمیدم ریشه در جای دیگری است: انتخاب charset اشتباه در زمان ساخت دیتابیس. از آن روز، هر بار این خطا را می‌بینم، پیش از هر چیز سراغ charset و collation می‌روم، نه سراغ داده.

خطای Incorrect string value در MySQL دقیقاً چیست؟

خطای Incorrect string value یکی از پیام‌های استاندارد MySQL است که زمانی ظاهر می‌شود که سرور می‌خواهد مقداری را در یک ستون ذخیره کند، ولی آن مقدار با charset تعریف‌شده برای آن ستون هم‌خوان نیست. پیام کامل معمولاً به‌شکل زیر است:

Incorrect string value: '\xF0\x9F\x98\x80' for column 'post_content' at row 1

بخش ابتدایی این پیام — یعنی \xF0\x9F\x98\x80 — نمایش هگزادسیمال بایت‌هایی است که MySQL نمی‌تواند در ستون مقصد ذخیره کند. این هگزادسیمال، در نگاه اول مبهم به نظر می‌رسد، ولی وقتی بدانید هر ایموجی و بسیاری از کاراکترهای فارسی از چند بایت تشکیل می‌شوند، معنای دقیق پیام روشن می‌شود.

نکته‌ای که در تجربه من بیش از همه به آن برخورده‌ام، این است که توسعه‌دهندگان تصور می‌کنند این خطا از کد PHP یا Python می‌آید. در واقع، این خطا تمام شده در لایه دیتابیس رخ می‌دهد؛ یعنی داده سالم به MySQL رسیده، ولی خود MySQL آن را نپذیرفته. همین تفکیک، مسیر دیباگ را به‌طور بنیادی تغییر می‌دهد: شما باید سراغ charset و collation ستون بروید، نه سراغ کد اپلیکیشن.

این خطا در مستندات رسمی MySQL در دسته خطاهای SQLSTATE 22007 یا گاهی 22021 قرار می‌گیرد و به‌طور خاص مربوط به کاراکترهای نامعتبر برای charset ستون است. اگر با خانواده خطاهای MySQL آشنا نیستید، پیشنهاد می‌کنم ابتدا مرور جامعی روی ساختار آن داشته باشید؛ مقاله «آموزش mysql از صفر» نقطه شروع مناسبی است.

Incorrect string value یعنی MySQL داده را سالم دریافت کرده، ولی charset ستون مقصد توانایی نگه‌داری آن را ندارد.

نکته مهم دیگر این است که این خطا بسته به کانتکست، پیام‌های نزدیک به خود را دارد. مثلاً اگر خطا مربوط به ایندکس باشد، پیام متفاوتی مثل Incorrect key file for table ظاهر می‌شود که ریشه‌اش کاملاً متفاوت است. برای درک دقیق‌تر این تفاوت، مرور «خطای Incorrect key file for table در MySQL» توصیه می‌شود؛ چون مرز بین این دو خطا، یکی از پرتکرارترین موارد سردرگمی در دیباگ است.

یک نکته کاربردی: این خطا در سیستم‌هایی که charset آن‌ها به‌درستی انتخاب شده، تقریباً هرگز رخ نمی‌دهد. ظهور مکرر این خطا در یک پروژه، نشانه مطمئنی است که لایه charset در آن پروژه صریح و استاندارد نیست و در آینده ممکن است به خطاهای بزرگ‌تر مثل خرابی داده منتهی شود.

دام بزرگ: utf8 در MySQL، UTF-8 واقعی نیست

یکی از گمراه‌کننده‌ترین نام‌گذاری‌های تاریخ دیتابیس‌ها، انتخاب نام utf8 برای یک charset در MySQL است. در UTF-8 استاندارد، هر کاراکتر می‌تواند بین ۱ تا ۴ بایت اشغال کند. ولی MySQL در نسخه‌های قدیمی، charsetی با نام utf8 معرفی کرد که فقط کاراکترهای تا ۳ بایت را پشتیبانی می‌کرد. یعنی ایموجی‌ها و بسیاری از کاراکترهای یونیکد که ۴ بایت‌اند، در این charset جا نمی‌گرفتند.

این تصمیم تاریخی، مبنای اصلی خطای Incorrect string value در پروژه‌های فارسی‌زبان است. اگرچه کاراکترهای فارسی و عربی در محدوده ۲ بایتی قرار می‌گیرند و مشکلی ندارند، ولی کاراکترهای یونیکد پیشرفته مثل ایموجی، نمادهای ریاضی، کاراکترهای چینی، و بخش قابل‌توجهی از کاراکترهای زبان‌های شرق آسیا در محدوده ۴ بایتی هستند و در charsetی که تحت نام utf8 در MySQL تعریف شده، ذخیره نمی‌شوند.

برای رفع این گمراه‌کننده، تیم MySQL از نسخه ۸ تصمیم گرفت نام‌گذاری را شفاف کند: آن‌چه قبلاً utf8 نامیده می‌شد، اکنون utf8mb3 نامیده می‌شود و charset جدیدی با نام utf8mb4 معرفی شد که تمام ۴ بایت‌های یونیکد را پشتیبانی می‌کند. در نسخه‌های جدیدتر، utf8mb4 به‌عنوان charset پیش‌فرض در نظر گرفته می‌شود، ولی دیتابیس‌هایی که سال‌ها پیش ساخته شده‌اند، هنوز روی charset قدیمی هستند.

charsetی به نام utf8 در MySQL، UTF-8 واقعی نیست؛ همین یک نام گمراه‌کننده، منبع نیمی از خطاهای charset در پروژه‌های فارسی‌زبان است.

نکته ظریف دیگر این است که این تفاوت در MySQL 5.7 و نسخه‌های قبل، حتی در سطح خود مستندات هم گمراه‌کننده بود. در برخی منابع قدیمی، از utf8_general_ci به‌عنوان charset استاندارد یاد می‌شد، در حالی که امروز می‌دانیم این charset برای پروژه‌های چندزبانه و مدرن کافی نیست. برای درک دقیق‌تر تفاوت‌های این خانواده charset در بافت پروژه‌های واقعی، مرور «خطاهای رایج MySQL» توصیه می‌شود؛ چون این خطا یکی از پرتکرارترین اعضای آن خانواده است.

یک نکته عملی: اگر در پروژه شما ایموجی یا کاراکترهای یونیکد پیشرفته ذخیره می‌شوند، حتی اگر تا امروز خطا نگرفته‌اید، احتمالاً داده‌ها با برش کاراکتری (character truncation) در سکوت خراب می‌شوند. این نوع خرابی، برخلاف خطای صریح، در نگاه اول دیده نمی‌شود ولی در بلندمدت به مشکلات جدی تبدیل می‌گردد.

تفاوت utf8، utf8mb3 و utf8mb4 در یک نگاه

برای این‌که در بازبینی‌های سریع بتوانید بدون مراجعه مکرر به مستندات، تفاوت‌ها را در ذهن داشته باشید، جدول زیر کمک‌کننده است:

charsetحداکثر بایت هر کاراکترپشتیبانی ایموجیوضعیت در MySQL 8
utf8 (نام قدیمی)۳خیرمترادف utf8mb3
utf8mb3۳خیرمنسوخ (deprecated)
utf8mb4۴بلهپیش‌فرض جدید
latin1۱خیرفقط برای کاراکترهای غربی
ucs2۲خیرمنسوخ
utf16۴بلهبرای کاربردهای خاص
utf32۴بلهکم‌کاربرد

نکته ظریف در این جدول، تفاوت بین utf8 و utf8mb3 است. در MySQL 8، این دو نام دقیقاً به یک charset اشاره می‌کنند و هر دو ۳ بایتی هستند. ولی در نسخه‌های ۵.۷ و قدیمی‌تر، نام utf8 وجود داشت و utf8mb3 به‌عنوان نام رسمی جایگزین آن معرفی شد. این تغییر نام، در مستندات رسمی MySQL به‌طور کامل توضیح داده شده است.

در انتخاب بین این charsetها، سه معیار اصلی وجود دارد: پشتیبانی از کاراکترهای مورد نیاز پروژه، حجم ذخیره‌سازی مورد قبول، و سازگاری با نسخه MySQL در سرور. برای پروژه‌های فارسی‌زبان، utf8mb4 انتخاب استاندارد است؛ چون:

  • کاراکترهای فارسی و عربی را پشتیبانی می‌کند.
  • ایموجی‌ها را پشتیبانی می‌کند.
  • در نسخه‌های ۵.۵.۳ به بعد MySQL پشتیبانی می‌شود.
  • برای همکاری با سیستم‌های مدرن و APIهای بین‌المللی آماده است.
  • مشکلات آینده‌نگر را از پایه حذف می‌کند.

هزینه اصلی utf8mb4 در مقابل utf8mb3، حجم ذخیره‌سازی بیشتر است. برای هر کاراکتر، حداکثر یک بایت اضافه مصرف می‌شود. در دیتابیس‌های با میلیون‌ها رکورد متنی، این تفاوت به‌شکل محسوس می‌شود، ولی در اکثر پروژه‌های واقعی، این هزینه در برابر مزایای آن ناچیز است. برای مقایسه عمیق‌تر با سایر انتخاب‌های charset، مرور «طراحی دیتابیس در mysql» توصیه می‌شود؛ چون یکی از تصمیم‌های اصلی در طراحی، همین انتخاب charset و collation است.

نقش collation و چرا مهم‌تر از charset است

در کنار charset، مفهوم دومی به‌نام collation وجود دارد که در بسیاری از بحث‌ها نادیده گرفته می‌شود ولی در واقع، اثر عملی آن‌ها ممکن است بیشتر از خود charset باشد. charset مشخص می‌کند چه کاراکترهایی قابل ذخیره هستند؛ ولی collation مشخص می‌کند چطور با این کاراکترها مقایسه و مرتب‌سازی انجام شود.

برای هر charset، چندین collation وجود دارد. مثلاً برای utf8mb4، این گزینه‌ها رایج هستند:

  • utf8mb4_general_ci — مقایسه ساده، سریع‌تر ولی دقت کم‌تر
  • utf8mb4_unicode_ci — مقایسه یونیکد، کندتر ولی دقیق‌تر
  • utf8mb4_unicode_520_ci — نسخه بهبودیافته یونیکد
  • utf8mb4_0900_ai_ci — استاندارد جدید MySQL 8، سریع و دقیق
  • utf8mb4_bin — مقایسه باینری، حساس به بزرگی و کوچکی حروف

نکته کاربردی این است که collation در پروژه‌های فارسی‌زبان می‌تواند اثر مستقیم روی جستجو داشته باشد. مثلاً در utf8mb4_general_ci، کاراکترهای عربی «ي» و «ی» به‌عنوان یکسان در نظر گرفته می‌شوند؛ در حالی که در utf8mb4_bin، این دو کاراکتر متفاوت هستند. همین تفاوت، روی کیفیت جستجو و روی ترتیب الفبایی اثر می‌گذارد.

یک نکته ظریف: اگر charset در سطح ستون و در سطح اتصال (connection) هم‌خوان نباشد، ممکن است خطای Incorrect string value حتی با utf8mb4 رخ دهد. مثلاً اگر اتصال شما با latin1 برقرار شود و داده‌ای فارسی بفرستید، MySQL نمی‌تواند آن را در ستون utf8mb4 ذخیره کند. به همین دلیل، یکی از اولین کارهایی که در دیباگ این خطا انجام می‌دهم، بررسی هم‌خوانی charset در سه سطح است:

-- بررسی charset در سطح دیتابیس
SELECT default_character_set_name, default_collation_name
FROM information_schema.SCHEMATA
WHERE schema_name = 'your_database';

-- بررسی charset در سطح جدول
SHOW CREATE TABLE your_table;

-- بررسی charset در سطح اتصال
SHOW VARIABLES LIKE 'character_set%';
SHOW VARIABLES LIKE 'collation%';

در اکثر پرونده‌هایی که به من رسیده، عدم هم‌خوانی در یکی از این سه سطح، ریشه اصلی خطا بوده است. راه‌حل استاندارد، تعریف charset و collation در همه سطوح به‌شکل صریح و یکسان است. برای مرور دقیق‌تر دستورات MySQL که در این نوع بررسی‌ها به کار می‌آید، مرور «دستورات پرکاربرد mysql» توصیه می‌شود.

charset مشخص می‌کند چه کاراکترهایی، ولی collation مشخص می‌کند چطور مقایسه شوند؛ بی‌توجهی به collation، حتی با charset درست، اثر می‌گذارد.

خطا در کدام لایه شکل می‌گیرد؟

یکی از سؤالاتی که در جلسات بازبینی کد زیاد می‌شنوم این است: این خطا دقیقاً در کدام لایه شکل می‌گیرد؟ آیا مربوط به کد اپلیکیشن است یا دیتابیس؟ پاسخ در ظاهر ساده است ولی در عمل مهم: این خطا در لایه دیتابیس شکل می‌گیرد، ولی دلایل آن می‌تواند در سه لایه متفاوت باشد. برای این‌که بتوانید در پرونده‌های واقعی سریع تشخیص دهید، سه لایه‌ای که در این نوع خطا نقش دارند را مرور کنیم:

لایه اول: لایه اپلیکیشن

در این لایه، charset اتصال به دیتابیس تعریف می‌شود. اگر کد PHP یا Python شما از یک charset اشتباه برای اتصال استفاده کند، داده به‌شکل اشتباه به MySQL می‌رسد و خطا رخ می‌دهد. در PHP، این لایه معمولاً در mysqli_set_charset یا در DSN مربوط به PDO تعریف می‌شود:

// در PDO
$pdo = new PDO(
  "mysql:host=localhost;dbname=your_db;charset=utf8mb4",
  $user,
  $pass
);

// در MySQLi
$mysqli->set_charset("utf8mb4");

نکته ظریف این است که charset در این لایه، باید با charset ستون‌ها هم‌خوان باشد. اگر ستون شما utf8mb4 است ولی اتصال شما utf8 (یعنی utf8mb3) است، داده ایموجی به‌شکل ناقص به MySQL می‌رسد و در ذخیره خطا می‌دهد. برای مرور دقیق‌تر این لایه در بافت PHP، مرور «اتصال php به mysql» و «آموزش pdo در php» توصیه می‌شود.

لایه دوم: لایه ستون و جدول

در این لایه، charset هر ستون تعریف می‌شود. اگر ستون شما روی utf8 (یعنی utf8mb3) تعریف شده باشد، داده‌ای که به ۴ بایت نیاز دارد در آن ذخیره نمی‌شود. برای بررسی این لایه:

SHOW FULL COLUMNS FROM your_table;

در خروجی این دستور، ستون Collation مشخص می‌کند هر ستون روی چه charset و collation تعریف شده است. اگر این مقدار utf8_general_ci یا utf8mb3_general_ci بود، آن ستون برای داده‌های ایموجی و پیشرفته مناسب نیست.

لایه سوم: لایه دیتابیس و سرور

در این لایه، charset پیش‌فرض دیتابیس و سرور تعریف می‌شود. اگر یک ستون charset خودش را تعریف نکرده باشد، charset پیش‌فرض دیتابیس را به ارث می‌برد. برای بررسی:

SHOW VARIABLES LIKE 'character_set_database';
SHOW VARIABLES LIKE 'character_set_server';

در اکثر پرونده‌ها، ریشه خطا در یکی از این سه لایه است. راه‌حل استاندارد، اطمینان از هم‌خوانی charset در هر سه لایه با utf8mb4 است. برای درک دقیق‌تر ساختار جدول‌ها و نقش charset در طراحی، مرور «بهینه‌سازی جداول MySQL برای سرعت بیشتر» توصیه می‌شود؛ چون انتخاب charset درست، هم روی صحت داده و هم روی سرعت اثر دارد.

هشت سناریوی واقعی که این خطا را فعال می‌کنند

در پرونده‌هایی که به من رسیده، تعداد الگوهایی که به این خطا منتهی می‌شوند بیشتر از آن‌چه انتظار می‌رود است. هشت سناریوی زیر، تقریباً همه پرونده‌های عملی را پوشش می‌دهند.

سناریو اول: ایموجی در محتوای کاربر

شایع‌ترین حالت. وقتی کاربر در دیدگاه، توضیح محصول یا متن پیام خود ایموجی وارد می‌کند و ستون روی utf8mb3 تعریف شده، خطای Incorrect string value رخ می‌دهد. راه‌حل، ارتقای charset ستون به utf8mb4 است.

سناریو دوم: کاراکترهای یونیکد در محتوای چندزبانه

در سایت‌های چندزبانه که محتوا به زبان‌های شرق آسیا یا زبان‌های با کاراکترهای یونیکد پیشرفته است، همین خطا رخ می‌دهد. راه‌حل، استانداردسازی charset در همه لایه‌ها است.

سناریو سوم: الحاق متن از منابع بیرونی

وقتی داده از یک API بیرونی گرفته می‌شود و آن API از کاراکترهای پیشرفته استفاده می‌کند، داده در لایه اپلیکیشن سالم است ولی در دیتابیس با محدودیت charset مواجه می‌شود. راه‌حل، اطمینان از ارتقای charset در همه لایه‌ها قبل از الحاق داده از منابع بیرونی است.

سناریو چهارم: ایمپورت داده از فایل‌های SQL

وقتی یک فایل SQL قدیمی که با charset اشتباه ساخته شده در دیتابیس جدید ایمپورت می‌شود، ممکن است در حین ایمپورت، خطای Incorrect string value رخ دهد. راه‌حل، تصریح charset در دستور ایمپورت است:

mysql --default-character-set=utf8mb4 -u user -p dbname < dump.sql

سناریو پنجم: تغییر charset در سطح دیتابیس بدون تغییر ستون‌ها

گاهی تیم فنی charset دیتابیس را به utf8mb4 ارتقا می‌دهد ولی ستون‌های موجود را تغییر نمی‌دهد. چون charset صریح ستون، از charset پیش‌فرض دیتابیس اولویت می‌گیرد، خطا همچنان رخ می‌دهد. راه‌حل، تغییر charset ستون‌ها به‌صورت صریح است.

سناریو ششم: اتصال با charset اشتباه در کد

در بعضی پروژه‌ها، charset اتصال در کد روی utf8 تعریف شده ولی ستون‌ها روی utf8mb4. این عدم هم‌خوانی، منبع مکرر خطا است. راه‌حل، تغییر charset اتصال به utf8mb4 است.

سناریو هفتم: ایموجی در کلید اصلی

در بعضی سناریوها، ایموجی در کلید اصلی یا ایندکس یکتا قرار می‌گیرد. چون این فیلد به‌طور پیش‌فرض از charset جدول ارث می‌برد، اگر جدول روی utf8mb3 باشد، خطا رخ می‌دهد. راه‌حل، تغییر charset جدول و ایندکس‌ها است.

سناریو هشتم: ذخیره اطلاعات در فایل‌های متنی

در بعضی پروژه‌ها، اطلاعات از فایل‌های CSV یا Excel خوانده می‌شود. اگر آن فایل‌ها با charset اشتباه ساخته شده باشند، داده‌ای که به MySQL می‌رسد ممکن است حاوی کاراکترهای نامعتبر باشد. راه‌حل، استانداردسازی charset در مرحله خواندن فایل و پیش از الحاق به دیتابیس است.

در همه هشت سناریو، یک نکته مشترک وجود دارد: جایی در لایه charset، هم‌خوانی کافی بین منبع داده و ستون مقصد وجود ندارد.

دام اختصاصی وردپرس و ووکامرس

در تجربه من، بخش بزرگی از پرونده‌های Incorrect string value مربوط به پروژه‌های وردپرسی و ووکامرسی است. دلیلش روشن است: این سیستم‌ها به‌طور تاریخی با charset utf8 (یعنی utf8mb3) ساخته می‌شوند و در نسخه‌های قدیمی، ارتقای charset به utf8mb4 به‌عنوان یک قابلیت اختیاری مطرح می‌شد.

ریشه تاریخی

وردپرس تا نسخه ۴.۲، از charset utf8 برای جداول خود استفاده می‌کرد. از نسخه ۴.۲ به بعد، این charset به utf8mb4 ارتقا یافت، ولی این ارتقا فقط در نصب‌های جدید اعمال می‌شد. برای نصب‌های قدیمی، تیم وردپرس یک فرآیند ارتقای خودکار طراحی کرد که باید به‌طور دستی از پیشخوان اجرا شود. اگر این فرآیند در پروژه شما انجام نشده، جداول شما همچنان روی utf8mb3 هستند و ایموجی‌ها در آن‌ها ذخیره نمی‌شوند.

بررسی وضعیت charset در وردپرس

برای بررسی وضعیت charset جداول وردپرس، می‌توانید از کوئری زیر استفاده کنید:

SELECT table_name, table_collation
FROM information_schema.tables
WHERE table_schema = 'your_wp_database'
  AND table_name LIKE 'wp_%';

اگر مقادیر این ستون شامل utf8mb3 یا utf8_general_ci باشند، پروژه شما هنوز روی charset قدیمی است.

ارتقای جداول وردپرس

وردپرس برای ارتقای charset جداول خود، یک فرآیند استاندارد دارد که می‌توانید از طریق دستور زیر در wp-config.php فعال کنید:

define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', 'utf8mb4_unicode_ci');

سپس باید از طریق پنل پیشخوان وردپرس، فرآیند ارتقا را اجرا کنید. این فرآیند ممکن است بسته به حجم دیتابیس، چند دقیقه تا چند ساعت طول بکشد. توصیه من این است که این کار در ساعات کم‌ترافیک و با بکاپ کامل انجام شود. برای مرور دقیق‌تر روش‌های بکاپ از دیتابیس، مرور «پشتیبان گیری از mysql» توصیه می‌شود.

دام افزونه‌های قدیمی

در بعضی پروژه‌های ووکامرسی، بعضی افزونه‌های قدیمی جداول خودشان را با charset اشتباه می‌سازند. حتی اگر جداول اصلی وردپرس روی utf8mb4 باشند، جداول افزونه‌ها می‌توانند روی utf8mb3 بمانند. راه‌حل، بررسی charset همه جداول و اصلاح دستی آن‌هایی است که با charset اشتباه ساخته شده‌اند. برای مرور دقیق‌تر افزونه‌های استاندارد در ووکامرس، مرور «بهترین افزونه‌های کاربردی برای ووکامرس» توصیه می‌شود؛ چون انتخاب افزونه‌های استاندارد، از بسیاری از این مشکلات جلوگیری می‌کند.

چطور ریشه این خطا را در پروژه ایزوله کنیم؟

فرض کنید همین امروز یک خطای Incorrect string value در محیط تولید ظاهر شده و می‌خواهید ریشه‌اش را پیدا کنید. روشی که در این نوع پرونده‌ها به کار می‌گیرم، شش گام دارد و هر گام، یک شرط را در ذهن من حذف می‌کند.

گام اول: نگاه دقیق به پیام خطا

اولین کاری که می‌کنم، خواندن دقیق پیام خطا است. سه بخش مهم در این پیام وجود دارد: مقدار هگزادسیمال (مثل \xF0\x9F...)، نام ستون، و شماره ردیف. با نگاه به هگزادسیمال، می‌توانم تشخیص دهم که مقدار مشکل‌دار چند بایت است و از چه خانواده‌ای است. اگر با \xF0 شروع شود، تقریباً همیشه ایموجی یا کاراکتر یونیکد ۴ بایتی است.

گام دوم: بررسی charset ستون مقصد

دومین کاری که می‌کنم، بررسی charset ستون مقصد است:

SHOW FULL COLUMNS FROM your_table LIKE 'your_column';

اگر ستون روی utf8mb3 یا utf8_general_ci باشد، تشخیص قطعی است: مسئله از charset ستون است.

گام سوم: بررسی charset اتصال

سومین کاری که می‌کنم، بررسی charset اتصال است:

SHOW VARIABLES LIKE 'character_set_client';
SHOW VARIABLES LIKE 'character_set_connection';
SHOW VARIABLES LIKE 'character_set_results';

اگر این سه مقدار با utf8mb4 هم‌خوان نباشند، حتی ستون‌های utf8mb4 هم ممکن است خطا بدهند.

گام چهارم: بررسی charset دیتابیس

چهارمین کاری که می‌کنم، بررسی charset دیتابیس است:

SELECT default_character_set_name, default_collation_name
FROM information_schema.SCHEMATA
WHERE schema_name = 'your_database';

اگر دیتابیس روی utf8mb4 باشد ولی ستون روی utf8mb3، charset ستون اولویت دارد و خطا رخ می‌دهد.

گام پنجم: بازتولید خطا در محیط امن

پنجمین کاری که می‌کنم، بازتولید خطا در یک محیط امن مثل محیط توسعه است. یک کوئری ساده می‌زنم که همان مقدار مشکل‌دار را درج کند:

INSERT INTO your_table (your_column) VALUES ('😀');

اگر این کوئری همان خطا را بدهد، تشخیص تأیید می‌شود. این گام در تجربه من بسیار به کارم آمده است؛ چون امکان تست تغییرات مختلف را فراهم می‌کند.

گام ششم: بررسی منبع داده در لایه اپلیکیشن

ششمین کاری که می‌کنم، بررسی منبع داده در لایه اپلیکیشن است. اگر داده از یک API بیرونی یا از یک فایل می‌آید، باید مطمئن شوم که charset آن درست است و در مرحله انتقال تغییر نمی‌کند. برای مرور دقیق‌تر این لایه در بافت زبان‌های مختلف، مرور «اتصال پایتون به mysql» توصیه می‌شود؛ چون در این لایه، تفاوت‌های ظریف در charset می‌توانند منبع خطاهای مبهم باشند.

در کنار این شش گام، یک تکنیک عملی مهم وجود دارد: در بافت ORMها مثل SQLAlchemy یا Eloquent، charset اتصال معمولاً از تنظیمات پیکربندی گرفته می‌شود. اگر در آن سطح charset به utf8 تنظیم شده باشد، حتی با ستون‌های utf8mb4 هم خطا رخ می‌دهد. توصیه من این است که در همه لایه‌ها، charset به‌شکل صریح روی utf8mb4 تعریف شود.

مهاجرت امن از utf8 به utf8mb4

بعد از تشخیص، نوبت به رفع است. در اکثر پرونده‌ها، راه‌حل نهایی، ارتقای charset از utf8mb3 به utf8mb4 است. ولی این کار باید با دقت انجام شود؛ چون:

  • مهاجرت نادرست می‌تواند به خرابی داده منتهی شود.
  • مهاجرت روی دیتابیس‌های بزرگ ممکن است ساعت‌ها طول بکشد.
  • در بافت MySQL 5.7 و قبل‌تر، برخی محدودیت‌ها روی حجم ایندکس وجود دارد.

پروتکلی که در پروژه‌های واقعی به کار می‌برم، پنج گام دارد:

گام اول: بکاپ کامل

پیش از هر تغییری، یک بکاپ کامل از دیتابیس و فایل‌های پیکربندی بگیرید. این بکاپ باید در بیرون سرور و در جای امن ذخیره شود. برای مرور دقیق‌تر این فرآیند، مرور «پشتیبان گیری از mysql» توصیه می‌شود.

گام دوم: بررسی سازگاری

پیش از مهاجرت، با کوئری زیر بررسی کنید که در ستون‌های متنی، کاراکترهایی هستند که با charset جدید هم‌خوان نیستند یا نه:

SELECT COUNT(*) FROM your_table
WHERE your_column REGEXP '[\xF0-\xF4]';

این کوئری، به‌شکل تقریبی، رکوردهایی را می‌شمارد که حاوی بایت‌های ۴ بایتی هستند.

گام سوم: مهاجرت تدریجی

مهاجرت را به‌شکل تدریجی انجام دهید. ابتدا دیتابیس، سپس جدول‌ها، سپس ستون‌ها:

ALTER DATABASE your_database
  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

ALTER TABLE your_table
  CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

توجه: دستور ALTER TABLE ... CONVERT TO CHARACTER SET روی همه ستون‌های متنی اعمال می‌شود و ممکن است زمان‌بر باشد. توصیه من این است که در پروژه‌های بزرگ، این کار را جدول‌به‌جدول انجام دهید.

گام چهارم: تغییر charset اتصال

در کد اپلیکیشن، charset اتصال را به utf8mb4 تغییر دهید. این کار در همه لایه‌های اتصال باید انجام شود.

گام پنجم: تست جامع

بعد از مهاجرت، تست‌های جامع زیر را اجرا کنید:

  • درج ایموجی در ستون‌های اصلی.
  • جستجو با کاراکترهای فارسی.
  • مرتب‌سازی بر اساس ستون‌های متنی.
  • ایمپورت و اکسپورت داده.
  • مقایسه داده‌های قدیمی با داده‌های جدید.

نکته کاربردی: اگر حجم دیتابیس بزرگ است و مهاجرت کامل ممکن نیست، می‌توانید از یک استراتژی ترکیبی استفاده کنید. یعنی charset را روی ستون‌هایی که ایموجی و کاراکترهای پیشرفته در آن‌ها ذخیره می‌شود تغییر دهید و ستون‌های دیگر را بدون تغییر بگذارید. این استراتژی، حجم کار را کم می‌کند ولی نیازمند مستندسازی دقیق است. برای درک عمیق‌تر تراکنش‌ها در مهاجرت‌های بزرگ، مرور «تراکنش ها در mysql» توصیه می‌شود.

مهاجرت charset یک پروژه کامل است، نه یک دستور تک‌خطی؛ هرچه دقیق‌تر برنامه‌ریزی شود، ریسک خرابی داده کمتر می‌شود.

الگوهای رفع و پیشگیری در کد مدرن

بعد از تشخیص، نوبت به رفع و پیشگیری است. شش الگوی عملی در پروژه‌ها به کارم آمده که هر کدام، در بافت متفاوتی مناسب است.

الگوی اول: تعریف صریح charset در سطح اتصال

همیشه charset را در سطح اتصال به‌شکل صریح تعریف کنید، نه با اتکا به پیش‌فرض سرور:

$pdo = new PDO(
  "mysql:host=localhost;dbname=your_db;charset=utf8mb4",
  $user,
  $pass
);

این الگو، در زبان‌های دیگر هم صادق است: در Python با pymysql، در Node.js با mysql2، و در سایر زبان‌ها، charset باید به‌شکل صریح تعریف شود.

الگوی دوم: تعریف صریح charset در سطح ستون

در ساخت ستون‌ها، charset را صریح تعریف کنید تا از پیش‌فرض جدول مستقل باشد:

CREATE TABLE your_table (
  id INT PRIMARY KEY,
  content TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
);

الگوی سوم: اعتبارسنجی داده پیش از درج

در بعضی سناریوها، لازم است داده پیش از درج اعتبارسنجی شود تا از charset مورد نیاز آن مطمئن شوید. در PHP:

function hasMultibyteChars(string $text): bool {
  return preg_match('/[xF0-xF4]/', $text) === 1;
}

این الگو، در پروژه‌هایی که نمی‌توانند charset را تغییر دهند، مفید است؛ چون اجازه می‌دهد پیش از درج، داده را بررسی و در صورت نیاز پاک‌سازی کنید.

الگوی چهارم: پاک‌سازی کاراکترهای مشکل‌دار

در موارد اضطراری، می‌توانید کاراکترهای مشکل‌دار را پیش از درج حذف کنید. ولی این روش فقط به‌عنوان راه‌حل موقت توصیه می‌شود:

function stripMultibyte(string $text): string {
  return preg_replace('/[xF0-xF4][x80-xBF]{3}/', '', $text);
}

توجه: این روش داده را از دست می‌دهد و فقط باید در سناریوهای اضطراری استفاده شود.

الگوی پنجم: تبدیل ایموجی به متن

در بعضی پروژه‌ها، به‌جای ذخیره ایموجی، از متن معادل استفاده می‌شود:

$text = preg_replace('/😀/', ':smile:', $text);

این الگو، در سیستم‌هایی که نمی‌توانند charset را تغییر دهند، مفید است ولی تجربه کاربری را تحت تأثیر قرار می‌دهد.

الگوی ششم: تست charset در CI/CD

در پروژه‌های بالغ، charset در فرآیند CI/CD تست می‌شود تا از عدم هم‌خوانی در لایه‌های مختلف جلوگیری شود:

SHOW VARIABLES LIKE 'character_set%';

این تست، می‌تواند در قالب یک اسکریپت پایتون یا bash پیاده شود. در انتخاب بین این شش الگو، هیچ‌کدام را نباید به‌عنوان نسخه «درست» در نظر گرفت؛ انتخاب، به بافت پروژه و اندازه تیم بستگی دارد. برای مرور جامع‌تر الگوهای مدیریت خطا در بافت دیتابیس، مطالعه «بهینه سازی کوئری های mysql» توصیه می‌شود.

اشتباهات رایجی که این خطا را تشدید می‌کنند

در بررسی پرونده‌های این خطا، هفت اشتباه تکراری دیده‌ام که هر کدام، به‌جای رفع مشکل، آن را پیچیده‌تر می‌کند.

  • استفاده از latin1 به‌جای utf8mb4. بعضی تیم‌ها به‌دلیل حجم کم‌تر، از latin1 استفاده می‌کنند. این کار برای پروژه‌های فارسی‌زبان مشکل‌ساز است و در نهایت به خطای charset منتهی می‌شود.
  • استفاده از utf8 با فرض این‌که UTF-8 است. شایع‌ترین اشتباه. نام‌گذاری گمراه‌کننده utf8 در MySQL باعث می‌شود تیم فنی تصور کند از UTF-8 استفاده می‌کند، در حالی که در واقع محدود به ۳ بایت است.
  • تغییر charset دیتابیس بدون تغییر charset ستون‌ها. چون charset صریح ستون اولویت دارد، تغییر charset دیتابیس معمولاً اثر عملی ندارد.
  • اتکا به پیش‌فرض سرور. اگر charset اتصال به‌شکل صریح تعریف نشود، ممکن است پیش‌فرض سرور اعمال شود که در محیط‌های مختلف متفاوت است.
  • نادیده گرفتن collation. charset درست با collation اشتباه، می‌تواند به مشکلات جستجو و مرتب‌سازی منتهی شود.
  • مهاجرت بدون بکاپ. مهاجرت charset روی دیتابیس بزرگ بدون بکاپ، یک ریسک جدی است.
  • نادیده گرفتن خطا در لاگ. اگر سیستم لاگ شما خطاهای charset را ثبت نمی‌کند، ممکن است خرابی داده در سکوت رخ دهد و در بلندمدت به مشکل بزرگ تبدیل شود.

در کنار این هفت اشتباه، یک هشتمین نکته هم وجود دارد که در تیم‌های بزرگ زیاد دیده‌ام: نداشتن استاندارد charset در سراسر پروژه. وقتی تیم فنی نداند که کدام charset استاندارد است، هر توسعه‌دهنده ممکن است به سلیقه خود عمل کند و این عدم یک‌دستی، در بلندمدت منبع خطاهای تکراری می‌شود.

ماتریس تست charset برای پروژه‌های چندزبانه

چیزی که در پروژه‌های بالغ به‌شکل منظم دیده‌ام، تست‌های اختصاصی برای charset است. ماتریسی که در پروژه‌ها استفاده می‌کنم، این شکلی است:

سناریوورودیخروجی مورد انتظار
کاراکتر ASCIIHelloذخیره موفق
کاراکتر فارسیسلامذخیره موفق
کاراکتر عربیمرحباذخیره موفق
کاراکتر چینی你好ذخیره موفق با utf8mb4
ایموجی ساده😀ذخیره موفق با utf8mb4
ایموجی پیچیده👨‍👩‍👧ذخیره موفق با utf8mb4
نماد ریاضی∑ ∫ √ذخیره موفق با utf8mb4
کاراکتر ترکیبیau0301ذخیره موفق
کاراکتر کنترلی\u0000بسته به تنظیمات
متن بسیار بلندمتن با ایموجی متعددذخیره موفق با utf8mb4

هر ردیف از این ماتریس، یک سناریوی واقعی را پوشش می‌دهد. اگر این تست‌ها را در پروژه خود بگذارید، نه‌فقط خطاهای زمان اجرا را زودتر می‌گیرید، بلکه وقتی تیم شما بزرگ‌تر می‌شود، رفتار برنامه در برابر تغییرات، قابل پیش‌بینی می‌ماند.

یک تذکر مهم: در تست‌های charset، مطمئن شوید که همه لایه‌ها (اتصال، جدول، ستون) با charset یکسان تست می‌شوند. اگر فقط یک لایه تست شود، ممکن است خطا در محیط واقعی رخ دهد.

پرسش‌های پرتکرار درباره Incorrect string value

در این بخش، پرسش‌هایی که بیشترین تکرار را در تیم‌های فنی، تیکت‌های پشتیبانی و جلسات بازبینی داشته‌اند، پاسخ می‌دهم. هدف این است که بتوانید به‌سرعت به پاسخ برسید، بدون اینکه لازم باشد کل مقاله را دوباره مرور کنید.

چرا باوجود utf8 در MySQL هنوز ایموجی ذخیره نمی‌شود؟

چون utf8 در MySQL، UTF-8 واقعی نیست؛ نسخه ۳ بایتی است که ایموجی‌ها را که ۴ بایت هستند، پشتیبانی نمی‌کند. راه‌حل، استفاده از utf8mb4 است.

تفاوت utf8 و utf8mb4 چیست؟

در MySQL 8، utf8 به utf8mb3 اشاره می‌کند و ۳ بایتی است. utf8mb4 نسخه ۴ بایتی است که تمام یونیکد را پشتیبانی می‌کند، از جمله ایموجی‌ها و کاراکترهای پیشرفته.

آیا utf8mb4 حتماً باعث افت سرعت می‌شود؟

افت سرعت در اکثر سناریوها ناچیز است. هزینه اصلی، حجم ذخیره‌سازی بیشتر است که در اکثر پروژه‌ها معنی‌دار نیست. برای پروژه‌های با میلیون‌ها رکورد متنی، ممکن است تفاوت محسوس شود.

آیا در MariaDB هم همین مشکل وجود دارد؟

بله. MariaDB نیز همان نام‌گذاری گمراه‌کننده را دارد. در نسخه‌های جدید، توصیه به استفاده از utf8mb4 در هر دو سیستم است.

چطور بفهمم دیتابیس من روی چه charset است؟

با کوئری زیر می‌توانید charset و collation دیتابیس را ببینید:

SELECT default_character_set_name, default_collation_name
FROM information_schema.SCHEMATA
WHERE schema_name = 'your_database';

آیا تغییر charset دیتابیس به‌طور خودکار روی ستون‌ها اثر می‌گذارد؟

خیر. ستون‌ها charset خودشان را دارند و از پیش‌فرض دیتابیس مستقل‌اند. باید برای هر ستون، ALTER TABLE ... MODIFY COLUMN اجرا شود.

آیا می‌توان از latin1 به utf8mb4 مهاجرت کرد؟

بله، ولی این مهاجرت حساس‌تر است؛ چون latin1 ۱ بایتی است و ممکن است برخی کاراکترها در لایه‌های میانی گم شده باشند. پیش از مهاجرت، بکاپ کامل و تست جامع ضروری است.

آیا در MySQL 5.7 هم می‌توان از utf8mb4 استفاده کرد؟

بله. utf8mb4 از MySQL 5.5.3 به بعد پشتیبانی می‌شود. ولی در نسخه‌های قدیمی‌تر، محدودیت‌هایی روی حجم ایندکس وجود دارد که باید مدنظر قرار گیرد.

آیا ORMها charset را به‌طور خودکار مدیریت می‌کنند؟

بخشی از کار را انجام می‌دهند، ولی معمولاً charset اتصال را از تنظیمات پیکربندی می‌گیرند. اگر در آن سطح charset اشتباه باشد، ORM هم نمی‌تواند کمکی کند.

آیا این خطا در بافت ووکامرس هم رخ می‌دهد؟

بله. اگر جداول ووکامرس روی charset قدیمی باشند، همین خطا در درج دیدگاه، توضیح محصول، یا پیام مشتری رخ می‌دهد. راه‌حل، ارتقای charset جداول و ووکامرس است.

نگاه معمارانه: charset به‌عنوان یک قرارداد سراسری

در پروژه‌های بالغ، charset به‌عنوان یک تصمیم طراحی سراسری مدیریت می‌شود، نه به‌عنوان یک تنظیم محلی. سه تصمیم معمارانه که در تیم‌های حرفه‌ای دیده‌ام، این کلاس خطا را از یک خطر پنهان به یک ابزار قابل کنترل تبدیل می‌کند.

لایه اول: استانداردسازی سراسری charset

تیم‌های حرفه‌ای برای کل پروژه، یک charset استاندارد تعریف می‌کنند. یعنی همه سرویس‌ها، همه اتصال‌ها، همه جدول‌ها، و همه ستون‌ها روی یک charset و collation یکسان تنظیم می‌شوند. با این استاندارد، هیچ‌کس به‌طور تصادفی از charset متفاوت استفاده نمی‌کند و تیم فنی می‌داند که در همه لایه‌ها، چه انتظاری باید داشته باشد.

لایه دوم: جدا کردن charset از کد اپلیکیشن

در پروژه‌های مدرن، به‌جای تکرار charset در کد اپلیکیشن، از یک لایه پیکربندی مرکزی استفاده می‌شود. یعنی charset در یک فایل پیکربندی یا متغیر محیطی تعریف می‌شود و همه لایه‌های اتصال آن را از همان منبع می‌خوانند. این جداسازی، هم نگهداری را ساده‌تر می‌کند و هم از عدم هم‌خوانی در پروژه‌های بزرگ جلوگیری می‌کند. برای درک دقیق‌تر این نوع معماری در بافت پروژه‌های وردپرسی، مرور «توسعه وردپرس چیست و از کجا شروع کنیم» توصیه می‌شود.

لایه سوم: تست charset در CI/CD

در پروژه‌های بالغ، charset در فرآیند CI/CD تست می‌شود. یعنی پیش از استقرار، یک تست ساده اجرا می‌شود که از charset همه لایه‌ها مطمئن شود. اگر در آن تست، charsetی متفاوت از استاندارد بود، استقرار متوقف می‌شود. این لایه، جلوی بسیاری از خطاهای charset را در تولید می‌گیرد.

در تمام این سه لایه، یک اصل مشترک وجود دارد: charset نه به‌عنوان یک تنظیم محلی، بلکه به‌عنوان یک قرارداد سراسری دیده می‌شود. همین دیدگاه است که تفاوت بین تیم‌هایی که از این کلاس خطا رنج می‌برند و تیم‌هایی که آن را به‌عنوان یک فرصت طراحی می‌بینند، ایجاد می‌کند.

وقتی charset به‌عنوان یک قرارداد سراسری دیده شود، از یک تنظیم محلی به یک تعهد سازمانی تبدیل می‌شود.

یک تصمیم کوچک، یک کلاس خطای ازیادرفته

خطای Incorrect string value در نگاه اول یک خطای کوچک به‌نظر می‌رسد، اما در عمل، آینه‌ای است که نشان می‌دهد لایه charset پروژه شما چقدر صریح و کنترل‌شده است. اگر این خطا در تولید ظاهر می‌شود، به احتمال زیاد جای دیگری از سیستم هم داده‌ای بدون استاندارد charset جریان دارد. به همین دلیل، توصیه عملی من سه چیز است: اول، charset را در همه لایه‌ها به‌شکل صریح روی utf8mb4 تعریف کنید؛ دوم، collation مناسب را با charset ستون‌ها هم‌راستا کنید؛ سوم، در تست‌های خود ماتریس سناریوهای charset را بگنجانید تا رفتار برنامه در برابر داده‌های چندزبانه، قابل پیش‌بینی بماند.

اگر خطای مشابهی را در یک پروژه واقعی تجربه کرده‌اید — مخصوصاً جایی که ریشه مشکل از آنچه انتظار داشتید دور بوده — تجربه‌تان را در دیدگاه بنویسید. برای من جالب است بدانم کدام لایه بیشترین زمان را از شما گرفت، و آیا الگویی پیدا کردید که با آنچه در این متن آمده، تفاوت داشت. تجربه‌های واقعی شما، این متن را برای خواننده بعدی دقیق‌تر می‌کند. 🗄️