Database
 sql >> Datenbank >  >> RDS >> Database

Transaktionsverwaltung mit Django 1.6

Wenn Sie jemals viel Zeit mit dem Transaktionsmanagement von Django-Datenbanken verbracht haben, wissen Sie, wie verwirrend es werden kann. In der Vergangenheit bot die Dokumentation ziemlich viel Tiefe, aber das Verständnis kam nur durch Bauen und Experimentieren.

Es gab eine Fülle von Dekorateuren, mit denen man arbeiten konnte, wie commit_on_success , commit_manually , commit_unless_managed , rollback_unless_managed , enter_transaction_management , leave_transaction_management , nur um ein paar zu nennen. Glücklicherweise geht mit Django 1.6 das alles aus der Tür. Sie müssen jetzt wirklich nur ein paar Funktionen kennen. Und dazu kommen wir gleich. Zunächst behandeln wir diese Themen:

  • Was ist Transaktionsverwaltung?
  • Was ist falsch an der Transaktionsverwaltung vor Django 1.6?

Bevor Sie hineinspringen:

  • Was ist richtig am Transaktionsmanagement in Django 1.6?

Und dann noch ein ausführliches Beispiel:

  • Streifenbeispiel
  • Transaktionen
  • Der empfohlene Weg
  • Einen Dekorateur verwenden
  • Transaktion per HTTP-Anfrage
  • Speicherpunkte
  • Verschachtelte Transaktionen

Was ist eine Transaktion?

Laut SQL-92 ist „eine SQL-Transaktion (manchmal einfach als „Transaktion“ bezeichnet) eine Abfolge von Ausführungen von SQL-Anweisungen, die in Bezug auf die Wiederherstellung atomar ist“. Mit anderen Worten, alle SQL-Anweisungen werden zusammen ausgeführt und festgeschrieben. Ebenso werden beim Zurücksetzen alle Anweisungen zusammen zurückgesetzt.

Zum Beispiel:

# START
note = Note(title="my first note", text="Yay!")
note = Note(title="my second note", text="Whee!")
address1.save()
address2.save()
# COMMIT

Eine Transaktion ist also eine einzelne Arbeitseinheit in einer Datenbank. Und diese einzelne Arbeitseinheit wird durch eine Starttransaktion und dann ein Commit oder ein explizites Rollback abgegrenzt.



Was ist mit der Transaktionsverwaltung vor Django 1.6 falsch?

Um diese Frage vollständig zu beantworten, müssen wir uns damit befassen, wie Transaktionen in der Datenbank, den Client-Bibliotheken und innerhalb von Django behandelt werden.


Datenbanken

Jede Anweisung in einer Datenbank muss in einer Transaktion ausgeführt werden, auch wenn die Transaktion nur eine Anweisung enthält.

Die meisten Datenbanken haben einen AUTOCOMMIT -Einstellung, die normalerweise standardmäßig auf True gesetzt ist. Dieses AUTOCOMMIT verpackt jede Anweisung in eine Transaktion, die sofort festgeschrieben wird, wenn die Anweisung erfolgreich ist. Natürlich können Sie so etwas wie START_TRANSACTION manuell aufrufen wodurch AUTOCOMMIT vorübergehend ausgesetzt wird bis Sie COMMIT_TRANSACTION aufrufen oder ROLLBACK .

Der Nachteil dabei ist jedoch, dass AUTOCOMMIT Die Einstellung wendet nach jeder Anweisung ein implizites Commit an .



Client-Bibliotheken

Dann gibt es noch die Client-Bibliotheken von Python wie sqlite3 und mysqldb, die es Python-Programmen ermöglichen, mit den Datenbanken selbst zu kommunizieren. Solche Bibliotheken befolgen eine Reihe von Standards für den Zugriff auf und die Abfrage der Datenbanken. Dieser Standard, DB API 2.0, wird in PEP 249 beschrieben. Auch wenn es etwas trocken klingt, ist eine wichtige Erkenntnis, dass PEP 249 angibt, dass die Datenbank AUTOCOMMIT sollte AUS sein standardmäßig.

Dies widerspricht eindeutig dem, was in der Datenbank passiert:

  • SQL-Anweisungen müssen immer in einer Transaktion ausgeführt werden, die die Datenbank in der Regel per AUTOCOMMIT für Sie öffnet .
  • Laut PEP 249 sollte dies jedoch nicht passieren.
  • Client-Bibliotheken müssen spiegeln, was in der Datenbank passiert, aber da sie AUTOCOMMIT nicht aktivieren dürfen standardmäßig aktiviert, verpacken sie einfach Ihre SQL-Anweisungen in einer Transaktion, genau wie die Datenbank.

Okay. Bleib noch ein bisschen bei mir.



Django

Geben Sie Django ein. Django hat auch etwas zum Transaktionsmanagement zu sagen. In Django 1.5 und früher lief Django grundsätzlich mit einer offenen Transaktion und hat diese Transaktion automatisch festgeschrieben, als Sie Daten in die Datenbank geschrieben haben. Also jedes Mal, wenn Sie so etwas wie model.save() aufgerufen haben oder model.update() , Django hat die entsprechenden SQL-Anweisungen generiert und die Transaktion festgeschrieben.

Auch in Django 1.5 und früher wurde empfohlen, dass Sie die TransactionMiddleware verwenden um Transaktionen an HTTP-Anforderungen zu binden. Jeder Anfrage wurde eine Transaktion zugeordnet. Wenn die Antwort ohne Ausnahmen zurückkehrte, würde Django die Transaktion festschreiben, aber wenn Ihre Ansichtsfunktion einen Fehler auslöste, ROLLBACK gerufen würde. Dadurch wurde AUTOCOMMIT tatsächlich deaktiviert . Wenn Sie eine standardmäßige Transaktionsverwaltung im Autocommit-Stil auf Datenbankebene wollten, mussten Sie die Transaktionen selbst verwalten - normalerweise durch Verwendung eines Transaktionsdekorators in Ihrer Ansichtsfunktion wie @transaction.commit_manually , oder @transaction.commit_on_success .

Hol erstmal Luft. Oder zwei.



Was bedeutet das?

Ja, da passiert viel, und es stellt sich heraus, dass die meisten Entwickler nur die Standard-Autocommits auf Datenbankebene wollen – was bedeutet, dass Transaktionen hinter den Kulissen bleiben und ihr Ding machen, bis Sie sie manuell anpassen müssen.




Was ist richtig am Transaktionsmanagement in Django 1.6?

Willkommen bei Django 1.6. Tun Sie Ihr Bestes, um alles zu vergessen, worüber wir gerade gesprochen haben, und denken Sie einfach daran, dass Sie in Django 1.6 die Datenbank AUTOCOMMIT verwenden und verwalten Sie Transaktionen bei Bedarf manuell. Im Wesentlichen haben wir ein viel einfacheres Modell, das im Grunde das tut, wofür die Datenbank ursprünglich entwickelt wurde.

Genug Theorie. Lassen Sie uns programmieren.



Stripe-Beispiel

Hier haben wir diese Beispielansichtsfunktion, die die Registrierung eines Benutzers und den Anruf bei Stripe für die Kreditkartenverarbeitung handhabt.

def register(request):
    user = None
    if request.method == 'POST':
        form = UserForm(request.POST)
        if form.is_valid():

            customer = Customer.create("subscription",
              email = form.cleaned_data['email'],
              description = form.cleaned_data['name'],
              card = form.cleaned_data['stripe_token'],
              plan="gold",
            )

            cd = form.cleaned_data
            try:
                user = User.create(cd['name'], cd['email'], cd['password'],
                   cd['last_4_digits'])

                if customer:
                    user.stripe_id = customer.id
                    user.save()
                else:
                    UnpaidUsers(email=cd['email']).save()

            except IntegrityError:
                form.addError(cd['email'] + ' is already a member')
            else:
                request.session['user'] = user.pk
                return HttpResponseRedirect('/')

    else:
      form = UserForm()

    return render_to_response(
        'register.html',
        {
          'form': form,
          'months': range(1, 12),
          'publishable': settings.STRIPE_PUBLISHABLE,
          'soon': soon(),
          'user': user,
          'years': range(2011, 2036),
        },
        context_instance=RequestContext(request)
    )

Diese Ansicht ruft zuerst Customer.create auf die tatsächlich Stripe aufruft, um die Kreditkartenverarbeitung zu übernehmen. Dann legen wir einen neuen Benutzer an. Wenn wir eine Antwort von Stripe erhalten, aktualisieren wir den neu erstellten Kunden mit der stripe_id . Wenn wir keinen Kunden zurückbekommen (Stripe ist ausgefallen), fügen wir einen Eintrag zu den UnpaidUsers hinzu Tabelle mit der neu erstellten Kunden-E-Mail, damit wir sie bitten können, ihre Kreditkartendaten später erneut zu versuchen.

Die Idee ist, dass sich der Benutzer auch dann registrieren und unsere Website nutzen kann, wenn Stripe nicht verfügbar ist. Wir werden sie einfach zu einem späteren Zeitpunkt erneut nach den Kreditkarteninformationen fragen.

Ich verstehe, dass dies ein bisschen wie ein erfundenes Beispiel sein kann, und es ist nicht die Art und Weise, wie ich eine solche Funktionalität implementieren würde, wenn ich müsste, aber der Zweck besteht darin, Transaktionen zu demonstrieren.

Weiter. Denken Sie an Transaktionen und denken Sie daran, dass uns Django 1.6 standardmäßig AUTOCOMMIT gibt Verhalten für unsere Datenbank, schauen wir uns den datenbankbezogenen Code etwas länger an.

cd = form.cleaned_data
try:
    user = User.create(
        cd['name'], cd['email'], 
        cd['password'], cd['last_4_digits'])

    if customer:
        user.stripe_id = customer.id
        user.save()
    else:
        UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    # ...

Können Sie Probleme erkennen? Nun, was passiert, wenn UnpaidUsers(email=cd['email']).save() Linie schlägt fehl?

Sie werden einen im System registrierten Benutzer haben, von dem das System denkt, dass er seine Kreditkarte verifiziert hat, aber in Wirklichkeit hat er die Karte nicht verifiziert.

Wir wollen nur eines von zwei Ergebnissen:

  1. Der Benutzer wird (in der Datenbank) erstellt und hat eine stripe_id .
  2. Der Benutzer wird (in der Datenbank) erstellt und hat keine stripe_id UND eine zugehörige Zeile in UnpaidUsers Tabelle mit derselben E-Mail-Adresse wird generiert.

Das heißt, wir wollen, dass die beiden separaten Datenbankanweisungen entweder beide festschreiben oder beide zurücksetzen. Ein perfekter Fall für die bescheidene Transaktion.

Lassen Sie uns zuerst einige Tests schreiben, um zu überprüfen, ob sich die Dinge so verhalten, wie wir es wollen.

@mock.patch('payments.models.UnpaidUsers.save', side_effect = IntegrityError)
def test_registering_user_when_strip_is_down_all_or_nothing(self, save_mock):

    #create the request used to test the view
    self.request.session = {}
    self.request.method='POST'
    self.request.POST = {'email' : '[email protected]',
                         'name' : 'pyRock',
                         'stripe_token' : '...',
                         'last_4_digits' : '4242',
                         'password' : 'bad_password',
                         'ver_password' : 'bad_password',
                        }

    #mock out stripe  and ask it to throw a connection error
    with mock.patch('stripe.Customer.create', side_effect =
                    socket.error("can't connect to stripe")) as stripe_mock:

        #run the test
        resp = register(self.request)

        #assert there is no record in the database without stripe id.
        users = User.objects.filter(email="[email protected]")
        self.assertEquals(len(users), 0)

        #check the associated table also didn't get updated
        unpaid = UnpaidUsers.objects.filter(email="[email protected]")
        self.assertEquals(len(unpaid), 0)

Der Decorator ganz oben im Test ist ein Mock, der einen „IntegrityError“ auslöst, wenn wir versuchen, unter UnpaidUsers zu speichern Tabelle.

Dies dient der Beantwortung der Frage „Was passiert, wenn UnpaidUsers(email=cd['email']).save() Leitung schlägt fehl?“ Das nächste Code-Bit erstellt einfach eine simulierte Sitzung mit den entsprechenden Informationen, die wir für unsere Registrierungsfunktion benötigen. Und dann den with mock.patch zwingt das System zu glauben, dass Stripe ausgefallen ist … endlich kommen wir zum Test.

resp = register(self.request)

Die obige Zeile ruft nur unsere Registeransichtsfunktion auf und übergibt die verspottete Anfrage. Dann prüfen wir einfach, ob die Tabellen nicht aktualisiert werden:

#assert there is no record in the database without stripe_id.
users = User.objects.filter(email="[email protected]")
self.assertEquals(len(users), 0)

#check the associated table also didn't get updated
unpaid = UnpaidUsers.objects.filter(email="[email protected]")
self.assertEquals(len(unpaid), 0)

Es sollte also fehlschlagen, wenn wir den Test ausführen:

======================================================================
FAIL: test_registering_user_when_strip_is_down_all_or_nothing (tests.payments.testViews.RegisterPageTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/j1z0/.virtualenvs/django_1.6/lib/python2.7/site-packages/mock.py", line 1201, in patched
    return func(*args, **keywargs)
  File "/Users/j1z0/Code/RealPython/mvp_for_Adv_Python_Web_Book/tests/payments/testViews.py", line 266, in test_registering_user_when_strip_is_down_all_or_nothing
    self.assertEquals(len(users), 0)
AssertionError: 1 != 0

----------------------------------------------------------------------

Hübsch. Scheint komisch zu sagen, aber genau das wollten wir. Denken Sie daran:Wir üben hier TDD. Die Fehlermeldung sagt uns, dass der Benutzer tatsächlich in der Datenbank gespeichert wird – genau das wollen wir nicht, weil er nicht bezahlt hat!

Transaktionen zur Rettung …



Transaktionen

Es gibt tatsächlich mehrere Möglichkeiten, Transaktionen in Django 1.6 zu erstellen.

Gehen wir ein paar durch.


Der empfohlene Weg

Laut Django 1.6-Dokumentation:

„Django bietet eine einzige API zur Steuerung von Datenbanktransaktionen. […] Atomarität ist die definierende Eigenschaft von Datenbanktransaktionen. atomic ermöglicht es uns, einen Codeblock zu erstellen, in dem die Atomarität in der Datenbank garantiert ist. Wenn der Codeblock erfolgreich abgeschlossen ist, werden die Änderungen an die Datenbank übertragen. Wenn es eine Ausnahme gibt, werden die Änderungen rückgängig gemacht.“

Atomic kann sowohl als Decorator als auch als Context_manager verwendet werden. Wenn wir es also als Kontextmanager verwenden, würde der Code in unserer Registerfunktion so aussehen:

from django.db import transaction

try:
    with transaction.atomic():
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
            UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    form.addError(cd['email'] + ' is already a member')

Beachten Sie die Zeile with transaction.atomic() . Der gesamte Code innerhalb dieses Blocks wird innerhalb einer Transaktion ausgeführt. Wenn wir also unsere Tests wiederholen, sollten sie alle bestehen! Denken Sie daran, dass eine Transaktion eine einzelne Arbeitseinheit ist, sodass alles innerhalb des Kontextmanagers zusammengerollt wird, wenn UnpaidUsers Anruf schlägt fehl.



Einen Dekorateur verwenden

Wir können auch versuchen, atomic als Decorator hinzuzufügen.

@transaction.atomic():
def register(request):
    # ...snip....

    try:
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
                UnpaidUsers(email=cd['email']).save()

    except IntegrityError:
        form.addError(cd['email'] + ' is already a member')

Wenn wir unsere Tests erneut ausführen, schlagen sie mit dem gleichen Fehler fehl, den wir zuvor hatten.

Warum ist das so? Warum wurde die Transaktion nicht korrekt rückgängig gemacht? Der Grund dafür ist transaction.atomic sucht nach einer Art Ausnahme und nun, wir haben diesen Fehler abgefangen (d. h. den IntegrityError in unserem Versuch außer Block), also transaction.atomic nie gesehen und daher der Standard AUTOCOMMIT Funktionalität übernommen.

Aber natürlich wird das Entfernen von try except dazu führen, dass die Ausnahme einfach in die Aufrufkette geworfen wird und höchstwahrscheinlich woanders explodiert. Also können wir das auch nicht.

Der Trick besteht also darin, den Atomic Context Manager in den Try-Except-Block einzufügen, was wir in unserer ersten Lösung getan haben. Schauen Sie sich den richtigen Code noch einmal an:

from django.db import transaction

try:
    with transaction.atomic():
        user = User.create(
            cd['name'], cd['email'], 
            cd['password'], cd['last_4_digits'])

        if customer:
            user.stripe_id = customer.id
            user.save()
        else:
            UnpaidUsers(email=cd['email']).save()

except IntegrityError:
    form.addError(cd['email'] + ' is already a member')

Wenn UnpaidUsers löst den IntegrityError aus die transaction.atomic() Kontext-Manager wird es abfangen und das Rollback durchführen. Bis unser Code im Ausnahmehandler ausgeführt wird (d. h. form.addError line) wird das Rollback durchgeführt und wir können bei Bedarf sicher Datenbankaufrufe durchführen. Beachten Sie auch alle Datenbankaufrufe vor oder nach transaction.atomic() Kontext-Manager wird unabhängig vom endgültigen Ergebnis von context_manager nicht beeinflusst.



Transaktion per HTTP-Anfrage

Django 1.6 (wie 1.5) ermöglicht es Ihnen auch, in einem „Transaktion pro Anfrage“-Modus zu arbeiten. In diesem Modus verpackt Django Ihre Ansichtsfunktion automatisch in eine Transaktion. Wenn die Funktion eine Ausnahme auslöst, setzt Django die Transaktion zurück, andernfalls wird die Transaktion festgeschrieben.

Um es einzurichten, müssen Sie ATOMIC_REQUEST setzen auf True in der Datenbankkonfiguration für jede Datenbank, die dieses Verhalten haben soll. In unserer „settings.py“ nehmen wir also die Änderung wie folgt vor:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(SITE_ROOT, 'test.db'),
        'ATOMIC_REQUEST': True,
    }
}

In der Praxis verhält sich das genau so, als ob Sie den Decorator auf unsere Ansichtsfunktion setzen würden. Es dient hier also nicht unseren Zwecken.

Es lohnt sich jedoch, dies bei beiden ATOMIC_REQUESTS zu beachten und @transaction.atomic decorator ist es möglich, diese Fehler immer noch abzufangen / zu behandeln, nachdem sie aus der Ansicht geworfen wurden. Um diese Fehler abzufangen, müssten Sie benutzerdefinierte Middleware implementieren, oder Sie könnten urls.hadler500 überschreiben oder eine 500.html-Vorlage erstellen.




Speicherpunkte

Obwohl Transaktionen atomar sind, können sie weiter in Sicherungspunkte unterteilt werden. Stellen Sie sich Savepoints als Teiltransaktionen vor.

Wenn Sie also eine Transaktion haben, für deren Abschluss vier SQL-Anweisungen erforderlich sind, können Sie nach der zweiten Anweisung einen Sicherungspunkt erstellen. Sobald dieser Sicherungspunkt erstellt ist, können Sie, selbst wenn die 3. oder 4. Anweisung fehlschlägt, einen teilweisen Rollback durchführen, indem Sie die 3. und 4. Anweisung entfernen, aber die ersten beiden beibehalten.

Es ist also im Grunde so, als würde man eine Transaktion in kleinere, leichtgewichtige Transaktionen aufteilen, die es Ihnen ermöglichen, teilweise Rollbacks oder Commits durchzuführen.

Aber denken Sie daran, wenn die Haupttransaktion zurückgesetzt werden soll (vielleicht wegen eines IntegrityError das ausgelöst und nicht abgefangen wurde, dann werden auch alle Sicherungspunkte zurückgesetzt).

Schauen wir uns ein Beispiel an, wie Sicherungspunkte funktionieren.

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

Hier befindet sich die gesamte Funktion in einer Transaktion. Nachdem wir einen neuen Benutzer erstellt haben, erstellen wir einen Sicherungspunkt und erhalten eine Referenz auf den Sicherungspunkt. Die nächsten drei Anweisungen-

user.name = 'starting down the rabbit hole'
user.stripe_id = 4
user.save()

- sind nicht Teil des bestehenden Savepoints, also haben sie die Chance, Teil des nächsten savepoint_rollback zu sein , oder savepoint_commit . Im Falle eines savepoint_rollback , die Zeile user = User.create('jj','inception','jj','1234') wird weiterhin in die Datenbank übernommen, obwohl die restlichen Aktualisierungen dies nicht tun.

Anders ausgedrückt, diese beiden folgenden Tests beschreiben, wie die Sicherungspunkte funktionieren:

def test_savepoint_rollbacks(self):

    self.save_points(False)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #note the values here are from the original create call
    self.assertEquals(users[0].stripe_id, '')
    self.assertEquals(users[0].name, 'jj')


def test_savepoint_commit(self):
    self.save_points(True)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #note the values here are from the update calls
    self.assertEquals(users[0].stripe_id, '4')
    self.assertEquals(users[0].name, 'starting down the rabbit hole')

Auch nachdem wir einen Sicherungspunkt festgeschrieben oder rückgängig gemacht haben, können wir mit der Arbeit in derselben Transaktion fortfahren. Und diese Arbeit wird vom Ergebnis des vorherigen Sicherungspunkts nicht beeinflusst.

Zum Beispiel, wenn wir unsere save_points aktualisieren Funktion als solche:

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.save()

    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

    user.create('limbo','illbehere@forever','mind blown',
           '1111')

Unabhängig davon, ob savepoint_commit oder savepoint_rollback aufgerufen wurde, wird der Benutzer „Limbo“ weiterhin erfolgreich erstellt. Es sei denn, etwas anderes führt dazu, dass die gesamte Transaktion rückgängig gemacht wird.



Verschachtelte Transaktionen

Zusätzlich zum manuellen Festlegen von Sicherungspunkten mit savepoint() , savepoint_commit und savepoint_rollback , erstellt das Erstellen einer verschachtelten Transaktion automatisch einen Sicherungspunkt für uns und setzt ihn zurück, wenn wir einen Fehler erhalten.

Wenn wir unser Beispiel etwas erweitern, erhalten wir:

@transaction.atomic()
def save_points(self,save=True):

    user = User.create('jj','inception','jj','1234')
    sp1 = transaction.savepoint()

    user.name = 'starting down the rabbit hole'
    user.save()

    user.stripe_id = 4
    user.save()

    if save:
        transaction.savepoint_commit(sp1)
    else:
        transaction.savepoint_rollback(sp1)

    try:
        with transaction.atomic():
            user.create('limbo','illbehere@forever','mind blown',
                   '1111')
            if not save: raise DatabaseError
    except DatabaseError:
        pass

Hier können wir sehen, dass wir, nachdem wir uns mit unseren Sicherungspunkten befasst haben, die transaction.atomic verwenden Kontextmanager, um unsere Erstellung des „Limbo“-Benutzers einzuschließen. Wenn dieser Kontextmanager aufgerufen wird, erstellt er tatsächlich einen Sicherungspunkt (weil wir uns bereits in einer Transaktion befinden) und dieser Sicherungspunkt wird beim Verlassen des Kontextmanagers festgeschrieben oder rückgängig gemacht.

Daher beschreiben die folgenden zwei Tests ihr Verhalten:

 def test_savepoint_rollbacks(self):

    self.save_points(False)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #savepoint was rolled back so we should have original values
    self.assertEquals(users[0].stripe_id, '')
    self.assertEquals(users[0].name, 'jj')

    #this save point was rolled back because of DatabaseError
    limbo = User.objects.filter(email="illbehere@forever")
    self.assertEquals(len(limbo),0)

def test_savepoint_commit(self):
    self.save_points(True)

    #verify that everything was stored
    users = User.objects.filter(email="inception")
    self.assertEquals(len(users), 1)

    #savepoint was committed
    self.assertEquals(users[0].stripe_id, '4')
    self.assertEquals(users[0].name, 'starting down the rabbit hole')

    #save point was committed by exiting the context_manager without an exception
    limbo = User.objects.filter(email="illbehere@forever")
    self.assertEquals(len(limbo),1)

In Wirklichkeit können Sie also entweder atomic verwenden oder savepoint zum Erstellen von Sicherungspunkten innerhalb einer Transaktion. Mit atomic man muss sich nicht explizit um den Commit/Rollback kümmern, wo wie bei savepoint Sie haben die volle Kontrolle darüber, wann das passiert.



Schlussfolgerung

Wenn Sie bereits Erfahrungen mit früheren Versionen von Django-Transaktionen gemacht haben, können Sie sehen, wie viel einfacher das Transaktionsmodell ist. Auch mit AUTOCOMMIT on by default ist ein großartiges Beispiel für „gesunde“ Standardeinstellungen, auf deren Bereitstellung Django und Python stolz sind. Bei vielen Systemen müssen Sie sich nicht direkt mit Transaktionen befassen, lassen Sie einfach AUTOCOMMIT seine Arbeit tun. Aber wenn Sie das tun, hat Ihnen dieser Beitrag hoffentlich die Informationen gegeben, die Sie benötigen, um Transaktionen in Django wie ein Profi zu verwalten.