Search code examples
ember.jsasync-awaitember-datarsvp.js

How to iteratively save child records in Ember.js?


I want to save a parent record and multiple child records to an external REST API using Ember Data. The problem I have is that I'm not sure what the best way to handle this asynchronously.

My first attempt looked something like this:

let parent = this.store.createRecord('parent');
parent.set('name', 'Bob');
parent.set('children', []);
parent.save().then(()=>{
  for (i=0;i<8;i++) {
    let child = this.store.createRecord('child');
    child.set('name', 'Child' + i);
    child.set('parent', parent);
    child.save().then(()=>{
      parent.get('children').pushObject(child);
      if (i == 7) {parent.save()}
    }
  }
})

This successfully creates the parent and all children but fails to save the ids of all children in the parent's record. Not all child.save() promises have resolved by time parent.save() is called.

I understand that RSVP is a library that can be used to resolve this problem, but I can't find any examples of its usage that make sense in context to what I'm trying to do.

Can anyone outline how I can rework this to ensure all children have been saved before attempting to save the parent?


Solution

  • Ola @Bean, thanks for your question 🎉

    As @jelhan asked in his comment the answer to this question really depends on what your API can support 🤔 I will answer your question with a few assumptions and generalisations but hopefully it will be useful!

    Firstly let's make something explicit, I'm going to define the models for both Parent and Child so that we know how it will work.

    // app/models/parent.js
    import DS from 'ember-data';
    const { Model, attr, hasMany } = DS;
    
    export default Model.extend({
      name: attr(),
      children: hasMany('child'),
    });
    
    // app/models/child.js
    import DS from 'ember-data';
    const { Model, attr, belongsTo } = DS;
    
    export default Model.extend({
      name: attr(),
      parent: belongsTo('parent'),
    });
    

    Now that we have the models I'm going to setup a parent and 8 children much like your example, but in mine I'm going to keep the saving separate for clarity:

    // create 1 parent
    let parent = this.store.createRecord('parent', {
      name: 'Bob'
    });
    
    // create 8 children
    let children = [];
    
    for (i=0;i<8;i++) {
      let child = this.store.createRecord('child', {
        name: 'Child' + i
      });
    
      children.push(child);
    }
    

    So now you will have 1 parent and 8 children in the store but no network requests because we haven't saved anything.

    You now have 2 options (depending on what your backend supports). You can either:

    • save the Parent first
    • set the parent on each of the children
    • save each of the children

    or you can:

    • save each of the children
    • set the children array of parent to be the set of child models
    • save the parent

    I'm going to go through both of these options now in an example. I'm using async-await to make it a bit easier to read so make sure you are using it correctly 👍

    // Save parent first
    await parent.save();
    
    let promises = children.map((child) => {
      child.set('parent', parent);
      return child.save();
    })
    
    await Promise.all(promises);
    
    // Save children first
    let promises = children.map((child) => {
      return child.save();
    })
    
    await Promise.all(promises);
    
    parent.set('children', children);
    
    await parent.save();
    

    Because of the way that ember-data works it should automatically associate the reverse relationship for you 🎉 If you want to find out more about this you can check out the documentation on the official Ember documentation https://guides.emberjs.com/release/models/relationships/#toc_explicit-inverses