Idempotenz
Eine idempotente Abfrage verändert keine Daten. Jede Abfrage sollte idempotent sein. Wenn das nicht möglich ist handelt es sich wahrscheinlich um keine Abfrage sondern eine Anweisung.
Eine Anweisung führt in der Regel zu veränderten Daten. Die Veränderungen finden unter Umständen nicht nur innerhalb der Software sondern auch bei externen Diensten statt. Im Gegensatz zu Abfragen bedeutet es bei Anweisungen etwas anderes wenn von Idempotenz gesprochen wird.
Eine Anweisung führt natürlich zu Veränderung, aber nur beim ersten Mal. Wenn die selbe Anweisung mehrfach entgegen genommen wird hat nur die erste Auswirkungen.
Diese Eigenschaft ist in der Praxis für robuste Systeme sehr nützlich. Bei einem Implementierungsfehler wodurch eine Anweisung mehrfach ausgeführt wird entsteht kein Schaden. In einer Situation mit schlechter Netzwerkverbindung können Anweisungen erneut gesendet werden. Das passiert zum Beispiel auch automatisch durch einen Webbrowser. Diese Wiederholung von HTTP-Requests führt ohne idempotente Anweisungen zu Problemen.
Implementierung
Folgender Ansatz funktioniert nur wenn eine einzige, ACID kompatible, Datenspeicher verwendet wird und es zu keinen sonstigen Datenveränderungen in externen Diensten kommt. Wenn diese Voraussetzungen nicht gegeben sind muss mit einem individuellen Wiederherstellungs-Prozess gearbeitet werden.
In einer Situation mit den beschriebenen Voraussetzungen ist der erste Schritt das garantieren einer sequenziellen Abarbeitung von identischen Anweisungen.
In PostgreSQL kann das mit dem Isolations Level Serializable
von Transaktionen erreicht werden.
(Relevante Dokumentation von PostgreSQL.)
Der zweite Schritt ist das erkennen dass eine Anweisung bereits ausgeführt wurde. In diesem Fall wird das ausführen der Anweisung übersprüngen und stattdessen sofort die Erfolgsantwort zurückgegeben.
Im folgenden Beispiel ist Pseudoquellcode für einen fiktiven HTTP-Endpunkt zur Registrierung eines Accounts zu sehen.
post '/accounts' do |request|
email = request.parameters.fetch(:email)
DB.transaction(isolation: :serializable) do
account = Account.find_by(email: email)
if account.exists?
return successful_account_registration(account)
else
account = Account.create(email: email)
return successful_account_registration(account)
end
end
end