A tutorial on transactional transaction management using the Python Django framework _python

Source: Internet
Author: User
Tags assert commit exception handling http request rollback savepoint


If you spend a lot of time doing Django database transactions, you'll learn that this is confusing.



In the past, only a simple basic document was provided, and in order to know exactly how it was used, it was also necessary to create and perform Django transactions.



Here are a number of Django transaction terms, such as: Commit_on_success, commit_manually, Commit_unless_maneged,rollback_unless_managed,enter_ Transaction_management,leace_transaction_management, these are just a part of it.



The most fortunate thing is that after the Django 1.6 release. Now you can use a few functions to implement transaction processing, as we enter a few seconds before learning these functions. First, we need to be clear about the following issues:


    • What is transaction processing?
    • What happens to the new things in Django 1.6?


Before entering the "What is the right transaction in the Django 1.6 version", please take a look at the following detailed cases:



Banded case



Transaction


    • Recommended by the way
    • Use separator
    • Each HTTP request transaction


Save Point



Nested transactions




What is a business?



According to SQL-92, "an SQL transaction (sometimes referred to as a transaction) is a sequence of SQL statement execution that can be atomically restored." In other words, all the SQL statements are executed and submitted together. Similarly, when rolling back, all statements are rolled back as well.



For example:


# START Note
= Note (title= ' My A ', text= ' yay! ')
Note = Note (title= "Me second note", text= "whee!")
Address1.save ()
address2.save ()
# COMMIT


So a transaction is a single unit of work in a database. It is started by a start transaction and a commit or explicit rollback end.



What's wrong with the transaction management prior to Django 1.6?



To fully answer this question, we have to explain how transactions are handled in databases, clients, and Django.



Database



Each statement in the database runs in a transaction, and the transaction can even contain only one statement.



Almost all databases have a autocommit setting, which is usually set to True by default. Autocommit wraps all the statements into a transaction, and the transaction is committed as soon as the statement is executed successfully. Of course you can also manually call Start_transaction, which will temporarily suspend autocommit until you call commit_transaction or rollback.



This way, then, will invalidate the implicit commit of the autocommit settings after each statement.




However, there are Python client libraries such as Sqlite3 and mysqldb that allow Python programs to connect to the database itself. These libraries follow a set of criteria for accessing and querying a database. The DB API 2.0 standard, described in Pep 249. Although it may let people read a little dry. One important thing to take away is that in THE PEP 249 State, the default database should turn off the autocommit feature.



This is obviously a conflict with what's going on in the database:


    • Database statements are always run in a single transaction. This database will typically turn on automatic submission for you.
    • However, according to Pep 249, this should not happen.
    • The client library must be reflected in the database what happened? However, because they do not allow the automatic submit feature to be turned on by default. They simply wrap the SQL statement in a transaction. Just like a database.


All right, stay with me a little longer.






Django



Entering the Django,django also has to say about the business process. In Django 1.5 and earlier versions. When you write data to a database, Django basically runs an open transaction and automatically submits the transaction function. So every time you name something like Model.save () or Model.update (), Django generates the corresponding SQL statement and submits the transaction.



There are also in Django 1.5 and earlier versions, it is recommended that you use Transactionmiddleware to bind HTTP request transactions. Each request provides a transaction. If the returned response is not abnormal, Django submits the transaction. But if your view feature throws an error, the rollback will be invoked. This actually means that it turns off the autocommit feature. If you want to standardize, database-level AUTOCOMMIT-style transaction management, you must manage your own transactions-usually by using transactions to decorate your view features, such as @transaction.commit_manually, or @transaction.commit_on_success.



Take a breath, or two bites.




What does that mean?



Yes, there are a lot of things to do there, and it turns out that most developers are in need of this standard database-level autocommit feature-and that meaningful transactions are often left behind. Do your own thing until you need to manually adjust them.



What is right about transaction management in the Django 1.6 release?



Now, welcome to Django 1.6. Try to forget everything, we're just talking, just remember in Django 1.6, you can use a database, and you can manually submit and manage transactions automatically when needed. In essence, we have a simpler model, basically to put the design of what kind of database in the first place.



All right! All right, let's write code?






Stripe case



Below, we use the example of handling a user registration to call the stripe to handle the credit card process.


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)
  )


The example first calls Customer.create, which is actually calling Stripe to handle the credit card process, and then we create a new user. If we get a response from stripe, we will update the newly created user with stripe_id. If we don't get a response (stripe is closed), we'll add a new entry to the Unpaidusers table with the newly created user's email so we can have them retry their credit card information later.




The idea is this: if the stripe is not responding, the user can still register and then start using our website. We will give the user credit card information at a later time.



"I understand this is a special example, and it's not the way I want to do it, but it's designed to show the deal."



Consider trading, bearing in mind that living in Django1.6 provides the "autocommit" functionality for a database. Next look at the database-related code:


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:


Can you find the problem? What happens if "unpaidusers" (email=cd[' email '). Save () run failed?



There is a user who registers the system, and the system thinks that the credit card has been checked. But in fact, the system has not been checked.



We just want one of the results:



1. The user was created in the database with a stripe_id



2. The user was created in the database, but there was no stripe_id. At the same time in the relevant "unpaidusers" line, there is the same e-mail address



This means that what we want is a separate database statement header to complete the task or rollback. This example is a good illustration of the transaction.




First, we write a few test cases to verify that things work the way we think they are:


@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' : 'python@rocks.com',
             '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="python@rocks.com")
    self.assertEquals(len(users), 0)
 
    #check the associated table also didn't get updated
    unpaid = UnpaidUsers.objects.filter(email="python@rocks.com")
    self.assertEquals(len(unpaid), 0)


When we try to save "unpaidusers", the interpreter above the test runs out of the abnormal ' integrityerror '.



The next step is to explain the answer to the question, "when" unpaidusers (email=cd[' email '). What happens when Save () runs? "The following code creates a conversation, and we need to give some appropriate information in the registration function." Then "with Mock.patch" forces the system to assume that the stripe is not responding and eventually jumps into our test cases.


RESP = Register (self.request)


The above passage simply calls our registered view to deliver the request. Then we just need to check if the table is updated:


#assert there is no record in the database without stripe_id.
Users = User.objects.filter (email= "python@rocks.com")
self.assertequals (len (users), 0)
 
#check the Associated table also didn ' t get updated
unpaid = UnpaidUsers.objects.filter (email= "python@rocks.com")
Self.assertequals (len (unpaid), 0)


So if we run the test case, it will fail to run:


======================================================================
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
 
------ ----------------------------------------------------------------


Praise. This is the result we eventually want.



Remember: We've practiced the ability to test-drive development here. The error message prompts us that the user information has been saved to the database, but this is not what we want because we have not paid for it!



Transaction transactions are used to salvage such problems ...



Transaction



There are a number of ways to create transactions for Django1.6.



Here is a brief introduction to several.
Recommended Methods



According to Django1.6 's documentation, "Django provides a simple API to control transaction transactions in the database ... Atomic operations are used to define the properties of a database transaction. Atomic operations allow us to create a bunch of code on the premise of a database guarantee. If the code is executed successfully, the corresponding changes are committed to the database. If an exception occurs, the operation is rolled back. ”



Atomic operations can be used to interpret operations or content management. So if we use it as content management, the code for our registration function will be as follows:


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 ')


Note that in the "with Transaction.atomic ()" line. This piece of code will be executed inside the transaction. So if we rerun our tests, they will all pass.



Remember that a transaction is a unit of work, so when the "unpaidusers" call fails, all the operations of content management are rolled back together.



Using adorners



In addition to the above approach, we can use the Python adorner feature to use transactions.


@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 ')


If we run the test again, it will fail as well.



Why, then? Why does the transaction not roll back correctly? The reason is that with transaction.atomic will try to catch some kind of exception, and our code is caught by human flesh (such as the integrityerror exception in the Try-except code block), so transaction.atomic never see this anomaly, So the standard autocommit process is ineffective.



However, deleting the Try-catch statement causes the exception to not be caught, and then the code flow is randomly lost. So, that means you can't get rid of Try-catch.




So, the trick is to put the atomic context manager into our Try-catch code snippet in the first solution.



Look at the correct code again:


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 ')


When unpaidusers triggers the Integrityerror, the context Manager transaction.atomic () captures it and performs a rollback operation. At this point our code executes in exception handling (Theform.adderrorline), which completes the rollback operation and, if necessary, makes the database call safe. Also note that any database calls before and after the context Manager thetransaction.atomic () are unaffected by its execution results.



Transaction transactions for each HTTP request



Both Django1.5 and version 1.6 allow the user to manipulate the request transaction mode. In this mode, Django automatically handles your view functions in the transaction. If the view function throws an exception, Django automatically rolls back the transaction, otherwise Django commits the transaction.



To implement this feature, you need to set "Atomic_request" as true in the configuration of the database you want to have this feature. So in our "settings.py" need to have the following settings:


DATABASES = {'
  default ': {
    ' ENGINE ': ' Django.db.backends.sqlite3 ', '
    NAME ': Os.path.join (Site_root, ' Test.db '),
    ' atomic_request ': True,
  }
}


If we put the interpreter in the view function, the above settings will take effect. So it doesn't fit our thinking.



However, it is still worth noting that the interpreter: "Atomic_requests" and "@transaction. ATOMIC" still have the potential to handle these errors when exceptions are thrown. To catch these errors, you need to do some regular middleware, or you need to overwrite "urls.hadler500", or create a new "500.html" template.



Save Point



Although transactions are atomic, they can be broken down into multiple "savepoint"-You can be considered "part of the transaction".



For example, if you have a transaction that contains 4 SQL statements, you can create a save point after the second SQL. Once the savepoint has been created, even if the third or fourth SQL fails, you can still make a partial rollback, ignoring the next two SQL, leaving only the preceding two.



Basically, it's like providing a cutting capability: a common transaction can be cut into more "lightweight" transactions, and can be partially rolled back or partially committed.



But be aware that the entire transaction is being rolled back for nothing (for example, because the Integrityerror exception is thrown but not caught, all of the savepoint will be rolled back)



Take a look at the sample code to see how to play the savepoint.





@transaction. Atomic ()
def save_points (self,save=true):
 
  user = User.create (' JJ ', ' Inception ', ' JJ ', ' 1234 ')
  SP1 = transaction.savepoint ()
 
  user.name = ' Zheli hui Guadiao, t.t '
  user.stripe_id = 4
  user.save ()
 
  If Save:
    transaction.savepoint_commit (SP1)
  else:
    transaction.savepoint_rollback (SP1)


In the example, the entire function belongs to one transaction. After the new user object was created, we created and got a savepoint. Then follow the 3 line statement--


  User.Name = ' Zheli hui Guadiao, t.t '
  user.stripe_id = 4
  user.save ()


--they are not part of the save point, so they may be parts of the Savepoint_rollback or Savepoint_commit below. Assuming it is savepoint_rollback, that line of code User = User.create (' JJ ', ' Inception ', ' JJ ', ' 1234 ') will still be successfully submitted to the database, and the following three lines will not succeed.



In another way, the following two test cases describe how the save point works:





def test_savepoint_rollbacks (self):
 
  self.save_points (False)
 
  #verify that everything is stored
  users = User.objects.filter (email= "Inception")
  Self.assertequals (len (Users), 1) #note The values here are 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 is stored
  users = User.objects.filter (email= "Inception")
  Self.assertequals (len (Users), 1) #note The values here are are from the
 
  Update calls
  self.assertequals (users[0].stripe_id, ' 4 ')
  self.assertequals (Users[0].name, ' starting down The Rabbit Hole ')


Again, after we commit or roll back the savepoint, we can still work in the same transaction. At the same time, the results of this operation are not affected by the output of the previous save point.



For example, if we update the "save_points" function according to the following example,





@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 ')


Even if the "Savepoint_commit" or "Savepoint_rollback" is invoked by the "limbo" user, the transaction will still be created successfully. If no success is created, the entire transaction is rolled back.



Nested transactions



Using "SavePoint ()", "Savepoint_commit" and "Savepoint_rollback" to manually specify the savepoint, a nested transaction is automatically created, and this nested transaction automatically creates a savepoint for us. And, if we encounter errors, this transaction will be rolled back.



The following is an extended example to illustrate:


@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


Here we can see that after we have processed the savepoint, we use the "thetransaction.atomic" context area to wipe out the "limbo" user we created. When context management is invoked, it creates a savepoint (because we are already in the transaction), and the save point is executed or rolled back according to the existing context manager.



So the following two test cases describe the wording:





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)



Therefore, in reality you can use atoms or create a savepoint in a transaction to save points. With atoms, you don't have to worry very carefully about submitting and rolling, and when this happens, you can take full control of the savepoint.
Conclusions



If you have any previous experience using Django's earlier version of the transaction, you can see many simpler transaction processing models. By default, there is an autocommit feature, which is a good example of the "sensible" default that Django and Python are both proud to offer. For so many systems, you will not need to deal directly with the transaction. Just let "autocommit" do its job, but if you do, I would like this post to provide you with the transactions you need to manage in Django like an expert.


Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.