8

I have two models with parent-child relationship: training and exercise:

App.Training = DS.Model.extend({
  exercises: DS.hasMany('App.Exercise')
})

App.Exercise = DS.Model.extend({
  training: DS.belongsTo('App.Training')
})

I want to have a page where a training with all its related exercises is displayed. If the user presses the Edit button, the page becomes editable with the possibility of adding new exercises. I also want to have a Cancel button which discards all the changes made.

Here is my controller:

App.TrainingsShowController = Em.ObjectController.extend({
  editing: false,

  edit: function() {
    this.set('editing', true);
    transaction = this.get('store').transaction();
    transaction.add(this.get('model'));
    this.get('model.exercises').forEach(function(x){
      transaction.add(x);
    });
  },

  cancel: function() {
    this.set('editing', false);
    this.get('model.transaction').rollback();
  },

  save: function() {
    this.set('editing', false);
    this.get('model.transaction').commit();
  },

  addExercise: function() {
    this.get('model.exercises').createRecord({});
  }
})

There are four event handlers in the controller:

  1. edit: The user pressed the Edit button: a transaction is created, the page is put into "Editing" mode.
  2. cancel: The user pressed the Cancel button: transaction is rolled back and back to "Normal" mode.
  3. save: The user pressed the Save button: transaction is commited and back to "Normal" mode.
  4. addExercise: The user pressed the Add exercise button: a new exercise is created (in the same transaction) and added to the trainings.

The rollback functionality works fine except for newly created records: if I push the Edit button, add a new exercise and push the Cancel button, the newly created exercise stays on the page.

What is the best way to get rid of the discarded child record?

UPDATE:

I've created a jsFiddle to reproduce problem, but it worked. Unlike my application here I used DS.FixtureAdapter: http://jsfiddle.net/tothda/LaXLG/13/

Then I've created an other one using DS.RESTAdapter and the problem showed up: http://jsfiddle.net/tothda/qwZc4/5/

In the fiddle try: Edit, Add new and then Rollback.

I figured it out, that in case of the RESTAdapter when I add a new child record to a hasMany relationship, the parent record won't become dirty. Which seems fine, but when I rollback the transaction, the newly created child record stays in the parent's ManyArray.

I still don't know, what's the best way to handle the situation.

4

4 回答 4

12

在 Ember Data 中严重缺乏对 hasMany 和 belongsTo 关系的正确脏检查和回滚。它当前的行为方式通常被报告为错误。对于许多开发人员来说,这是一个很大的痛点,这里正在讨论如何解决这个问题:

https://github.com/emberjs/rfcs/pull/21

在找到适当的解决方案之前,您可以使用以下方法解决此问题。

首先,您需要重新打开 DS.Model 并对其进行扩展。如果您使用全局变量,您可以将它(例如 DS.Model.reopen({}))放在任何地方,但如果您使用 Ember CLI,最好创建一个初始化程序(例如 ember g 初始化程序模型):

import DS from 'ember-data';

export function initialize(/* container, application */) {

    DS.Model.reopen({

        saveOriginalRelations: function() {

            this.originalRelations = {};
            this.constructor.eachRelationship(function(key, relationship) {

                if (relationship.kind === 'belongsTo')
                    this.originalRelations[key] = this.get(key);

                if (relationship.kind === 'hasMany')
                    this.originalRelations[key] = this.get(key).toArray();

            }, this);
        },

        onLoad: function() {

            this.saveOriginalRelations();

        }.on('didLoad', 'didCreate', 'didUpdate'),

        onReloading: function() {

            if (!this.get('isReloading'))
                this.saveOriginalRelations();

        }.observes('isReloading'),    

        rollback: function() {

            this._super();

            if (!this.originalRelations)
                return;

            Ember.keys(this.originalRelations).forEach(function(key) {

                // careful, as Ember.typeOf for ArrayProxy is 'instance'
                if (Ember.isArray(this.get(key))) {
                    this.get(key).setObjects(this.originalRelations[key]);
                    this.get(key).filterBy('isDirty').invoke('rollback');
                    return;
                }

                if (Ember.typeOf(this.get(key)) === 'instance') {
                    this.set(key, this.originalRelations[key]);
                    return;
                }

            }, this);
        },

        isDeepDirty: function() {
            if (this._super('isDirty'))
                return true;

            if (!this.originalRelations)
                return false;

            return Ember.keys(this.originalRelations).any(function(key) {

                if (Ember.isArray(this.get(key))) {
                    if (this.get(key).anyBy('isDirty'))
                        return true;

                    if (this.get(key).get('length') !== this.originalRelations[key].length)
                        return true;

                    var dirty = false;
                    this.get(key).forEach(function(item, index) {
                        if (item.get('id') !== this.originalRelations[key][index].get('id'))
                            dirty = true;
                    }, this);

                    return dirty;
                }

                return this.get(key).get('isDirty') || this.get(key).get('id') !== this.originalRelations[key].get('id');

            }, this);
        }
    });
};

export default {
    name: 'model',
    initialize: initialize
};

上面的代码本质上存储了加载或更新时的原始关系,以便以后可以用于回滚和脏检查。

model.rollback() 现在应该回滚所有内容,包括 hasMany 和 belongsTo 关系。不过,我们还没有完全解决“isDirty”检查。为此,我们需要在模型的具体实现中重写 isDirty。之所以需要在这里做而我们不能在 DS.Model 中做一般的事情是因为 DS.Model 不知道要注意哪些属性变化。这是一个使用 Ember CLI 的示例。全局变量也可以使用相同的方法,只是您要将此类分配给 App.Book 之类的东西:

import DS from 'ember-data';

var Book = DS.Model.extend({

    publisher: DS.belongsTo('publisher'),

    authors: DS.hasMany('author'),

    isDirty: function() {
        return this.isDeepDirty();
    }.property('currentState', 'publisher', 'authors.[]', 'authors.@each.isDirty').readOnly()

});

export default Book;

对于 isDirty 的依赖参数,请确保包含所有 belongsTo 关系,并为每个 hasMany 关系包含 'array.[]' 和 'array.@each.isDirty'。现在 isDirty 应该按预期工作。

于 2014-11-28T07:54:45.953 回答
1

这并不漂亮,但您可以通过手动弄脏父记录来强制它回滚:

parent.send('becomeDirty');
parent.rollback();
parent.get('children.length'); // => 0
于 2013-04-11T03:44:08.780 回答
0

@tothda 和其他读者关注。由于Ember Data : 1.0.0-beta.10+canary.7db210f29a父级仍然没有设计为在子级回滚时将parentTraining.isDirty()值设为真。Ember Data确实在属性更改时认为父记录是的,但在数组发生更改时不会(这允许 save()工作,因此您可以更新服务器上对父属性的任何更改)。DS.hasMany

对于提到的情况,您想要对rollback()新创建的子项执行 a 的解决方法是用您要丢弃的子记录上的.rollback()a替换。.deleteRecord()然后 Ember Data 会自动知道将其从DS.hasMany数组中删除,您可以轻拍自己的背部以完成回滚!

于 2014-08-28T13:58:13.373 回答
0

派对迟到了,但我们开始吧:

我创建了一个插件来解决这个问题。只需打电话rollbackRelationships(),它就会回滚你所有的关系(belongsTo & hasMany)。查看 README 以获得更多选项。

https://www.npmjs.com/package/ember-rollback-relationships

于 2016-12-15T16:16:34.363 回答