Ember Data: บทช่วยสอนที่ครอบคลุมสำหรับ ember-data Library

เผยแพร่แล้ว: 2022-03-11

Ember Data (aka ember-data หรือ ember.data) เป็นไลบรารีสำหรับจัดการข้อมูลโมเดลในแอปพลิเคชัน Ember.js อย่างมีประสิทธิภาพ นักพัฒนาของ Ember Data ระบุว่าได้รับการออกแบบมาให้ไม่เชื่อเรื่องพระเจ้าต่อกลไกการคงอยู่ที่ซ่อนอยู่ ดังนั้นจึงทำงานได้ดีกับ JSON API ผ่าน HTTP เช่นเดียวกับการสตรีม WebSockets หรือที่เก็บข้อมูล IndexedDB ในเครื่อง มันมีสิ่งอำนวยความสะดวกมากมายที่คุณพบในการแมปเชิงสัมพันธ์ของวัตถุฝั่งเซิร์ฟเวอร์ (ORM) เช่น ActiveRecord แต่ได้รับการออกแบบมาโดยเฉพาะสำหรับสภาพแวดล้อมเฉพาะของ JavaScript ในเบราว์เซอร์

แม้ว่า Ember Data อาจต้องใช้เวลาพอสมควร แต่เมื่อคุณทำสำเร็จแล้ว คุณจะพบว่ามันคุ้มค่ากับการลงทุน ในที่สุดจะทำให้การพัฒนา เพิ่มประสิทธิภาพ และบำรุงรักษาระบบของคุณง่ายขึ้นมาก

เมื่อ API ถูกแสดงโดยใช้โมเดล Ember Data อะแดปเตอร์ และซีเรียลไลเซอร์ แต่ละการเชื่อมโยงจะกลายเป็นชื่อฟิลด์ สิ่งนี้จะสรุปรายละเอียดภายในของแต่ละการเชื่อมโยง ดังนั้นจึงป้องกันรหัสที่เหลือของคุณจากการเปลี่ยนแปลงในการเชื่อมโยงด้วยตัวมันเอง รหัสที่เหลือของคุณจะไม่สนใจ ตัวอย่างเช่น หากการเชื่อมโยงใด ๆ เป็นแบบ polymorphic หรือเป็นผลมาจากแผนที่ของการเชื่อมโยงหลายๆ อย่าง

นอกจากนี้ ฐานโค้ดของคุณส่วนใหญ่ได้รับการป้องกันจากการเปลี่ยนแปลงแบ็กเอนด์ แม้ว่าจะมีนัยสำคัญก็ตาม เนื่องจากฐานโค้ดทั้งหมดของคุณคาดว่าจะเป็นฟิลด์และฟังก์ชันบนโมเดล ไม่ใช่การแสดง JSON หรือ XML หรือ YAML ของโมเดล

ในบทช่วยสอนนี้ เราจะแนะนำคุณสมบัติที่โดดเด่นที่สุดของ Ember Data และสาธิตวิธีที่ช่วยลดการปั่นโค้ดให้น้อยที่สุด โดยเน้นที่ตัวอย่างในโลกแห่งความเป็นจริง

มีภาคผนวกที่กล่าวถึงหัวข้อและตัวอย่างข้อมูลของ Ember ขั้นสูงอีกจำนวนหนึ่ง

หมายเหตุ: บทความนี้สันนิษฐานว่ามีความคุ้นเคยกับ Ember.js ขั้นพื้นฐาน หากคุณไม่คุ้นเคยกับ Ember.js โปรดดูบทแนะนำเกี่ยวกับ Ember.js ยอดนิยมของเรา เรายังมีคู่มือ JavaScript แบบฟูลสแตกในภาษารัสเซีย โปรตุเกส และสเปน

ข้อเสนอมูลค่าข้อมูลของ Ember

ตัวอย่างวิธีการที่ห้องสมุด Ember Data สามารถช่วยตอบสนองความต้องการของลูกค้าได้

เริ่มต้นด้วยการพิจารณาตัวอย่างง่ายๆ

สมมติว่าเรามีฐานรหัสที่ใช้งานได้สำหรับระบบบล็อกพื้นฐาน ระบบประกอบด้วยโพสต์และแท็กซึ่งมีความสัมพันธ์แบบกลุ่มต่อกลุ่มระหว่างกัน

ทุกอย่างเรียบร้อยดี จนกว่าเราจะได้รับข้อกำหนดในการสนับสนุนเพจ ข้อกำหนดยังระบุด้วยว่า เนื่องจากเราสามารถแท็กเพจใน WordPress ได้ เราจึงควรทำได้เช่นกัน

ดังนั้นตอนนี้ แท็กจะไม่มีผลเฉพาะกับโพสต์อีกต่อไป แต่ยังใช้กับเพจได้อีกด้วย ด้วยเหตุนี้ ความเชื่อมโยงง่ายๆ ของเราระหว่างแท็กและโพสต์จะไม่เพียงพออีกต่อไป แต่เราจำเป็นต้องมีความสัมพันธ์แบบหลายต่อกลุ่มแบบด้านเดียวแบบกลุ่มต่อกลุ่มดังต่อไปนี้:

  • แต่ละโพสต์สามารถแท็กได้และมีแท็กมากมาย
  • แต่ละหน้าสามารถแท็กได้และมีแท็กมากมาย
  • แต่ละแท็กมี Taggables ที่หลากหลาย

การเปลี่ยนไปใช้กลุ่มการเชื่อมโยงใหม่ที่ซับซ้อนยิ่งขึ้นนี้มีแนวโน้มที่จะมีการแตกสาขาที่สำคัญตลอดทั้งโค้ดของเรา ส่งผลให้เกิดการเลิกราหลายครั้ง เนื่องจากเราไม่รู้ว่าจะสร้างความสัมพันธ์แบบ polymorphic กับ 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 Data Primer

ก่อนดำดิ่งสู่ตัวอย่างในโลกแห่งความเป็นจริง เรามาทบทวนปัจจัยพื้นฐานบางอย่างของ Ember Data กันก่อน

เส้นทางและรุ่น

ใน Ember.js เราเตอร์มีหน้าที่แสดงเทมเพลต โหลดข้อมูล และตั้งค่าสถานะแอปพลิเคชัน เราเตอร์จะจับคู่ URL ปัจจุบันกับเส้นทางที่คุณกำหนด ดังนั้น Route มีหน้าที่ระบุโมเดลที่เทมเพลตจะแสดง (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 และเพิ่มความสามารถ เช่น การบันทึกหรืออัปเดตระเบียนเดียวหรือหลายระเบียนเพื่อความสะดวก

ในการสร้าง Model ใหม่ เราสร้าง subclass ของ DS.Model (เช่น App.User = DS.Model.extend({}) )

Ember Data คาดหวังโครงสร้าง JSON ที่เข้าใจง่ายและชัดเจนจากเซิร์ฟเวอร์ และทำให้ระเบียนที่สร้างขึ้นใหม่เป็นอนุกรมใน JSON ที่มีโครงสร้างเดียวกัน

Ember Data ยังมีชุดของคลาสอาร์เรย์ เช่น DS.RecordArray สำหรับการทำงานกับ Models สิ่งเหล่านี้มีหน้าที่รับผิดชอบ เช่น การจัดการความสัมพันธ์แบบหนึ่งต่อกลุ่มหรือกลุ่มต่อกลุ่ม การจัดการการดึงข้อมูลแบบอะซิงโครนัส เป็นต้น

คุณสมบัติของโมเดล

แอตทริบิวต์ของโมเดลพื้นฐานถูกกำหนดโดยใช้ DS.attr ; เช่น:

 App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') });

เฉพาะฟิลด์ที่สร้างโดย DS.attr จะรวมอยู่ในเพย์โหลดที่ส่งผ่านไปยังเซิร์ฟเวอร์เพื่อสร้างหรืออัปเดตเรกคอร์ด

DS.attr ยอมรับข้อมูลสี่ประเภท: string , number , boolean และ date

RESTซีเรียลไลเซอร์

โดยค่าเริ่มต้น:

  • Ember Data ใช้ RESTSerializer เพื่อสร้างออบเจ็กต์จากการตอบสนอง API (ดีซีเรียลไลเซชัน) และสำหรับสร้าง JSON สำหรับคำขอ API (การทำให้เป็นอนุกรม)
  • RESTSerializer คาดว่าฟิลด์ที่สร้างโดย DS.belongsTo จะมีฟิลด์ที่มีชื่อ user รวมอยู่ในการตอบสนอง JSON จากเซิร์ฟเวอร์ ฟิลด์นั้นมี id ของเร็กคอร์ดที่อ้างอิง
  • 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 คาดหวังให้ ID ของโมเดลที่เกี่ยวข้องมีให้สำหรับแต่ละรุ่น เช่น:

 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 */ } ] }

ในทำนองเดียวกัน จะรวม ID ของโมเดลที่เกี่ยวข้องไว้ในเพย์โหลดคำขอ:

 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);}) .

เซิร์ฟเวอร์จะตอบกลับด้วยอาร์เรย์ของ ID สำหรับความคิดเห็นที่เกี่ยวข้องในโพสต์:

 GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }

… และมี ID สำหรับแต่ละความคิดเห็น:

 GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] }

RESTSerializer เพิ่ม id ของโพสต์ที่เกี่ยวข้องในความคิดเห็น:

 POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } }

โปรดทราบว่าตามค่าเริ่มต้น RESTSerializer จะ ไม่ เพิ่ม DS.hasMany ID ที่เชื่อมโยงไปยังอ็อบเจ็กต์ที่ทำให้เป็นอนุกรม เนื่องจากการเชื่อมโยงเหล่านั้นระบุไว้ที่ด้าน "หลายรายการ" (กล่าวคือ ที่มีการเชื่อมโยง DS.belongsTo ) ดังนั้น ในตัวอย่างของเรา แม้ว่าโพสต์จะมีความคิดเห็นจำนวนมาก แต่ ID เหล่านั้นจะ ไม่ ถูกเพิ่มไปยังออบเจกต์ Post:

 POST /posts { "post": { "content": "" /* no associated post IDs added here */ } }

คุณสามารถใช้ Embedded Records Mixin เพื่อ "บังคับ" DS.hasMany IDs หลายรายการให้เป็นอนุกรมได้เช่นกัน

ความสัมพันธ์แบบกลุ่มต่อกลุ่ม

สมมติว่าในรูปแบบของเรา ผู้เขียนอาจมีหลายโพสต์และโพสต์อาจมีผู้เขียนหลายคน

เพื่อแสดงความสัมพันธ์นี้ใน 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);})

เซิร์ฟเวอร์จะตอบสนองด้วยอาร์เรย์ของ ID สำหรับอ็อบเจ็กต์ที่เกี่ยวข้อง เช่น:

 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 จะเพิ่มอาร์เรย์ของ ID ของอ็อบเจ็กต์ที่เกี่ยวข้อง เช่น:

 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 ProviderOrders):

     App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
  • ความสัมพันธ์ แบบหนึ่งต่อกลุ่ม ระหว่างผู้ให้บริการและคำสั่งซื้อของผู้ให้บริการ (ผู้ให้บริการแต่ละรายอาจเชื่อมโยงกับ 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 และ Items (แต่ละ ProviderOrder ประกอบด้วย 1 ถึง n รายการ):

     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'); } });

ตอนนี้แต่ละ ProviderOrder มีผู้ให้บริการหนึ่งราย ซึ่งเป็นเป้าหมายหลักของเรา รายการจะถูกย้ายจาก Order ไปยัง ProviderOrder และสันนิษฐานว่ารายการทั้งหมดใน ProviderOrder หนึ่งรายการเป็นของผู้ให้บริการรายเดียว

การลดรหัส Churn

ขออภัย มีการเปลี่ยนแปลงบางอย่างที่นี่ มาดูกันว่า Ember Data สามารถช่วยเราลดการปั่นรหัสที่เกิดขึ้นในฐานรหัสของเราได้อย่างไร

ก่อนหน้านี้เราทำการ push item ด้วย 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 มากกว่างานคอนโทรลเลอร์ จะดีกว่าถ้าเราย้ายรหัสนี้ไปที่ 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 สำหรับความสัมพันธ์แบบ polymorphic จะมีประโยชน์ดังนี้ 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 ด้านบนบ่งชี้ว่ามีผู้ให้บริการหลายประเภทที่สามารถเชื่อมโยงกับ ProviderOrder (ในกรณีของเรา ไม่ว่าจะเป็นร้านค้าหรือร้านหนังสือ)

เมื่อความสัมพันธ์เป็นแบบ polymorphic การตอบสนองของเซิร์ฟเวอร์ควรระบุทั้ง ID และ ประเภทของวัตถุที่ส่งคืน ( 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}) });

แม้ว่าจะมีปัญหาที่เหลืออยู่: ผู้ให้บริการมีการเชื่อมโยงแบบ non-polymorphic กับรายการ แต่ Item เป็นประเภทนามธรรม ดังนั้นเราจึงมีสองทางเลือกในการแก้ไขปัญหานี้:

  1. กำหนดให้ผู้ให้บริการทั้งหมดเชื่อมโยงกับประเภทรายการเดียวกัน (เช่น ประกาศประเภทรายการเฉพาะสำหรับการเชื่อมโยงกับผู้ให้บริการ)
  2. ประกาศการเชื่อมโยง items บนผู้ให้บริการเป็น polymorphic

ในกรณีของเรา เราต้องใช้ตัวเลือก # 2 และประกาศการเชื่อมโยง items บนผู้ให้บริการเป็นแบบพหุสัณฐาน:

 App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });

โปรดทราบว่าสิ่งนี้ไม่ได้แนะนำการปั่นโค้ด ใดๆ การเชื่อมโยงทั้งหมดทำงานแบบที่พวกเขาทำก่อนการเปลี่ยนแปลงนี้ ขุมพลังของ Ember Data ที่ดีที่สุด!

Ember Data สามารถ สร้างแบบจำลองข้อมูลทั้งหมดของฉันได้หรือไม่

มีข้อยกเว้นแน่นอน แต่ฉันถือว่าข้อตกลงของ ActiveRecord เป็นวิธีมาตรฐานและยืดหยุ่นในการจัดโครงสร้างและการสร้างแบบจำลองข้อมูล ดังนั้นให้ฉันแสดงให้คุณเห็นว่าข้อตกลงของ ActiveRecord จับคู่กับ Ember Data อย่างไร:

has_many :users through: :ownerships or Representing Intermediate Models

การดำเนินการนี้จะพิจารณา รูปแบบ เดือยที่เรียกว่าความเป็นเจ้าของเพื่อค้นหาผู้ใช้ที่เกี่ยวข้อง หาก โมเดล pivot นั้นเป็น pivot table คุณสามารถหลีกเลี่ยงการสร้างโมเดลระดับกลางใน Ember Data และแสดงความสัมพันธ์กับ DS.hasMany ทั้งสองด้าน

อย่างไรก็ตาม หากคุณต้องการความสัมพันธ์แบบ pivot ภายใน front-end ของคุณ ให้ตั้งค่า Ownership model ที่มี 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 :mappings, as: ระบุตำแหน่งได้

ในวัตถุ ActiveRecord ของเรา เรามีความสัมพันธ์แบบ polymorphic ทั่วไป:

 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

นี่คือความสัมพันธ์แบบหลาย (polymorphic) กับหลาย ๆ (แบบปกติที่ไม่ใช่แบบ polymorphic) ใน 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 เซิร์ฟเวอร์ควรส่งคืน ID สำหรับตำแหน่งที่เกี่ยวข้อง:

 GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }

สำหรับ Location เซิร์ฟเวอร์ควรส่งคืนทั้ง ID และประเภทของ 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 สำหรับจัดการกับข้อมูลเรียลไทม์:

  1. เขียนอะแดปเตอร์สำหรับช่องทางการสื่อสารแบบเรียลไทม์ของคุณและใช้แทน RESTadapter
  2. ส่งบันทึกไปยังร้านค้าหลักทุกครั้งที่มี

ข้อเสียของตัวเลือกแรกคือมันค่อนข้างคล้ายกับการคิดค้นล้อใหม่ สำหรับตัวเลือกที่สอง เราจำเป็นต้องเข้าถึงร้านค้าหลัก ซึ่งมีให้ในทุกเส้นทางเป็น route#store

สรุป

ในบทความนี้ เราได้แนะนำให้คุณรู้จักกับโครงสร้างหลักและกระบวนทัศน์ของ Ember Data ซึ่งแสดงให้เห็นถึงคุณค่าที่ข้อมูลนี้สามารถมอบให้คุณในฐานะนักพัฒนา Ember Data มอบเวิร์กโฟลว์การพัฒนาที่ยืดหยุ่นและคล่องตัวยิ่งขึ้น โดยลดการปั่นโค้ดให้น้อยที่สุดเพื่อตอบสนองต่อการเปลี่ยนแปลงที่ส่งผลกระทบสูง

การลงทุนล่วงหน้า (เวลาและช่วงการเรียนรู้) ที่คุณใช้กับ Ember Data สำหรับโครงการของคุณจะพิสูจน์ได้ว่าคุ้มค่าอย่างไม่ต้องสงสัย เนื่องจากระบบของคุณมีวิวัฒนาการอย่างหลีกเลี่ยงไม่ได้ และจำเป็นต้องขยาย ปรับเปลี่ยน และปรับปรุงอย่างหลีกเลี่ยงไม่ได้


ภาคผนวก: หัวข้อข้อมูล Ember ขั้นสูง

ภาคผนวกนี้แนะนำหัวข้อ Ember Data ขั้นสูงจำนวนหนึ่ง ซึ่งรวมถึง:

  • การออกแบบโมดูลาร์ของ Ember
  • ไซด์โหลด
  • ลิงค์
  • Active Model Serializer และ Adapter
  • Embedded Records Mixin
  • ตัวแก้ไขการเชื่อมโยง (Async, Inverse และ Polymorphic)
  • พารามิเตอร์ 'id' ใน GET Requests

การออกแบบโมดูลาร์

Ember Data มีการออกแบบโมดูลาร์ภายใต้ประทุน ส่วนประกอบที่สำคัญ ได้แก่ :

  1. Adapters มีหน้าที่ในการจัดการการสื่อสาร ปัจจุบัน REST ผ่าน HTTP เท่านั้น
  2. Serializers จัดการการสร้างแบบจำลองจาก JSON หรือย้อนกลับ
  3. Store แคชที่สร้างเร็กคอร์ด
  4. Container ติดกาวทั้งหมดนี้เข้าด้วยกัน

ประโยชน์ของการออกแบบนี้รวมถึง:

  1. การดีซีเรียลไลซ์เซชั่นและการจัดเก็บข้อมูลทำงานโดยไม่ขึ้นกับช่องทางการสื่อสารที่ใช้และทรัพยากรที่ร้องขอ
  2. การกำหนดค่าการทำงาน เช่น ActiveModelSerializer หรือ EmbeddedRecordsMixin มีให้ตั้งแต่แกะกล่อง
  3. แหล่งข้อมูล (เช่น LocalStorage, การใช้ CouchDB เป็นต้น) สามารถสลับเข้าและออกได้โดยการเปลี่ยนอะแดปเตอร์
  4. แม้จะมีหลักการมากกว่าคอนฟิกูเรชันมาก แต่ก็สามารถกำหนดคอนฟิกทุกอย่างและแบ่งปันคอนฟิกูเรชัน/การใช้งานของคุณกับชุมชนได้

ไซด์โหลด

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 endpoint แทน 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 ยอมรับลิงก์แทน ID สมาคม เมื่อมีการเข้าถึงการเชื่อมโยงที่ระบุเป็นลิงก์ 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 ใหม่เท่านั้นและจะไม่ส่งผลต่อการเชื่อมโยง

Active Model Serializer และ Adapter

เนื้อหา ActiveModelSerializer และ ActiveModelAdapter นั้นใช้ในทางปฏิบัติมากกว่า RESTSerializer และ RESTAdapter โดยเฉพาะอย่างยิ่ง เมื่อแบ็กเอนด์ใช้ Ruby on Rails และ ActiveModel::Serializers gem ตัวเลือกที่ดีที่สุดคือใช้ ActiveModelSerializer และ ActiveModelAdapter เนื่องจากรองรับ ActiveModel::Serializers ทันที

อย่างไรก็ตาม โชคดีที่ความแตกต่างในการใช้งานระหว่าง ActiveModelSerializer / ActiveModelAdapter และ RESTSerializer / RESTAdapter นั้นค่อนข้างจำกัด กล่าวคือ:

  1. ActiveModelSerializer จะใช้ชื่อฟิลด์ snake_case ในขณะที่ RESTSerializer ต้องการชื่อฟิลด์ camelCased
  2. ActiveModelAdapter ออกคำขอไปยังวิธี snake_case API ในขณะที่ RESTSerializer ออกไปยังวิธี camelCased API
  3. ActiveModelSerializer คาดว่าชื่อฟิลด์ที่เกี่ยวข้องกับการเชื่อมโยงจะลงท้ายด้วย _id หรือ _ids ในขณะที่ RESTSerializer คาดว่าชื่อฟิลด์ที่เกี่ยวข้องกับการเชื่อมโยงจะเหมือนกับฟิลด์ความสัมพันธ์

โดยไม่คำนึงถึงตัวเลือก Adapter และ Serializer รุ่น 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 และ Adapter เซิร์ฟเวอร์ควรคาดหวัง:

 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"} ] ] }

Embedded Records Mixin

DS.EmbeddedRecordsMixin เป็นส่วนขยายสำหรับ DS.ActiveModelSerializer ซึ่งช่วยให้กำหนดค่าว่าการเชื่อมโยงต่างๆ ได้รับการซีเรียลไลซ์หรือดีซีเรียลไลซ์อย่างไร แม้ว่าจะยังไม่สมบูรณ์ (โดยเฉพาะอย่างยิ่งเกี่ยวกับการเชื่อมโยงหลายมิติ) แต่ก็เป็นที่น่าสนใจ

คุณสามารถเลือก:

  1. ไม่ให้ซีเรียลไลซ์หรือดีซีเรียลไลซ์ความสัมพันธ์
  2. เพื่อทำให้เป็นอันดับหรือดีซีเรียลไลซ์ความสัมพันธ์กับ id หรือ id
  3. เพื่อทำให้เป็นอนุกรมหรือดีซีเรียลไลซ์ความสัมพันธ์กับโมเดลแบบฝัง

สิ่งนี้มีประโยชน์อย่างยิ่งในความสัมพันธ์แบบหนึ่ง-ต่อ-กลุ่ม โดยค่าเริ่มต้น DS.hasMany ID ที่เกี่ยวข้องจะไม่ถูกเพิ่มไปยังอ็อบเจ็กต์ที่ถูกทำให้เป็นอนุกรม ยกตัวอย่างตะกร้าสินค้าที่มีหลายรายการ ในตัวอย่างนี้ รถเข็นถูกสร้างขึ้นในขณะที่ทราบรายการ อย่างไรก็ตาม เมื่อคุณบันทึกรถเข็น Ember Data จะไม่ใส่ ID ของรายการที่เกี่ยวข้องลงในเพย์โหลดคำขอโดยอัตโนมัติ

อย่างไรก็ตาม การใช้ DS.EmbeddedRecordsMixin เป็นไปได้ที่จะบอกให้ Ember Data ทำซีเรียลไลซ์ ID รายการบนรถเข็นดังนี้:

 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' : รวม ID ที่เกี่ยวข้องในข้อมูลซีเรียลไลซ์/ดีซีเรียลไลซ์ - 'records ': รวมคุณสมบัติจริง (เช่น บันทึกค่าฟิลด์) เป็นอาร์เรย์ในข้อมูลซีเรียลไลซ์/ดีซีเรียลไลซ์

ตัวแก้ไขการเชื่อมโยง (Async, Inverse และ Polymorphic)

รองรับตัวแก้ไขการเชื่อมโยงต่อไปนี้: polymorphic , inverse และ async

ตัวดัดแปลง Polymorphic

ในการเชื่อมโยงแบบหลายมิติ หนึ่งหรือทั้งสองด้านของการเชื่อมโยงแสดงถึงคลาสของอ็อบเจ็กต์ แทนที่จะเป็นอ็อบเจ็กต์เฉพาะ

นึกถึงตัวอย่างบล็อกก่อนหน้านี้ที่เราจำเป็นต้องสนับสนุนความสามารถในการแท็กทั้งบทความและหน้า เพื่อรองรับสิ่งนี้ เราได้มาถึงโมเดลต่อไปนี้:

  • แต่ละโพสต์สามารถแท็กได้และมีแท็กมากมาย
  • แต่ละหน้าสามารถแท็กได้และมีแท็กมากมาย
  • แต่ละแท็กมี 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: []) .