من JSON إلى SQL: عبارات INSERT وتصميم الجداول ومزالق تحويل الأنواع
الخلاصة: يخفي التحويل من JSON إلى SQL INSERT تقسيم الجداول المتداخلة، والفرق بين NULL والحقل الغائب، والهروب، والفروق بين اللهجات. عبارات قابلة للتشغيل وجداول مقارنة لكل مزلق.

الخلاصة: قرر أي كيان أعمال يمثله الصف الواحد قبل تصميم الجداول، ثم اكتب VALUES متعددة الصفوف صريحة داخل معاملة واحدة؛ واحسم تسمية الحقول وتحويل الأنواع قبل الاستيراد بدلًا من ترقيعها في SQL.
يصف JSON الشكل؛ ويصف SQL القيود أيضًا. وعندما تنقل الأول إلى الثاني، يعود كل قيد لم تكتبه لاحقًا بوصفه بيانات قذرة.
1. قرر أولًا: جدول واحد أم سجل واحد لكل صف JSON
انظر إلى الطبقة الخارجية للبيانات قبل تصميم أي جدول. وإن أخطأت هذا القرار فيجب إعادة كتابة كل INSERT بعده.
| بنية JSON | تصميم الجدول المقترح | الخطر الرئيسي |
|---|---|---|
مصفوفة مسطحة من الكائنات [{...},{...}] |
جدول واحد، صفوف كثيرة | المفاتيح غير المتسقة بين السجلات تترك معظم أعمدة الاتحاد NULL |
كائن يغلّف مصفوفة {"data":[...],"total":100} |
جدول واحد، خُذ data فقط |
كتابة total في كل صف تُفقد الحقل معناه |
كائن متداخل {"user":{"name":"x"}} |
جدول أب مع أعمدة مسطحة، أو أب مع جدول مرتبط | توسع غير مضبوط، وأسماء أعمدة مكررة |
حقل مصفوفة {"tags":["a","b"]} |
جدول ابن، أو عمود JSON/TEXT | علاقة واحد-إلى-متعدد تُحشر في خلية واحدة فلا يمكن الاستعلام بالوسم |
| مفاتيح غير متسقة | أعمدة اتحاد، أو تقسيم الجداول حسب النوع | "المفتاح غائب" و"القيمة null" ينهاران إلى NULL نفسه |
لا يوجد إلا معيار واحد: أي كيان أعمال يمثله صف واحد؟ فإن كان الصف طلبًا، فيجب أن تعيش بنوده في جدولها الخاص. وإن كان الصف مستخدمًا، فالوسوم تنتمي إلى جدول ابن أو عمود JSON، لا إلى نص موصول بفواصل.
2. من مصفوفة إلى INSERT: أصغر قالب قابل للتشغيل
الشكل الأسلم يذكر أسماء الأعمدة صراحةً دائمًا ولا يعتمد على ترتيب الأعمدة أبدًا:
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE
);
INSERT INTO users (id, name, email) VALUES
(1, 'Ada', 'ada@example.com'),
(2, 'Linus', NULL);
ثلاثة تفاصيل مهمة:
- اكتب كل اسم عمود. فعندما يكتسب المخطط عمودًا لاحقًا، لن ينزاح شيء إلى حقل خاطئ.
VALUESمتعددة الصفوف تعمل في SQLite وPostgreSQL وMySQL، وتوفر عددًا كبيرًا من دورات الشبكة مقارنة بحلقة من إدخالات صف واحد.NULLكلمة محجوزة؛ لا تضعها بين علامتي اقتباس. فكتابة'NULL'تخزن نصًا من أربعة أحرف.
إن لم تكن مفاتيح JSON مطابقة لأسماء الأعمدة (userId مقابل user_id)، فأضف طبقة تعيين. ولا تخمّن بـ COALESCE داخل SQL. فإصلاح أسماء الحقول في طبقة البيانات أرخص بكثير من ترقيعها في الاستعلامات.
3. الكائنات المتداخلة: جدول أب وجدول ابن
الطلبات هي الحالة الكلاسيكية لواحد-إلى-متعدد:
INSERT INTO orders (id, customer_name, total_cents) VALUES
(1001, 'Ada', 12900);
INSERT INTO order_items (order_id, sku, quantity) VALUES
(1001, 'A-1', 2),
(1001, 'B-7', 1);
يقابل هذا JSON {"id":1001,"customer":"Ada","total":129.00,"items":[{"sku":"A-1","quantity":2},{"sku":"B-7","quantity":1}]}.
النقاط الأساسية:
- طول مصفوفة
itemsيحدد عدد صفوف الابن؛ ويحصل جدول الأب على صف واحد بالضبط. - خزّن المال كسنتات صحيحة (
total_cents)، ولا تخزنه أبدًا كـFLOAT. - أعطِ جدول الابن مفتاحًا أجنبيًا
order_idوأرسل الإدخالين في معاملة واحدة، وإلا فقد يوجد الأب بينما بنوده غائبة. - إن كانت البيانات للقراءة فقط ولا أحد يصفّي حسب بند الطلب، فعمود JSON مقبول (
jsonbفي PostgreSQL، أوJSONفي MySQL، أوTEXTفي SQLite مع دوال json1)، لكن اقبل التكلفة: لا فهرسة واستعلامات غير مريحة.
سطّح الكائنات المتداخلة الثابتة مثل user.address إلى أعمدة (address_city، address_zip). ولا تقسّم إلى جدول إلا عندما يستمر التداخل في النمو؛ فتقسيم شكل ثابت يضيف JOINs فقط.
4. NULL مقابل الحقول الغائبة: لا يوجد "مفتاح غائب" في SQL
يسمح JSON بأن يكون المفتاح غائبًا تمامًا. أما أعمدة SQL فثابتة. وبعد الاستيراد تبدو الحالتان متطابقتين — NULL — رغم أنهما قد تعنيان أمرين مختلفين:
| حالة JSON | ما تعرضه قاعدة البيانات | المعالجة المقترحة |
|---|---|---|
| المفتاح غائب | NULL |
أضف قيمة افتراضية في تعريف الجدول، أو عمود علامة _present |
المفتاح موجود بقيمة null |
NULL |
يعني أن المستخدم مسح القيمة صراحةً |
| المفتاح موجود بنص فارغ | '' |
قابل للتمييز عن NULL، لكن يسهل الخلط في الاستعلامات |
| حقل رقمي غائب | NULL |
لا تدع 0 يتظاهر بأنه "غائب" أبدًا |
خطأان شائعان:
- استخدام نص فارغ بدل NULL. لاحقًا يفوت
WHERE email IS NULLتلك الصفوف، فيُنقص مقياس "المستخدمون بلا بريد" بصمت. - الاختبار بـ
col = NULL. في منطق SQL ثلاثي القيم، تُقيّمNULL = NULLإلىNULLلا إلىTRUE. ويجب أن تكتبIS NULL:
SELECT COUNT(*) FROM users WHERE email IS NULL;
إن احتجت إلى حفظ ما إذا كان المفتاح موجودًا أصلًا، فأضف عمود علامة منطقية بدلًا من استنتاجه من NULL. ويجب أن ينص عقد API على ذلك أيضًا؛ راجع تصميم JSON API وتسمية الحقول.
5. الاقتباس والهروب: لا تلصق نصوص SQL أبدًا
قد تحتوي نصوص JSON علامات اقتباس مفردة ومزدوجة وشرطات مائلة عكسية وأسطرًا جديدة وحتى \u0000. وبالنصوص المبنية يدويًا، يُزيح هروب واحد مفقود بقية العبارة، وفي أسوأ الحالات يصبح حقن SQL.
في SQL القياسي يُهرَّب اقتباس المفرد بمضاعفته:
INSERT INTO notes (id, content) VALUES (1, 'It''s a ''quoted'' value');
وشيفرة الإنتاج يجب أن تستخدم ربط المعاملات وتدع السائق يتعامل مع الهروب والأنواع:
// PostgreSQL uses $1/$2, MySQL uses ?; this example uses node-postgres
await client.query('INSERT INTO users (id, name) VALUES ($1, $2)', [row.id, row.name]);
لا تهرب يدويًا إلا عندما يجب أن تصدّر ملف .sql، وحينها كحد أدنى:
- استبدل
'بـ''؛ - إن كانت الأسطر الجديدة محفوظة، فتأكد أن قاعدة البيانات الهدف تقبل حرفيات نصية متعددة الأسطر؛
- انزع
\u0000، لأن معظم قواعد البيانات ترفض NUL داخل النصوص؛ - تعامل MySQL الشرطة المائلة العكسية كمحرف هروب افتراضيًا، فيجب أن يصبح
\هو\\، أو انتقل إلى الوضع القياسي.
علامات الاقتباس المزدوجة القادمة من JSON لا تحتاج هروبًا داخل نص SQL محاط باقتباس مفرد. وإضافة طبقة هروب زائدة هي الطريقة التي تنتهي بها شرطات مائلة عكسية شائعة في بياناتك.
6. تحويل الأنواع: أين تهبط قيم JSON الست في SQL
لا يمتلك JSON إلا النص والعدد والمنطقي وnull والكائن والمصفوفة. ويمتلك SQL نظام أنواع أدق بكثير. والأخطاء تحدث عادةً في الدقة والدلالات لا في الصياغة:
| قيمة JSON | نوع SQL المقترح | أين تنكسر |
|---|---|---|
| عدد صحيح | INTEGER / BIGINT |
فوق 2^53 يكون جانب JavaScript قد فقد الدقة بالفعل، فالقيمة المخزنة خاطئة أيضًا |
| عشري، مال | NUMERIC / DECIMAL |
FLOAT فاصلة عائمة ثنائية؛ و0.1 + 0.2 ليس دقيقًا |
| نص | TEXT / VARCHAR(n) |
حدود الطول تقطع؛ والمقارنة تتغير بالترتيب اللغوي |
| منطقي | BOOLEAN / TINYINT(1) |
لا يمتلك SQLite نوعًا منطقيًا ويخزن 0/1 |
| null | NULL |
لا يمكن تمييزه عن حقل غائب |
| نص تاريخ | DATE / TIMESTAMP / TIMESTAMPTZ |
منطقة زمنية مفقودة، وميلي ثانية مقطوعة، وصيغ غير صالحة |
| نص تعداد | TEXT مع CHECK، أو جدول تعداد |
اختلافات حالة الأحرف والقيم القذرة بلا قيد |
| كائن / مصفوفة | جدول ابن، أو JSON / JSONB |
مخزونة كنص لا يمكن فهرستها أو الاستعلام بها |
تخفي القيم المنطقية أدق خلل: false في JSON تصبح 0 في TINYINT(1) في MySQL، وتبدو الرحلات ذهابًا وعودة سليمة، ولا أحد يلاحظ. لكن اللحظة التي تفعل فيها شيفرة ما if (value) على النص "false" تُقيَّم صحيحة. ويجب أن تستدعي سكربتات التحويل Boolean(value) صراحةً.
يُحلِّل JSONB في PostgreSQL المفاتيح ويزيل تكرارها، محتفظًا بالتكرار الأخير، وهو ما يناسب الاستعلام؛ ويحافظ JSON على النص الأصلي وترتيب المفاتيح. أما JSON في SQLite فهو TEXT مضافًا إليه مجموعة دوال، بلا قيد نوع حقيقي. وقبل الاختيار، اسأل هل يحتاج هذا الجدول تصفية على حقول داخل JSON.
7. الإدخال بالجملة والمعاملات: دُفعات، وتثبيت، وإعادة تشغيل آمنة
مع نمو الحجم، تحدد ثلاثة أمور ما إذا كان الاستيراد يبقى مضبوطًا:
- دُفعات. حلقة من N عبارة
INSERTأحادية الصف تكلف N دورة شبكة. ادمج من 500 إلى 1000 صف فيVALUESمتعددة الصفوف واحدة، أو استخدم محمّل الجملة الخاص بقاعدة البيانات (COPYفي PostgreSQL، وLOAD DATAفي MySQL، والإدخالات متعددة الصفوف داخل معاملة SQLite). - استخدم معاملة. الكل أو لا شيء، فلا يبقى جدول نصف محمَّل بعد خطأ:
BEGIN;
INSERT INTO users (id, name) VALUES (1, 'Ada'), (2, 'Linus');
INSERT INTO order_items (order_id, sku, quantity) VALUES (1, 'A-1', 2);
COMMIT;
- اجعله عائدًا للنتيجة نفسها (idempotent). سكربتات الاستيراد تُعاد باستمرار. استخدم upsert بدلًا من الحذف ثم الإدخال:
| قاعدة البيانات | الشكل العائد للنتيجة نفسها |
|---|---|
| PostgreSQL / SQLite | INSERT ... ON CONFLICT (id) DO UPDATE SET ... |
| MySQL | INSERT ... ON DUPLICATE KEY UPDATE ... |
يتطلب ON CONFLICT قيدًا فريدًا أو مفتاحًا أساسيًا، وإلا فلن يعمل أبدًا. وعندما لا يوجد مفتاح طبيعي، ابنِ فهرسًا فريدًا على مفتاح عملي مثل email. وعندما لا يوجد مفتاح على الإطلاق، أضف id مستقرًا في جانب JSON — ولا تعتمد أبدًا على id تلقائي الزيادة للعائدية، لأن إعادة التشغيل ستُدخل تكرارات.
8. فروق اللهجات في لمحة
| القدرة | SQLite | PostgreSQL | MySQL |
|---|---|---|---|
VALUES متعددة الصفوف |
نعم | نعم | نعم |
| Upsert | ON CONFLICT |
ON CONFLICT |
ON DUPLICATE KEY UPDATE |
| مفتاح تلقائي الزيادة | INTEGER PRIMARY KEY |
GENERATED ... AS IDENTITY |
AUTO_INCREMENT |
| نوع منطقي | لا يوجد، يخزن 0/1 | boolean |
TINYINT(1) |
| استيراد بالجملة | إدخالات متعددة الصفوف في معاملة | COPY |
LOAD DATA |
| دعم JSON | TEXT مع دوال json1 |
json / jsonb |
JSON |
| اقتباس المعرّفات | علامات اقتباس مزدوجة | علامات اقتباس مزدوجة | علامات اقتباس عكسية (افتراضيًا) |
لإبقاء ملف SQL واحد يعمل عبر الثلاثة، تجاوز على الأقل ON CONFLICT والعلامات العكسية وSERIAL. وفي المشاريع متعددة القواعد، ولّد المخطط وعبارات upsert على حدة؛ ولا تتوقع أن يناسب سكربت واحد الجميع.
9. ثلاثة فحوص عشوائية بعد الاستيراد
قبل حذف الملف المصدر، تحقق من ثلاثة أمور:
- عدد الصفوف. يجب أن يساوي
SELECT COUNT(*) FROM users;طول مصفوفة JSON. وصف ناقص واحد يعني عادةً أن السجل الأخير قطع. - العمود الأعلى في نسبة NULL. يشير ذلك إلى أسماء مفاتيح غير متسقة أو تداخل غير موسَّع. ارجع وتحقق هل يستخدم المصدر
userIdوuser_idمعًا. - المال والمعرّفات الطويلة. خذ القيمتين القصوى والدنيا وتأكد أن تحويل الفاصلة العائمة أو العدد الصحيح لم يقطعهما.
إن احتجت إلى فهم الشكل الجدولي أولًا، فاستخدم نهج التسطيح في تحويل JSON وCSV والبيانات الجدولية. وإن كان الجمهور زملاء يريدون قراءة البيانات فقط، فإن الطرق الأربع للتحويل من JSON إلى Excel أقل عناءً من إقامة قاعدة بيانات.
لماذا تفقد الأعداد دقتها بعد استيراد JSON إلى SQL؟
الأعداد الصحيحة فوق 2^53 فقدت الدقة بالفعل أثناء JSON.parse في JavaScript، وكتابتها في BIGINT لا تستعيدها. ويجب أن تكون هذه المعرّفات نصوصًا في JSON وأن تُحوَّل إلى BIGINT أو TEXT أثناء الاستيراد.
ماذا أستخدم مفتاحًا أساسيًا عندما لا يملك JSON أي id؟
دع قاعدة البيانات تولّد واحدًا، أو أنشئ UUID مستقرًا قبل الاستيراد. ولا تستخدم فهرس المصفوفة مفتاحًا أبدًا: فإعادة التشغيل تعيد ترتيب البيانات وتوجّه صفوفًا خاطئة إلى سجلات خاطئة. ويُفعّل فهرس فريد على مفتاح عملي (البريد، رقم الطلب) عمليات upsert أيضًا.
ماذا أفعل عندما تصطدم عملية INSERT بالجملة بتعارض مفتاح فريد؟
استبدل INSERT العادية بـ upsert المطابق للهجة قاعدة بياناتك، وتأكد أن العمود المتعارض عليه قيد فريد. والحذف أولًا ثم الإدخال يعمل أيضًا، لكنه يجب أن يحدث داخل معاملة واحدة، وأن تقبل نافذة قصيرة بلا صفوف.
هل تحتاج المصفوفات المتداخلة دائمًا جدول ابن؟
لا. للعرض للقراءة فقط دون استعلام بالعنصر الفرعي، عمود JSON أبسط. وبمجرد أن تحتاج إلى تصفية أو تجميع بالوسم أو SKU، فالجدول الابن مع فهارس هو الاختيار المستدام. والعامل الحاسم هو كيفية استعلامك لا شكل البيانات.