Parse.Cloud.afterSave не работает с parse-сервером
Я использовал 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), чтобы сохранить называется сохранить более одного раза.
Функция сохраняет данные должным образом, но также вызывает бесконечный цикл. Я понимаю, что это происходит, потому что я звоню request.object.save()
это изменит League
снова класс, поэтому afterSave
событие снова срабатывает и так далее. Я не знаю, как я могу справиться с этим условием. Кто-то предложил мне добавить таймаут, но не уверен, как. Можете ли вы помочь с решением этой проблемы.
Спасибо
1 ответ
Ваш подход имеет две проблемы:
На гонке есть условие
leaderboard
, Когда обещание первого сохранения разрешится, не будетleaderboard
и тогда это будет волшебным образом "в какой-то момент в будущем". Намного лучше установить начальное значение вbeforeSave
так что состояниеleague
известен и предсказуем.Существует также условие гонки на
membersCount
, Представьте, что два обновления добавляют и / или удаляютmembers
приходит в то же время. Между прочтением отношений и записью счетчика может произойти другое обновление. Вы можете получить неправильный счет или даже отрицательное число!
Чтобы решить 1, мы просто перемещаем создание leaderboard
в beforeSave
, Для решения 2 мы переместим расчет membersCount
в beforeSave
, используйте предоставленную информацию о грязном объекте о member
складывает и вычитает и, наконец, мы используем increment
чтобы убедиться, что обновление является атомарным и избежать условия гонки.
Ниже приведен рабочий код с модульным тестом. Обратите внимание, что если бы я делал свой собственный анализ кода этого, я бы а) захотел проверить добавление нескольких и вычесть несколько членов б) разбить большой первый тест на несколько тестов, где только один тест проверяется на тест. в) проверить добавление и удаление в том же сохранении.
Я использую конструкции 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);
});
});