Django 使用原生的 SQL 語句操作 MySQL 數據庫
在 Django 中有兩種操作 MySQL 數據庫的方式,一種是使用原生的 SQL 語句操作 MySQL,另一種方式就是使用 Django 內置的 ORM 模型完成數據庫的增刪改查操作。后者是 Django 框架的一個的核心模塊,它讓開發者對數據庫的操作更友好和優雅。
1. python 操作 MySQL 數據庫
1.1 Python DB-API
在沒有 Python DB-API 之前,各數據庫之間的應用接口非?;靵y,實現各不相同。如果項目需要更換數據庫時,則需要在代碼層面做大量的修改,使用非常不便,之后 Python DB-API 的出現就是為了解決這樣的問題。
Python 的 DB-API,為大多數的數據庫實現了接口,使用它連接各數據庫后,就可以用相同的方式操作各數據庫。這就意味著我們不必區分底層連接的是 MySQL 還是 Oracle等等,可以使用相同的代碼來對連接的數據庫進行增刪改查操作。
DB-API 是一個規范, 它定義了一系列必須的對象和數據庫存取方式, 以便為各種各樣的底層數據庫系統和多種多樣的數據庫接口程序提供一致的訪問接口 。Python 的 DB-API,為大多數的數據庫實現了接口,使用它連接各數據庫后,就可以用相同的方式操作各數據庫。不同的數據庫需要下載不同的 DB API 模塊,例如我們需要訪問 Oracle 數據庫和 MySQL 數據庫,就需要下載 Oracle 和 MySQL 數據庫的 API 模塊(也稱之為驅動模塊)。
Python DB-API 的使用流程如下:
- 引入 API 模塊;
- 獲取與數據庫的連接;
- 執行 SQL 語句和存儲過程;
- 關閉數據庫連接。
1.2 Python 中常用的 MySQL 驅動模塊
Python 中常見的 MySQL 的 驅動模塊有:
- MySQLdb: 它是對 C 語言操作 MySQL 數據庫的一個簡單封裝。遵循并實現了 Python DB API v2 協議。但是只支持 Python2, 目前還不支持 Python3;
- mysqlclient: 是 MySQLdb 的另外一個分支。支持 Python3 并且修復了一些 bug;
- pymysql: 純 Python 實現的一個驅動。因為是純 Python 編寫的,因此執行效率不如前面二者;
- MySQL Connector/Python: MySQL 官方推出的使用純 Python 連接 MySQL 的驅動。同樣是純 Python 開發的,效率也不高。
其中 mysqlclient 和 pymysql 是在 python 開發中最常使用的 MySQL 驅動模塊。而在 Django 內部,我們接下來會看到,它的 ORM 模型其實是在 mysqlclient 基礎上再次封裝起來的。
1.3 實戰 python 操作 MySQL 數據庫
這里我們將使用前面提到的 mysqlclient 模塊來操作 MySQL 數據庫。
第一步安裝 mysqlclient 模塊:
$ pip3 install mysqlclient -i https://pypi.tuna.tsinghua.edu.cn/simple
安裝好了之后,我們可以在 python 解釋器中導入下模塊:
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import MySQLdb
>>> MySQLdb.__version__
'1.4.6'
>>>
我們事先準備好了一個 MySQL 服務, 部署在云服務器上。本地安裝好 mysql 客戶端,然后通過如下方式連接 MySQL 數據庫:
[shen@shen ~]$ mysql -h 180.76.152.113 -P 9002 -u store -pstore.123@
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 68920
Server version: 5.7.26 MySQL Community Server (GPL)
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
新建一個數據庫,名為 django-manual,然后在該數據庫中新建了一個簡單的 user 表。接下來我們會使用 mysqlclient 模塊對該 user 表中的數據進行增刪改查操作:
mysql> create database django_manual default charset utf8;
Query OK, 1 row affected (0.14 sec)
mysql> use django_manual
Database changed
MySQL [django_manual]> show tables;
Empty set (0.00 sec)
mysql> CREATE TABLE `user` (
-> `id` int(11) NOT NULL AUTO_INCREMENT,
-> `name` char(30) NOT NULL,
-> `password` char(10) NOT NULL,
-> `email` char(30) NOT NULL,
-> PRIMARY KEY (`id`)
-> ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARSET = utf8;
mysql> show tables;
+-------------------------+
| Tables_in_django_manual |
+-------------------------+
| user |
+-------------------------+
1 row in set (0.00 sec)
來看看如和使用 mysqlclient,模塊操作數據庫 django-manual。
>>> import MySQLdb
>>> conn = MySQLdb.connect(host='180.76.152.113', port=9002, user='store', passwd='store.123@', db='django_manual') # 連接數據庫
>>> sql = "insert into user(`name`, `password`, `email`) values ('test', 'xxxxxx', '[email protected]')" # 插入數據的sql語句
>>> cur = conn.cursor() # 獲取游標
>>> cur.execute(sql) # 執行sql語句
1
>>> conn.commit() # 提交操作
# commit 成功后,去另一個窗口查看 mysql 中的數據庫數據
mysql > select * from user;
+----+------+----------+------------+
| id | name | password | email |
+----+------+----------+------------+
| 10 | test | xxxxxx | 222@qq.com |
+----+------+----------+------------+
1 row in set (0.00 sec)
這里我們可以看到 mysqlclient 模塊中的幾個常用方法:
-
MySQLdb.connect() 方法:連接 mysql 數據庫,會在這里輸入 mysql 服務地址,開放端口,用戶名和密碼以及要使用到的數據庫名;
-
conn.cursor():創建游標,固定做法;
-
cur.execute():通過游標的 execute() 方法可以執行 sql 語句,其返回值表示的是操作的記錄數,比如這里我們新增了一條記錄,返回的值為1;
-
conn.commit():對于數據庫有更新的動作,比如新增數據、修改數據和刪除數據等,最后需要使用 commit() 方法提交動作,而對于查詢操作而言則不需要。如果想自動 commit 動作,也是有辦法的:
>>> conn = MySQLdb.connect(...) >>> conn.autocommit(True) >>> ...
上面是新增單條記錄,我們也可以新增多條記錄,操作如下:
>>> # 在前面的基礎上繼續執行
>>> conn.autocommit(True) # 設置自動提交
>>> cur = conn.cursor()
>>> data = (('user%d' % i, 'xxxxxx', '28%[email protected]' % i) for i in range(10))
>>> cur.executemany('insert into user(`name`, `password`, `email`) values (%s, %s, %s);', data)
10
# 在另一個窗口,可以看到 user 表中的記錄已經有11條了
select count(*) from user;
+----------+
| count(*) |
+----------+
| 11 |
+----------+
1 row in set (0.00 sec)
這里插入多條數據,使用的是游標的 executemany() 方法。如果在插入多條記錄中遇到異常,需要執行回滾動作,一般寫法如下:
conn = MySQLdb.connect(...)
try:
# 執行動作
...
except Exception as e:
conn.rollback()
此外,我們一般用到的比較多的是查詢相關的操作。這里有游標的方法:
- fetchone():只取一條記錄,然后游標后移一位;
- fetchmany():取多條記錄,參數為獲取的記錄數,執行完后游標移動相應位置;
- fetchall():取出 sql 執行的所有記錄,游標移動至末尾;
下面我們用前面生成的 11 條記錄來進行操作:
>>> # 假設前面已經獲得連接信息conn和游標cur
>>> sql = 'select * from user where 1=1 and name like "user%"'
>>> cur.execute(sql)
10
>>> data1 = cur.fetchone()
>>> print(data1)
(11, 'user0', 'xxxxxx', '[email protected]')
# 看到再次獲取一條記錄時,取得是下一條數據
>>> data2 = cur.fetchone()
>>> print(data2)
(12, 'user1', 'xxxxxx', '[email protected]')
# 這次獲取5條數據,從user2開始
>>> data3 = cur.fetchmany(5)
>>> print(data3)
((13, 'user2', 'xxxxxx', '[email protected]'), (14, 'user3', 'xxxxxx', '[email protected]'), (15, 'user4', 'xxxxxx', '[email protected]'), (16, 'user5', 'xxxxxx', '[email protected]'), (17, 'user6', 'xxxxxx', '[email protected]'))
# 最后用fetchall()方法獲取最后的所有數據,還剩下10-1-1-5=3條記錄
>>> print(data4)
((18, 'user7', 'xxxxxx', '[email protected]'), (19, 'user8', 'xxxxxx', '[email protected]'), (20, 'user9', 'xxxxxx', '[email protected]'))
# 游標指向最后位置,再次獲取時已經沒有數據了
>>> data5 = cur.fetchone()
>>> print(data5)
None
通過上面的代碼演示,我想我們應該理解游標的作用了,就是每執行一次 fetch 函數,對應的游標會向后移動相應位置。
2. Django 使用原生 SQL 操作 MySQL 數據庫
在 Django 中配置數據庫驅動以及填寫相應信息的位置在 settings.py 文件中的 DATABASE 變量:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'django_manual',
'USER': 'store',
'PASSWORD': 'store.123@',
'HOST': '180.76.152.113',
'PORT': '9002',
}
}
接下來,我們使用 django 自帶的 shell 進入交互式模式進行操作。我們同樣使用前面已經創建的 user 表和生成的11條數據進行 sql 操作,具體如下:
[root@server ~]# cd django-manual/first_django_app/
[root@server first_django_app]# pyenv activate django-manual
pyenv-virtualenv: prompt changing will be removed from future release. configure `export PYENV_VIRTUALENV_DISABLE_PROMPT=1' to simulate the behavior.
(django-manual) [root@server first_django_app]# clear
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.db import connection
>>> cur = connection.cursor()
>>> cur.execute('select * from user where 1=1 and name like "user%"')
10
>>> data1 = cur.fetchone()
>>> print(data1)
(11, 'user0', 'xxxxxx', '[email protected]')
>>> data2 = cur.fetchone()
>>> print(data2)
(12, 'user1', 'xxxxxx', '[email protected]')
>>> data3 = cur.fetchmany(5)
>>> print(data3)
((13, 'user2', 'xxxxxx', '[email protected]'), (14, 'user3', 'xxxxxx', '[email protected]'), (15, 'user4', 'xxxxxx', '[email protected]'), (16, 'user5', 'xxxxxx', '[email protected]'), (17, 'user6', 'xxxxxx', '[email protected]'))
>>> data4 = cur.fetchall()
>>> print(data4)
((18, 'user7', 'xxxxxx', '[email protected]'), (19, 'user8', 'xxxxxx', '[email protected]'), (20, 'user9', 'xxxxxx', '[email protected]'))
>>> data5 = cur.fetchone()
>>> print(data5)
None
這里,我們可以看到,在 Django 內部中使用原生的 SQL 操作和我們前面使用 mysqlclient 操作數據庫幾乎是一模一樣,函數接口、返回值以及用法都是一致的。接下來我們可以進入下源碼內部一探究竟,看看 Django 內部的 connection 究竟是怎么做的。
# 源碼位置 django/db/__init__.py
# 忽略部分代碼
DEFAULT_DB_ALIAS = 'default'
# 忽略部分代碼
class DefaultConnectionProxy:
"""
Proxy for accessing the default DatabaseWrapper object's attributes. If you
need to access the DatabaseWrapper object itself, use
connections[DEFAULT_DB_ALIAS] instead.
"""
def __getattr__(self, item):
return getattr(connections[DEFAULT_DB_ALIAS], item)
def __setattr__(self, name, value):
return setattr(connections[DEFAULT_DB_ALIAS], name, value)
def __delattr__(self, name):
return delattr(connections[DEFAULT_DB_ALIAS], name)
def __eq__(self, other):
return connections[DEFAULT_DB_ALIAS] == other
# For backwards compatibility. Prefer connections['default'] instead.
connection = DefaultConnectionProxy()
...
當我們執行 cur = connection.cursor()
時,其實會執行 __getattr__
這個魔法函數,我們看到它又去調用connections
這個類實例的 cursor()
方法。我們繼續追蹤 connections
,這個也在 __init__.py 文件中:
# django/db/__init__.py
# ...
connections = ConnectionHandler()
# ...
# django/db/utils.py
# 省略部分代碼
class ConnectionHandler:
def __init__(self, databases=None):
"""
databases is an optional dictionary of database definitions (structured
like settings.DATABASES).
"""
self._databases = databases
self._connections = local()
@cached_property
def databases(self):
if self._databases is None:
# 獲取settings.DATABASES中的值,并解析相關參數
self._databases = settings.DATABASES
if self._databases == {}:
self._databases = {
DEFAULT_DB_ALIAS: {
'ENGINE': 'django.db.backends.dummy',
},
}
if DEFAULT_DB_ALIAS not in self._databases:
raise ImproperlyConfigured("You must define a '%s' database." % DEFAULT_DB_ALIAS)
if self._databases[DEFAULT_DB_ALIAS] == {}:
self._databases[DEFAULT_DB_ALIAS]['ENGINE'] = 'django.db.backends.dummy'
return self._databases
def ensure_defaults(self, alias):
"""
Put the defaults into the settings dictionary for a given connection
where no settings is provided.
"""
try:
conn = self.databases[alias]
except KeyError:
raise ConnectionDoesNotExist("The connection %s doesn't exist" % alias)
conn.setdefault('ATOMIC_REQUESTS', False)
conn.setdefault('AUTOCOMMIT', True)
conn.setdefault('ENGINE', 'django.db.backends.dummy')
if conn['ENGINE'] == 'django.db.backends.' or not conn['ENGINE']:
conn['ENGINE'] = 'django.db.backends.dummy'
conn.setdefault('CONN_MAX_AGE', 0)
conn.setdefault('OPTIONS', {})
conn.setdefault('TIME_ZONE', None)
for setting in ['NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']:
conn.setdefault(setting, '')
# 省略部分方法
def __getitem__(self, alias):
if hasattr(self._connections, alias):
return getattr(self._connections, alias)
self.ensure_defaults(alias)
self.prepare_test_settings(alias)
db = self.databases[alias]
# 使用mysql引擎
backend = load_backend(db['ENGINE'])
conn = backend.DatabaseWrapper(db, alias)
setattr(self._connections, alias, conn)
return conn
# 忽略部分代碼
# 忽略部分代碼
這里最核心的地方在于這個__getitem__()
魔法函數。首先我們在前面的 connection 中調用 __gatattr__
魔法函數,而該函數中又使用了 connections[DEFAULT_DB_ALIAS]
這樣的操作,這個操作又會調用 __getitem__
魔法函數。
def __getattr__(self, item):
return getattr(connections[DEFAULT_DB_ALIAS], item)
來重點看__getitem__()
這個魔法函數:
def __getitem__(self, alias):
if hasattr(self._connections, alias):
return getattr(self._connections, alias)
self.ensure_defaults(alias)
self.prepare_test_settings(alias)
db = self.databases[alias]
# 使用mysql引擎
backend = load_backend(db['ENGINE'])
conn = backend.DatabaseWrapper(db, alias)
setattr(self._connections, alias, conn)
return conn
注意:代碼首先是要獲取 settings.py 中關于數據庫的配置,注意我們前面設置的 db[‘ENGINE’] 的值為:django.db.backends.mysql
,下面的 load_backend() 方法只是一個簡單的導入模塊,最核心的就是一句:import_module('%s.base' % backend_name)
,相當于導入了模塊 django.db.backends.mysql.base
:
def load_backend(backend_name):
"""
Return a database backend's "base" module given a fully qualified database
backend name, or raise an error if it doesn't exist.
"""
# This backend was renamed in Django 1.9.
if backend_name == 'django.db.backends.postgresql_psycopg2':
backend_name = 'django.db.backends.postgresql'
try:
# 最核心的部分
return import_module('%s.base' % backend_name)
except ImportError as e_user:
# 異常處理,代碼省略
...
在前面導入的 django.db.backends.mysql.base
文件中,我們可以看到如下代碼段:
# 源碼位置 django/db/backends/mysql/base.py
try:
import MySQLdb as Database
except ImportError as err:
raise ImproperlyConfigured(
'Error loading MySQLdb module.\n'
'Did you install mysqlclient?'
) from err
# ...
class DatabaseWrapper(BaseDatabaseWrapper):
# ...
Database = Database
# ...
def get_new_connection(self, conn_params):
return Database.connect(**conn_params)
# ...
# 源碼位置 django/db/backends/base/base.py
# ...
class BaseDatabaseWrapper:
# ...
def connect(self):
"""Connect to the database. Assume that the connection is closed."""
# Check for invalid configurations.
...
# Establish the connection
conn_params = self.get_connection_params()
############ 注意,這里的連接會調用下面這個方法得到 ######################
self.connection = self.get_new_connection(conn_params)
####################################################################
...
# ...
其實從我簡化的代碼來看,可以看到在 Django 中,對于 MySQL 數據庫的連接來說,使用的就是 python 中的 mysqlclient 模塊,只不過 Django 在 mysqlclient 基礎上又封裝了一層,包括里面定義的游標,以及游標的方法都是 mysqlclient 中的函數。后面再介紹 Django 的內置 ORM 模型時候,我們會繼續分析這個 mysql 引擎目錄下的源碼,看 Django 如何一步步封裝 mysqlcient 進而實現自己內置的 ORM 模型。
3. 小結
本小節中我們介紹了 Python DB-API 相關概念, 然后介紹了 Python 中操作 MySQL 常用第三方模塊,并以 pymysql 為例對數據庫進行了增刪改查操作。接下來介紹了在 Django 中如何使用原生的 SQL 語句來操作 MySQL 數據庫并進行了代碼演示和說明,此外還追蹤了部分源碼,發現其內部實現機制就是在 mysqlcient 上做的二次封裝。接下來,我將繼續為大家介紹 Django 中操作數據庫的常用方式-基于內嵌的 ORM 模型。