Models
Models are objects that represent the underlying data that your application presents to the user. Different apps will have very different models, depending on what problems they're trying to solve.
For example, a photo sharing application might have a Photo
model to represent a particular photo, and a PhotoAlbum
that
represents a group of photos. In contrast, an online shopping app would
probably have different models, like ShoppingCart
, Invoice
, or
LineItem
.
Models tend to be persistent. That means the user does not expect model data to be lost when they close their browser window. To make sure no data is lost, if the user makes changes to a model, you need to store the model data somewhere that it will not be lost.
Typically, most models are loaded from and saved to a server that uses a database to store data. Usually you will send JSON representations of models back and forth to an HTTP server that you have written. However, Ember makes it easy to use other durable storage, such as saving to the user's hard disk with IndexedDB, or hosted storage solutions that let you avoid writing and hosting your own servers.
Once you've loaded your models from storage, components know how to translate model data into a UI that your user can interact with. For more information about how components get model data, see the Specifying a Route's Model guide.
Ember Data, included by default when you create a new application, is a library that integrates tightly with Ember to make it easy to retrieve models from your server as JSON, save updates back to the server, and create new models in the browser.
Thanks to its use of the adapter pattern, Ember Data can be configured to work with many different kinds of backends. There is an entire ecosystem of adapters that allow your Ember app to talk to different types of servers without you writing any networking code.
If you need to integrate your Ember.js app with a server that does not have an adapter available (for example, you handrolled an API server that does not adhere to any JSON specification/), Ember Data is designed to be configurable to work with whatever data your server returns.
Ember Data is also designed to work with streaming servers, like those powered by WebSockets. You can open a socket to your server and push changes into Ember Data whenever they occur, giving your app a real-time user interface that is always up-to-date.
Thinking in Ember Data
At first, using Ember Data may feel different than the way you're used to writing JavaScript applications. Many developers are familiar with using AJAX to fetch raw JSON data from an endpoint, which may appear easy at first. Over time, however, complexity leaks out into your application code, making it hard to maintain.
With Ember Data, managing models as your application grows becomes both simple and easy.
Once you have an understanding of Ember Data, you will have a much better way to manage the complexity of data loading in your application, allowing your code to evolve without becoming a mess.
The Store and a Single Source of Truth
One common way of building web applications is to tightly couple user interface elements to data fetching. For example, imagine you are writing the admin section of a blogging app, which has a feature that lists the drafts for the currently logged in user.
You might be tempted to make the component responsible for fetching that data and storing it:
import Component from 'ember-component';
export default Component.extend({
willRender() {
$.getJSON('/drafts').then(data => {
this.set('drafts', data);
});
}
});
You could then show the list of drafts in your component's template like this:
While this works, it is not ideal for several reasons.
First, by tightly coupling data fetching to an isolated UI component, the opportunity to share data is lost. As you build out the features of your application, other components may want to access that same data.
For example, imagine that the next feature you add to the blog editor is a widget in the app's toolbar that shows a count of unpublished drafts. That component needs the list of drafts as well (so it can count them), but because the two components are responsible for their own data fetching, your app will make two separate requests for the same information.
Not only is the redundant data fetching costly in terms of wasted bandwidth and affecting the perceived speed of your app, it's easy for the two values to get out-of-sync. You yourself have probably used a web application where the list of items gets out of sync with the counter in a toolbar, leading to a frustrating and inconsistent experience.
The second reason this is not ideal is that it creates a tight coupling between your application's UI and the network code. If the format of the JSON payload changes, it is likely to break all of your UI components in ways that are hard to track down.
The SOLID principles of good design tell us that objects should have a single responsibility. The responsibility of a component should be presenting model data to the user, not fetching the model.
Good Ember apps take a different approach. Ember Data gives you a single store that is the central repository of models in your application. Components and routes can ask the store for models, and the store is responsible for knowing how to fetch them.
It also means that the store can detect that two different components are asking for the same model, allowing your app to only fetch the data from the server once. You can think of the store as a read-through cache for your app's models. Both your components and routes have access to this shared store; when they need to display or modify a model, they first ask the store for it.
You use the store to retrieve records (an instance of a model), as well
to create new ones. For example, we might want to find a person
with
the ID of 1
from our route's model
hook:
export default Ember.Route.extend({
model() {
return this.store.findRecord('person', 1);
}
});
Convention Over Configuration with JSON API
You can significantly reduce the amount of code you need to write and maintain by relying on Ember's conventions. By adopting these conventions, your don't just write less code; your code will be much easier to maintain and be understood by other developers on your team.
Rather than create an arbitrary set of conventions, Ember Data is designed to work out of the box with JSON API. JSON API is a formal specification for building conventional, robust, and performant APIs that allow clients and servers to communicate model data.
JSON API standardizes how JavaScript applications talk to servers, so you decrease the coupling between your frontend and backend, and have more freedom to change pieces of your stack.
As an analogy, JSON API is to JavaScript apps and API servers what SQL is to server-side frameworks and databases. Popular frameworks like Ruby on Rails, Laravel, Django, Spring and more work out of the box with many different databases, like MySQL, PostgreSQL, SQL Server, and more.
Frameworks (or apps built on those frameworks) don't need to write lots of custom code to add support for a new database; as long as that database supports SQL, adding support for it is relatively easy.
So too with JSON API. By using JSON API to interop between your Ember app and your server, you can entirely change your backend stack without breaking your frontend. And as you add apps for other platforms, such as iOS and Android, you will be able to leverage JSON API libraries for those platforms to easily consume the same API your Ember app uses.
Models
In Ember Data, each model is represented by a subclass of Model
that
defines the attributes, relationships, and behavior of the data that you
present to the user.
Models define the type of data that will be provided by your server. For
example, a Person
model might have a firstName
attribute that is a
string, and a birthday
attribute that is a date:
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
firstName: attr('string'),
birthday: attr('date')
});
A model also describes its relationships with other objects. For
example, an order
may have many line-items
, and a
line-item
may belong to a particular order
.
import Model from 'ember-data/model';
import { hasMany } from 'ember-data/relationships';
export default Model.extend({
lineItems: hasMany('line-item')
});
import Model from 'ember-data/model';
import { belongsTo } from 'ember-data/relationships';
export default Model.extend({
order: belongsTo('order')
});
Models don't have any data themselves; they just define the attributes, relationships and behavior of specific instances, which are called records.
Records
A record is an instance of a model that contains data loaded from a server. Your application can also create new records and save them back to the server.
A record is uniquely identified by its model type and ID.
For example, if you were writing a contact management app, you might
have a Person
model. An individual record in your app might
have a type of person
and an ID of 1
or steve-buscemi
.
this.store.findRecord('person', 1); // => { id: 1, name: 'steve-buscemi' }
An ID is usually assigned to a record by the server when you save it for the first time, but you can also generate IDs client-side.
Adapter
An adapter is an object that translates requests from Ember (such as "find the user with an ID of 123") into a requests to a server.
For example, if your application asks for a Person
with an ID of
123
, how should Ember load it? Over HTTP or a WebSocket? If
it's HTTP, is the URL /person/1
or /resources/people/1
?
The adapter is responsible for answering all of these questions. Whenever your app asks the store for a record that it doesn't have cached, it will ask the adapter for it. If you change a record and save it, the store will hand the record to the adapter to send the appropriate data to your server and confirm that the save was successful.
Adapters let you completely change how your API is implemented without impacting your Ember application code.
Automatic Caching
The store will automatically cache records for you. If a record had already been loaded, asking for it a second time will always return the same object instance. This minimizes the number of round-trips to the server, and allows your application to render its UI to the user as fast as possible.
For example, the first time your application asks the store for a
person
record with an ID of 1
, it will fetch that information from
your server.
However, the next time your app asks for a person
with ID 1
, the
store will notice that it had already retrieved and cached that
information from the server. Instead of sending another request for the
same information, it will give your application the same record it had
provided it the first time. This feature—always returning the same
record object, no matter how many times you look it up—is sometimes
called an identity map.
Using an identity map is important because it ensures that changes you make in one part of your UI are propagated to other parts of the UI. It also means that you don't have to manually keep records in sync—you can ask for a record by ID and not have to worry about whether other parts of your application have already asked for and loaded it.
Architecture Overview
The first time your application asks the store for a record, the store sees that it doesn't have a local copy and requests it from your adapter. Your adapter will go and retrieve the record from your persistence layer; typically, this will be a JSON representation of the record served from an HTTP server.
As illustrated in the diagram above, the adapter cannot always return the requested record immediately. In this case, the adapter must make an asynchronous request to the server, and only when that request finishes loading can the record be created with its backing data.
Because of this asynchronicity, the store immediately returns a
promise from the find()
method. Similarly, any requests that the
store makes to the adapter also return promises.
Once the request to the server returns with a JSON payload for the requested record, the adapter resolves the promise it returned to the store with the JSON.
The store then takes that JSON, initializes the record with the JSON data, and resolves the promise returned to your application with the newly-loaded record.
Let's look at what happens if you request a record that the store already has in its cache.
In this case, because the store already knew about the record, it returns a promise that it resolves with the record immediately. It does not need to ask the adapter (and, therefore, the server) for a copy since it already has it saved locally.
These are the core concepts you should understand to get the most out of Ember Data. The following sections go into more depth about each of these concepts, and how to use them together.