مستودعات مفيدة مع Eloquent؟

في الأسبوع الماضي ، كتبت مقالًا عن عدم جدوى قالب السجل للكيانات البليغة ، لكنني وعدت أن أخبرني كيف أستخدمه جزئيًا مع فائدة. للقيام بذلك ، سأحاول تحليل كيفية استخدام هذا القالب عادة في المشاريع. الحد الأدنى المطلوب من مجموعة طرق المستودع:


<?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); } 

ومع ذلك ، في المشروعات الحقيقية ، إذا تقرر استخدام المستودعات ، فغالبًا ما يضيفون طرقًا لاختيار السجلات:


 <?php interface PostRepository { public function getById($id): Post; public function save(Post $post); public function delete($id); public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } 

يمكن تنفيذ هذه الأساليب من خلال نطاقات Eloquent ، لكن الحمل الزائد على فئات الكيانات والمسؤوليات عن جلب أنفسهم ليس فكرة جيدة ويعد وضع هذه المسؤولية في فئات المستودع منطقيًا. هل هذا صحيح؟ أنا على وجه التحديد قسمت هذه الواجهة إلى قسمين. سيتم استخدام الجزء الأول من الأساليب في عمليات الكتابة.


عمليات الكتابة القياسية هي:


  • بناء كائن جديد واستدعاء PostRepository :: save
  • PostRepository :: getById ، معالجة الكيان واستدعاء PostRepository :: save
  • استدعاء PostRepository :: delete

لا توجد طرق لأخذ العينات في عمليات الكتابة. في عمليات القراءة ، يتم استخدام أساليب get * فقط. إذا قرأت عن مبدأ الفصل بين الواجهة (الحرف I في SOLID ) ، فسيصبح من الواضح أن واجهتنا كبيرة جدًا وتفي بمسؤوليتين مختلفتين على الأقل. حان الوقت لتقسيمها إلى قسمين. طريقة getById مطلوبة في كليهما ، ولكن إذا أصبح التطبيق أكثر تعقيدًا ، فسيكون تنفيذه مختلفًا. هذا سنرى لاحقا. كتبت عن عدم جدوى جزء الكتابة في مقال سابق ، لذلك في هذا فقط أنسى الأمر.


قراءة الجزء يبدو لي لا طائل منه ، لأنه حتى بالنسبة لل Eloquent يمكن أن يكون هناك العديد من التطبيقات. كيفية تسمية الفصل؟ يمكنك قراءة ReadPostRepository ، لكن لديها بالفعل علاقة قليلة بقالب السجل . يمكنك ببساطة PostQueries :


 <?php interface PostQueries { public function getById($id): Post; public function getLastPosts(); public function getTopPosts(); public function getUserPosts($userId); } 

تنفيذه باستخدام Eloquent بسيط للغاية:


 <?php final class EloquentPostQueries implements PostQueries { public function getById($id): Post { return Post::findOrFail($id); } /** * @return Post[] | Collection */ public function getLastPosts() { return Post::orderBy('created_at', 'desc') ->limit(/*some limit*/) ->get(); } /** * @return Post[] | Collection */ public function getTopPosts() { return Post::orderBy('rating', 'desc') ->limit(/*some limit*/) ->get(); } /** * @param int $userId * @return Post[] | Collection */ public function getUserPosts($userId) { return Post::whereUserId($userId) ->orderBy('created_at', 'desc') ->get(); } } 

يجب أن ترتبط الواجهة بالتطبيق ، على سبيل المثال في AppServiceProvider :


 <?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, EloquentPostQueries::class); } } 

هذه الفئة مفيدة بالفعل. وقال انه يدرك مسؤوليته عن طريق تفريغ إما وحدات التحكم أو فئة الكيان. في وحدة التحكم ، يمكن استخدامه مثل هذا:


 <?php final class PostsController extends Controller { public function lastPosts(PostQueries $postQueries) { return view('posts.last', [ 'posts' => $postQueries->getLastPosts(), ]); } } 

أسلوب PostsController :: lastPosts يسأل نفسه ببساطة عن بعض من تنفيذ PostsQueries ويعمل معها. في الموفر ، قمنا بربط PostQueries بفئة EloquentPostQueries وسيتم استبدال هذه الفئة في وحدة التحكم.


دعونا نتخيل أن تطبيقنا أصبح شائعًا جدًا. الآلاف من المستخدمين في الدقيقة الواحدة فتح الصفحة مع أحدث المنشورات. تتم قراءة المنشورات الأكثر شعبية في كثير من الأحيان. قواعد البيانات لا تتعامل مع مثل هذه الأحمال بشكل جيد للغاية ، وبالتالي فإنها تستخدم الحل القياسي - ذاكرة التخزين المؤقت. بالإضافة إلى قاعدة البيانات ، يتم تخزين كتلة معينة من البيانات في وحدة تخزين محسّنة لبعض العمليات - memcached أو redis .


عادةً ما يكون منطق التخزين المؤقت معقدًا ، لكن تنفيذه في EloquentPostQueries ليس صحيحًا جدًا (على الأقل بسبب مبدأ المسؤولية الفردية ). من الطبيعي استخدام قالب Decorator وتطبيق التخزين المؤقت كتزيين للعمل الرئيسي:


 <?php use Illuminate\Contracts\Cache\Repository; final class CachedPostQueries implements PostQueries { const LASTS_DURATION = 10; /** @var PostQueries */ private $base; /** @var Repository */ private $cache; public function __construct( PostQueries $base, Repository $cache) { $this->base = $base; $this->cache = $cache; } /** * @return Post[] | Collection */ public function getLastPosts() { return $this->cache->remember('last_posts', self::LASTS_DURATION, function(){ return $this->base->getLastPosts(); }); } //      } 

تجاهل واجهة مستودع التخزين في المنشئ. لسبب ما ، قرروا استدعاء الواجهة للتخزين المؤقت في Laravel.


تطبق فئة CachedPostQueries التخزين المؤقت فقط. $ this-> cache-> تذكر عمليات التحقق لمعرفة ما إذا كان الإدخال المحدد في ذاكرة التخزين المؤقت وإذا لم يكن كذلك ، فإنه يستدعي رد الاتصال ويكتب القيمة التي يتم إرجاعها إلى ذاكرة التخزين المؤقت. يبقى فقط لتنفيذ هذه الفئة في التطبيق. نحتاج إلى جميع الفئات التي في التطبيق تطلب تنفيذ واجهة PostQueries لتلقي مثيل لفئة CachedPostQueries . ومع ذلك ، يجب أن تتلقى CachedPostQueries نفسها فئة EloquentPostQueries كمعلمة في المُنشئ ، لأنه لا يمكن أن يعمل دون تطبيق "حقيقي". تغيير AppServiceProvider :


 <?php final class AppServiceProvider extends ServiceProvider { public function register() { $this->app->bind(PostQueries::class, CachedPostQueries::class); $this->app->when(CachedPostQueries::class) ->needs(PostQueries::class) ->give(EloquentPostQueries::class); } } 

كل رغباتي موصوفة بشكل طبيعي في المزود. وبالتالي ، قمنا بتطبيق التخزين المؤقت لطلباتنا فقط عن طريق كتابة فئة واحدة وتغيير تكوين الحاوية. لم يتم تغيير رمز بقية التطبيق.


بطبيعة الحال ، من أجل التنفيذ الكامل للتخزين المؤقت ، لا يزال من الضروري تطبيق إبطال المفعول حتى لا يتم تعليق المقالة المحذوفة على الموقع لبعض الوقت ، ولكنها تغادر على الفور (كتب مؤخرًا مقالًا حول التخزين المؤقت ، يمكن أن يساعد في التفاصيل).


خلاصة القول: استخدمنا ليست واحدة ، ولكن اثنين من أنماط كاملة. يوفر قالب فصل مسؤولية استعلامات الأوامر (CQRS) فصلًا تامًا عن عمليات القراءة والكتابة على مستوى الواجهة. جئت إليه من خلال مبدأ الفصل بين الواجهات ، مما يعني أنني أتقن القوالب والمبادئ بمهارة وأستنتج أحدها من الآخر كنظرية: بالطبع ، لا يحتاج كل مشروع إلى مثل هذا التجريد على عينات الكيانات ، لكنني سأشارك التركيز معك. في المرحلة الأولى من تطوير التطبيق ، يمكنك ببساطة إنشاء فئة PostQueries مع تنفيذ منتظم من خلال Eloquent:


 <?php final class PostQueries { public function getById($id): Post { return Post::findOrFail($id); } //   } 

عند الحاجة إلى التخزين المؤقت ، يمكنك بسهولة إنشاء واجهة (أو فئة مجردة) بدلاً من فئة PostQueries هذه ، ونسخ تنفيذها إلى فئة EloquentPostQueries والانتقال إلى المخطط الذي وصفته سابقًا. لا يلزم تغيير باقي رمز التطبيق.


ومع ذلك ، لا تزال هناك مشكلة في استخدام نفس كيانات النشر التي يمكنها تعديل البيانات. هذا ليس بالضبط CQRS.



لا أحد يكترث بالحصول على كيان النشر من PostQueries ، وقم بتعديله وحفظ التغييرات باستخدام بسيط -> حفظ () . وسوف تعمل.
بعد فترة ، سيقوم الفريق بالتبديل إلى النسخ المتماثل للرقيق في قاعدة البيانات وسيعمل PostQueries مع النسخ المتماثلة للقراءة. عادة ما يتم حظر عمليات الكتابة على النسخ المتماثلة للقراءة. سيتم فتح الخطأ ، ولكن سيتعين عليك العمل بجد لإصلاح كل هذه الدعامات.


الحل الواضح هو فصل أجزاء القراءة والكتابة بالكامل. يمكنك الاستمرار في استخدام Eloquent ، ولكن عن طريق إنشاء فصل دراسي للطرز القابلة للقراءة فقط. مثال: https://github.com/adelf/freelance-example/blob/master/app/ReadModels/ReadModel.php تم حظر جميع عمليات تعديل البيانات. قم بإنشاء نموذج جديد ، على سبيل المثال ReadPost (يمكنك ترك نشر ، ولكن نقله إلى مساحة اسم أخرى):


 <?php final class ReadPost extends ReadModel { protected $table = 'posts'; } interface PostQueries { public function getById($id): ReadPost; } 

يمكن أن تكون هذه النماذج للقراءة فقط ويمكن تخزينها مؤقتًا بأمان.



خيار آخر: التخلي عن الفصيح. قد يكون هناك عدة أسباب لهذا:


  • جميع حقول الجدول لا تكاد تكون مطلوبة. بالنسبة لطلب lastPosts ، عادةً ما تكون هناك حاجة فقط إلى المعرف والعنوان والحقول المنشورة . سيؤدي طلب العديد من النصوص الثقيلة للمنشورات فقط إلى تحميل غير ضروري على قاعدة البيانات أو ذاكرة التخزين المؤقت. يمكن للبلاغة تحديد الحقول المطلوبة فقط ، ولكن كل هذا ضمني للغاية. لا يعرف عملاء PostQueries الحقول التي يتم تحديدها دون الدخول إلى التطبيق.
  • يستخدم التخزين المؤقت التسلسل بشكل افتراضي. الطبقات البليغة تشغل مساحة كبيرة في شكل متسلسل. إذا لم يكن هذا ملحوظًا بالنسبة للكيانات البسيطة ، فبالنسبة للكيانات المعقدة ذات العلاقات ، تصبح هذه مشكلة (كيف يمكنك حتى سحب الكائنات ذات حجم الميجابايت من ذاكرة التخزين المؤقت؟). في مشروع واحد ، استغرق الفصل العادي مع الحقول العامة في ذاكرة التخزين المؤقت مساحة أقل 10 مرات من خيار Eloquent (كان هناك الكثير من العناصر الفرعية الصغيرة). لا يمكنك تخزين السمات مؤقتًا إلا عند التخزين المؤقت ، ولكن هذا سيعقد العملية بدرجة كبيرة.

مثال بسيط على كيفية ظهور هذا:


 <?php final class PostHeader { public int $id; public string $title; public DateTime $publishedAt; } final class Post { public int $id; public string $title; public string $text; public DateTime $publishedAt; } interface PostQueries { public function getById($id): Post; /** * @return PostHeader[] */ public function getLastPosts(); /** * @return PostHeader[] */ public function getTopPosts(); /** * @var int $userId * @return PostHeader[] */ public function getUserPosts($userId); } 

بالطبع ، كل هذا يبدو وكأنه تعقيد مفرط للمنطق. "خذ النطاقات الفصيحة وكل شيء سيكون على ما يرام. لماذا الخروج بكل هذا؟" لمشاريع أبسط ، وهذا هو الصحيح. ليست هناك حاجة على الإطلاق لإعادة اختراع النطاقات. لكن عندما يكون المشروع كبيرًا ويشارك العديد من المطورين في عملية التطوير ، والتي تتغير غالبًا (يتم إنهاءها ويأتي مستخدمون جدد) ، فإن قواعد اللعبة تصبح مختلفة قليلاً. يجب كتابة التعليمات البرمجية بحيث لا يتمكن المطور الجديد بعد بضع سنوات من القيام بشيء خاطئ. بالطبع ، من المستحيل استبعاد هذا الاحتمال تمامًا ، لكن من الضروري تقليل احتمالية حدوثه. بالإضافة إلى ذلك ، هذا هو التحلل نظام مشترك. يمكنك جمع كل أدوات وفئات التخزين المؤقت للتخزين المؤقت لإبطال ذاكرة التخزين المؤقت في "وحدة تخزين مؤقت" معينة ، وبالتالي توفير بقية تطبيق المعلومات حول التخزين المؤقت. اضطررت للبحث عن الطلبات الكبيرة التي كانت تحيط بها مكالمات ذاكرة التخزين المؤقت. يحصل في الطريق. خاصة إذا كان منطق التخزين المؤقت ليس بهذه البساطة الموضح أعلاه.

Source: https://habr.com/ru/post/ar445452/


All Articles