Dane Ember: kompleksowy samouczek dotyczący biblioteki danych ember
Opublikowany: 2022-03-11Ember Data (aka ember-data lub ember.data) to biblioteka do niezawodnego zarządzania danymi modelu w aplikacjach Ember.js. Twórcy Ember Data twierdzą, że jest on zaprojektowany tak, aby był niezależny od bazowego mechanizmu utrwalania, więc działa równie dobrze z interfejsami API JSON przez HTTP, jak ze strumieniowymi WebSockets lub lokalnym magazynem IndexedDB. Zapewnia wiele udogodnień, które można znaleźć w mapowaniach relacyjnych obiektów po stronie serwera (ORM), takich jak ActiveRecord, ale jest zaprojektowany specjalnie dla unikalnego środowiska JavaScript w przeglądarce.
Chociaż Ember Data może zająć trochę czasu, gdy to zrobisz, prawdopodobnie uznasz, że było to warte zainwestowania. Ostatecznie znacznie ułatwi to rozwój, ulepszanie i konserwację Twojego systemu.
Gdy interfejs API jest reprezentowany za pomocą modeli danych, adapterów i serializatorów Ember, każde powiązanie staje się po prostu nazwą pola. Zawiera to wewnętrzne szczegóły każdej asocjacji, izolując w ten sposób resztę kodu przed zmianami w samych asocjacjach. Reszta twojego kodu nie będzie obchodzić, na przykład, czy konkretna asocjacja jest polimorficzna, czy jest wynikiem mapy wielu asocjacji.
Co więcej, twoja baza kodu jest w dużej mierze odizolowana od zmian zaplecza, nawet jeśli są one znaczące, ponieważ wszystko, czego oczekuje twoja baza kodu, to pola i funkcje w modelach, a nie reprezentacja modelu w formacie JSON, XML lub YAML.
W tym samouczku przedstawimy najistotniejsze funkcje Ember Data i zademonstrujemy, w jaki sposób pomaga zminimalizować utratę kodu, skupiając się na przykładzie ze świata rzeczywistego.
Dostępny jest również załącznik, w którym omówiono szereg bardziej zaawansowanych tematów i przykładów dotyczących danych Ember.
Uwaga: ten artykuł zakłada podstawową znajomość Ember.js. Jeśli nie znasz Ember.js, zapoznaj się z naszym popularnym samouczkiem Ember.js, aby uzyskać wprowadzenie. Mamy również pełny przewodnik po JavaScript dostępny w języku rosyjskim, portugalskim i hiszpańskim.
Propozycja wartości danych Ember
Zacznijmy od rozważenia prostego przykładu.
Powiedzmy, że mamy działającą bazę kodu dla podstawowego systemu blogowego. System zawiera posty i tagi, które mają ze sobą relacje wiele-do-wielu.
Wszystko jest w porządku, dopóki nie otrzymamy wymogu obsługi Pages. Wymaganie mówi również, że skoro możliwe jest otagowanie strony w WordPressie, my również powinniśmy być w stanie to zrobić.
Dlatego teraz tagi nie będą już dotyczyć tylko postów, mogą również dotyczyć stron. W rezultacie nasze proste powiązanie między tagami a postami nie będzie już wystarczające. Zamiast tego będziemy potrzebować jednostronnej polimorficznej relacji wiele do wielu, takiej jak:
- Każdy post jest oznaczony tagiem i ma wiele tagów
- Każda strona jest tagowalna i ma wiele tagów
- Każdy tag ma wiele polimorficznych tagów
Przejście do tego nowego, bardziej złożonego zestawu skojarzeń prawdopodobnie będzie miało znaczące konsekwencje w całym naszym kodzie, powodując wiele rezygnacji. Ponieważ nie mamy pojęcia, jak serializować powiązanie polimorficzne do JSON, prawdopodobnie utworzymy więcej punktów końcowych API, takich jak GET /posts/:id/tags i GET /pages/:id/tags . Następnie wyrzucimy wszystkie nasze istniejące funkcje parsera JSON i napiszemy nowe dla dodanych nowych zasobów. Uch. Żmudne i bolesne.
Zastanówmy się teraz, jak podeszlibyśmy do tego za pomocą danych Ember.
W Ember Data dostosowanie tego zmodyfikowanego zestawu skojarzeń wymagałoby po prostu przejścia z:
App.Post = DS.Model.extend({ tags: DS.hasMany('tag', {async: true}) }); App.Tag = DS.Model.extend({ post: DS.belongsTo('post', {async: true}) });do:
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}) }); Wynikająca z tego rezygnacja z pozostałej części naszego kodu byłaby minimalna i bylibyśmy w stanie ponownie wykorzystać większość naszych szablonów. Zwróć uwagę w szczególności, że nazwa powiązania tags w Poście pozostaje niezmieniona. Ponadto reszta naszego kodu opiera się tylko na istnieniu powiązania tags i jest nieświadoma jego szczegółów.
Ember Data Primer
Zanim zagłębimy się w przykład ze świata rzeczywistego, przyjrzyjmy się podstawom dotyczącym danych Ember.
Trasy i modele
W Ember.js router odpowiada za wyświetlanie szablonów, ładowanie danych i inne konfigurowanie stanu aplikacji. Router dopasowuje bieżący adres URL do tras, które zostały zdefiniowane, więc Route odpowiada za określenie modelu, który ma wyświetlić szablon (Ember oczekuje, że ten model będzie podklasą Ember.Object ):
App.ItemsRoute = Ember.Route.extend({ model: function(){ // GET /items // Retrieves all items. return this.modelFor('orders.show').get('items'); } }); Ember Data zapewnia DS.Model , który jest podklasą Ember.Object i dodaje możliwości, takie jak zapisywanie lub aktualizowanie pojedynczego rekordu lub wielu rekordów dla wygody.
Aby stworzyć nowy model, tworzymy podklasę DS.Model (np. App.User = DS.Model.extend({}) ).
Ember Data oczekuje od serwera dobrze zdefiniowanej, intuicyjnej struktury JSON i serializuje nowo utworzone rekordy do tej samej struktury JSON.
Ember Data zapewnia również zestaw klas tablicowych, takich jak DS.RecordArray do pracy z modelami. Mają one obowiązki, takie jak obsługa relacji jeden-do-wielu lub wiele-do-wielu, obsługa asynchronicznego pobierania danych i tak dalej.
Atrybuty modelu
Podstawowe atrybuty modelu są definiowane za pomocą DS.attr ; np:
App.User = DS.Model.extend({ firstName: DS.attr('string'), lastName: DS.attr('string') }); Tylko pola utworzone przez DS.attr zostaną uwzględnione w ładunku przekazywanym do serwera w celu tworzenia lub aktualizowania rekordów.
DS.attr akceptuje cztery typy danych: string , number , boolean i date .
RESTerializator
Domyślnie:
- Ember Data wykorzystuje
RESTSerializerdo tworzenia obiektów z odpowiedzi API (deserializacja) oraz do generowania JSON dla żądań API (serializacja). -
RESTSerializeroczekuje, że pola utworzone przezDS.belongsTobędą miały pole o nazwieuserzawarte w odpowiedzi JSON z serwera. To pole zawiera identyfikator rekordu, do którego się odwołuje. -
RESTSerializerdodaje poleuserdo ładunku przekazanego do interfejsu API z identyfikatorem skojarzonego zamówienia.
Odpowiedź może na przykład wyglądać tak:
GET http://api.example.com/orders?ids[]=19&ids[]=28 { "orders": [ { "id": "19", "createdAt": "1401492647008", "user": "1" }, { "id": "28", "createdAt": "1401492647008", "user": "1" } ] } A żądanie HTTP utworzone przez RESTSerializer w celu zapisania zamówienia może wyglądać tak:
POST http://api.example.com/orders { "order": { "createdAt": "1401492647008", "user": "1" } }Relacje jeden-do-jednego
Powiedzmy na przykład, że każdy Użytkownik ma unikalny Profil. Możemy reprezentować tę relację w Ember Data za pomocą DS.belongsTo zarówno w przypadku użytkownika, jak i profilu:
App.User = DS.Model.extend({ profile: DS.belongsTo('profile', {async: true}) }); App.Profile = DS.Model.extend({ user: DS.belongsTo('user', {async: true}) }); Następnie możemy uzyskać powiązanie za pomocą user.get('profile') lub ustawić je za pomocą user.set('profile', aProfile) .
RESTSerializer oczekuje, że dla każdego modelu zostanie podany identyfikator skojarzonego modelu; np:
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 */ } ] }Podobnie zawiera powiązany identyfikator modelu w ładunku żądania:
POST /profiles { "profile": { "user": "17" /* ID of user associated with this profile */ } }Relacje jeden-do-wielu i wiele-do-jednego
Załóżmy, że mamy model, w którym post ma wiele komentarzy. W Ember Data możemy przedstawić tę relację za pomocą DS.hasMany('comment', {async: true}) on Post i DS.belongsTo('post', {async: true}) on 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}) }); Następnie możemy uzyskać powiązane elementy za pomocą post.get('comments', {async: true}) i dodać nowe skojarzenie za pomocą post.get('comments').then(function(comments){ return comments.pushObject(aComment);}) .
Serwer odpowie następnie tablicą identyfikatorów dla odpowiednich komentarzy do posta:
GET /posts { "posts": [ { "id": "12", "content": "", "comments": ["56", "58"] } ] }… oraz z identyfikatorem dla każdego komentarza:
GET /comments?ids[]=56&ids[]=58 { "comments": [ { "id": "56", "message": "", "post": "12" }, { "id": "58", "message": "", "post": "12" } ] } RESTSerializer dodaje identyfikator powiązanego posta do komentarza:
POST /comments { "comment": { "message": "", "post": "12" /* ID of post associated with this comment */ } } Należy jednak zauważyć, że domyślnie RESTSerializer nie doda powiązanych identyfikatorów DS.hasMany do obiektów, które serializuje, ponieważ te powiązania są określone po stronie „wiele” (tj. te, które mają powiązanie DS.belongsTo ). Tak więc w naszym przykładzie, chociaż Post ma wiele komentarzy, te identyfikatory nie zostaną dodane do obiektu Post:
POST /posts { "post": { "content": "" /* no associated post IDs added here */ } } Aby „zmusić” DS.hasMany ID do serializacji, możesz użyć miksera Embedded Records Mixin.
Relacje wiele do wielu
Załóżmy, że w naszym modelu autor może mieć wielu postów, a post może mieć wielu autorów.
Aby przedstawić tę relację w Ember Data, możemy użyć DS.hasMany('author', {async: true}) na Post i DS.hasMany('post', {async: true}) na 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}) }); Następnie możemy pobrać powiązane elementy za pomocą author.get('posts') i dodać nowe skojarzenie za pomocą author.get('posts').then(function(posts){ return posts.pushObject(aPost);}) .
Serwer odpowie następnie tablicą identyfikatorów dla odpowiednich obiektów; np:
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 */ } ] }Ponieważ jest to relacja wiele-do-wielu, RESTSerializer dodaje tablicę identyfikatorów skojarzonych obiektów; np:
POST /posts { "post": { "content": "", "authors": ["1", "4"] /* IDs of authors associated with this post */ } }Przykład ze świata rzeczywistego: ulepszanie istniejącego systemu zamówień
W naszym istniejącym systemie zamówień każdy Użytkownik ma wiele Zamówień, a każde Zamówienie ma wiele Pozycji. Nasz system ma wielu dostawców (tj. dostawców), od których można zamawiać produkty, ale każde zamówienie może zawierać tylko pozycje od jednego dostawcy.
Nowe wymaganie nr 1: Włącz w jednym zamówieniu pozycje od wielu dostawców.
W istniejącym systemie istnieje relacja jeden-do-wielu między dostawcami a zamówieniami. Kiedy jednak rozszerzymy zamówienie na produkty od wielu dostawców, ta prosta zależność nie będzie już wystarczająca.
W szczególności, jeśli dostawca jest powiązany z całym zamówieniem, w rozszerzonym systemie to zamówienie może równie dobrze obejmować pozycje zamówione u innych dostawców. Dlatego musi istnieć sposób na wskazanie, która część każdego zamówienia jest istotna dla każdego dostawcy. Ponadto, gdy dostawca uzyskuje dostęp do swoich zamówień, powinien mieć wgląd tylko w zamówione u niego pozycje, a nie w inne pozycje, które klient mógł zamówić u innych dostawców.
Jednym z podejść mogłoby być wprowadzenie dwóch nowych stowarzyszeń wiele-do-wielu; jeden między Zamówieniem a Pozycją, a drugi między Zamówieniem a Dostawcą.
Jednak, aby uprościć sprawę, wprowadzamy do modelu danych nową konstrukcję, którą nazywamy „ProviderOrder”.
Relacje kreślarskie
Ulepszony model danych będzie musiał uwzględniać następujące asocjacje:
Relacja jeden-do-wielu między Użytkownikami a Zamówieniami (każdy Użytkownik może być powiązany z 0 do n Zamówieniami) oraz relacja Jeden-do-wielu między Użytkownikami a Dostawcami (każdy Użytkownik może być powiązany z 0 do n Dostawcami)
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}) });Relacja jeden-do-wielu między zamówieniami a zamówieniami dostawcy (każde zamówienie składa się z 1 do n zamówień dostawcy):
App.Order = DS.Model.extend({ createdAt: DS.attr('date'), user: DS.belongsTo('user', {async: true}), providerOrders: DS.hasMany('providerOrders', {async: true}) });Relacja jeden-do-wielu między dostawcami a zamówieniami dostawców (każdy dostawca może być powiązany z 0 do n zamówieniami dostawców):
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}) });Relacja jeden-do-wielu między zamówieniami dostawcy a pozycjami (każde zamówienie dostawcy składa się z 1 do n pozycji):
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}) });
I nie zapominajmy o naszej definicji trasy :
App.OrdersRoute = Ember.Route.extend({ model: function(){ // GET /orders // Retrieves all orders. return this.store.find('order'); } });Teraz każde zamówienie dostawcy ma jednego dostawcę, co było naszym głównym celem. Pozycje są przenoszone z Order do ProviderOrder i zakłada się, że wszystkie pozycje w jednym ProviderOrder należą do jednego dostawcy.
Minimalizowanie zmian kodu
Niestety jest tu kilka przełomowych zmian. Zobaczmy więc, jak Ember Data może pomóc nam zminimalizować wszelkie wynikowe zmiany kodu w naszej bazie kodu.
Wcześniej wypychaliśmy elementy za pomocą items.pushObject(item) . Teraz musimy najpierw znaleźć odpowiedni ProviderOrder i wepchnąć do niego element:
order.get('providerOrders').then(function(providerOrders){ return providerOrders.findBy('id', item.get('provider.id') ) .get('items').then(functions(items){ return items.pushObject(item); }); }); Ponieważ jest to dużo churn i więcej pracy Orderu niż kontrolera, lepiej, jeśli przeniesiemy ten kod do 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); }); }); } }); Teraz możemy dodawać elementy bezpośrednio na zamówienie, takie jak order.pushItem(item) .
A do wystawiania pozycji każdego zamówienia:
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'); } });Związki polimorficzne
Wprowadźmy teraz do naszego systemu dodatkową prośbę o ulepszenie, która jeszcze bardziej komplikuje sprawę:
Nowe wymaganie nr 2: Obsługa wielu typów dostawców.
Dla naszego prostego przykładu załóżmy, że zdefiniowano dwa rodzaje dostawców („Sklep” i „Księgarnia”):
App.Shop = App.Provider.extend({ status: DS.attr('string') }); App.BookStore = App.Provider.extend({ name: DS.attr('string') }); W tym miejscu przyda się wsparcie Ember Data dla relacji polimorficznych. Ember Data obsługuje relacje polimorficzne jeden-do-jednego, jeden-do-wielu i wiele-do-wielu. Odbywa się to po prostu przez dodanie atrybutu polymorphic: true zgodnie ze specyfikacją powiązania. Na przykład:
App.Provider = DS.Model.extend({ providerOrders: DS.hasMany('providerOrder', {async: true}) }); App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}) }); Powyższa flaga polymorphic wskazuje, że istnieją różne typy dostawców, które mogą być powiązane z zamówieniem dostawcy (w naszym przypadku sklep lub księgarnia).
Gdy relacja jest polimorficzna, odpowiedź serwera powinna wskazywać zarówno identyfikator, jak i typ zwróconego obiektu ( RESTSerializer robi to domyślnie); np:
GET /providerOrders { "providerOrders": [{ "status": "in_delivery", "provider": 1, "providerType": "shop" }] }Spełnianie nowych wymagań
Aby spełnić wymagania, potrzebujemy dostawców i elementów polimorficznych. Ponieważ ProviderOrder łączy dostawców z pozycjami, możemy zmienić jego asocjacje na asocjacje polimorficzne:
App.ProviderOrder = DS.Model.extend({ provider: DS.belongsTo('provider', {polymorphic: true, async: true}), items: DS.hasMany('item', {polymorphic: true, async: true}) });Pozostaje jednak problem: Dostawca ma niepolimorficzne skojarzenie z elementami, ale element jest typem abstrakcyjnym. Mamy zatem dwie możliwości rozwiązania tego problemu:
- Wymagaj, aby wszyscy dostawcy byli powiązani z tym samym typem pozycji (tj. zadeklaruj określony typ pozycji dla powiązania z dostawcą)
- Zadeklaruj powiązanie
itemsu dostawcy jako polimorficzne
W naszym przypadku musimy skorzystać z opcji nr 2 i zadeklarować skojarzenie items na Provider jako polimorficzne:

App.Provider = DS.Model.extend({ /* ... */ items: DS.hasMany('items', {polymorphic: true, async: true}) });Zauważ, że nie powoduje to zmiany kodu; wszystkie stowarzyszenia po prostu działają tak samo, jak przed tą zmianą. Moc Ember Data w najlepszym wydaniu!
Czy Ember Data naprawdę może modelować wszystkie moje dane?
Są oczywiście wyjątki, ale uważam konwencje ActiveRecord za standardowy i elastyczny sposób strukturyzacji i modelowania danych, więc pozwólcie, że pokażę, jak konwencje ActiveRecord mapują się na dane Ember:
has_many :users poprzez: :ownerships lub Reprezentowanie modeli pośrednich
Spowoduje to sprawdzenie modelu przestawnego o nazwie Własność, aby znaleźć powiązanych użytkowników. Jeśli model przestawny jest w zasadzie tabelą przestawną , możesz uniknąć tworzenia modelu pośredniego w Ember Data i reprezentować relację z DS.hasMany po obu stronach.
Jeśli jednak potrzebujesz tej relacji przestawnej w swoim interfejsie, skonfiguruj model własności, który obejmuje DS.belongsTo('user', {async: true}) i DS.belongsTo('provider', {async: true}) , a następnie dodać właściwość zarówno dla Użytkowników, jak i Dostawców, która odwzorowuje powiązanie za pomocą Własności; np:
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, jako: lokalizowalne
W naszym obiekcie ActiveRecord mamy typową relację polimorficzną:
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 Jest to relacja wiele (polimorficzna) do wielu (normalna niepolimorficzna). W Ember Data możemy wyrazić to za pomocą polimorficznego DS.hasMany('locatable', {polymorphic: true, async: true}) i statycznego 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}) });W przypadku Lokalizacji, takich jak Użytkownik, serwer powinien zwrócić identyfikatory powiązanych lokalizacji:
GET /users { "users": [ { "id": "1", "userName": "Pooyan", "locations": ["1"] } ] }W przypadku lokalizacji serwer powinien zwrócić zarówno identyfikator, jak i typ obiektu Locatable w tablicy obiektów:
GET /locations { "locations": [ { "id": "1", "locatables": [ {"id": "1", "type": "user"}, {"id": "2", "type": "provider"} ] } ] }Możesz także reprezentować relacje według typu za pomocą statycznej relacji wiele-do-wielu:
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}) });A co z danymi w czasie rzeczywistym?
Ember Data posiada funkcję push , pushPayload i update . Zawsze możesz ręcznie wepchnąć nowe/zaktualizowane rekordy do lokalnej pamięci podręcznej Ember Data (nazywanej magazynem), a ona zajmie się całą resztą.
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); }); } }); Osobiście używam gniazd tylko na wydarzenia z bardzo małymi ładunkami. Typowym zdarzeniem jest 'recordUpdated' z ładunkiem {"type": "shop", "id": "14"} , a następnie w ApplicationRoute sprawdzę, czy ten rekord jest w lokalnej pamięci podręcznej (store) i czy to ja po prostu go odzyskam.
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); } }); } });W ten sposób możemy wysyłać zaktualizowane rekordy zdarzeń do wszystkich klientów bez niedopuszczalnych narzutów.
W Ember Data istnieją zasadniczo dwa podejścia do radzenia sobie z danymi w czasie rzeczywistym:
- Napisz adapter dla swojego kanału komunikacji w czasie rzeczywistym i użyj go zamiast RESTAdaptera.
- Przesyłaj rekordy do głównego sklepu, gdy tylko są dostępne.
Wadą pierwszej opcji jest to, że przypomina ona odkrywanie koła na nowo. W przypadku drugiej opcji musimy uzyskać dostęp do głównego sklepu, który jest dostępny na wszystkich trasach jako route#store .
Zakończyć
W tym artykule przedstawiliśmy Ci kluczowe konstrukcje i paradygmaty Ember Data, demonstrując wartość, jaką może zapewnić Tobie jako programiście. Ember Data zapewnia bardziej elastyczny i usprawniony przepływ pracy programistycznej, minimalizując rotację kodu w odpowiedzi na zmiany, które w przeciwnym razie byłyby znaczące.
Wstępna inwestycja (czas i krzywa uczenia się), którą poczynisz w celu wykorzystania danych Ember w swoim projekcie, bez wątpienia okaże się opłacalna, ponieważ Twój system nieuchronnie ewoluuje i musi być rozszerzony, zmodyfikowany i ulepszony.
DODATEK: Zaawansowane tematy dotyczące danych Ember
Ten dodatek przedstawia szereg bardziej zaawansowanych tematów dotyczących danych Ember, w tym:
- Modułowa konstrukcja Ember
- Ładowanie boczne
- Spinki do mankietów
- Serializator i adapter aktywnego modelu
- Osadzone rekordy Mixin
- Modyfikatory asocjacji (asynchroniczne, odwrotne i polimorficzne)
- Parametr „ids” w żądaniach GET
Modułowa konstrukcja
Ember Data ma pod maską konstrukcję modułową. Kluczowe elementy obejmują:
- Za obsługę komunikacji odpowiadają
Adapters, obecnie tylko REST przez HTTP. -
Serializerszarządzają tworzeniem modeli z formatu JSON lub odwrotnie. -
Storepamięci podręczne utworzone rekordy. -
Containerskleja to wszystko razem.
Zalety tego projektu to:
- Deserializacja i przechowywanie danych działa niezależnie od zastosowanego kanału komunikacji i żądanych zasobów.
- Konfiguracje robocze, takie jak ActiveModelSerializer lub EmbeddedRecordsMixin, są dostarczane natychmiast po zainstalowaniu.
- Źródła danych (np. LocalStorage, implementacja CouchDB itp.) można wymieniać między sobą, zmieniając adaptery.
- Pomimo wielu konwencji dotyczących konfiguracji, możliwe jest skonfigurowanie wszystkiego i udostępnienie swojej konfiguracji/implementacji społeczności.
Ładowanie boczne
Ember Data obsługuje „ładowanie boczne” danych; tj. wskazanie danych pomocniczych, które należy pobrać (wraz z żądanymi danymi podstawowymi), aby pomóc w konsolidacji wielu powiązanych żądań HTTP.
Typowym przypadkiem użycia jest boczne ładowanie powiązanych modeli. Na przykład każdy sklep ma wiele artykułów spożywczych, więc możemy uwzględnić wszystkie powiązane artykuły spożywcze w odpowiedzi /shops :
GET /shops { "shops": [ { "id": "14", "groceries": ["98", "99", "112"] } ] }Po uzyskaniu dostępu do stowarzyszenia spożywczego Ember Data wyda:
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" } ] } Jeśli jednak zamiast tego zwrócimy powiązane artykuły spożywcze w punkcie końcowym /shops , Ember Data nie będzie musiała wysyłać kolejnego żądania:
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" } ] }Spinki do mankietów
Ember Data akceptuje linki w miejsce identyfikatorów stowarzyszenia. Po uzyskaniu dostępu do powiązania określonego jako łącze, Ember Data wyśle żądanie GET do tego łącza w celu uzyskania powiązanych rekordów.
Na przykład możemy zwrócić link do stowarzyszenia spożywczego:
GET /shops { "shops": [ { "id": "14", "links": { "groceries": "/shops/14/groceries" } } ] } A Ember Data wyśle wniosek do /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" } ] }Pamiętaj, że nadal musisz reprezentować powiązanie w danych; linki sugerują tylko nowe żądanie HTTP i nie wpływają na powiązania.
Serializator i adapter aktywnego modelu
Prawdopodobnie ActiveModelSerializer i ActiveModelAdapter są częściej używane w praktyce niż RESTSerializer i RESTAdapter . W szczególności, gdy backend używa Ruby on Rails i klejnotu ActiveModel::Serializers , najlepszą opcją jest użycie ActiveModelSerializer i ActiveModelAdapter , ponieważ obsługują one ActiveModel::Serializers po wyjęciu z pudełka.
Na szczęście jednak różnice w użyciu między ActiveModelSerializer / ActiveModelAdapter i RESTSerializer / RESTAdapter są dość ograniczone; mianowicie:
-
ActiveModelSerializerużyje nazw pól snake_case, podczas gdyRESTSerializerwymaga nazw pól camelCased. -
ActiveModelAdapterwysyła żądania do metod interfejsu API snake_case, podczas gdyRESTSerializerżądania do metod interfejsu API camelCased. -
ActiveModelSerializeroczekuje, że nazwy pól związanych z powiązaniami będą kończyć się na_idlub_ids, podczas gdyRESTSerializeroczekuje, że nazwy pól związanych z powiązaniami będą takie same jak pola powiązania.
Niezależnie od wyboru adaptera i serializatora modele danych Ember będą dokładnie takie same. Tylko reprezentacja JSON i punkty końcowe interfejsu API będą się różnić.
Weźmy jako przykład nasze ostatnie zamówienie dostawcy:
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}) });W przypadku serializatora i adaptera Active Model serwer powinien oczekiwać:
Post /provider_orders { "provider_order": [ "status": "", "provider": {"id": "13", "type": "shop"} "order_id": "68", ] }… i powinien odpowiedzieć:
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"} ] ] }Osadzone rekordy Mixin
DS.EmbeddedRecordsMixin to rozszerzenie dla DS.ActiveModelSerializer , które umożliwia konfigurowanie sposobu serializacji lub deserializacji powiązań. Choć nie jest jeszcze kompletny (zwłaszcza w odniesieniu do asocjacji polimorficznych), jest jednak intrygujący.
Możesz wybrać:
- Nie serializować ani deserializować skojarzeń.
- Aby serializować lub deserializować skojarzenia z identyfikatorem lub identyfikatorami.
- Do serializacji lub deserializacji skojarzeń z modelami osadzonymi.
Jest to szczególnie przydatne w relacjach jeden-do-wielu, w których domyślnie identyfikatory powiązane z DS.hasMany nie są dodawane do obiektów, które są serializowane. Weźmy na przykład koszyk z wieloma przedmiotami. W tym przykładzie koszyk jest tworzony, gdy przedmioty są znane. Jednak podczas zapisywania koszyka dane Ember nie umieszczają automatycznie identyfikatorów powiązanych produktów w ładunku żądania.
Korzystając DS.EmbeddedRecordsMixin , można jednak nakazać Ember Data wykonanie serializacji identyfikatorów przedmiotów w koszyku w następujący sposób:
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}) }); Jak pokazano w powyższym przykładzie, EmbeddedRecordsMixin pozwala na wyraźne określenie skojarzeń do serializacji i/lub deserializacji za pośrednictwem obiektu attrs . Prawidłowe wartości serialize i deserialize to: - 'no' : nie uwzględniaj powiązania w serializowanych/deserializowanych danych - 'id' lub 'ids' : uwzględnij powiązane identyfikatory w serializowanych/deserializowanych danych - 'records ': uwzględnij rzeczywiste właściwości (tj. zapisz wartości pól) jako tablicę w serializowanych/deserializowanych danych
Modyfikatory asocjacji (asynchroniczne, odwrotne i polimorficzne)
Obsługiwane są następujące modyfikatory asocjacji: polymorphic , inverse i async
Modyfikator polimorficzny
W asocjacji polimorficznej jedna lub obie strony asocjacji reprezentują klasę obiektów, a nie konkretny obiekt.
Przypomnij sobie nasz wcześniejszy przykład bloga, w którym musieliśmy wspierać możliwość tagowania zarówno postów, jak i stron. Aby to wesprzeć, doszliśmy do następującego modelu:
- Każdy post jest oznaczony tagiem i ma wiele tagów
- Każda strona jest tagowalna i ma wiele tagów
- Każdy tag ma wiele polimorficznych tagów
Zgodnie z tym modelem można użyć modyfikatora polymorphic , aby zadeklarować, że Tagi są powiązane z dowolnym typem elementu „Tagable” (którym może być post lub strona), w następujący sposób:
// 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}) });Modyfikator odwrotności
Zwykle asocjacje są dwukierunkowe. Na przykład „Wpis zawiera wiele komentarzy” będzie jednym z kierunków skojarzenia, podczas gdy „Komentarz należy do posta” będzie drugim (tj. „odwrotnym”) kierunkiem tego skojarzenia.
W przypadkach, w których nie ma niejednoznaczności w powiązaniu, należy określić tylko jeden kierunek, ponieważ dane Ember mogą wywnioskować odwrotną część powiązania.
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/commentsHowever, Ember Data expects API endpoints to be flat, and not nested; np:
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: []) .
