Ember DATA

Store

The store is a service provided by Ember. It will allow you to keep all the data loaded from the backend while the session is active. Newly created record will also be present in the store, even before they're sent to the backend in order to be persisted.

You will mostly need it in your routes, controllers and components. To use it, you need to import the service import { inject as service } from '@ember/service'; , then declare it in your class as an attribute:

// Ember Classic
store: service(),

//Ember Octane
@service store;

We will now see how the store keeps its data, and how to interact with it.

Models

In Ember Data, models are objects that represent the underlying data that your application presents to the user.

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. In our case, data will be send via the help of our dear adapters.

We will now come back a bit about the basics, but don't leave quite yet if you're comfortable with that. The last section of this page talks about how we serialize nested attributes rightly for the back-end and that might interest you.

Model vs model()

Ember Data models share the same name with the model method on routes, although they are a different concept.

The model() method of a given route will set the model attribute of its corresponding controller, through the setupController() method

For example, let's analyse this model method:

model(params) {
    return this.store.findRecord('company', params.company_id);
}

In this case, this method returns an instance of a Company, which is the Model class representing our company object, and it will be assigned to the model attribute of the corresponding controller.

controller.model // Contains the company returned by the model() method

Model Class

Model classes represents all data records, mostly provided by both the backend and the user. With Ember, we implement our model classes by extending the Ember DS.Model class.

Inside the class, we simply have to declare the record's attributes and relationships. We can implement methods and computed attributes and we also have the possibility of having validations on our model (as a constant).

Check this out with this next example, it is the model of an user (user.js):

import DS from 'ember-data';
import {belongsTo, hasMany} from 'ember-data/relationships';
import {buildValidations, validator} from 'ember-cp-validations';

const {attr, belongsTo, hasMany} = DS;

const Validations = buildValidations({
  firstName: [
    validator('presence', {
      presence: true,
      message: 'this field can\'t be blank'
    })
  ]
});

export default DS.Model.extend(Validations, {
  company: belongsTo('company', { async: false }),
  lines: hasMany:('line', { async:false }),
  
  firstName: attr('string'),
  email: attr('string')
  
  firstNameOrEmail: computed('firstName', 'email', function(){
    return this.get('firstName') || this.get('email');
  })
});

Finding Records

If we want to retrieve a single record, we can use the store by either calling findRecord() or peekRecord().

let company = this.store.findRecord('company', 1); // GET api/private/companies/1
let company = this.store.peekRecord('company', 1); // No network request

The difference between those two methods are that the first one is going to send a GET request to the backend, while the second one will just search in the store, and return the data it if it is already there.

The calls above will resolve in an instance of Company, which inherits from ember-data/model like all Ember Data models do.

If you want to retrieve all records of a specific model, the procedure is quite similar. The only difference is that you are going to call findAll('company') or peekAll('company') on the store.

Creating Records

To create a new record, we will once again use the store by calling its createRecord() method.

let company = store.createRecord('company', {
  name: 'Prospect.io'
});

Persisting Records

The company we created below exists in the Ember store, but has not been persisted in our backend.

To persist a record, we will call the save() method directly on the record, which will send a POST request to our backend, and the later will be in charge of saving the record in our database. In other words, Ember will say to Rails: "Hey, I've just created this new company, could you please save it for me?".

company.save();

Nested attributes

The company we have just created has a pretty cool name, but it lacks some employees. Indeed, a company should have multiple employees working for it (this is a one-to-many relationship).

Now, we want to assign all the employees to this company. Let's imagine we have a variable allEmployees that consists of an array containing every employee of this company (considering an employee being an instance of the model class employee.js). How do we assign it to the company?

First, our company model must have the right attribute. In this case, it would be employees: hasMany('employee', { async: false }). In parallel, our employee model also has to define the symmetrical attribute, which would be: company: belongsTo('company', { async: false }). Note here that both end of our one-to-many relation are explicit in both attributes (hasMany & belongsTo).

If we have that, we can simply do:

company.employees = allEmployees;

Great! Our company now has a cool name and awesome people working for it. But don't forget, we have to save it in order to persist the data. Otherwise, all the people will be gone the next time you come back.

Here is the touchy part: calling the save() method on the company won't send every employees to the backend, it will just send their IDs. As some of them could be newly created, they won't have an ID yet. In this case, we want to send plain objects (the company instance + every instance of its employees).

But don't worry, we have the solution! At Prospect.io, we developed a custom mixin that you can use to serialize records with nested entities. Basically, we need to create a custom serializer for our parent record (in this case, the company), and this serializer has to implement our IncludedRecordsMixin. In our example, we will call this serializer company.js and place the file in the serializers folder of our frontend application.

import Application from './application';
import IncludedRecordsMixin from './mixins/included-records';

export default Application.extend(IncludedRecordsMixin, {
  attrs: {
    employees: {
      serialize: 'included'
    }
  }
});

And voilà! With that serializer, calling company.save() will now send the company & every instances of its employees to the backend.

As we saw, before doing that we just had the IDs of every employees, and you could find them in the relationships attribute of the JSON sent to the backend.

Now, in addition to that, we will have every serialized employee in the included attribute of the same JSON. If you want more informations, don't hesitate to check out this awesome guide.

You can now go to the next section to see how Ember helps us sending this serialized data to the backend!

Last updated