When working with a data store, you'll need to be able to handle relationships. In this recipe, we'll go over some common relationships from one-to-many and many-to-one and also how to use it with Ember Data.
As with the other recipes, we'll be using Ember CLI Mirage to mock our backend. In this recipe, we'll create a simple one-to-many and many-to-one relationship. We'll mock a school that has instructors and classes. For every class, there is one instructor. Every instructor will have one or more classes.
$ ember install ember-cli-mirage $ ember g model instructor $ ember g model class $ ember g route index $ ember g helper addone $ ember g adapter application $ ember g fixture classes $ ember g fixture instructors
This will create the models, route, adapter, and helper that we'll need for this application.
mirage
fixtures
folder, update these two files, classes.js
and instructors.js
:// app/mirage/fixtures/classes.js export default [ {id: 1, subject: 'History',instructor:[1]}, {id: 2, subject: 'Spanish',instructor:[1]}, {id: 3, subject: 'Government',instructor:[3]}, {id: 4, subject: 'English',instructor:[2]}, {id: 5, subject: 'German',instructor:[2]}, {id: 6, subject: 'Social Studies',instructor:[4]}, {id: 7, subject: 'Math',instructor:[]} ];
The classes.js
file has a list of classes and subjects.
instructors.js
file:// app/mirage/fixtures/instructors.js export default [ {id: 1, name: 'John', age: 17, classes:[1,2]}, {id: 2, name: 'Jack', age: 18, classes:[4,5]}, {id: 3, name: 'Suze', age: 17, classes:[3]}, {id: 4, name: 'Jane', age: 16, classes:[6]} ];
As you can see, each young instructor has a list of classes that they teach. Each class has one, and only one, instructor for that class.
config.js
file for Mirage. Add the new routes:// app/mirage/config.js export default function() { this.get('/instructors',['instructors','classes']); this.get('/classes',['instructors','classes']); }
instructor
and class
data. This is done via sideloading. Here is an example of a JSON response sideloaded:{ "instructors": [ { "id": 1, "name": "John", "age": "17", "classes": [1,2] }, { "id": 2, "name": "Jack", "age": "18", "classes": [3,4] } ], "classes": [ { "id": 1, "subject": "History", "instructor": [1] }, { "id": 2, "subject": "Spanish", "instructor": [1] }, { "id": 3, "subject": "Government", "instructor": [2] }, { "id": 4, "subject": "English", "instructor": [2] }, ] }
As you can see from the preceding example, both the instructor
and class
data was returned. This is the default that RESTAdapter
expects.
On the other hand, we could return the data using asynchronous relationships. When this occurs, the server data store returns records only for one model. Ember then does one or more HTTP requests to retrieve data for the other model. For the simplicity of this example, we'll assume that the data is sideloaded.
// app/mirage/scenarios/default.js export default function( server ) { server.loadFixtures(); }
This will load both fixtures so that they can be returned to the Ember client.
application.js
file in the adapter
folder. Set it to RESTAdapter
:import DS from 'ember-data'; export default DS.RESTAdapter.extend({ });
RESTAdapter
will be used for this recipe.
class.js
and instructor.js
files in the models
folder. Add the new properties for the model:// app/models/class.js import DS from 'ember-data'; export default DS.Model.extend({ subject: DS.attr('string'), instructor: DS.belongsTo('instructor') });
In this example, we need to make sure that the class has one instructor
. This can be accomplished using the DS.belongsTo
method. This tells Ember to look at the instructor
model for this property.
DS.hasMany()
method and providing the name of the model:// app/models/instructor.js import DS from 'ember-data'; export default DS.Model.extend({ name: DS.attr('string'), age: DS.attr('number'), classes: DS.hasMany('class') });
index.js
file in the routes
folder. Specify it to return all the instructor
data:// app/routes/index.js import Ember from 'ember'; export default Ember.Route.extend({ model(){ return this.store.findAll('instructor'); } });
This route uses the Ember Data findAll
method to return all the instructor
data.
helper
file:// app/helpers/addone.js import Ember from 'ember'; export function addone(params) { return +params+1; } export default Ember.Helper.helper(addone);
Helpers in Ember are used to manipulate template data. You can pass information to one and return information. In this example, we are doing some simple mathematics.
index.hbs
file with the model data:// app/templates/index.hbs {{outlet}} {{#each model as |instructor|}} Name of instructor: <b>{{instructor.name}}</b><br> Teaches Classes:<br> {{#each instructor.classes as |class index|}} <b>{{addone index}}: {{class.subject}}</b><br> {{/each}} <br> <br> {{/each}}
In this template, we are using the each
helper to display the instructor's name. To access the class information, another each
helper iterates over instructor.classes
. In each iteration, we display the subject
and index
class. As the index starts at zero, we can pass it to the addone
helper. This helper increments the number passed to it.
ember server
and you should see all the data displayed from the fixture data:Ember uses the DS.hasMany
and DS.belongsTo
methods to signify a one-to-many and a one-to-one relationship. Ember, by convention, assumes that you are using the JSON API adapter. At the time of writing this, the JSON API is the default adapter for Ember Data. It communicates with a server via well-defined JSON via XHR. Its goal is to be easy to work with on the client and server side while working with a broad set of use cases, including relationships. For the most part, the REST adapter works fine. So, I've included it in the book instead of the JSON API adapter. Be aware that you can use either to accomplish your goals.
This can be changed using RESTAdapter
instead. RESTAdapter
assumes that all keys are camel-cased and that any data sent is sideloaded. This is done to help developers easily integrate their backend APIs and data stores with Ember.