AppEngine: Le maintien de la cohérence DataStore Lorsque Création d'enregistrements
-
22-08-2019 - |
Question
Je l'ai touché un petit dilemme! J'ai un gestionnaire appelé vote; lorsqu'il est appelé, il définit le vote d'un utilisateur à tout ce qu'ils ont pris. Pour ne pas oublier les options qu'ils ont choisi précédemment, je stocke un choix de VoteRecord qui décrit en détail ce que leur vote actuel est réglé sur.
Bien sûr, la première fois qu'ils votent, je dois créer l'objet et le stocker. Mais votes successifs devraient simplement changer la valeur de la VoteRecord existante. Mais il vient le problème: dans certaines circonstances, deux VoteRecords peuvent être créés. Il est rare (seulement arrivé une fois dans tous les 500 votes, nous avons vu jusqu'à présent), mais encore mal quand il le fait.
Le problème se produit parce que deux gestionnaires distincts pour les deux font essentiellement ceci:
query = VoteRecord.all().filter('user =', session.user).filter('poll =', poll)
if query.count(1) > 0:
vote = query[0]
poll.votes[vote.option] -= 1
poll.votes[option] += 1
poll.put()
vote.option = option
vote.updated = datetime.now()
vote.put()
else:
vote = VoteRecord()
vote.user = session.user
vote.poll = poll
vote.option = option
vote.put()
poll.votes[option] += 1
poll.put()
session.user.votes += 1
session.user.xp += 3
session.user.put()
incr('votes')
Ma question est: quelle est la plus efficace et la plus rapide pour traiter ces demandes, tout en assurant qu'aucune demande est perdue et aucune demande crée deux objets VoteRecord
La solution
La question est cette partie:
if vote.count(1) == 0:
obj = VoteRecord()
obj.user = user
obj.option = option
obj.put()
Sans une transaction, votre code pourrait exécuter dans cet ordre dans deux instances d'interpréteur:
if vote.count(1) == 0:
obj = VoteRecord()
obj.user = user
if vote.count(1) == 0:
obj = VoteRecord()
obj.user = user
obj.option = option
obj.put()
obj.option = option
obj.put()
Ou une combinaison étrange de ceux-ci. Le problème est le test de comptage fonctionne à nouveau avant la vente a eu lieu, de sorte que le second fil passe par la première partie du conditionnel au lieu du second.
Vous pouvez résoudre ce problème en plaçant le code dans une fonction, puis en utilisant
db.run_in_transaction()
pour exécuter la fonction.
Le problème est que vous semblez compter sur le nombre d'objets retournés par une requête pour votre logique de décision qui doit être mis dans la transaction. Si vous lisez les pourparlers d'E / S de Google ou de regarder le groupe, vous verrez que cela ne soit pas recommandé. C'est parce que vous ne pouvez pas transactionalize une requête. Au lieu de cela, vous devez stocker le compte en tant que valeur d'entité quelque part, requête pour elle à l'extérieur de la fonction de transaction, et ensuite passer la clé de cette entité à votre fonction de transaction.
Voici un exemple d'une fonction de transaction qui vérifie une propriété d'entité. Il est passé la clé comme paramètre:
def checkAndLockPage(pageKey):
page = db.get(pageKey)
if page.locked:
return False
else:
page.locked = True
page.put()
return True
Un seul utilisateur à la fois peut verrouiller cette entité, et il n'y aura jamais de verrous en double.
Autres conseils
La meilleure façon de le faire est d'utiliser des noms clés pour vos objets de vote, et utiliser Model.get_or_insert. Tout d'abord, venir avec un schéma de nommage pour vos noms clés - nommant après le scrutin est une bonne idée - et puis faire un get_or_insert chercher ou créer l'entité concernée:
vote = VoteRecord.get_or_insert(pollname, parent=session.user, user=session.user, poll=poll, option=option)
if vote.option != option:
# Record already existed; we need to update it
vote.option = option
vote.put()