41 - 數據庫-pymysql41 - 數據庫-pymysql-DBUtils

1 Python操做數據庫

        Python 提供了程序的DB-API,支持衆多數據庫的操做。因爲目前使用最多的數據庫爲MySQL,因此咱們這裏以Python操做MySQL爲例子,同時也由於有成熟的API,因此咱們沒必要去關注使用什麼數據,由於操做邏輯和方法是相同的。python

2 安裝模塊

        Python 程序想要操做數據庫,首先須要安裝 模塊 來進行操做,Python 2 中流行的模塊爲 MySQLdb,而該模塊在Python 3 中將被廢棄,而使用PyMySQL,這裏以PyMySQL模塊爲例。下面使用pip命令安裝PyMSQL模塊mysql

pip3 install pymysql

若是沒有pip3命令那麼須要確認環境變量是否有添加,安裝完畢後測試是否安裝完畢。sql

lidaxindeMacBook-Pro:~ DahlHin$ python3
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pymysql
>>>
# 若是沒有報錯,則表示安裝成功

3 基本使用

        首先咱們須要手動安裝一個MySQL數據庫,這裏再也不贅述,參考博文:http://www.cnblogs.com/dachenzi/articles/7159510.html數據庫

鏈接數據庫並執行sql語句的通常流程是:編程

  1. 創建鏈接
  2. 獲取遊標(建立)
  3. 執行SQL語句
  4. 提交事務
  5. 釋放資源

對應到代碼上的邏輯爲:緩存

  1. 導入相應的Python模塊
  2. 使用connect函數鏈接數據庫,並返回一個Connection對象
  3. 經過Connection對象的cursor方法,返回一個Cursor對象
  4. 經過Cursor對象的execute方法執行SQL語句
  5. 若是執行的是查詢語句,經過Cursor對象的fetchall語句獲取返回結果
  6. 調用Cursor對象的close關閉Cursor
  7. 調用Connection對象的close方法關閉數據庫鏈接

3.1 建立一個鏈接

使用pymysql.connect方法來鏈接數據庫安全

import pymysql
 
pymysql.connect(host=None, user=None, password="",
                 database=None, port=0, unix_socket=None,
                 charset=''......)

主要的參數有:服務器

  • host:表示鏈接的數據庫的地址
  • user:表示鏈接使用的用戶
  • password:表示用戶對應的密碼
  • database:表示鏈接哪一個庫
  • port:表示數據庫的端口
  • unix_socket:表示使用socket鏈接時,socket文件的路徑
  • charset:表示鏈接使用的字符集 
  • read_default_file:讀取mysql的配置文件中的配置進行鏈接

3.2 鏈接數據庫

        調用connect函數,將建立一個數據庫鏈接並獲得一個Connection對象,Connection對象定義了不少的方法和異常。session

host = '10.0.0.13'
port = 3306
user = 'dahl'
password = '123456'
database = 'test'

conn = pymysql.connect(host, user, password, database, port)
print(conn)  # <pymysql.connections.Connection object at 0x000001ABD3063550>
conn.ping()  # 沒有返回值,沒法鏈接會提示異常

這裏conn就是一個Connection對象,它具備一下屬性和方法:

  • begin:開始事務
  • commit:提交事務
  • rollback:回滾事務
  • cursor:返回一個Cursor對象
  • autocommit:設置事務是否自動提交
  • set_character_set:設置字符集編碼
  • get_server_info:獲取數據庫版本信息
  • ping(reconnect=True): 測試數據庫是否活着,reconnect表示斷開與服務器鏈接後是否重連,鏈接關閉時拋出異常(通常用來測通斷)

        在實際的編程過程當中,通常不會直接調用begin、commit和rollback函數,而是經過上下文管理器實現事務的提交與回滾操做

3.3 遊標

        遊標是系統爲用戶開設的一個數據緩存區,存放SQL語句執行的結果,用戶能夠用SQL語句逐一從遊標中獲取記錄,並賦值給變量,交由Python進一步處理。
        在數據庫中,遊標是一個十分重要的概念。遊標提供了一種對從表中檢索出的數據進行操做的靈活手段,就本質而言,遊標其實是一種能從包括多條數據記錄的結果集中每次提取一條記錄的機制。遊標老是與一條SQL 選擇語句相關聯由於遊標由結果集(能夠是零條、一條或由相關的選擇語句檢索出的多條記錄)和結果集中指向特定記錄的遊標位置組成。當決定對結果集進行處理時,必須聲明一個指向該結果集的遊標。
        正如前面咱們使用Python對文件進行處理,那麼遊標就像咱們打開文件所獲得的文件句柄同樣,只要文件打開成功,該文件句柄就可表明該文件。對於遊標而言,其道理是相同的。

3.3.1 利用遊標操做數據庫

在進行數據庫的操做以前須要建立一個遊標對象,來執行sql語句。

cursor = conn.cursor()

下面利用遊標來執行sql語句:

import pymysql
 
def connect_mysql():
 
    db_config = {
        'host':'127.0.0.1',
        'port':3306,
        'user':'root',
        'password':'abc.123',
        'charset':'utf8'
    }
 
    return  pymysql.connect(**db_config)
 
if __name__ == '__main__':
    conn = connect_mysql()
    cursor = conn.cursor()    # 建立遊標
    sql = r" select user,host from mysql.user "  # 要執行的sql語句
    cursor.execute(sql)  # 交給 遊標執行
    result = cursor.fetchall()  # 獲取遊標執行結果
    print(result)

3.3.2 事務管理

Connection對象包含以下三個方法用於事務管理:

  • begin:開始事務
  • commit:提交事務
  • rollback:回滾事務
import pymysql

def connect_mysql():
    db_config = {
        'host': '10.0.0.13',
        'port': 3306,
        'user': 'dahl',
        'password': '123456',
        'charset': 'utf8'
    }

    return pymysql.connect(**db_config)


if __name__ == '__main__':
    conn = connect_mysql()
    cursor = conn.cursor()  # 建立遊標

    conn.begin()
    sql = r"insert into test.student (id,name,age) VALUES (5,'dahl',23)"  # 要執行的sql語句
    res = cursor.execute(sql)  # 交給 遊標執行
    print(res)
    conn.commit()

這樣使用是極其不安全的,由於sql語句有可能執行失敗,當失敗時,咱們應該進行回滾

if __name__ == '__main__':
    conn = None 
    cursor = None
    try:
        conn = connect_mysql()
        cursor = conn.cursor()  # 建立遊標
        sql = r"insert into test.student (id,name,age) VALUES (6,'dahl',23)"  # 要執行的sql語句
        cursor.execute(sql)  # 交給 遊標執行
        conn.commit()    # 提交事務
    except:
        conn.rollback()   # 當SQL語句執行失敗時,回滾
    finally:
        if cursor:   
            cursor.close()    # 關閉遊標
        if conn:
            conn.close()   # 關閉鏈接

3.3.3 執行SQL語句

用於執行SQL語句的兩個方法爲:

  • cursor.execute(sql):執行一條sql語句
  • executemany(sql,parser):執行多條語句
sql = r"select * from test.student" 
res = cursor.execute(sql)

3.3.3.1 批量執行

executemany用於批量執行sql語句,

  • sql 爲模板,c風格的佔位符。
  • parser:爲模板填充的數據(可迭代對象)
sql = r"insert into test.student (id,name,age) values (%s,'daxin',20)"
cursor.executemany(sql,(7,8,9,))

3.3.3.2 SQL注入攻擊

咱們通常在程序中使用sql,可能會有以下代碼:

# 接受用戶id,而後拼接查詢用戶信息的SQL語句,而後查詢數據庫。
userid = 10 # 來源於程序
sql = 'select * from user where user_id = {}'.format(userid)

userid是可變的,好比經過客戶端發來的request請求,直接拼接到查詢字符串中。若是客戶端傳遞的userid是 '5 or 1='呢

sql = 'select * from test.student where id = {}'.format('5 or 1=1')

此時真正在數據庫中查詢的sql語句就變成了

select * from test.student where id = 5 or 1=1

這條語句的where條件的函數含義是:當id等於5,或者1等於1時,列出全部匹配的字段,這樣就輕鬆的查到了標準全部的數據!!!

永遠不要相信客戶端傳來的數據是規範及安全的。

3.3.3.3 參數化查詢

        cursor.execute(sql)方法還額外提供了一個args參數,用於進行參數化查詢,它的類型能夠爲元組、列表、字典。若是查詢字符串使用命名關鍵字(%(name)s),那麼就必須使用字典進行傳參。

sql = 'select * from test.student where id = %s'
cursor.execute(sql,args=(2,))

sql = 'select * from test.student where id = %(id)s'
cursor.execute(sql,args={'id':2})

        咱們說使用參數化查詢,效率會高一點,爲何呢?由於SQL語句緩存。數據庫通常會對SQL語句編譯和緩存,編譯只對SQL語句部分,因此參數中就算有SQL指令也不會被執行。編譯過程,須要此法分析、語法分析、生成AST、優化、生成執行計劃等過程,比較耗費資源。服務端會先查找是不是同一條語句進行了緩存,若是緩存未失效,則不須要再次編譯,從而下降了編譯的成本,下降了內存消耗。

能夠認爲SQL語句字符串就是一個key,若是使用拼接方案,每次發過去的SQL語句都不同,都須要編譯並緩存。大量查詢的時候,首選使用參數化查詢,以節省資源。

3.4 獲取查詢結果

Cursor類提供了三種查詢結果集的方法:(返回值爲元組,多個值爲嵌套元組)

  • fetchone():獲取一條
  • fetchmany(size=None):獲取多條,當size爲None時,返回一個包含一個元素的嵌套元組
  • fetchall():獲取全部
sql = 'select * from test.student'
cursor.execute(sql)
print(cursor.fetchall())
print(cursor.fetchone())  # None
print(cursor.fetchmany(2))  # None

        fetch存在一個指針,fetchone一次,就讀取結果集中的一個結果,若是fetchall,那麼一次就會讀取完全部的結果。能夠經過調整這個指針來重複讀取

  • cursor.rownumber: 返回當前行號,能夠修改,支持 負數
  • cursor.rowcount: 返回總行數
sql = 'select * from test.student'
cursor.execute(sql)
print(cursor.fetchall())  # ((2, 'dahl', 20), (3, 'dahl', 21), (4, 'daxin', 22), (5, 'dahl', 23), (6, 'dahl', 23), (7, 'daxin', 20), (8, 'daxin', 20), (9, 'daxin', 20))
cursor.rownumber = 0
print(cursor.fetchone())  # (2, 'dahl', 20)
print(cursor.fetchmany()) # ((3, 'dahl', 21),)

fetch操做的是結果集,結果集是保存在客戶端的,也就是說fetch的時候,查詢已經結束了。

3.4.1 帶列明的查詢

        結果中不包含字段名,除非咱們記住字段的順序,否則很麻煩,那麼下面來解決這個問題。

觀察cursor原碼,咱們發現,它接受一個參數cursor,有一個參數是:DictCursor,查看源碼得知,它是Cursor的Mixin子類。

def cursor(self, cursor=None):
    """
    Create a new cursor to execute queries with.

    :param cursor: The type of cursor to create; one of :py:class:`Cursor`,
        :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`.
        None means use Cursor.
    """
    if cursor:
        return cursor(self)
    return self.cursorclass(self)

觀察DictCursor的原碼,得知結果集會返回一個字典,一個字段名:值的結果,因此,只須要傳入cursor參數便可

from pymysql import cursors  # DictCursor存在與cursors模塊中
    ... ... 
    cursor = conn.cursor(cursors.DictCursor) 
    sql = 'select * from test.student'
    cursor.execute(sql)
    print(cursor.fetchone())  # {'id': 2, 'name': 'dahl', 'age': 20}

3.5 上下文支持

Connection和Cursor類實現了__enter__和__exit__方法,因此它支持上下文管理。

  • Connection:進入時返回一個cursor,退出時若是有異常,則回滾,不然提交
  • Cursor: 進入時返回cursor自己,退出時,關閉cursor鏈接

因此利用上下文的特性,咱們能夠這樣使用

import pymysql

def connect_mysql():
    db_config = {
        'host': '10.0.0.13',
        'port': 3306,
        'user': 'dahl',
        'password': '123456',
        'charset': 'utf8'
    }

    return pymysql.connect(**db_config)

if __name__ == '__main__':
conn = connect_mysql()
with conn as cursor:
    sql = 'select * from student'
    cursor.execute(sql)
    print(cursor.fetchmany(2))

# conn的exit只是提交了,並無關閉curosr
cursor.close()
conn.close()

若是要自動關閉cursor,能夠進行以下改寫

if __name__ == '__main__':
conn = connect_mysql()
with conn as cursor:
    with cursor   # curosr的exit會關閉cursor
        sql = 'select * from student'
        cursor.execute(sql)
        print(cursor.fetchmany(2))

conn.close()

多個 cursor共享一個 conn

4 DBUtils鏈接池

        在python編程中可使用MySQLdb/pymysql等模塊對數據庫的鏈接及諸如查詢/插入/更新等操做,可是每次鏈接mysql數據庫請求時,都是獨立的去請求訪問,至關浪費資源,並且訪問數量達到必定數量時,對mysql的性能會產生較大的影響。所以,實際使用中,一般會使用數據庫的鏈接池技術,來訪問數據庫達到資源複用的目的。

db_utils

鏈接池對性能的提高表如今:

  1. 在程序建立鏈接的時候,能夠從一個空閒的鏈接中獲取,不須要從新初始化鏈接,提高獲取鏈接的速度
  2. 關閉鏈接的時候,把鏈接放回鏈接池,而不是真正的關閉,因此能夠減小頻繁地打開和關閉鏈接

        DBUtils是一套Python數據庫鏈接池包,並容許對非線程安全的數據庫接口進行線程安全包裝。DBUtils來自Webware for Python。

DBUtils提供兩種外部接口:

  • PersistentDB:提供線程專用的數據庫鏈接,並自動管理鏈接。(爲每個線程建立一個)
  • PooledDB:提供線程間可共享的數據庫鏈接,並自動管理鏈接。

爲每一個線程建立一個,資源消耗太大,因此咱們經常使用的是PooledDB.

PooledDB經常使用的參數爲:

  • creator=pymysql: 使用連接數據庫的模塊
  • maxconnections=6: 鏈接池容許的最大鏈接數,0和None表示不限制鏈接數
  • mincached=2: 初始化時,連接池中至少建立的空閒的連接,0表示不建立,若是空閒鏈接數小於這個數,pool會建立一個新的鏈接
  • maxcached=5: 連接池中最多閒置的連接,0和None不限制,若是空閒鏈接數大於這個數,pool會關閉空閒鏈接
  • maxshared=3: 連接池中最多共享的連接數量,0和None表示所有共享。PS: 無用,由於pymysql和MySQLdb等模塊的 threadsafety都爲1,全部值不管設置爲多少,_maxcached永遠爲0,因此永遠是全部連接都共享。
  • blocking=True: 鏈接池中若是沒有可用鏈接後,是否阻塞等待。True,等待;False,不等待而後報錯
  • maxusage=None: 一個連接最多被重複使用的次數,None表示無限制
  • setsession=[]: 開始會話前執行的命令列表。如:["set datestyle to ...", "set time zone ..."]
  • ping=0: ping MySQL服務端,檢查是否服務可用。
    • 0 = None = never
    • 1 = default = whenever it is requested
    • 2 = when a cursor is created
    • 4 = when a query is executed
    • 7 = always
  • host='127.0.0.1',
  • port=3306,
  • user='root',
  • password='123',
  • database='pooldb',
  • charset='utf8'

安裝DBUtils

pip install DBUtils

示例代碼:

import threading
import time
from DBUtils.PooledDB import PooledDB
import pymysql

POOL = PooledDB(
    creator=pymysql,
    maxconnections=6,
    mincached=2,
    maxcached=5,
    # maxshared=3,
    blocking=True,
    maxusage=None,
    setsession=[],
    ping=0,
    host='10.0.0.13',
    port=3306,
    user='dahl',
    password='123456',
    database='test',
    charset='utf8'
)


def func():
    conn = POOL.connection()
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    time.sleep(2)
    cursor.execute('select * from employees')
    res = cursor.fetchall()
    conn.close()
    print(threading.get_ident(), res)


if __name__ == '__main__':
    for i in range(10):
        threading.Thread(target=func).start()
相關文章
相關標籤/搜索