Tutorial on writing a database module in Python
This article describes how to compile a database module in Python. The code in this article is based on Python2.x. For more information, see
In a Web App, all data, including user information, published logs, and comments, are stored in the database. In awesome-python-app, we select MySQL as the database.
There are many places in the Web App that need to access the database. To access the database, you must create database connection and cursor objects, execute SQL statements, handle exceptions, and clear resources. If these database access code is dispersed into various functions, it will inevitably be unable to be maintained, and it is not conducive to code reuse.
In addition, multiple users access a Web App at the same time, and the system processes the requests of each user in multi-process or multi-thread mode. Taking multithreading as an example, each thread must create a connection that only belongs to itself when accessing the database, which is invisible to other threads. Otherwise, database operations will be disordered.
Therefore, we also need to create a simple and reliable database access model to operate databases securely and simply in one thread.
Why not choose SQLAlchemy? SQLAlchemy is too large, and over-Ground Object design leads to complicated APIs.
Therefore, we decided to design a database module that encapsulates basic SELECT, INSERT, UPDATE, and DELETE operations: transwarp. db.
Design db Interface
The principle of designing the underlying module is to design simple and easy-to-use API Interfaces Based on the upper-layer callers, and then implement the internal code of the module.
Assuming that the transwarp. db module has been compiled, we want to call it in this way:
First, initialize the database connection information through the create_engine () function:
?
1 2 |
From transwarp import db Db. create_engine (user = 'root', password = 'Password', database = 'test', host = '2017. 0.0.1 ', port = 127) |
Then, you can directly operate SQL.
If you need to make a query, you can directly call the select () method and return the list. Each element is the corresponding row represented by dict:
?
1 2 3 4 5 6 7 |
Users = db. select ('select * from user ') # Users => #[ # {"Id": 1, "name": "Michael "}, # {"Id": 2, "name": "Bob "}, # {"Id": 3, "name": "Adam "} #] |
If you want to execute INSERT, UPDATE, or DELETE operations, execute the update () method and return the number of affected rows:
?
1 |
N = db. update ('insert into user (id, name) values (?, ?) ', 4, 'jack ') |
The update () function signature is:
?
Unified use? As a placeholder and passed in variable parameters to bind, fundamentally avoiding SQL injection attacks.
Each select () or update () call implicitly automatically opens and closes the database connection. In this way, upper-layer callers do not have to worry about the underlying database connection.
However, what if I want to execute multiple SQL statements in a database connection? We use a with statement to implement:
?
1 2 3 4 |
With db. connection (): Db. select ('...') Db. update ('...') Db. update ('...') |
What if I want to execute multiple SQL statements in a database transaction? We still use a with statement to implement:
?
1 2 3 4 |
With db. transaction (): Db. select ('...') Db. update ('...') Db. update ('...') |
Implement the db Module
Because the module is a global object and the module variable is a globally unique variable, there are two important module variables:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# Db. py # Database Engine objects: Class _ Engine (object ): Def _ init _ (self, connect ): Self. _ connect = connect Def connect (self ): Return self. _ connect () Engine = None # Context object holding the database connection: Class _ DbCtx (threading. local ): Def _ init _ (self ): Self. connection = None Self. transactions = 0 Def is_init (self ): Return not self. connection is None Def init (self ): Self. connection = _ LasyConnection () Self. transactions = 0 Def cleanup (self ): Self. connection. cleanup () Self. connection = None Def cursor (self ): Return self. connection. cursor () _ Db_ctx = _ DbCtx () |
Because _ db_ctx is a threadlocal object, the database connections it holds are different for each thread. No thread can access the database connections held by other threads.
With these two global variables, we continue to implement the context of the database connection to automatically obtain and release the connection:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Class _ ConnectionCtx (object ): Def _ enter _ (self ): Global _ db_ctx Self. should_cleanup = False If not _ db_ctx.is_init (): _ Db_ctx.init () Self. should_cleanup = True Return self Def _ exit _ (self, exctype, excvalue, traceback ): Global _ db_ctx If self. should_cleanup: _ Db_ctx.cleanup () Def connection (): Return _ ConnectionCtx () |
Objects defining _ enter _ () and _ exit _ () can be used in the with statement to ensure that the _ exit _ () method can be called in any situation.
Write the _ ConnectionCtx scope to a function call:
?
1 2 |
With connection (): Do_some_db_operation () |
But the simpler method is to write @ decorator:
?
1 2 3 |
@ With_connection Def do_some_db_operation (): Pass |
In this way, it is easier to implement the select () and update () methods:
?
1 2 3 4 5 6 7 |
@ With_connection Def select (SQL, * args ): Pass @ With_connection Def update (SQL, * args ): Pass |
Note that the Connection object is stored in the threadlocal object _ DbCtx. Therefore, nested use of with connection () is no problem. _ DbCtx always checks whether a Connection exists. If yes, use it directly. If no Connection exists, open a new Connection.
The same is true for transaction. with transaction () defines a database transaction:
?
1 2 3 4 |
With db. transaction (): Db. select ('...') Db. update ('...') Db. update ('...') |
The transaction in the function scope also has a simplified @ decorator:
?
1 2 3 |
@ With_transaction Def do_in_transaction (): Pass |
The transaction can also be nested, and the inner transaction is automatically merged into the outer transaction. This transaction model is sufficient to meet 99% of the requirements.
Transaction Nesting is a little more complex than Connection nesting, because the transaction nesting requires counting, each layer of nesting is + 1, leaving the layer of nesting is-1, and finally committing the transaction at 0:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
Class _ TransactionCtx (object ): Def _ enter _ (self ): Global _ db_ctx Self. should_close_conn = False If not _ db_ctx.is_init (): _ Db_ctx.init () Self. should_close_conn = True _ Db_ctx.transactions = _ db_ctx.transactions + 1 Return self Def _ exit _ (self, exctype, excvalue, traceback ): Global _ db_ctx _ Db_ctx.transactions = _ db_ctx.transactions-1 Try: If _ db_ctx.transactions = 0: If exctype is None: Self. commit () Else: Self. rollback () Finally: If self. should_close_conn: _ Db_ctx.cleanup () Def commit (self ): Global _ db_ctx Try: _ Db_ctx.connection.commit () Except t: _ Db_ctx.connection.rollback () Raise Def rollback (self ): Global _ db_ctx _ Db_ctx.connection.rollback () |
Finally, the select () and update () methods are implemented, and the db module is complete.