© The Author(s), under exclusive license to APress Media, LLC , part of Springer Nature 2021
D. TangPro Ember Datahttps://doi.org/10.1007/978-1-4842-6561-1_1

1. Ember Data Overview

David Tang1  
(1)
Playa Vista, CA, USA
 

This chapter is meant to provide an architectural overview and basic summary of Ember Data. If you haven’t used Ember Data before, I recommend reading the section on Ember Data in the Ember Guides1 to get acquainted. This chapter will provide a foundation of how all the pieces in Ember Data fit together so that you can start customizing it to fit your API.

Architectural Overview

Ember Data’s architecture can be diagrammed as follows.
../images/504826_1_En_1_Chapter/504826_1_En_1_Figa_HTML.jpg
On the left is the application, which interacts with the store. By default, routes and controllers have access to the store, for example:
import Route from '@ember/routing/route';
export default class ContactsRoute extends Route {
  model() {
    return this.store.findAll('contact');
  }
}
The store can also be injected into other parts of an application like components:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
export default class ContactsListComponent extends Component {
  @service store;
  @tracked contacts;
  constructor() {
    super(...arguments);
    this.store.findAll('contact').then((contacts) => {
      this.contacts = contacts;
    });
  }
}

The Store

The store is an instance of the Store class, and it is a service that acts as a data access layer and cache for the records (instances of models) in an application. It is responsible for instantiating models on the client and saving the data via an API. The store can also request data from an API and turn that data into rich client-side models which are referred to as records. These records are then cached for subsequent retrieval.

The store also implements an identity map to prevent multiple references of the same record existing in an application. For example, let’s say we make two requests. The first request is for a list of contacts and this list contains a contact with an id of 1. A second request happens somewhere else in the application also for the contact with an id of 1. This means there are two contact objects with an id of 1 in memory. Manually keeping these duplicate contact objects in sync often results in a poor data architecture that isn’t very scalable and reusable. Through identity mapping, the store will keep track of a single contact record with an id of 1. If multiple AJAX requests return the contact with an id of 1, such as GET /contacts and GET /contacts/1, the data will be mapped to a single contact record in the store. Through identity mapping, the store will preserve object identity and return the same records, regardless of how many times you ask for it.

The Adapter

The store delegates the specifics of how to work with an API to an adapter. This is the adapter design pattern in use. Think of the adapter pattern like handling electrical outlets when you travel abroad. If you have a three-pronged electrical plug, it won’t fit in a two-pronged wall outlet. Instead, you need to use a travel adapter to convert the existing three-pronged plug configuration to conform to the socket of the country you are visiting. Same idea here, but instead of having an adapter to fit the electrical outlet, you have an adapter to fit your API. Your backend could be an HTTP or WebSocket protocol-based API or even browser storage technologies like Local Storage or IndexedDB. By isolating the specifics of where the data comes from in an adapter from the rest of an application, if the way the application communicates with the backend changes in the future, only the adapter will need to change instead of across the application.

The Serializer

Between the adapter and the API is the serializer. The serializer has two jobs. First, it is used to format data sent to the server, also known as serialization. Second, the serializer is used to format data received from the server, known as normalization . As we’ll see later in this book, Ember Data ships with three different serializers, which can be extended to fit any API.

Now that we have a good idea of the core pieces behind Ember Data, let’s go through an example.

Model Attributes and Transforms

To start working with Ember Data, we first need to think about the underlying data in an application and represent them as models. For example, a cat application might have models cat, home, and owner. We can use Ember CLI to generate a cat model class:
ember generate model cat

This will generate the following model class:

app/models/cat.js
import Model from '@ember-data/model';
export default class CatModel extends Model {}

Next, we can define the schema of our model via attributes and specify their types using transforms. Transforms allow us to transform properties from the server before they are set as attributes on a record or sent back to the server. Here is an example of the cat model using the four built-in transforms – string, number, boolean, and date:

app/models/cat.js
import Model, { attr } from '@ember-data/model';
export default class CatModel extends Model {
  @attr('string') name;
  @attr('number') age;
  @attr('boolean') adopted;
  @attr('date') birthday;
}
The built-in transforms are listed in Table 1-1.
Table 1-1

Built-in Transforms

Transform Name

Usage

String

attr(‘string’)

Number

attr(‘number’)

Boolean

attr(‘boolean’)

Date

attr(‘date’)

When a model is created, the attributes are coerced to the types specified in the corresponding attr() decorator call. For example, let’s say a cat resource came in from the server looking like the following:
{
  "id": 1,
  "name": "Frisky",
  "age": "10",
  "adopted": "true",
  "birthday": "2005-11-05T13:15:30Z",
  "color": "white"
}

On the cat record, the name attribute would be set as a string, the age attribute would be coerced to the number 10, the adopted attribute would be coerced to a boolean value of true, and the birthday attribute would be coerced to a Date object. Lastly, because the color attribute was not specified on the model class, it wouldn’t get set on the cat record.

Behind the scenes, each of these attr() decorator calls maps to a specific transform class that extends from Transform. If we don’t pass anything to attr(), the value will be passed through as is. This can be useful as we’ll see in Chapter 8 – Working with Nested Data and Embedded Records.

The built-in transforms are self-explanatory for the most part. The string transform will coerce the value to a string using the native String constructor function. The number transform will coerce the value to a number using the native Number constructor function. If the attribute is not a number, null is returned. The boolean transform not only transforms boolean values, but the strings “true”, “t”, or “1” in any casing and the number 1 will all coerce to true, and anything else will coerce to false. The date transform will construct a Date object using the native Date constructor function. When a date attribute is serialized, such as when saving a record, the date will be converted to the ISO 8601 format via Date.prototype.toISOString()2 (YYYY-MM-DDTHH:mm:ss.sssZ).

/

The API

Now before we continue any further, let’s create a JSON:API-compliant API in Node.js with Express that returns hard-coded data. If you aren’t familiar with JSON:API, don’t worry. We’ll be going over that in more detail in Chapter 3 – API Response Formats and Serializers, but if you want to get a head start, check out the JSON:API specification3 for more information.

Create an api folder with a server.js file :
mkdir api
cd api
touch server.js
Place the following in server.js:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/api/v1/cats', (request, response) => {
  response.json({
    data: [
      {
        type: 'cats',
        id: 1,
        attributes: {
          name: 'Frisky',
          age: 10,
          adopted: true,
          birthday: '2005-11-05T13:15:30Z'
        },
        relationships: {
          home: {
            data: { type: 'homes', id: 1 }
          }
        }
      }
    ]
  });
});
app.get('/api/v1/homes/:id', (request, response) => {
  response.json({
    data: {
      type: 'homes',
      id: request.params.id,
      attributes: {
        street: '123 Purrfect Avenue'
      }
    }
  });
});
app.listen(8000, () => {
  console.log('Listening on port 8000');
});
Next, let’s install the dependencies and run the API:
npm install
node server.js

If you visit http://localhost:8000/api/v1/cats, you should see a list of cats in JSON.

Using the Store

Now that we have an API running and we’ve set up our cat model, let’s start interacting with the store to access data from the API. As stated before, the store is automatically injected into routes. Let’s say we have a route for the path /cats:

app/routes/cats.js
import Route from '@ember/routing/route';
export default class CatsRoute extends Route {
  model() {
    return this.store.findAll('cat');
  }
}
We are using the route’s model hook to fetch a collection of cat resources from our server using the findAll() method on the store. This is just one of the methods available on the store, but other common methods include those listed in Table 1-2.
Table 1-2

Commonly used Store methods

Store Method

Description

findRecord()

Finds a single record for a given type and ID and returns a promise

peekRecord()

Finds a record in the store synchronously for a given type and ID

peekAll()

Returns all records for a given type in the store

createRecord()

Creates a new instance of a model and puts it in the store

If we run our application and navigate to /cats, we’ll notice an error in the console saying that http://localhost:4200/cats could not be found. This is happening because we haven’t told Ember Data what the host of our API is. Let’s do that by creating an adapter.

Adapters

Ember Data allows us to create an adapter that can be used across our application. This is called an application adapter and can be generated with the following command:
ember generate adapter application
Alternatively, we can create adapters for specific models:
ember generate adapter cat
If a model-specific adapter exists, Ember Data will use that instead of the application adapter when working with that model. Otherwise, the application adapter will be used. In this case, we don’t need the flexibility of a model-specific adapter, so let’s just create an application adapter with the properties host and namespace:
app/adapters/application.js
import JSONAPIAdapter from '@ember-data/adapter/json-api';
export default class ApplicationAdapter extends JSONAPIAdapter {
  host = 'http://localhost:8000';
  namespace = 'api/v1';
}

With this change, Ember Data is now hitting our API successfully! I’ll leave it to you to render the cat records.

Ember Data uses JSON:API by default for both the adapter and serializer. This is why our application works even without having created a serializer. We’ll look at how to create and extend serializers in future chapters.

We’re almost done with the basics. We just need to tackle relationships.

Relationships

Models can have relationships with other models. Let’s look at a very common relationship, one-to-many.

A cat, if fortunate, belongs to a home. Let’s say we want to get the home record from the cat record. We can specify this relationship using belongsTo() :
app/models/cat.js
import Model, { belongsTo } from '@ember-data/model';
export default class CatModel extends Model {
  // ...
  @belongsTo('home') home;
}
Let’s say we have a home record, and from the home record we want to access all of the cats for that home. This is the other side of the one-to-many relationship. We can specify this relationship using hasMany() because a home can have one or more cats:
app/models/home.js
import Model, { hasMany } from '@ember-data/model';
export default class HomeModel extends Model {
  @hasMany('cat') cats;
}

Ember Data allows us to define relationships on models both synchronously and asynchronously. By default, model relationships are asynchronous. When an asynchronous relationship is accessed, if the related records aren’t already in the store, Ember Data will trigger a fetch and return a promise that resolves with the related record. If that record is already in the store, a promise will be returned that resolves with the related record.

Go ahead and render the home’s street address for the cat and see what happens. If you do, you’ll notice that a GET request is made to the /api/v1/homes/:id endpoint that we defined in server.js earlier.

As previously mentioned, a relationship can also be declared as synchronous. When a synchronous relationship is accessed and the related record is already in the store, the record will be returned. If that record isn’t in the store, an error will be thrown. To see this in action, change the home relationship on the cat model to be synchronous:
app/models/cat.js
import Model, { belongsTo } from '@ember-data/model';
export default class CatModel extends Model {
  // ...
  @belongsTo('home', { async: false }) home;
}

Next, try and render cat.home.street and you will get the following error:

Error

You looked up the ‘home’ relationship on a ‘cat’ with id 1 but some of the associated records were not loaded. Either make sure they are all loaded together with the parent record, or specify that the relationship is async ( belongsTo({ async: true }) )

Choosing an asynchronous or synchronous relationship depends on your application and the API. Personally, I lean toward making relationships synchronous as much as possible, which I have found to be the most clear and straightforward.

We’ve covered the basics of model relationships, but we haven’t discussed how an API should return relationship data. We will look at that more in Chapter 3 – API Response Formats and Serializers.

Summary

We’ve gone through a simple example that covered fetching a collection of resources (even though the collection only had one cat resource). The example isn’t comprehensive of all common data operations, but check out the Ember Guides to learn more about the basics of Ember Data like creating, updating, and deleting data. Let’s now dive into adapters in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.56.194