Parse.Cloud.afterSave不使用parse-server

时间:2017-03-30 17:01:45

标签: javascript parse-platform parse-server parse-cloud

我使用的是parse.com,编写了一个完美的云代码功能。当我转移到自托管解析服务器后端时,一些云代码功能停止工作。

Parse.Cloud.afterSave("League", function (request) {

    if (request.object.get("leaderboard") == null) {

        var leaderboard = Parse.Object.extend("Leaderboard");
        var newInstance = new leaderboard();

        newInstance.save(null , {useMasterKey: true})
            .then(function (result) {
                request.object.set("leaderboard", result);
                request.object.save(null ,{useMasterKey: true});
            },
            function (error) {
                console.log("Error");
            });
        });
    }else{
        var membersRelation = request.object.relation("members");
        var membersQuery = membersRelation.query();
        membersQuery.count(null , {useMasterKey: true})
            .then(function (totalNumber) {
                request.object.set("memberCount", totalNumber)
                request.object.save(null ,{useMasterKey: true});
            }, function (error) {
                console.log("Error")
            })
     }

如您所见,我为afterSave类定义了League挂钩。在我的钩子中,当我设置一个新值(排行榜和/或membersCount)时,我必须再次更新同一个对象,因此save被称为多次保存。

该函数可以正确保存数据,但也会导致无限循环。我理解这种情况发生是因为我调用request.object.save()会再次更改League类,因此afterSave事件会再次触发,依此类推。我不知道如何处理这种情况。有人建议我添加超时但不确定如何。你能帮忙解决这个问题。

由于

1 个答案:

答案 0 :(得分:1)

您的方法有两个问题:

  1. leaderboard上有竞争条件。当第一次保存的承诺结算时,将没有leaderboard,然后它将在未来的某个时刻神奇地存在“。更好的是在beforeSave中设置初始值,以便league的状态是已知且可预测的。

  2. membersCount上也有竞争条件。想象一下,添加和/或删除members的两个更新同时进入。在读取关系和写入计数之间,可能会发生其他更新。你最终可能得错了,甚至是负数!

  3. 要解决1,我们只需将leaderboard的创建移动到beforeSave即可。要解决2,我们将membersCount的计算移动到beforeSave,使用提供的关于member脏对象的信息添加和减去最后我们使用increment来制作确保更新是原子的并避免竞争条件。

    下面是带有单元测试的工作代码。请注意,如果我正在对此进行自己的代码审查,我会a)想要测试添加多个并减去多个成员b)将大的第一个测试分成多个测试,其中每个测试只测试一件事。 c)在同一个保存中测试添加和删除。

    我正在使用es6构造因为我喜欢它们;)。

    尝试发表很多评论,但随时可以问我是否有些令人困惑。

    PS如果您不知道如何对云代码进行单元测试,请提出另一个问题,因为它对于弄清楚这些东西的工作原理是非常宝贵的(并且查看解析服务器单元测试是最好的你会找到的文件)

    祝你好运!

    const addLeaderboard = function addLeaderboard(league) {
      // note the simplified object creation without using extends.
      return new Parse.Object('Leaderboard')
        // I was surprised to find that I had to save the new leaderboard
        // before saving the league. too bad & unit tests ftw.
        .save(null, { useMasterKey: true })
        // "fat arrow" function declaration.  If there's only a single
        // line in the function and you don't use {} then the result
        // of that line is the return value.  cool!
        .then(leaderboard => league.set('leaderboard', leaderboard));
    }
    
    const leagueBeforeSave = function leagueBeforeSave(request, response) {
      // Always prefer immutability to avoid bugs!
      const league = request.object;
    
      if (league.op('members')) {
        // Using a debugger to see what is available on the league
        // is super helpful, cause I have never seen this stuff
        // documented, but its obvious in a debugger.
        const membersAdded = league.op('members').relationsToAdd.length;
        const membersRemoved = league.op('members').relationsToRemove.length;
        const membersChange = membersAdded - membersRemoved;
        if (membersChange !== 0) {
          // by setting increment when the save is done, the
          // change in this value will be atomic.  By using a change
          // in the value rather than an absolute number
          // we avoid a race condition when paired with the atomicity of increment
          league.increment('membersCount', membersChange);
        }
      }
    
      if (!league.get('leaderboard')) {
        // notice we don't have to save the league, we just
        // add the leaderboard.  When we call success, the league
        // will be saved and the leaderboard will be there....
        addLeaderboard(league)
          .then(() => response.success(league))
          .catch(response.error);
      } else {
        response.success(league);
      }
    };
    
    // The rest of this is just to test our beforeSave hook.
    describe('league save logic', () => {
    
      beforeEach(() => {
        Parse.Cloud.beforeSave('League', leagueBeforeSave);
      });
    
      it('should create league and increment properly', (done) => {
        Parse.Promise.when([
          new Parse.Object('Member').save(),
          new Parse.Object('Member').save(),
          new Parse.Object('Member').save(),
          new Parse.Object('Member').save(),
        ])
          .then((members) => {
            const league = new Parse.Object('League');
            const memberRelation = league.relation('members');
            memberRelation.add(members);
            // I want to use members in the next promise block,
            // there are a number of ways to do this, but I like
            // passing the value this way.  See Parse.Promise.when
            // doc if this is mysterious.
            return Parse.Promise.when(
              league.save(null, { useMasterKey: true }),
              members);
          })
          .then((league, members) => {
            expect(league.get('leaderboard').className).toBe('Leaderboard');
            expect(league.get('membersCount')).toBe(4);
            const memberRelation = league.relation('members');
            memberRelation.remove(members[0]);
            return league.save(null, { useMasterKey: true });
          })
          .then((league) => {
            expect(league.get('membersCount')).toBe(3);
            // just do a save with no change to members to make sure
            // we don't have something that breaks in that case...
            return league
              .set('foo', 'bar')
              .save(null, { useMasterKey: true })
          })
          .then(league => {
            expect(league.get('foo')).toBe('bar');
            done();
          })
          .catch(done.fail);
      });
    
      it('should work to create new without any members too', (done) => {
        new Parse.Object('League')
          .save() // we don't really need the useMasterKey in unit tests unless we setup `acl`s..:).
          .then((league) => {
            expect(league.get('leaderboard').className).toBe('Leaderboard');
            done();
          })
          .catch(done.fail);
      });
    });