Parse.Cloud.afterSave 无法使用解析服务器
Parse.Cloud.afterSave not working with parse-server
我正在使用 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")
})
}
如您所见,我为 League
class 定义了 afterSave
挂钩。在我的钩子中,当我设置一个新值 (leaderboard and/or membersCount) 时,我必须再次更新同一个对象,所以保存不止一次被调用。
函数保存数据正常,但也造成死循环。我知道它发生是因为我调用 request.object.save()
将再次更改 League
class 因此 afterSave
事件再次触发,依此类推。我不知道我该如何处理这种情况。有人建议我添加超时但不确定如何。你能帮忙解决这个问题吗?
谢谢
你的方法有两个问题:
leaderboard
上存在竞争条件。当第一次保存的承诺解决时,不会有 leaderboard
,然后它会神奇地出现在那里 "at some point in the future"。最好在 beforeSave
中设置初始值,这样 league
的状态是已知且可预测的。
membersCount
上也存在竞争条件。想象一下,添加 and/or 删除 members
的两个更新同时进入。在您读取关系和写入计数之间,可能会发生其他更新。您最终可能会得到错误的计数,甚至是负数!
要解决 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);
});
});
我正在使用 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")
})
}
如您所见,我为 League
class 定义了 afterSave
挂钩。在我的钩子中,当我设置一个新值 (leaderboard and/or membersCount) 时,我必须再次更新同一个对象,所以保存不止一次被调用。
函数保存数据正常,但也造成死循环。我知道它发生是因为我调用 request.object.save()
将再次更改 League
class 因此 afterSave
事件再次触发,依此类推。我不知道我该如何处理这种情况。有人建议我添加超时但不确定如何。你能帮忙解决这个问题吗?
谢谢
你的方法有两个问题:
leaderboard
上存在竞争条件。当第一次保存的承诺解决时,不会有leaderboard
,然后它会神奇地出现在那里 "at some point in the future"。最好在beforeSave
中设置初始值,这样league
的状态是已知且可预测的。membersCount
上也存在竞争条件。想象一下,添加 and/or 删除members
的两个更新同时进入。在您读取关系和写入计数之间,可能会发生其他更新。您最终可能会得到错误的计数,甚至是负数!
要解决 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);
});
});