في البداية ، سأوضح بعض المشاكل والميزات عند العمل مع قاعدة البيانات ، وسأظهر ثقوبًا في التجريد. بعد ذلك ، سنقوم بتحليل تجريد أبسط بناءً على المناعة.
من المفترض أن يكون القارئ على دراية بأنماط السجل النشط وخريطة البيانات وخريطة الهوية ووحدة العمل .
يتم النظر في المشكلات والحلول في سياق مشاريع كبيرة بما يكفي لا يمكن التخلص منها وإعادة كتابتها بسرعة.
خريطة الهوية
المشكلة الأولى هي مشكلة الحفاظ على الهوية. الهوية هي شيء يحدد الكيان بشكل فريد. في قاعدة البيانات ، هذا هو المفتاح الأساسي ، وفي الذاكرة ، الرابط (المؤشر). من الجيد أن تشير الروابط إلى كائن واحد فقط.
بالنسبة لمكتبات روبي ActiveRecord ، هذا ليس كذلك:
post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id
أي نحصل على مراجع 2 إلى كائنات مختلفة في الذاكرة.
وبالتالي ، يمكننا أن نفقد التغييرات إذا بدأنا عن غير قصد العمل مع نفس الكيان ، لكننا نمثلنا بأشياء مختلفة.
يوجد في الإسبات جلسة ، في الواقع ذاكرة تخزين مؤقت من المستوى الأول تخزن تعيين معرف الكيان لكائن في الذاكرة. إذا قمنا بإعادة طلب نفس الكيان ، فسنحصل على رابط لكائن موجود. أي يطبق السبات نمط خريطة الهوية .
المعاملات الطويلة
ولكن ماذا لو لم نختار بواسطة المعرف؟ لمنع حالة الكائنات وحالة قاعدة البيانات من المزامنة ، قم بإسبات السبات قبل طلب تحديد.
أي تفريغ الكائنات القذرة في قاعدة البيانات بحيث يقرأ الطلب البيانات المتفق عليها.
يفرض هذا النهج عليك الاحتفاظ المعاملة قاعدة البيانات مفتوحة أثناء المعاملة التجارية قيد التقدم.
إذا كانت المعاملة التجارية طويلة ، فإن العملية المسؤولة عن الاتصال في قاعدة البيانات نفسها تكون خامدة أيضًا. على سبيل المثال ، يمكن أن يحدث هذا إذا طلبت معاملة تجارية بيانات عبر الشبكة أو نفذت حسابات معقدة.
N + 1
ولعل أكبر "ثقب" في تجريد ORM هو مشكلة استعلام N + 1.
مثال على روبي لمكتبة ActiveRecord:
posts = Post.all
يقود ORM المبرمج إلى فكرة أنه يعمل ببساطة مع الكائنات الموجودة في الذاكرة. لكنه يعمل مع خدمة متوفرة عبر الشبكة ، وعلى إنشاء اتصالات ونقل البيانات
يستغرق وقتا. حتى إذا تم تنفيذ الطلب 50 مللي ثانية ، فسيتم تنفيذ 20 طلبًا لثانية واحدة.
بيانات إضافية
قل لتجنب مشكلة N + 1 الموضحة أعلاه ، تكتب مثل هذا
طلب :
SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true;
أي بالإضافة إلى سمات المنشور ، يتم أيضًا تحديد كل سمات الشكل الأخير. ما الكيان الذي تعينه هذه البيانات؟ في هذه الحالة ، يمكنك إرجاع زوجين من المشاركة وما شابه ذلك ، بسبب النتيجة تحتوي على جميع السمات الضرورية.
ولكن ماذا لو اخترنا فقط جزءًا من الحقول ، أو الحقول المحددة غير الموجودة في النموذج ، على سبيل المثال ، عدد المنشورات التي يحبها؟ هل يحتاجون إلى تعيينهم على الكيانات؟ ربما اتركهم مجرد بيانات؟
الدولة والهوية
النظر في رمز js:
const alice = { id: 0, name: 'Alice' };
هنا ، أعطيت مرجع الكائن اسم alice
. بسبب إنه ثابت ، ثم لا توجد وسيلة لاستدعاء أليس كائن آخر. في الوقت نفسه ، بقي الكائن نفسه قابلاً للتغيير.
على سبيل المثال ، يمكننا تعيين معرف موجود:
const bob = { id: 1, name: 'Bob' }; alice.id = bob.id;
واسمحوا لي أن أذكرك أن الكيان له هوياتان: رابط ومفتاح أساسي في قاعدة البيانات. والثوابت لا يمكن أن تتوقف عن صنع أليس بوب ، حتى بعد الادخار.
الكائن ، الرابط الذي أطلقنا عليه alice
، يؤدي واجبين: في وقت واحد يصور الهوية والدولة. الحالة هي قيمة تصف كيانًا في وقت معين.
ولكن ماذا لو فصلنا بين هاتين المسؤوليتين واستخدمنا هياكل ثابتة للدولة؟
function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM!
Ref
- حاوية لحالة لا تتغير ، مما يسمح باستبدالها الخاضع للرقابة. Ref
نماذج الهوية مثلما نسمي الأشياء. نحن ندعو نهر الفولغا ، ولكن في كل لحظة من الزمن لديه حالة مختلفة لا تتغير.
التخزين
النظر في API التالية:
storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); });
t.get
و t.create
إرجاع مثيل من Ref
.
نفتح المعاملة التجارية t
، ونجد Alice بمعرفها ، وننشئ Bob ونشير إلى أن Alice تعتبر Bob صديقها.
كائن t
يتحكم في إنشاء ref
.
يمكن أن يخزن داخل نفسه تعيين معرفات الكيانات لحالة ref
التي تحتوي عليها. أي يمكن تنفيذ خريطة الهوية. في هذه الحالة ، يعمل t
كذاكرة تخزين مؤقت ؛ بناءً على طلب Alice المتكرر ، لن يكون هناك طلب لقاعدة البيانات.
يمكن أن يتذكر الحالة الأولية للكيانات لتتبع التغييرات التي يجب كتابتها في قاعدة البيانات في نهاية المعاملة. أي يمكن تنفيذ وحدة العمل . أو إذا تمت إضافة دعم المراقب إلى Ref
، يصبح من الممكن إعادة تعيين التغييرات على قاعدة البيانات مع كل تغيير في ref
. هذه مقاربات متفائلة ومتشائمة لإصلاح التغييرات.
مع اتباع نهج متفائل ، تحتاج إلى تتبع إصدارات حالة الكيانات.
عند التغيير من قاعدة البيانات ، يجب أن نتذكر الإصدار ، وعند تنفيذ التغييرات ، تأكد من أن إصدار الكيان في قاعدة البيانات لا يختلف عن الإصدار الأولي. خلاف ذلك ، تحتاج إلى تكرار المعاملة التجارية. يسمح هذا النهج باستخدام عمليات إدراج وحذف المجموعة ومعاملات قاعدة البيانات القصيرة جدًا ، مما يوفر الموارد.
مع اتباع نهج متشائم ، تكون معاملة قاعدة البيانات متوافقة تمامًا مع معاملة تجارية. أي نحن مضطرون إلى سحب الاتصال من التجمع في كل وقت اكتمال الصفقة التجارية.
تسمح لك واجهة برمجة التطبيقات باستخراج الكيانات واحدًا في كل مرة ، وهو أمر غير مثالي للغاية. بسبب قمنا بتطبيق نمط خريطة الهوية ، ثم يمكننا إدخال طريقة preload
في واجهة برمجة التطبيقات:
storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache });
استفسارات
إذا لم نكن نريد معاملات طويلة ، فلن نتمكن من القيام بتحديدات بواسطة مفتاح تعسفي ، لأن قد تحتوي الذاكرة على كائنات قذرة وسيعود التحديد بنتيجة غير متوقعة.
يمكننا استخدام الاستعلام واسترداد أي بيانات (الحالة) خارج المعاملة وإعادة قراءة البيانات أثناء المعاملة.
const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); });
وبالتالي هناك تقسيم للمسؤولية. للاستعلامات ، يمكننا استخدام محركات البحث لتوسيع نطاق القراءة باستخدام النسخ المتماثلة. و API التخزين يعمل دائما مع التخزين الرئيسية (ماجستير). وبطبيعة الحال ، سوف تحتوي النسخ المتماثلة على بيانات قديمة ، وإعادة قراءة البيانات في المعاملة يحل هذه المشكلة.
الأوامر
هناك حالات يمكن فيها إجراء العملية دون قراءة البيانات. على سبيل المثال ، خصم رسم شهري من حسابات جميع العملاء. أو قم بإدخال وتحديث البيانات (المغازلة) في حالة التعارض.
في حالة حدوث مشاكل في الأداء ، يمكن استبدال الحزمة من التخزين والاستعلام بمثل هذا الأمر.
الاتصالات
إذا كانت الكيانات تشير بشكل عشوائي إلى بعضها البعض ، فمن الصعب ضمان الاتساق عند تغييرها. تحاول العلاقات تبسيط ، وتبسيط ، والتخلي عن لا لزوم لها.
المجاميع هي طريقة لتنظيم العلاقات. يحتوي كل تجميع على كيان جذري وكيانات متداخلة. يمكن لأي كيان خارجي الرجوع فقط إلى جذر التجميع. الجذر يضمن سلامة الوحدة بأكملها. لا يمكن أن تتجاوز المعاملة حدًا إجماليًا ؛ وبعبارة أخرى ، فإن المجموع الكلي متورط في المعاملة.
قد يتكون المجموع ، على سبيل المثال ، من Lent (الجذر) وترجماته. أو الترتيب ومواقفه.
يعمل API لدينا مع المجاميع كلها. في الوقت نفسه ، فإن التكامل المرجعي بين المجاميع يكمن في التطبيق. لا تدعم واجهة برمجة التطبيقات التحميل البطيء للروابط.
ولكن يمكننا اختيار اتجاه العلاقات. النظر في العلاقة واحد لكثير المستخدم - نشر. يمكننا تخزين معرف المستخدم في المنشور ، ولكن هل سيكون مناسبًا؟ سنحصل على مزيد من المعلومات إذا قمنا بتخزين مجموعة من معرفات النشر في المستخدم.
الخاتمة
أكدت المشاكل عند العمل مع قاعدة البيانات ، وأظهرت خيار استخدام الحصانة.
لا يسمح تنسيق المقال بالكشف عن الموضوع بالتفصيل.
إذا كنت مهتمًا بهذا النهج ، فاحرص على الانتباه إلى تطبيق كتابي من البداية ، والذي يصف إنشاء تطبيق ويب من البداية مع التركيز على الهندسة المعمارية. إنه يفهم SOLID ، الهندسة المعمارية النظيفة ، وأنماط العمل مع قاعدة البيانات. تتم كتابة نماذج التعليمات البرمجية الموجودة في الكتاب والتطبيق نفسه بلغة Clojure ، والتي تتشرب بأفكار الحصانة وراحة معالجة البيانات.