Ember Data: برنامج تعليمي شامل لمكتبة بيانات الأعضاء
نشرت: 2022-03-11Ember Data (المعروفة أيضًا باسم ember-data أو ember.data) هي مكتبة لإدارة بيانات النموذج بقوة في تطبيقات Ember.js. يذكر مطورو Ember Data أنه مصمم ليكون محايدًا لآلية الاستمرارية الأساسية ، لذلك فهو يعمل بشكل جيد مع واجهات برمجة تطبيقات JSON عبر HTTP كما هو الحال مع WebSockets المتدفقة أو تخزين IndexedDB المحلي. يوفر العديد من التسهيلات التي تجدها في التعيينات الارتباطية للكائنات من جانب الخادم (ORMs) مثل ActiveRecord ، ولكنها مصممة خصيصًا للبيئة الفريدة لجافا سكريبت في المتصفح.
على الرغم من أن Ember Data قد تستغرق بعض الوقت لتتذمر ، فبمجرد قيامك بذلك ، ستجد على الأرجح أنها تستحق الاستثمار. سيؤدي في النهاية إلى جعل تطوير نظامك وتحسينه وصيانته أسهل بكثير.
عندما يتم تمثيل API باستخدام نماذج ومحولات ومسلسلات Ember Data ، يصبح كل ارتباط ببساطة اسم حقل. يؤدي ذلك إلى تلخيص التفاصيل الداخلية لكل ارتباط ، وبالتالي عزل بقية التعليمات البرمجية الخاصة بك عن التغييرات التي تطرأ على الارتباطات نفسها. لن تهتم بقية التعليمات البرمجية ، على سبيل المثال ، إذا كان ارتباط معين متعدد الأشكال أو نتيجة خريطة للعديد من الارتباطات.
علاوة على ذلك ، فإن قاعدة التعليمات البرمجية الخاصة بك معزولة إلى حد كبير عن تغييرات الواجهة الخلفية ، حتى لو كانت مهمة ، نظرًا لأن كل ما تتوقعه قاعدة التعليمات البرمجية هو حقول ووظائف في النماذج ، وليس تمثيل JSON أو XML أو YAML للنموذج.
في هذا البرنامج التعليمي ، سنقدم أبرز ميزات Ember Data ونوضح كيف تساعد في تقليل اضطراب الكود ، من خلال التركيز على مثال من العالم الحقيقي.
يتوفر أيضًا ملحق يناقش عددًا من موضوعات وأمثلة بيانات Ember الأكثر تقدمًا.
ملاحظة: تفترض هذه المقالة بعض المعرفة الأساسية بـ Ember.js. إذا لم تكن معتادًا على Ember.js ، فراجع البرنامج التعليمي الشهير Ember.js للحصول على مقدمة. لدينا أيضًا دليل JavaScript كامل المكدس متوفر باللغات الروسية والبرتغالية والإسبانية.
عرض قيمة البيانات Ember
لنبدأ بالنظر في مثال بسيط.
لنفترض أن لدينا قاعدة رمز عمل لنظام مدونة أساسي. يحتوي النظام على منشورات وعلامات لها علاقة أطراف بأطراف مع بعضها البعض.
كل شيء على ما يرام حتى نحصل على شرط لدعم الصفحات. ينص الشرط أيضًا على أنه نظرًا لأنه من الممكن وضع علامة على صفحة في WordPress ، يجب أن نكون قادرين على القيام بذلك أيضًا.
حتى الآن ، لن يتم تطبيق العلامات بعد الآن على المشاركات فقط ، بل قد تنطبق أيضًا على الصفحات. نتيجةً لذلك ، لن يكون الارتباط البسيط بين العلامات والمشاركات مناسبًا بعد الآن. بدلاً من ذلك ، سنحتاج إلى علاقة متعددة الأشكال من طرف واحد إلى متعدد ، مثل ما يلي:
- كل منشور هو عبارة عن علامة مميزة ولها العديد من العلامات
- كل صفحة هي علامة مميزة ولها العديد من العلامات
- كل علامة لديها العديد من Taggables متعددة الأشكال
من المحتمل أن يكون للانتقال إلى هذه المجموعة الجديدة الأكثر تعقيدًا من الارتباطات تشعبات كبيرة في جميع أنحاء التعليمات البرمجية ، مما يؤدي إلى الكثير من الاضطراب. نظرًا لأنه ليس لدينا أي فكرة عن كيفية إجراء تسلسل لرابط متعدد الأشكال إلى JSON ، فربما سننشئ المزيد من نقاط نهاية API مثل GET /posts/:id/tags
و GET /pages/:id/tags
. وبعد ذلك ، سوف نتخلص من جميع وظائف محلل JSON الموجودة لدينا ونكتب وظائف جديدة للموارد الجديدة المضافة. قرف. مملة ومؤلمة.
الآن دعنا نفكر في كيفية تعاملنا مع هذا باستخدام Ember Data.
في Ember Data ، سيتضمن استيعاب هذه المجموعة المعدلة من الارتباطات ببساطة الانتقال من:
App.Post = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Tag = DS.Model.extend({ post: DS.belongsTo('post', {async: true}) });
ل:
App.PostType = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Post = App.PostType.extend({ }); App.Page = App.PostType.extend({ }); App.Tag = DS.Model.extend({ posts: DS.hasMany('postType', {polymorphic: true, async: true}) });
سيكون التغيير الناتج في بقية الكود لدينا ضئيلاً وسنتمكن من إعادة استخدام معظم القوالب الخاصة بنا. لاحظ على وجه الخصوص أن اسم اقتران tags
في المنشور يظل دون تغيير. بالإضافة إلى ذلك ، فإن بقية قاعدة التعليمات البرمجية الخاصة بنا تعتمد فقط على وجود اقتران tags
، وهي غافلة عن تفاصيلها.
تمهيدي بيانات Ember
قبل الغوص في مثال من العالم الحقيقي ، دعنا نراجع بعض أساسيات Ember Data.
الطرق والنماذج
في Ember.js ، يكون جهاز التوجيه مسؤولاً عن عرض القوالب وتحميل البيانات وإعداد حالة التطبيق بطريقة أخرى. يطابق جهاز التوجيه عنوان URL الحالي مع المسارات التي حددتها ، لذا فإن المسار مسؤول عن تحديد النموذج الذي سيعرضه القالب (يتوقع Ember أن يكون هذا النموذج فئة فرعية من Ember.Object
):
App.ItemsRoute = Ember.Route.extend({ model: function(){ // GET /items // Retrieves all items. return this.modelFor('orders.show').get('items'); } });
يوفر Ember Data DS.Model
وهو فئة فرعية من Ember.Object
ويضيف إمكانات مثل حفظ أو تحديث سجل واحد أو سجلات متعددة للراحة.
لإنشاء نموذج جديد ، نقوم بإنشاء فئة فرعية من DS.Model
(على سبيل المثال ، App.User = DS.Model.extend({})
).
تتوقع Ember Data بنية JSON محددة جيدًا وبديهية من الخادم وتسلسل السجلات المنشأة حديثًا إلى نفس JSON المهيكل.
يوفر Ember Data أيضًا مجموعة من فئات المصفوفات مثل DS.RecordArray
للعمل مع النماذج. هذه لديها مسؤوليات مثل التعامل مع علاقات واحد لكثير أو كثير إلى كثير ، ومعالجة الاسترداد غير المتزامن للبيانات ، وما إلى ذلك.
سمات النموذج
يتم تعريف سمات النموذج الأساسية باستخدام DS.attr
؛ على سبيل المثال:
App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') });
سيتم فقط تضمين الحقول التي تم إنشاؤها بواسطة DS.attr
في الحمولة التي تم تمريرها إلى الخادم لإنشاء السجلات أو تحديثها.
يقبل DS.attr
أربعة أنواع من البيانات: string
number
boolean
date
.
ريتستيريزر
بشكل افتراضي:
- تستخدم Ember Data
RESTSerializer
لإنشاء كائنات من استجابات API (إلغاء التسلسل) ولتوليد JSON لطلبات API (التسلسل). - يتوقع
RESTSerializer
الحقول التي تم إنشاؤها بواسطةDS.belongsTo
أن يكون لديك حقل باسمuser
مضمن في استجابة JSON من الخادم. يحتوي هذا الحقل على معرف السجل المشار إليه. - يضيف
RESTSerializer
حقلuser
إلى الحمولة التي تم تمريرها إلى API بمعرف الأمر المرتبط.
قد تبدو الإجابة ، على سبيل المثال ، كما يلي:
GET http://api.example.com/orders?ids[]=19&ids[]=28 { "orders": [ { "id": "19", "createdAt": "1401492647008", "user": "1" }, { "id": "28", "createdAt": "1401492647008", "user": "1" } ] }
وقد يبدو طلب HTTP الذي تم إنشاؤه بواسطة RESTSerializer
لحفظ الأمر كما يلي:
POST http://api.example.com/orders { "order": { "createdAt": "1401492647008", "user": "1" } }
العلاقات واحد لواحد
قل ، على سبيل المثال ، أن كل مستخدم لديه ملف تعريف فريد. يمكننا تمثيل هذه العلاقة في Ember Data باستخدام DS.belongsTo
في كل من المستخدم والملف الشخصي:
App.User = DS.Model.extend({ profile: DS.belongsTo('profile', {async: true}) }); App.Profile = DS.Model.extend({ user: DS.belongsTo('user', {async: true}) });
يمكننا بعد ذلك الحصول على الارتباط بـ user.get('profile')
أو تعيينه باستخدام user.set('profile', aProfile)
.
يتوقع RESTSerializer
أن يتم توفير معرف النموذج المرتبط لكل طراز ؛ على سبيل المثال:
GET /users { "users": [ { "id": "14", "profile": "1" /* ID of profile associated with this user */ } ] } GET /profiles { "profiles": [ { "id": "1", "user": "14" /* ID of user associated with this profile */ } ] }
وبالمثل ، يتضمن معرف النموذج المرتبط في حمولة الطلب:
POST /profiles { "profile": { "user": "17" /* ID of user associated with this profile */ } }
علاقات رأس بأطراف وعديد إلى واحد
لنفترض أن لدينا نموذجًا حيث يحتوي المنشور على العديد من التعليقات. في Ember Data ، يمكننا تمثيل هذه العلاقة مع DS.hasMany('comment', {async: true})
في Post و DS.belongsTo('post', {async: true})
على التعليق:
App.Post = DS.Model.extend({ content: DS.attr('string'), comments: DS.hasMany('comment', {async: true}) }); App.Comment = DS.Model.extend({ message: DS.attr('string'), post: DS.belongsTo('post', {async: true}) });
يمكننا بعد ذلك الحصول على العناصر المرتبطة بـ post.get('comments', {async: true})
وإضافة ارتباط جديد بـ post.get('comments').then(function(comments){ return comments.pushObject(aComment);})
.
سيرد الخادم بعد ذلك بمصفوفة من المعرفات للتعليقات المقابلة على المنشور:
GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }
... ومع معرف لكل تعليق:
GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] }
يضيف RESTSerializer
معرف المنشور المرتبط بالتعليق:
POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } }
لاحظ أنه ، بشكل افتراضي ، RESTSerializer ، لن يضيف DS.hasMany
من المعرفات المرتبطة بالكائنات التي يتم إجراء تسلسل لها ، حيث يتم تحديد تلك الارتباطات في الجانب "متعدد" (أي تلك التي لها ارتباط DS.belongsTo
). لذلك ، في مثالنا ، على الرغم من أن المنشور يحتوي على العديد من التعليقات ، فلن تتم إضافة هذه المعرفات إلى كائن المنشور:
POST /posts { "post": { "content": "" /* no associated post IDs added here */ } }
لفرض DS.hasMany
IDs ليتم تسلسلها أيضًا ، يمكنك استخدام Embedded Records Mixin.
علاقات كثير إلى كثير
لنفترض أنه في نموذجنا قد يكون للمؤلف منشورات متعددة وقد يكون للمنشور مؤلفين متعددين.
لتمثيل هذه العلاقة في Ember Data ، يمكننا استخدام DS.hasMany('author', {async: true})
في Post و DS.hasMany('post', {async: true})
في المؤلف:
App.Author = DS.Model.extend({ name: DS.attr('string'), posts: DS.hasMany('post', {async: true}) }); App.Post = DS.Model.extend({ content: DS.attr('string'), authors: DS.hasMany('author', {async: true}) });
يمكننا بعد ذلك الحصول على العناصر المرتبطة بـ author.get('posts')
وإضافة ارتباط جديد بـ author.get('posts').then(function(posts){ return posts.pushObject(aPost);})
.
سوف يستجيب الخادم بعد ذلك بمصفوفة من المعرفات للكائنات المقابلة ؛ على سبيل المثال:
GET /authors { "authors": [ { "id": "1", "name": "", "posts": ["12"] /* IDs of posts associated with this author */ } ] } GET /posts { "posts": [ { "id": "12", "content": "", "authors": ["1"] /* IDs of authors associated with this post */ } ] }
نظرًا لأن هذه علاقة أطراف بأطراف ، يضيف RESTSerializer مصفوفة من معرفات الكائنات المرتبطة ؛ على سبيل المثال:
POST /posts { "post": { "content": "", "authors": ["1", "4"] /* IDs of authors associated with this post */ } }
مثال من العالم الحقيقي: تحسين نظام ترتيب موجود
في نظام الطلبات الحالي لدينا ، لكل مستخدم العديد من الطلبات ولكل طلب العديد من العناصر. يحتوي نظامنا على مزودين متعددين (أي البائعين) يمكن طلب المنتجات منهم ، ولكن كل طلب يمكن أن يحتوي فقط على عناصر من مزود واحد.
المتطلب الجديد رقم 1: تمكين طلب واحد لتضمين عناصر من عدة مزودين.
في النظام الحالي ، توجد علاقة رأس بأطراف بين الموفرين والطلبات. بمجرد أن نقوم بتمديد طلب ليشمل عناصر من عدة مزودين ، لن تكون هذه العلاقة البسيطة كافية بعد الآن.
على وجه التحديد ، إذا كان الموفر مرتبطًا بطلب كامل ، في النظام المحسّن ، قد يتضمن هذا الطلب عناصر تم طلبها من مزودين آخرين أيضًا. لذلك ، يجب أن تكون هناك طريقة للإشارة إلى أي جزء من كل طلب يتعلق بكل مزود. علاوة على ذلك ، عندما يصل مقدم الخدمة إلى طلباته ، يجب أن يكون لديه رؤية فقط للعناصر المطلوبة منه ، وليس أي عناصر أخرى قد يكون العميل قد طلبها من مزودين آخرين.
يمكن أن يتمثل أحد الأساليب في تقديم جمعيتين جديدتين متعددتين ؛ واحد بين الطلب والعنصر والآخر بين الطلب والموفر.
ومع ذلك ، لإبقاء الأمور أبسط ، نقدم بنية جديدة في نموذج البيانات الذي نشير إليه باسم "ProviderOrder".
صياغة العلاقات
سيحتاج نموذج البيانات المحسّن إلى استيعاب الارتباطات التالية:
علاقة رأس بأطراف بين المستخدمين والطلبات (قد يرتبط كل مستخدم بـ 0 إلى n من الطلبات) وعلاقة رأس بأطراف بين المستخدمين والمقدمين (قد يرتبط كل مستخدم بمقدمين من 0 إلى n )
App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string'), isAdmin: DS.attr('boolean'), orders: DS.hasMany('order', {async: true}), providers: DS.hasMany('provider', {async: true}) });
علاقة رأس بأطراف بين الطلبات وطلبات المزود (يتكون كل طلب من 1 إلى n من أوامر المزود):
App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
علاقة رأس بأطراف بين مقدمي الطلبات و ProviderOrders (قد يكون كل مزود مرتبطًا بـ 0 إلى n ProviderOrders):
App.Provider = DS.Model.extend({ link: DS.attr('string'), admin: DS.belongsTo('user', {async: true}), orders: DS.belongsTo('providerOrder', {async: true}), items: DS.hasMany('items', {async: true}) });
علاقة واحد بأطراف بين ProviderOrders والعناصر (يتكون كل مقدم مقدم من عنصر واحد إلى ن ):
App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ name: DS.attr('string'), price: DS.attr('number'), order: DS.belongsTo('order', {async: true}), provider: DS.belongsTo('provider', {async: true}) });
ودعنا لا ننسى تعريف المسار الخاص بنا:
App.OrdersRoute = Ember.Route.extend({ model: function(){ // GET /orders // Retrieves all orders. return this.store.find('order'); } });
الآن كل مقدم طلب لديه مزود واحد ، والذي كان هدفنا الرئيسي. يتم نقل العناصر من Order إلى ProviderOrder والافتراض هو أن جميع العناصر في ProviderOrder واحد تنتمي إلى مزود واحد.
التقليل من تمضغ الشفرة
للأسف ، هناك بعض التغييرات العاجلة هنا. لذلك دعونا نرى كيف يمكن أن تساعدنا Ember Data في تقليل أي اضطراب ناتج في الكود في قاعدة الشفرة الخاصة بنا.
في السابق ، كنا ندفع العناصر باستخدام items.pushObject(item)
. نحتاج الآن أولاً إلى العثور على ProviderOrder المناسب ودفع عنصر إليه:
order.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); });
نظرًا لأن هذا يمثل الكثير من الاضطراب ، ومهام الطلب أكثر من وظيفة وحدة التحكم ، فمن الأفضل نقل هذا الرمز إلى Order#pushItem
:
App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}), /** returns a promise */ pushItem: function(item){ return this.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); }); } });
الآن يمكننا إضافة عناصر مباشرة عند الطلب مثل order.pushItem(item)
.
ولإدراج عناصر كل طلب:
App.Order = DS.Model.extend({ /* ... */ /** returns a promise */ items: function(){ return this.get('restaurantOrders').then(function(restaurantOrders){ var arrayOfPromisesContainingItems = restaurantOrders.mapBy('items') return arrayOfPromisesContainingItems.then(function(items){ return items.reduce(function flattenByReduce(memo, index, element){ return memo.pushObject(element); }, Ember.A([])); }); }); }.property('[email protected]') /* ... */ }); App.ItemsRoute = Ember.Route.extend({ model: function(){ // Multiple GET /items with ids[] query parameter. // Returns a promise. return this.modelFor('orders.show').get('items'); } });
العلاقات متعددة الأشكال
دعنا نقدم الآن طلب تحسين إضافي لنظامنا يزيد الأمور تعقيدًا:
المتطلب الجديد رقم 2: دعم أنواع متعددة من مقدمي الخدمات.
في مثالنا البسيط ، لنفترض أنه تم تعريف نوعين من مقدمي الخدمات ("المتجر" و "متجر الكتب"):
App.Shop = App.Provider.extend({ status: DS.attr('string') }); App.BookStore = App.Provider.extend({ name: DS.attr('string') });
هنا سيكون دعم Ember Data للعلاقات متعددة الأشكال مفيدًا. تدعم Ember Data العلاقات متعددة الأشكال وواحد بأطراف ومتعددة الأشكال. يتم ذلك ببساطة عن طريق إضافة السمة polymorphic: true
مع مواصفات الارتباط. علي سبيل المثال:
App.Provider = DS.Model.extend({ providerOrders: DS.hasMany('providerOrder', {async: true}) }); App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}) });
تشير العلامة polymorphic
أعلاه إلى أن هناك أنواعًا مختلفة من مقدمي الخدمة يمكن ربطهم بمقدم الطلب (في حالتنا ، إما متجر أو مكتبة).
عندما تكون العلاقة متعددة الأشكال ، يجب أن تشير استجابة الخادم إلى كل من المعرف ونوع الكائن الذي تم إرجاعه (يقوم RESTSerializer
ذلك بشكل افتراضي) ؛ على سبيل المثال:
GET /providerOrders { "providerOrders": [{ "status": "in_delivery", "provider": 1, "providerType": "shop" }] }
تلبية المتطلبات الجديدة
نحن بحاجة إلى مزودين متعددي الأشكال وعناصر لتلبية المتطلبات. نظرًا لأن ProviderOrder يربط مقدمي الخدمة بالعناصر ، يمكننا تغيير ارتباطاته إلى ارتباطات متعددة الأشكال:
App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });
هناك مشكلة متبقية ، على الرغم من: الموفر لديه ارتباط غير متعدد الأشكال بالعناصر ولكن العنصر هو نوع مجرد. لذلك لدينا خياران لمعالجة هذا:
- طلب ربط جميع الموفرين بنوع العنصر نفسه (على سبيل المثال ، الإعلان عن نوع معين من العناصر للارتباط بالموفر)
- أعلن عن اقتران
items
في المزود على أنه متعدد الأشكال
في حالتنا ، نحتاج إلى الانتقال إلى الخيار رقم 2 والإعلان عن اقتران items
في الموفر على أنه متعدد الأشكال:
App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });
لاحظ أن هذا لا يؤدي إلى حدوث أي خلل في التعليمات البرمجية ؛ تعمل جميع الجمعيات بالطريقة التي كانت تعمل بها قبل هذا التغيير. قوة Ember Data في أفضل حالاتها!

هل يمكن لـ Ember Data حقًا تصميم جميع بياناتي؟
هناك استثناءات بالطبع ، لكنني أعتبر اصطلاحات ActiveRecord طريقة قياسية ومرنة لهيكلة البيانات ونمذجةها ، لذا دعني أوضح لك كيف ترتبط اصطلاحات ActiveRecord ببيانات Ember:
has_many: المستخدمين من خلال: الملكية أو تمثيل النماذج الوسيطة
سيؤدي هذا إلى استشارة نموذج محوري يسمى الملكية للعثور على المستخدمين المرتبطين. إذا كان النموذج المحوري هو أساسًا جدول محوري ، فيمكنك تجنب إنشاء نموذج وسيط في Ember Data وتمثيل العلاقة مع DS.hasMany
على كلا الجانبين.
ومع ذلك ، إذا كنت بحاجة إلى هذه العلاقة المحورية داخل الواجهة الأمامية ، فقم بإعداد نموذج ملكية يتضمن DS.belongsTo('user', {async: true})
و DS.belongsTo('provider', {async: true})
، ثم إضافة خاصية على كل من المستخدمين والمقدمين والتي يتم تعيينها إلى الجمعية باستخدام الملكية ؛ على سبيل المثال:
App.User = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership'), /** returns a promise */ providers: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('provider'); }); }.property('[email protected]') }); App.Ownership = DS.Model.extend({ // One of ['approved', 'pending'] status: DS.attr('string'), user: DS.belongsTo('user', {async: true}), provider: DS.belongsTo('provider', {async: true}) }); App.Provider = DS.Model.extend({ // Omitted ownerships: DS.hasMany('ownership', {async: true}), /** returns a promise */ users: function(){ return this.get('ownerships').then(function(ownerships){ return ownerships.mapBy('user'); }); }.property('[email protected]') });
has_many: التعيينات ، مثل: locatable
في كائن ActiveRecord الخاص بنا ، لدينا علاقة نموذجية متعددة الأشكال:
class User < ActiveRecord::Base has_many :mappings, as: locatable has_many :locations, through: :mappings end class Mapping < ActiveRecord::Base belongs_to :locatable, polymorphic: true belongs_to :location end class Location < ActiveRecord::Base has_many :mappings has_many :users, through: :mappings, source: :locatable, source_type: 'User' has_many :providers, through: :mappings, source: :locatable, source_type: 'Provider' def locatables users + providers end end
هذه علاقة متعددة (متعددة الأشكال) إلى كثير (عادية غير متعددة الأشكال). في Ember Data ، يمكننا التعبير عن ذلك باستخدام DS.hasMany('locatable', {polymorphic: true, async: true})
وثابت DS.hasMany('location', {async: true})
:
App.Locatable = DS.Model.extend({ locations: DS.hasMany('location', {async: true}) }); App.User = App.Locatable.extend({ userName: DS.attr('string') }); App.Location = DS.Model.extend({ locatables: DS.hasMany('locatable', {polymorphic: true, async: true}) });
بالنسبة إلى Locatables ، مثل User ، يجب أن يُرجع الخادم معرفات المواقع المرتبطة:
GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }
بالنسبة للموقع ، يجب أن يعرض الخادم كلاً من المعرف ونوع Locatable في مصفوفة الكائنات:
GET /locations { "locations": [ { "id": "1", "locatables": [ {"id": "1", "type": "user"}, {"id": "2", "type": "provider"} ] } ] }
يمكنك أيضًا تمثيل العلاقات حسب النوع بعلاقة أطراف بأطراف ثابتة:
App.User = App.Locatable.extend({ userName: DS.attr('string'), locations: DS.hasMany('location', {async: true}) }); App.Provider = App.Locatable.extend({ link: DS.attr('string'), locations: DS.hasMany('location, {async: true} }); App.Location = DS.Model.extend({ users: DS.hasMany('user', {async: true}), providers: DS.hasMnay('provider', {async: true}) });
وماذا عن بيانات الوقت الفعلي؟
تمتلك Ember Data ميزة push
pushPayload
update
. يمكنك دائمًا دفع السجلات الجديدة / المحدثة يدويًا إلى ذاكرة التخزين المؤقت المحلية لـ Ember Data (تسمى المتجر) وستتعامل مع كل الباقي.
App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var payload = response.payload; this.store.pushPayload(type, payload); }); } });
أنا شخصياً أستخدم المقابس فقط للأحداث ذات الحمولات الصغيرة جدًا. حدث نموذجي هو "RecordUpdated" مع حمولة {"type": "shop", "id": "14"}
ثم في ApplicationRoute ، سأتحقق مما إذا كان هذا السجل في ذاكرة التخزين المؤقت المحلية (المتجر) وما إذا كان سوف أحضره فقط.
App.ApplicationRoute = Ember.Route.extend({ activate: function(){ socket.on('recordUpdated', function(response){ var type = response.type; var id = response.id; if( this.store.hasRecordForId(type, id) ){ this.store.find(type, id); } }); } });
بهذه الطريقة يمكننا إرسال أحداث قياسية محدثة إلى جميع العملاء دون تكاليف غير مقبولة.
هناك طريقتان أساسيتان في Ember Data للتعامل مع بيانات الوقت الفعلي:
- اكتب محولًا لقناة الاتصال في الوقت الفعلي واستخدمه بدلاً من RESTAdapter.
- ادفع السجلات إلى المتجر الرئيسي متى كانت متاحة.
الجانب السلبي للخيار الأول هو أنه يشبه إلى حد ما إعادة اختراع العجلة. بالنسبة للخيار الثاني ، نحتاج إلى الوصول إلى المتجر الرئيسي ، والمتوفر في جميع الطرق مثل route#store
.
يتم إحتوائه
في هذه المقالة ، قدمنا لك التركيبات والنماذج الرئيسية لـ Ember Data ، مما يوضح القيمة التي يمكن أن توفرها لك كمطور. توفر Ember Data سير عمل تطوير أكثر مرونة وانسيابية ، مما يقلل من حدوث تغييرات في التعليمات البرمجية استجابة لما يمكن أن يكون بخلاف ذلك تغييرات عالية التأثير.
الاستثمار المسبق (منحنى الوقت والتعلم) الذي تقوم به في استخدام Ember Data لمشروعك سيثبت بلا شك أنه مفيد لأن نظامك يتطور حتماً ويحتاج إلى التوسيع والتعديل والتحسين.
الملحق: موضوعات بيانات Ember المتقدمة
يقدم هذا الملحق عددًا من موضوعات Ember Data الأكثر تقدمًا بما في ذلك:
- تصميم وحدات إمبر
- تحميل الجانب
- الروابط
- نشط النموذج المسلسل والمحول
- السجلات المضمنة Mixin
- معدِّلات الاقتران (غير متزامن ومعكوس ومتعدد الأشكال)
- معلمة 'ids' في طلبات GET
تصميم وحدات
تمتلك Ember Data تصميمًا معياريًا تحت الغطاء. تشمل المكونات الرئيسية ما يلي:
-
Adapters
هي المسؤولة عن التعامل مع الاتصالات ، حاليًا فقط REST عبر HTTP. - يدير
Serializers
إنشاء نماذج من JSON أو العكس. -
Store
إنشاء السجلات. -
Container
الغراء كل هذه معًا.
تشمل مزايا هذا التصميم ما يلي:
- تعمل إزالة التسلسل وتخزين البيانات بشكل مستقل عن قناة الاتصال المستخدمة والموارد المطلوبة.
- يتم توفير تكوينات العمل ، مثل ActiveModelSerializer أو EmbeddedRecordsMixin ، خارج الصندوق.
- يمكن تبديل مصادر البيانات (على سبيل المثال ، LocalStorage ، وتنفيذ CouchDB ، وما إلى ذلك) من خلال تغيير المحولات.
- على الرغم من الكثير من الاصطلاحات حول التكوين ، فمن الممكن تكوين كل شيء ومشاركة التكوين / التنفيذ الخاص بك مع المجتمع.
تحميل الجانب
تدعم Ember Data "التحميل الجانبي" للبيانات ؛ أي ، الإشارة إلى البيانات المساعدة التي ينبغي استرجاعها (مع البيانات الأولية المطلوبة) للمساعدة في دمج طلبات HTTP المتعددة ذات الصلة.
حالة الاستخدام الشائعة هي التحميل الجانبي للنماذج المرتبطة. على سبيل المثال ، يحتوي كل متجر على العديد من محلات البقالة ، لذلك يمكننا تضمين جميع محلات البقالة ذات الصلة في /shops
:
GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ] }
عند الوصول إلى رابطة البقالة ، ستصدر Ember Data:
GET /groceries?ids[]=98&ids[]=99&ids[]=112 { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }
ومع ذلك ، إذا قمنا بدلاً من ذلك بإرجاع البقالة المرتبطة في /shops
، فلن تحتاج Ember Data إلى إصدار طلب آخر:
GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ], "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }
الروابط
تقبل Ember Data الروابط بدلاً من معرفات الاقتران. عند الوصول إلى ارتباط محدد كارتباط ، ستصدر Ember Data طلب GET لهذا الارتباط من أجل الحصول على السجلات المرتبطة.
على سبيل المثال ، يمكننا إرجاع رابط لجمعية بقالة:
GET /shops { "shops": [ { "id": "14", "links": { "groceries": "/shops/14/groceries" } } ] }
وتقوم Ember Data بعد ذلك بإصدار طلب إلى /shops/14/groceries
GET /shops/14/groceries { "groceries": [ { "id": "98", "provider": "14", "type": "shop" }, { "id": "99", "provider": "14", "type": "shop" }, { "id": "112", "provider": "14", "type": "shop" } ] }
ضع في اعتبارك أنك لا تزال بحاجة إلى تمثيل الارتباط في البيانات ؛ الروابط تقترح فقط طلب HTTP جديدًا ولن تؤثر على الارتباطات.
نشط النموذج المسلسل والمحول
يمكن القول ، إن ActiveModelSerializer
و ActiveModelAdapter
يستخدمان في الممارسة أكثر من RESTSerializer
و RESTAdapter
. على وجه الخصوص ، عندما تستخدم الواجهة الخلفية Ruby on Rails و ActiveModel::Serializers
gem ، فإن أفضل خيار هو استخدام ActiveModelSerializer
و ActiveModelAdapter
، نظرًا لأنهما يدعمان ActiveModel::Serializers
خارج الصندوق.
لحسن الحظ ، بالرغم من ذلك ، فإن اختلافات الاستخدام بين ActiveModelSerializer
/ ActiveModelAdapter
و RESTSerializer
/ RESTAdapter
محدودة للغاية ؛ يسمى:
- سيستخدم
ActiveModelSerializer
أسماء حقولRESTSerializer
بينما يتطلب RESTSerializer أسماء حقول camelCased. - يصدر
ActiveModelAdapter
طلبات لأساليب snake_case API أثناء مشكلاتRESTSerializer
لأساليب camelCased API. - يتوقع
ActiveModelSerializer
أن تنتهي أسماء الحقول المرتبطة بالاقتران بـ_id
أو_ids
بينما يتوقعRESTSerializer
أن تكون أسماء الحقول المرتبطة بالاقتران مماثلة لحقل الاقتران.
بغض النظر عن اختيار المحول والمسلسل ، ستكون نماذج Ember Data متطابقة تمامًا. سيكون تمثيل JSON ونقاط نهاية API فقط مختلفين.
خذ طلب ProviderOrder النهائي لدينا كمثال:
App.ApplicationSerializer = DS.ActiveModelSerializer.extend({ }); App.ProviderOrder = DS.Model.extend({ // One of ['processed', 'in_delivery', 'delivered'] status: DS.attr('string'), provider: DS.belongsTo('provider', {polymorphic: true, async: true}), order: DS.belongsTo('order', {async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });
باستخدام برنامج Active Model Serializer والمحول ، يجب أن يتوقع الخادم ما يلي:
Post /provider_orders { "provider_order": [ "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", ] }
... ويجب الرد بـ:
GET /provider_orders { "provider_orders": [ "id": "1", "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", "items": [ {"id": "57", "type": "grocery"}, {"id": "89", "type": "grocery"} ] ] }
السجلات المضمنة Mixin
DS.EmbeddedRecordsMixin
هو امتداد لـ DS.ActiveModelSerializer
والذي يسمح بتكوين كيفية تسلسل الجمعيات أو إلغاء تسلسلها. على الرغم من أنه لم يكتمل بعد (خاصة فيما يتعلق بالارتباطات متعددة الأشكال) ، إلا أنه مثير للاهتمام.
يمكنك اختيار:
- عدم إجراء تسلسل أو إلغاء تسلسل الجمعيات.
- لإجراء تسلسل أو إلغاء تسلسل الارتباطات بالمعرف أو المعرفات.
- لإجراء تسلسل أو إلغاء تسلسل الارتباطات مع النماذج المضمنة.
هذا مفيد بشكل خاص في علاقات واحد لكثير حيث ، بشكل افتراضي ، DS.hasMany
لا يتم إضافة العديد من المعرفات المرتبطة إلى الكائنات التي يتم تسلسلها. خذ عربة تسوق تحتوي على العديد من العناصر كمثال. في هذا المثال ، يتم إنشاء سلة التسوق بينما تكون العناصر معروفة. ومع ذلك ، عند حفظ عربة التسوق ، لن تضع Ember Data تلقائيًا معرّفات العناصر المرتبطة في حمولة الطلب.
باستخدام DS.EmbeddedRecordsMixin
، من الممكن إخبار Ember Data بإجراء تسلسل لمعرفات العناصر في سلة التسوق كما يلي:
App.CartSerializer = DS.ActiveModelSerializer .extend(DS.EmbeddedRecordsMixin) .extend{ attrs: { // thanks EmbeddedRecordsMixin! items: {serialize: 'ids', deserialize: 'ids'} } }); App.Cart = DS.Model.extend({ items: DS.hasMany('item', {async: true}) }); App.Item = DS.Model.extend({ cart: DS.belongsTo('item', {async: true}) });
كما هو موضح في المثال أعلاه ، يتيح EmbeddedRecordsMixin تحديدًا صريحًا للارتباطات التي تريد إجراء تسلسل و / أو إلغاء التسلسل عبر كائن attrs
. القيم الصالحة serialize
deserialize
هي: - 'no'
: لا تقم بتضمين الاقتران في البيانات المتسلسلة / غير المتسلسلة - 'id'
أو 'ids'
: قم بتضمين المعرفات المرتبطة في البيانات المتسلسلة / غير المتسلسلة - 'records
": تتضمن الخصائص الفعلية (على سبيل المثال ، قيم حقل التسجيل) كمصفوفة في البيانات المتسلسلة / غير المتسلسلة
معدِّلات الاقتران (غير متزامن ومعكوس ومتعدد الأشكال)
يتم دعم معدلات الارتباط التالية: polymorphic
، inverse
، وغير async
معدل متعدد الأشكال
في الارتباط متعدد الأشكال ، يمثل أحد جانبي الارتباط أو كلاهما فئة من الكائنات ، وليس كائنًا محددًا.
تذكر مثالنا السابق للمدونة حيث احتجنا إلى دعم القدرة على تمييز كل من المنشورات والصفحات. لدعم ذلك ، توصلنا إلى النموذج التالي:
- كل منشور هو عبارة عن علامة مميزة ولها العديد من العلامات
- كل صفحة هي علامة مميزة ولها العديد من العلامات
- كل علامة لديها العديد من Taggables متعددة الأشكال
باتباع هذا النموذج ، يمكن استخدام مُعدِّل polymorphic
للإعلان عن ارتباط العلامات بأي نوع من "العلامات" (والتي قد تكون إما منشورًا أو صفحة) ، على النحو التالي:
// A Taggable is something that can be tagged (ie, that has tags) App.Taggable = DS.Model.extend({ tags: DS.hasMany('tag') }); // A Page is a type of Taggable App.Page = App.Taggable.extend({}); // A Post is a type of Taggable App.Post = App.Taggable.extend App.Tag = DS.Model.extend({ // the "other side" of this association (ie, the 'taggable') is polymorphic taggable: DS.belongsTo('taggable', {polymorphic: true}) });
معدل معكوس
عادة ما تكون الجمعيات ثنائية الاتجاه. على سبيل المثال ، سيكون "المنشور به العديد من التعليقات" هو اتجاه واحد للجمعية ، بينما "التعليق ينتمي إلى المنشور" سيكون الاتجاه الآخر (أي "معكوس") لذلك الارتباط.
في الحالات التي لا يوجد فيها غموض في الارتباط ، يجب تحديد اتجاه واحد فقط لأن Ember Data يمكنها استنتاج الجزء العكسي من الارتباط.
However, in cases where objects in your model have multiple associations with one another, Ember Data cannot derive the inverse of each association automatically and it therefore needs to be specified using the invers
modifier.
Consider, for example, a case where:
- Each Page may have many Users as Collaborators
- Each Page may have many Users as Maintainers
- Each User may have many Favorite Pages
- Each User may have many Followed Pages
This would need to be specified as follows in our model:
App.User = DS.Model.extend({ favoritePages: DS.hasMany('page', {inverse: 'favoritors}), followedPages: DS.hasMany('page', {inverse: 'followers'}), collaboratePages: DS.hasMany('page', {inverse: 'collaborators'}), maintainedPages: DS.hasMany('page', {inverse: 'maintainers'}) }); App.Page = DS.Model.extend({ favoritors: DS.hasMany('user', {inverse: 'favoritePages'}), followers: DS.hasMany('user', {inverse: 'followedPages'}), collaborators: DS.hasMany('user', {inverse: 'collaboratedPages}), maintainers: DS.hasMany('user', {inverse: 'maintainedPages'}) });
Async Modifier
When data needs to be retrieved based on relevant associations, that associated data may or may not already have been loaded. If not, a synchronous association will throw an error since the associated data has not been loaded.
{async: true}
indicates that the request for the associated data should be handled asynchronously. The request therefore immediately returns a promise and the supplied callback is invoked once the associated data has been retrieved and is available.
When async
is false
, getting associated objects would be done as follows:
post.get('comments').pushObject(comment);
When async
is true
, getting associated objects would be done as follows (note the callback function specified):
post.get('comments').then(function(comments){ comments.pushObject(comment); })
Note well: The current default value of async
is false
, but in Ember Data 1.0 the default will be true
.
'ids' Parameter in GET Requests
By default, Ember Data expects that resources aren't nested. Take Posts which have many Comments as an example. In a typical interpretation of REST, API endpoints might look like these:
GET /users GET /users/:id/posts GET /users/:id/posts/:id/comments
However, Ember Data expects API endpoints to be flat, and not nested; على سبيل المثال:
GET /users with ?ids[]=4 as query parameters. GET /posts with ?ids[]=18&ids[]=27 as query parameters. GET /comments with ?ids[]=74&ids[]=78&ids[]=114&ids[]=117 as query parameters.
In the above example, ids
is the name of the array, []
indicates that this query parameter is an array, and =
indicates that a new ID is being pushed onto the array.
Now Ember Data can avoid downloading resources it already has or defer downloading them to very last moment.
Also, to white list an array parameter using the StrongParameters gem, you can declare it as params.require(:shop).permit(item_ids: [])
.