Cascade Deleting Relationships in Ember Data

Friday, February 10th, 2017

In Ember Data, if you delete a record from the store, related records are kept in the store. Let’s say you have the following models:

// app/models/post.js
export default DS.Model.extend({
  body: DS.attr(),
  comments: DS.hasMany()
});
// app/models/comment.js
export default DS.Model.extend({
  body: DS.attr()
});

Let’s assume a post record is loaded into the store along with its comments. If the post is deleted, it is unloaded from the store, but all of its comments remain in the store. Sometimes you want this. Sometimes you don’t. If the comments are orphaned records and are no longer used, it might be a good idea to remove them to free up space.

One way to do this is to unload the related records manually. For example:

let comments = post.hasMany('comments').value().toArray();
post.destroyRecord().then(() => {
  comments.forEach((comment) => {
    // need access to the store
    this.store.unloadRecord(comment);
  });
});

This works, but the clarity of the code suffers a little bit, especially if multiple related records need to be unloaded. Also, if this behavior is required in multiple places in your app, it can get repetitive.

Wouldn’t it be nice if we could declaratively state that comments should be unloaded when post.destroyRecord() is called? Maybe something like this:

// app/models/post.js
export default DS.Model.extend({
  body: DS.attr(),
  comments: DS.hasMany('comment', { cascadeDelete: true })
});

The cascadeDelete option isn’t a property recognized by Ember Data, but we can configure our adapter to use it.

Let’s create a mixin to contain this behavior and update our application adapter:

// app/mixins/cascade-delete.js
import Ember from 'ember';

export default Ember.Mixin.create({
  // our implementation will go here
});
// app/adapters/application.js
import DS from 'ember-data';
import CascadeDeleteMixin from './../mixins/cascade-delete';

export default DS.JSONAPIAdapter.extend(CascadeDeleteMixin, {
});

Now let’s implement the mixin. From a high level, we’ll override the deleteRecord method on the adapter. Before a record is deleted, we’ll collect all related records that were declared with the cascadeDelete option. Then, we’ll proceed to delete the record, and if the request succeeds, the collected related records will be unloaded from the store. Here is an implementation:

import Ember from 'ember';

export default Ember.Mixin.create({
  deleteRecord(store, type, snapshot) {
    let recordsToUnload = [];

    // collect all records to unload into recordsToUnload variable
    snapshot.record.eachRelationship((name, descriptor) => {
      let { options, kind } = descriptor;
      let relationshipName = descriptor.key;

      if (options.cascadeDelete && kind === 'hasMany') {
        let hasManyRecordsArray = [];
        let hasManyRecords = snapshot.record.hasMany(relationshipName).value();
        if (hasManyRecords !== null) {
          hasManyRecordsArray = hasManyRecords.toArray();
        }
        recordsToUnload = recordsToUnload.concat(hasManyRecordsArray);
      }

      if (options.cascadeDelete && kind === 'belongsTo') {
        let belongsToRecords = snapshot.record.belongsTo(relationshipName).value();
        recordsToUnload = recordsToUnload.concat([ belongsToRecords ]);
      }
    });

    return this._super(...arguments).then((response) => {
      recordsToUnload.forEach((childRecord) => {
        store.unloadRecord(childRecord);
      });

      return response;
    });
  }
});

Now a more detailed explanation.

First, we can get access to the record we’re about to delete from the snapshot via snapshot.record. Read more about Ember Data snapshots in What are Ember Data Snapshots?.

Next, we can use the DS.Model#eachRelationship method to iterate over the deleted record’s relationships, which allows access to the cascadeDelete option through descriptor.options.

We can also get access to the relationship name “comments” with descriptor.key.

To get the related records, we can use Ember Data’s ds-references feature to get the related data (i.e. comments) synchronously. Check out this blog post on the Ember blog to learn more about ds-references.

Lastly, the original deleteRecord on the adapter is called, and if that succeeds, the related records can be unloaded from the store.

Now, whenever a post is deleted, all of its comments will be unloaded from the store. This mixin can also be used to cascade delete belongsTo relationships.

Here is the code in a working demo.

Interested in learning more about Ember Data and how to use it with any API? Check out my book Ember Data in the Wild - Getting Ember Data to Work With Your API.


Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.

comments powered by Disqus