As we saw in the section, Understanding the Ember Data identity map – DS.Store, all interactions with the server are handled by the adapter. Adapter in ember-data
understands how to connect to the server, in order to fetch and save the records from/to the server. Similarly, serializer
, as the name implies, is responsible for serializing or converting the response received from the server to DS.Model
objects.
By default in ember-data
, the store will use the DS.RESTAdapter
and DS.RESTSerializer
. As the name implies, the DS.RESTAdapter
is based on the REST (Representational State Transfer) principles to load or save a record from/to the server. The DS.RESTAdapter
communicates with the HTTP server by sending and receiving JSON (JavaScript Object Notation) data, via AJAX requests. If the server with which the Ember.js application talks with, understands and follows the URL conventions according to REST principles, then your ember-data
communication will work flawlessly without any customization to the adapter or serializer.
Let us see what the URL endpoints for our book model (chapter-6/example1/app/models/book.js
) discussed when used with different actions:
Action |
HTTP Verb |
URL |
---|---|---|
Find |
|
|
Find All |
|
|
Update |
|
|
Create |
|
|
Delete |
|
|
The URL endpoints which DS.RESTAdapter makes call to when used with different actions
As you can see, the DS.RESTAdapter
uses plural form of the model name to create the URLs, hence the URL for the book
model becomes /books/
. Another important thing to note here is the use of HTTP
verbs while making calls to the server. To fetch a record or records from the server, the adapter uses GET
verb or action; to create a new record, the POST
verb is used; to update an existing record, the PUT
verb is used; and to delete a record, the DELETE
verb is used.
Now that we have understood how the adapter creates the URLs to communicate with the server, let's understand how we need to send the JSON response back from the server, in order for it to be compliant with the DS.RESTAdapter
and DS.RESTSerializer
.
In order to understand the JSON response from the server, let us take the look at the book
model:
import DS from "ember-data"; export default DS.Model.extend({ title: DS.attr('string'), isbn: DS.attr('string'), pages: DS.attr('number'), description: DS.attr('string'), authors: DS.hasMany('author',{ async: true }), publisher: DS.belongsTo('publisher',{ async: true }), reviews: DS.hasMany("review",{ async: true }) });
The book model is present at chapter6/example1/app/models/book.js
Here, you can see that the book model contains both attributes and relationships. When we make a call to fetch a particular book from the server using store.find("book",1)
, this gets translated to GET /books/1
that should return a JSON response, which can be de-serialized into book
model object. The JSON response returned from the server should be of the following structure:
{ "book": { "id":1 "title": "Ember.js Essentials", "isbn": "ISBN1", "pages": 180, "description": "Ember.js essentials to master", "authors": [1], "publisher": 1, "reviews": [1,2,3] } }
Here, you can see that we return a JSON response that has a top-level key that is the same as the name of the model, book
in our case. This is followed by the attributes and their values. One important thing to note is that it is mandatory to send the ID of the object with the JSON response, the ID
attribute is defined automatically on every DS.Model
object and is used by the store to uniquely identify the records.
Let's look at relationships now. The book record has three relationships, namely authors, publisher, and reviews. The relationships of DS.hasMany
type use the plural form of the model and expect the relationship IDs in an array.
The relationship response "reviews": [1,2,3]
tells the serializer that this book object has three reviews, whose IDs are 1, 2, and 3. Similarly, the "publisher": 1
tells the adapter and serializer that a book has one publisher with ID 1.
Now, as we used {async:true}
in defining our relationships of the book
model, the DS.RESTAdapter
and DS.RESTSerializer
that the JSON response of the book object will only contain the IDs of the relationships in proper format. These relationship objects (authors, publisher and reviews) should be fetched separately only when they are referred to by the application code. When such references to the relationships are encountered, a call store.find('reviews',1)
or GET /reviews/1
is sent for each object present in the "reviews": [1,2,3]
array.
The async
property is very helpful in cases where an object has a lot of relationships, but initially only the details of the objects are visible on the screen as the relationships are fetched lazily when required by the application.
Till now, we have seen the response structure when relationships are defined with {async: true}
. There could be cases when you don't want this behavior, and want to load the complete object in one go. This is also the default behavior of the relationships in ember-data
library. Let us see how we should structure the response in order for it to load the book object with its associated relationships synchronously, in one go.
For side loading the relationships, we will have to define the book
model without the async
property set to true in its relationships, as follows:
import DS from "ember-data"; export default DS.Model.extend({ title: DS.attr('string'), isbn: DS.attr('string'), pages: DS.attr('number'), description: DS.attr('string'), authors: DS.hasMany('author'), publisher: DS.belongsTo('publisher'), reviews: DS.hasMany("review") });
The side-loaded response from the server should be of the following structure:
{ "book": { "id":1, "title": "Ember.js Essentials", "isbn": "ISBN1", "pages": 180, "description": "Ember.js essentials to master", "authors": [1], "publisher": 1, "reviews": [1,2,3] }, "publisher": { "id": 1, "name": "Packt Publishing", "organizationName": "Pact", "address":"Packt Publishing London", "book":1 }, "authors": [{ "id": 1, "firstName": "firstName1", "lastName" : "LastName1", "bio": "Great Guy", "book":[1] }], "reviews": [{ "id": 1 "name": "Reviewer 1", "comment": "comment 1", "book":1 },{ "id": 2 "name": "Reviewer 2", "comment": "comment 2", "book":1 }, { "id": 3 "name": "Reviewer 3", "comment": "comment 3", "book":1 }] }
In the preceding response, you can see that instead of sending just the book
objectJSON
, we also side load the relationships of the book
model that is, we also side load the publisher
, authors
, and reviews
. Please note that all DS.hasMany
relationships are provided as array of objects, and DS.belongsTo
relationships are provided as single objects.
Till now, we have discussed the behavior of the default DS.RESTAdapter
, but there are situations, especially when integration with API which is not fully compliant with REST conventions, which require to customize and modify the adapter, according to your needs. When using Ember CLI, all the customizations of the adapter go in app/adapters/
directory. You can customize the behavior for the adapter for the whole application as well as restrict it to only one or more model classes.
Application-wide changes to the adapter should go in app/adapters/application.js
, and any model specific customizations should go in app/adapters/<name of the model>.js
, just as if we had to customize the adapter for only the book model, we would have created an adapter for it in app/adapters/book.js
.
If you want to define any prefix that is prepended to the URL, you can do that by defining the namespace
property of your adapter.
import DS from 'ember-data'; export default DS.RESTAdapter.extend({ namespace: 'api' });
Application adapter using custom namespace is present at chapter-6/example1/app/adapters/application.js
Setting the namespace property of the application adapter will prefix all the generated URL for all the models of the application with api
, for example this.store.find ("book",1)
will now become /api/books/1
, instead of /books/1
. If your API server runs on a different domain or you are integrating with third party APIs directly from your Ember.js application, you will need to change the host name while building the URLs for your data models.
You can do that by setting the host
property of the adapter as follows:
import DS from 'ember-data'; export default DS.RESTAdapter.extend({ host: 'http://api.someotherdomain.com/' });
By setting the host property of the adapter, the call for this.store.find ("book",1)
will now go to http://api.someotherdomain.com/books/1
instead of just /books/1
, which goes to the same server that hosts your Ember.js application.
Please note here that in order for the above functionality of cross-domain functionality to work properly, you will have use a browser that supports CORS (cross-origin resource sharing), and you will have to configure the server to send in the correct CORS headers.
By default, Ember Data comes with a few more adapters other than DS.RESTAdapter
, which are mentioned as follows:
DS.FixtureAdapter
– This adapter is used primarily for testing the model classes. The fixture adapters don't make the actual calls to the backend server but serve the model data from the fixtures already loaded and configured in the memory.DS.Adapter
– This is base adapter class, it does not implement any functionality but is primarily used as a contract to implement new adapters.DS.ActiveModelAdapter
– This adapter extends the default DS.RESTAdapter
and hence most of the features and conventions remain the same, here. The main benefit of this adapter is that it works well with the Ruby on Rails active model serializer.3.142.136.226