Data Ember: Tutorial Komprehensif untuk Pustaka data bara
Diterbitkan: 2022-03-11Ember Data (alias ember-data atau ember.data) adalah perpustakaan untuk mengelola data model dengan kuat dalam aplikasi Ember.js. Pengembang Ember Data menyatakan bahwa itu dirancang untuk menjadi agnostik terhadap mekanisme persistensi yang mendasarinya, sehingga berfungsi dengan baik dengan JSON API melalui HTTP seperti halnya dengan streaming WebSockets atau penyimpanan IndexedDB lokal. Ini menyediakan banyak fasilitas yang akan Anda temukan di pemetaan relasional objek sisi server (ORM) seperti ActiveRecord, tetapi dirancang khusus untuk lingkungan unik JavaScript di browser.
Meskipun Ember Data mungkin memerlukan beberapa waktu untuk di-grok, setelah Anda melakukannya, kemungkinan besar Anda akan menganggapnya layak untuk investasi. Ini pada akhirnya akan membuat pengembangan, peningkatan, dan pemeliharaan sistem Anda menjadi lebih mudah.
Saat API direpresentasikan menggunakan model, adaptor, dan serializer Data Ember, setiap asosiasi hanya menjadi nama bidang. Ini merangkum detail internal setiap asosiasi, sehingga mengisolasi sisa kode Anda dari perubahan pada asosiasi itu sendiri. Sisa kode Anda tidak akan peduli, misalnya, jika asosiasi tertentu adalah polimorfik atau merupakan hasil dari peta banyak asosiasi.
Selain itu, basis kode Anda sebagian besar terisolasi dari perubahan backend, bahkan jika itu signifikan, karena semua yang diharapkan basis kode Anda adalah bidang dan fungsi pada model, bukan representasi model JSON atau XML atau YAML.
Dalam tutorial ini, kami akan memperkenalkan fitur paling menonjol dari Data Ember dan mendemonstrasikan bagaimana hal itu membantu meminimalkan churn kode, melalui fokus pada contoh dunia nyata.
Sebuah lampiran juga disediakan yang membahas sejumlah topik dan contoh Data Ember yang lebih canggih.
Catatan: Artikel ini mengasumsikan beberapa keakraban dasar dengan Ember.js. Jika Anda tidak terbiasa dengan Ember.js, lihat tutorial Ember.js kami yang populer untuk pengenalan. Kami juga memiliki panduan JavaScript lengkap yang tersedia dalam bahasa Rusia, Portugis, dan Spanyol.
Proposisi Nilai Data Ember
Mari kita mulai dengan mempertimbangkan contoh sederhana.
Katakanlah kita memiliki basis kode yang berfungsi untuk sistem blog dasar. Sistem berisi Posting dan Tag, yang memiliki hubungan banyak ke banyak satu sama lain.
Semuanya baik-baik saja sampai kami mendapatkan persyaratan untuk mendukung Halaman. Persyaratan juga menyatakan bahwa, karena mungkin untuk menandai Halaman di WordPress, kita juga harus bisa melakukannya.
Jadi sekarang, Tag tidak lagi hanya berlaku untuk Postingan, tetapi juga berlaku untuk Halaman. Akibatnya, hubungan sederhana kami antara Tag dan Postingan tidak lagi memadai. Sebagai gantinya, kita membutuhkan hubungan polimorfik satu sisi banyak ke banyak, seperti berikut:
- Setiap Posting adalah Taggable dan memiliki banyak Tag
- Setiap Halaman adalah Taggable dan memiliki banyak Tag
- Setiap Tag memiliki banyak Taggable polimorfik
Transisi ke kumpulan asosiasi baru yang lebih kompleks ini kemungkinan akan memiliki konsekuensi yang signifikan di seluruh kode kita, menghasilkan banyak churn. Karena kami tidak tahu cara membuat serial asosiasi polimorfik ke JSON, kami mungkin hanya akan membuat lebih banyak titik akhir API seperti GET /posts/:id/tags
dan GET /pages/:id/tags
. Dan kemudian, kita akan membuang semua fungsi parser JSON yang ada dan menulis yang baru untuk sumber daya baru yang ditambahkan. Ugh. Membosankan dan menyakitkan.
Sekarang mari kita pertimbangkan bagaimana kita akan mendekati ini menggunakan Data Ember.
Di Data Ember, mengakomodasi kumpulan asosiasi yang dimodifikasi ini hanya akan melibatkan perpindahan dari:
App.Post = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Tag = DS.Model.extend({ post: DS.belongsTo('post', {async: true}) });
ke:
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}) });
Hasil churn di sisa kode kami akan minimal dan kami dapat menggunakan kembali sebagian besar template kami. Perhatikan khususnya bahwa nama asosiasi tags
pada Postingan tetap tidak berubah. Selain itu, sisa basis kode kami hanya bergantung pada keberadaan asosiasi tags
, dan tidak menyadari detailnya.
Primer Data Ember
Sebelum masuk ke contoh dunia nyata, mari kita tinjau beberapa dasar Ember Data.
Rute dan Model
Di Ember.js, router bertanggung jawab untuk menampilkan template, memuat data, dan mengatur status aplikasi. Router mencocokkan URL saat ini dengan rute yang telah Anda tetapkan, jadi Route bertanggung jawab untuk menentukan model yang akan ditampilkan oleh template (Ember mengharapkan model ini menjadi subkelas dari Ember.Object
):
App.ItemsRoute = Ember.Route.extend({ model: function(){ // GET /items // Retrieves all items. return this.modelFor('orders.show').get('items'); } });
Data Ember menyediakan DS.Model
yang merupakan subkelas dari Ember.Object
dan menambahkan kemampuan seperti menyimpan atau memperbarui satu atau beberapa catatan untuk kenyamanan.
Untuk membuat Model baru, kita membuat subkelas DS.Model
(misalnya, App.User = DS.Model.extend({})
).
Ember Data mengharapkan struktur JSON yang terdefinisi dengan baik dan intuitif dari server dan membuat serial catatan yang baru dibuat ke JSON terstruktur yang sama.
Ember Data juga menyediakan rangkaian kelas array seperti DS.RecordArray
untuk bekerja dengan Model. Ini memiliki tanggung jawab seperti menangani hubungan satu-ke-banyak atau banyak-ke-banyak, menangani pengambilan data asinkron, dan sebagainya.
Atribut Model
Atribut model dasar didefinisikan menggunakan DS.attr
; misalnya:
App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') });
Hanya bidang yang dibuat oleh DS.attr
yang akan disertakan dalam muatan yang diteruskan ke server untuk membuat atau memperbarui catatan.
DS.attr
menerima empat tipe data: string
, number
, boolean
dan date
.
RESTSerializer
Secara default:
- Data Ember menggunakan
RESTSerializer
untuk membuat objek dari respons API (deserialisasi) dan untuk menghasilkan JSON untuk permintaan API (serialisasi). -
RESTSerializer
mengharapkan bidang yang dibuat olehDS.belongsTo
memiliki bidang bernamauser
yang disertakan dalam respons JSON dari server. Bidang itu berisi id dari catatan yang direferensikan. -
RESTSerializer
menambahkan bidanguser
ke muatan yang diteruskan ke API dengan id dari pesanan terkait.
Sebuah tanggapan mungkin, misalnya, terlihat seperti ini:
GET http://api.example.com/orders?ids[]=19&ids[]=28 { "orders": [ { "id": "19", "createdAt": "1401492647008", "user": "1" }, { "id": "28", "createdAt": "1401492647008", "user": "1" } ] }
Dan permintaan HTTP yang dibuat oleh RESTSerializer
untuk menyimpan pesanan mungkin terlihat seperti ini:
POST http://api.example.com/orders { "order": { "createdAt": "1401492647008", "user": "1" } }
Hubungan satu-ke-satu
Katakanlah, misalnya, bahwa setiap Pengguna memiliki Profil unik. Kami dapat merepresentasikan hubungan ini dalam Data Ember menggunakan DS.belongsTo
pada Pengguna dan Profil:
App.User = DS.Model.extend({ profile: DS.belongsTo('profile', {async: true}) }); App.Profile = DS.Model.extend({ user: DS.belongsTo('user', {async: true}) });
Kami kemudian bisa mendapatkan asosiasi dengan user.get('profile')
atau mengaturnya dengan user.set('profile', aProfile)
.
RESTSerializer
mengharapkan ID model terkait disediakan untuk setiap model; misalnya:
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 */ } ] }
Demikian pula, ini menyertakan ID model terkait dalam payload permintaan:
POST /profiles { "profile": { "user": "17" /* ID of user associated with this profile */ } }
Hubungan satu-ke-banyak dan Banyak-ke-satu
Katakanlah kita memiliki model di mana Post memiliki banyak Komentar. Dalam Data Ember, kita dapat merepresentasikan hubungan ini dengan DS.hasMany('comment', {async: true})
pada Post dan DS.belongsTo('post', {async: true})
pada Comment:
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}) });
Kami kemudian bisa mendapatkan item terkait dengan post.get('comments', {async: true})
dan menambahkan asosiasi baru dengan post.get('comments').then(function(comments){ return comments.pushObject(aComment);})
.
Server kemudian akan merespons dengan serangkaian ID untuk komentar terkait pada Postingan:
GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }
… dan dengan ID untuk setiap Komentar:
GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] }
RESTSerializer
menambahkan id Posting terkait ke Komentar:
POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } }
Perhatikan bahwa, secara default, RESTSerializer, tidak akan menambahkan ID terkait DS.hasMany
ke objek yang diserialisasikan, karena asosiasi tersebut ditentukan di sisi "banyak" (yaitu, yang memiliki asosiasi DS.belongsTo
). Jadi, dalam contoh kita, meskipun Post memiliki banyak komentar, ID tersebut tidak akan ditambahkan ke objek Post:
POST /posts { "post": { "content": "" /* no associated post IDs added here */ } }
Untuk "memaksa" DS.hasMany
ID menjadi serial juga, Anda dapat menggunakan Embedded Records Mixin.
Hubungan Banyak-ke-banyak
Katakan bahwa dalam model kami, seorang Penulis mungkin memiliki beberapa Postingan dan sebuah Postingan mungkin memiliki beberapa Penulis.
Untuk merepresentasikan hubungan ini dalam Data Ember, kita dapat menggunakan DS.hasMany('author', {async: true})
pada Post dan DS.hasMany('post', {async: true})
pada Author:
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}) });
Kita kemudian bisa mendapatkan item terkait dengan author.get('posts')
dan menambahkan asosiasi baru dengan author.get('posts').then(function(posts){ return posts.pushObject(aPost);})
.
Server kemudian akan merespon dengan array ID untuk objek yang sesuai; misalnya:
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 */ } ] }
Karena ini adalah hubungan banyak ke banyak, RESTSerializer menambahkan larik ID objek terkait; misalnya:
POST /posts { "post": { "content": "", "authors": ["1", "4"] /* IDs of authors associated with this post */ } }
Contoh Dunia Nyata: Meningkatkan Sistem Pemesanan yang Ada
Dalam sistem pemesanan kami yang ada, setiap Pengguna memiliki banyak Pesanan dan setiap Pesanan memiliki banyak Barang. Sistem kami memiliki beberapa Penyedia (yaitu, vendor) dari siapa produk dapat dipesan, tetapi setiap pesanan hanya dapat berisi item dari satu penyedia.
Persyaratan baru #1: Aktifkan satu pesanan untuk menyertakan item dari beberapa penyedia.
Dalam sistem yang ada, ada hubungan satu-ke-banyak antara Penyedia dan Pesanan. Namun, begitu kami memperluas pesanan untuk menyertakan item dari beberapa penyedia, hubungan sederhana ini tidak lagi memadai.
Khususnya, jika penyedia dikaitkan dengan seluruh pesanan, dalam sistem yang disempurnakan, pesanan itu mungkin juga mencakup item yang dipesan dari penyedia lain. Oleh karena itu, perlu ada cara untuk menunjukkan bagian mana dari setiap pesanan yang relevan untuk setiap penyedia. Selain itu, ketika penyedia mengakses pesanan mereka, mereka seharusnya hanya memiliki visibilitas ke item yang dipesan dari mereka, bukan item lain yang mungkin dipesan pelanggan dari penyedia lain.
Salah satu pendekatannya adalah dengan memperkenalkan dua asosiasi banyak-ke-banyak yang baru; satu antara Pesanan dan Barang dan satu lagi antara Pesanan dan Penyedia.
Namun, untuk membuatnya lebih sederhana, kami memperkenalkan konstruksi baru ke dalam model data yang kami sebut sebagai "Pesanan Penyedia".
Merancang Hubungan
Model data yang disempurnakan perlu mengakomodasi asosiasi berikut:
Hubungan satu-ke-banyak antara Pengguna dan Pesanan (setiap Pengguna dapat dikaitkan dengan 0 hingga n Pesanan) dan hubungan Satu-ke-banyak antara Pengguna dan Penyedia (setiap Pengguna dapat dikaitkan dengan 0 hingga n Penyedia)
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}) });
Hubungan satu-ke-banyak antara Pesanan dan ProviderOrders (setiap Pesanan terdiri dari 1 hingga n ProviderOrders):
App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });
Hubungan satu-ke-banyak antara Penyedia dan ProviderOrders (setiap Provider dapat dikaitkan dengan 0 hingga 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}) });
Hubungan satu-ke-banyak antara ProviderOrders dan Items (setiap ProviderOrder terdiri dari 1 hingga n item):
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}) });
Dan jangan lupa definisi Rute kami:
App.OrdersRoute = Ember.Route.extend({ model: function(){ // GET /orders // Retrieves all orders. return this.store.find('order'); } });
Sekarang setiap ProviderOrder memiliki satu Provider, yang merupakan tujuan utama kami. Item dipindahkan dari Order ke ProviderOrder dan asumsinya adalah bahwa semua item dalam satu ProviderOrder milik satu Provider.
Meminimalkan Kode Churn
Sayangnya, ada beberapa perubahan yang melanggar di sini. Jadi mari kita lihat bagaimana Ember Data dapat membantu kita meminimalkan churn kode yang dihasilkan di basis kode kita.
Sebelumnya, kami mendorong item dengan items.pushObject(item)
. Sekarang kita harus terlebih dahulu menemukan ProviderOrder yang sesuai dan mendorong Item ke sana:
order.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); });
Karena ini banyak churn, dan lebih banyak pekerjaan Order daripada controller, lebih baik jika kita memindahkan kode ini ke 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); }); }); } });
Sekarang kita dapat menambahkan item langsung pada pesanan seperti order.pushItem(item)
.
Dan untuk daftar Item dari setiap Pesanan:
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'); } });
Hubungan Polimorfik
Sekarang mari kita perkenalkan permintaan peningkatan tambahan ke sistem kami yang semakin memperumit masalah:
Persyaratan baru #2: Mendukung berbagai jenis Penyedia.
Untuk contoh sederhana kami, katakanlah dua jenis Penyedia ("Toko" dan "Toko Buku") didefinisikan:
App.Shop = App.Provider.extend({ status: DS.attr('string') }); App.BookStore = App.Provider.extend({ name: DS.attr('string') });
Di sinilah dukungan Ember Data untuk hubungan polimorfik akan berguna. Data Ember mendukung hubungan polimorfik satu-ke-satu, satu-ke-banyak, dan banyak-ke-banyak. Ini dilakukan hanya dengan menambahkan atribut polymorphic: true
ke spesifikasi asosiasi. Sebagai contoh:
App.Provider = DS.Model.extend({ providerOrders: DS.hasMany('providerOrder', {async: true}) }); App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}) });
Bendera polymorphic
di atas menunjukkan bahwa ada berbagai jenis Penyedia yang dapat dikaitkan dengan Pesanan Penyedia (dalam kasus kami, baik Toko atau Toko Buku).
Ketika suatu relasi bersifat polimorfik, respons server harus menunjukkan ID dan tipe objek yang dikembalikan ( RESTSerializer
melakukan ini secara default); misalnya:
GET /providerOrders { "providerOrders": [{ "status": "in_delivery", "provider": 1, "providerType": "shop" }] }
Memenuhi Persyaratan Baru
Kami membutuhkan Penyedia dan Item polimorfik untuk memenuhi persyaratan. Karena ProviderOrder menghubungkan Penyedia dengan Item, kita dapat mengubah asosiasinya menjadi asosiasi polimorfik:
App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });
Ada masalah yang tersisa, meskipun: Penyedia memiliki asosiasi non-polimorfik ke Item tetapi Item adalah tipe abstrak. Oleh karena itu, kami memiliki dua opsi untuk mengatasi hal ini:
- Mengharuskan semua penyedia untuk dikaitkan dengan jenis barang yang sama (yaitu, menyatakan jenis Barang tertentu untuk dikaitkan dengan Penyedia)
- Deklarasikan asosiasi
items
pada Penyedia sebagai polimorfik
Dalam kasus kami, kami harus menggunakan opsi #2 dan mendeklarasikan asosiasi items
pada Penyedia sebagai polimorfik:

App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });
Perhatikan bahwa ini tidak memperkenalkan churn kode apa pun ; semua asosiasi hanya bekerja seperti yang mereka lakukan sebelum perubahan ini. Kekuatan Data Ember yang terbaik!
Bisakah Ember Data benar-benar memodelkan semua data saya?
Tentu saja ada pengecualian, tetapi saya menganggap konvensi ActiveRecord sebagai cara standar dan fleksibel untuk menyusun dan memodelkan data, jadi izinkan saya menunjukkan kepada Anda bagaimana konvensi ActiveRecord memetakan ke Data Ember:
has_many :pengguna melalui: :kepemilikan atau Mewakili Model Menengah
Ini akan berkonsultasi dengan model pivot yang disebut Kepemilikan untuk menemukan Pengguna terkait. Jika model pivot pada dasarnya adalah tabel pivot , Anda dapat menghindari pembuatan model perantara di Ember Data dan mewakili hubungan dengan DS.hasMany
di kedua sisi.
Namun jika Anda memerlukan hubungan pivot di dalam front-end Anda, siapkan model Kepemilikan yang menyertakan DS.belongsTo('user', {async: true})
dan DS.belongsTo('provider', {async: true})
, dan kemudian menambahkan properti pada Pengguna dan Penyedia yang memetakan melalui asosiasi menggunakan Kepemilikan; misalnya:
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 :pemetaan, sebagai: dapat ditemukan
Dalam objek ActiveRecord kami, kami memiliki hubungan polimorfik yang khas:
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
Ini adalah hubungan banyak (polimorfik) ke banyak (non-polimorfik normal). Dalam Data Ember kita dapat mengekspresikannya dengan DS.hasMany('locatable', {polymorphic: true, async: true})
dan DS.hasMany('location', {async: true})
statis :
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}) });
Untuk Locatables, seperti Pengguna, server harus mengembalikan ID untuk lokasi terkait:
GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }
Untuk Lokasi, server harus mengembalikan ID dan tipe Locatable dalam larik objek:
GET /locations { "locations": [ { "id": "1", "locatables": [ {"id": "1", "type": "user"}, {"id": "2", "type": "provider"} ] } ] }
Selain itu, Anda dapat merepresentasikan hubungan menurut jenisnya dengan hubungan banyak-ke-banyak statis:
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}) });
Dan bagaimana dengan data waktu nyata?
Data Ember memiliki push
, pushPayload
, dan update
. Anda selalu dapat secara manual mendorong catatan baru/yang diperbarui ke cache lokal Data Ember (disebut toko) dan itu akan menangani sisanya.
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); }); } });
Saya pribadi hanya menggunakan soket untuk acara dengan muatan yang sangat kecil. Peristiwa khas adalah 'recordUpdated' dengan muatan {"type": "shop", "id": "14"}
dan kemudian di ApplicationRoute saya akan memeriksa apakah catatan itu ada di cache lokal (toko) dan jika itu saya hanya akan mengambilnya kembali.
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); } }); } });
Dengan cara ini kami dapat mengirim rekaman acara yang diperbarui ke semua klien tanpa biaya tambahan yang tidak dapat diterima.
Pada dasarnya ada dua pendekatan dalam Data Ember untuk menangani data waktu nyata:
- Tulis Adaptor untuk saluran komunikasi waktu nyata Anda dan gunakan sebagai pengganti RESTAdapter.
- Dorong rekaman ke toko utama kapan pun tersedia.
Kelemahan dengan opsi pertama adalah agak mirip dengan menciptakan kembali roda. Untuk opsi kedua, kita perlu mengakses toko utama, yang tersedia di semua rute sebagai route#store
.
Bungkus
Dalam artikel ini, kami telah memperkenalkan Anda pada konstruksi dan paradigma utama Ember Data, yang menunjukkan nilai yang dapat diberikannya kepada Anda sebagai pengembang. Ember Data menyediakan alur kerja pengembangan yang lebih fleksibel dan ramping, meminimalkan churn kode sebagai respons terhadap apa yang seharusnya menjadi perubahan berdampak tinggi.
Investasi di muka (kurva waktu dan pembelajaran) yang Anda buat dalam menggunakan Data Ember untuk proyek Anda pasti akan terbukti bermanfaat karena sistem Anda pasti berkembang dan perlu diperpanjang, dimodifikasi, dan ditingkatkan.
LAMPIRAN: Topik Data Ember Tingkat Lanjut
Lampiran ini memperkenalkan sejumlah topik Data Ember yang lebih canggih termasuk:
- Desain Modular Ember
- Sideloading
- Tautan
- Serializer dan Adaptor Model Aktif
- Campuran Rekaman Tertanam
- Pengubah Asosiasi (Async, Inverse, dan Polymorphic)
- Parameter 'id' dalam GET Requests
Desain modular
Ember Data memiliki desain modular di bawah tenda. Komponen utama meliputi:
-
Adapters
bertanggung jawab untuk menangani komunikasi, saat ini hanya REST melalui HTTP. -
Serializers
mengelola pembuatan model dari JSON atau sebaliknya. -
Store
cache yang dibuat catatan. -
Container
merekatkan semua ini bersama-sama.
Manfaat dari desain ini antara lain:
- Deserialisasi dan penyimpanan data bekerja secara independen dari saluran komunikasi yang digunakan dan sumber daya yang diminta.
- Konfigurasi yang berfungsi, seperti ActiveModelSerializer atau EmbeddedRecordsMixin, disediakan secara langsung.
- Sumber data (misalnya, Penyimpanan Lokal, implementasi CouchDB, dll.) dapat ditukar masuk dan keluar dengan mengubah adaptor.
- Meskipun banyak konvensi tentang konfigurasi, dimungkinkan untuk mengonfigurasi semuanya dan membagikan konfigurasi/implementasi Anda dengan komunitas.
Sideloading
Ember Data mendukung “sideloading” data; yaitu, menunjukkan data tambahan yang harus diambil (bersama dengan data primer yang diminta) untuk membantu mengkonsolidasikan beberapa permintaan HTTP terkait.
Kasus penggunaan yang umum adalah mengesampingkan model terkait. Misalnya, setiap Toko memiliki banyak bahan makanan, jadi kami dapat memasukkan semua bahan makanan terkait di /shops
response:
GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ] }
Saat asosiasi bahan makanan diakses, Data Ember akan mengeluarkan:
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" } ] }
Namun, jika kami mengembalikan Groceries terkait di /shops
endpoint, Ember Data tidak perlu mengeluarkan permintaan lain:
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" } ] }
Tautan
Data Ember menerima tautan sebagai pengganti ID asosiasi. Saat asosiasi yang ditentukan sebagai tautan diakses, Ember Data akan mengeluarkan permintaan GET ke tautan itu untuk mendapatkan catatan terkait.
Misalnya, kami dapat mengembalikan tautan untuk asosiasi bahan makanan:
GET /shops { "shops": [ { "id": "14", "links": { "groceries": "/shops/14/groceries" } } ] }
Dan Ember Data kemudian akan mengeluarkan permintaan ke /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" } ] }
Ingatlah bahwa Anda masih perlu merepresentasikan asosiasi dalam data; tautan hanya menyarankan permintaan HTTP baru dan tidak akan memengaruhi asosiasi.
Serializer dan Adaptor Model Aktif
Diperdebatkan, ActiveModelSerializer
dan ActiveModelAdapter
lebih banyak digunakan dalam praktik daripada RESTSerializer
dan RESTAdapter
. Khususnya, ketika backend menggunakan Ruby on Rails dan permata ActiveModel::Serializers
, opsi terbaik adalah menggunakan ActiveModelSerializer
dan ActiveModelAdapter
, karena keduanya mendukung ActiveModel::Serializers
di luar kotak.
Untungnya, perbedaan penggunaan antara ActiveModelSerializer
/ ActiveModelAdapter
dan RESTSerializer
/ RESTAdapter
cukup terbatas; yaitu:
-
ActiveModelSerializer
akan menggunakan nama bidang snake_case sementaraRESTSerializer
membutuhkan nama bidang camelCased. -
ActiveModelAdapter
mengeluarkan permintaan ke metode API snake_case sementaraRESTSerializer
mengeluarkan permintaan ke metode API camelCased. -
ActiveModelSerializer
mengharapkan nama bidang terkait asosiasi diakhiri dengan_id
atau_ids
sementaraRESTSerializer
mengharapkan nama bidang terkait asosiasi sama dengan bidang asosiasi.
Terlepas dari pilihan Adaptor dan Serializer, model Ember Data akan sama persis. Hanya representasi JSON dan titik akhir API yang akan berbeda.
Ambil ProviderOrder terakhir kami sebagai contoh:
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}) });
Dengan Serializer dan Adaptor Model Aktif, server harus mengharapkan:
Post /provider_orders { "provider_order": [ "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", ] }
… dan harus merespons dengan:
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"} ] ] }
Campuran Rekaman Tertanam
DS.EmbeddedRecordsMixin
adalah ekstensi untuk DS.ActiveModelSerializer
yang memungkinkan konfigurasi bagaimana asosiasi menjadi serial atau deserialized. Meskipun belum lengkap (terutama yang berkaitan dengan asosiasi polimorfik), ini tetap menarik.
Kamu bisa memilih:
- Tidak untuk membuat serial atau deserialize asosiasi.
- Untuk membuat serial atau deserialize asosiasi dengan id atau id.
- Untuk membuat serial atau deserialize asosiasi dengan model yang disematkan.
Ini sangat berguna dalam hubungan satu-ke-banyak di mana, secara default, DS.hasMany
terkait ID tidak ditambahkan ke objek yang serial. Ambil keranjang belanja yang memiliki banyak barang sebagai contoh. Dalam contoh ini, Keranjang sedang dibuat saat Item diketahui. Namun, saat Anda menyimpan Keranjang, Data Ember tidak akan secara otomatis memasukkan ID Item terkait pada payload permintaan.
Namun, menggunakan DS.EmbeddedRecordsMixin
, dimungkinkan untuk memberi tahu Ember Data untuk membuat serial ID item pada Keranjang sebagai berikut:
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}) });
Seperti yang ditunjukkan dalam contoh di atas, EmbeddedRecordsMixin memungkinkan spesifikasi eksplisit dari asosiasi mana yang akan dibuat serial dan/atau deserialize melalui objek attrs
. Nilai yang valid untuk serialize
dan deserialize
adalah: - 'no'
: jangan sertakan asosiasi dalam data serial/deserialisasi - 'id'
atau 'ids'
: sertakan ID terkait dalam data serial/deserialisasi - 'records
': sertakan properti aktual (yaitu, merekam nilai bidang) sebagai larik dalam data serial/deserialisasi
Pengubah Asosiasi (Async, Inverse, dan Polymorphic)
Pengubah asosiasi berikut didukung: polymorphic
, inverse
, dan async
Pengubah Polimorfik
Dalam asosiasi polimorfik, satu atau kedua sisi asosiasi mewakili kelas objek, bukan objek tertentu.
Ingat contoh blog kami sebelumnya di mana kami perlu mendukung kemampuan untuk menandai pos dan halaman. Untuk mendukung ini, kami telah sampai pada model berikut:
- Setiap Posting adalah Taggable dan memiliki banyak Tag
- Setiap Halaman adalah Taggable dan memiliki banyak Tag
- Setiap Tag memiliki banyak Taggable polimorfik
Mengikuti model itu, pengubah polymorphic
dapat digunakan untuk menyatakan bahwa Tag terkait dengan semua jenis "Taggable" (yang dapat berupa Postingan atau Halaman), sebagai berikut:
// 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}) });
Pengubah Terbalik
Biasanya asosiasi bersifat dua arah. Misalnya, "Posting memiliki banyak Komentar" akan menjadi satu arah asosiasi, sedangkan "Komentar milik sebuah Postingan" akan menjadi arah lain (yaitu, "terbalik") dari asosiasi itu.
Dalam kasus di mana tidak ada ambiguitas dalam asosiasi, hanya satu arah yang perlu ditentukan karena Ember Data dapat menyimpulkan bagian kebalikan dari asosiasi.
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; misalnya:
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: [])
.