Python中操作MySQL

pymysql(第三方庫):

 首先,必須先和數據庫建立一個傳輸數據的連接通道,需要用到pymysql下的connect()方法

​ pymysql.connect() 方法返回的是Connections類下的Connection 實例,connect() 方法傳參就是在給Connection類的 _init_ 初始化不定長參數,也可以理解爲 connect() 方法就是在創建新的 Connetion 對象

connect() / Connection初始化 常用參數說明
host 主機ip
user 用戶名
password 密碼
database 數據庫
port 端口號
charset 字符集
import pymysql
# 建立傳輸數據的鏈接通道
conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8")
print(conn)     # <pymysql.connections.Connection object at 0x000001ADB3CD4E50>

在使用pymysql.connect() 方法與數據庫建立連接後,想要操作數據庫時,就需要使用遊標 Cursor

​通過連接好的數據庫(此處爲conn)調用 cursor() 方法即可返回一個新的遊標對象,在連接沒有關閉之前,遊標對象可以反覆使用

# 獲取遊標,用於操作數據庫
cursor = conn.cursor()
print(cursor)    # <pymysql.cursors.Cursor object at 0x000001ADB3ED4FD0>

 常用基礎方法:

execute():發送指令

fetchone():取出下一條記錄

fetchchall():取出所有數據

commit():提交變化到穩定存儲(在對數據庫進行增刪改的時候需要做的操作)

import pymysql
# 建立傳輸數據的鏈接通道
conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8")
print(conn)     # <pymysql.connections.Connection object at 0x000001C3C6687FD0>
# 獲取遊標,用於操作數據庫
cursor = conn.cursor()
print(cursor)   # <pymysql.cursors.Cursor object at 0x000001C3C6687F70>

# 執行一個查詢(即:發送指令)
cursor.execute("show databases")
# 取出下一條記錄(有點像可迭代對象(iterable),具有next方法,可以逐一獲取每行數據,但不是一個可迭代對象)
result = cursor.fetchone()
print(result)   # ('db1',)
# 取出所有行,由於上面代碼去了一條記錄,所以fetchall獲取的數據沒有上面的那條數據
result = cursor.fetchall()
print(result)   # (('information_schema',), ('mysql',), ('performance_schema',), ('sys',))

# 創建數據庫
cursor.execute("create database db3 default charset utf8 collate utf8_general_ci")
# 增、刪、改  都需要使用commit (提交變化到穩定存儲)。
conn.commit()
cursor.execute("show databases")
print(cursor.fetchall())     # (('db1',), ('db3',), ('information_schema',), ('mysql',), ('performance_schema',), ('sys',))

# 刪除數據庫
cursor.execute("drop database db3")
conn.commit()
cursor.execute("show databases")
print(cursor.fetchall())    # (('db1',), ('information_schema',), ('mysql',), ('performance_schema',), ('sys',))

cursor.execute("use db1")
print(cursor.fetchall())    # ()
cursor.execute("show tables")
result = cursor.fetchall()
print(result)       # (('123',), ('emp',), ('employee',), ('student',), ('student_1',))

SQL注入

假如,你開發了一個用戶認證的系統,應該用戶登錄成功後才能正確的返回相應的用戶結果

import pymysql

# 輸入用戶名和密碼
user = input("請輸入用戶名:") # ' or 1=1 -- 
pwd = input("請輸入密碼:") # 123


conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', passwd='root123', charset="utf8",db='usersdb')
cursor = conn.cursor()

# 基於字符串格式化來 拼接SQL語句
# sql = "select * from users where name='root' and password='123'"
# sql = "select * from users where name='' or 1=1 -- ' and password='123'"
sql = "select * from users where name='{}' and password='{}'".format(user, pwd)
cursor.execute(sql)

result = cursor.fetchone()
print(result) # None,不是None

cursor.close()
conn.close()

如果用戶在輸入user時,輸入了: ' or 1=1 -- ,這樣即使用戶輸入的密碼不存在,也會可以通過驗證。

爲什麼呢?

因爲在SQL拼接時,拼接後的結果是:

select * from users where name='' or 1=1 -- ' and password='123'

注意:在MySQL中 -- 表示註釋。

 

那麼,在Python開發中 如何來避免SQL注入呢?

切記,SQL語句不要在使用python的字符串格式化,而是使用pymysql的execute方法。

# SQL語句不在用python的字符串格式化,而是使用pymsql的execute方法
import pymysql

# 輸入賬號密碼
user = input("輸入賬號")
pw = input("輸入密碼")

# 創建數據庫鏈接
conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8")
# 創建遊標
cursor = conn.cursor()

# 方式一:發送指令,execute會自動幫助我們檢測,user、pw中是否存在特殊字符並幫助處理。
cursor.execute("select * from user where name=%s and password=%s", [user, pw])
# 方式二:
# cursor.execute("select * from user where name=%(name)s and password=%(passwd)s", {"name": user, "passwd": pw})

result = cursor.fetchall()
print(result)

# 關閉遊標、關閉鏈接
cursor.close()
conn.close()

 

Python代碼:事務

什麼是數據庫事務

數據庫事務是訪問並可能操作各種數據項的一個數據庫操作(包括增刪改,不包括查詢)序列,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部數據庫操作組成。

import pymysql

# 創建鏈接
conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8")
# 這樣寫可以直接進入到數據庫shool中
# conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8", db="shool")
# 創建遊標
cursor = conn.cursor()

# 進入數據庫
cursor.execute("use shool")


# 開啓事務
conn.begin()

# 嘗試執行事務
try:
    cursor.execute("update student set sname=%s where sid=8", ["某人飛"])
except Exception as e:
    print("發生異常,進行回滾")
    conn.rollback()
else:
    print("提交事務")
    conn.commit()

# 關閉遊標、關閉鏈接
cursor.close()
conn.close()

在用MySQL時,不知你是否會疑問:同時有很多做更新、插入、刪除動作,MySQL如何保證數據不出錯呢?

 

MySQL中自帶了鎖的功能,可以幫助我們實現開發過程中遇到的同時處理數據的情況。對於數據庫中的鎖,從鎖的範圍來講有:

  • 表級鎖,即A操作表時,其他人對整個表都不能操作,等待A操作完之後,才能繼續。

  • 行級鎖,即A操作表時,其他人對指定的行數據不能操作,其他行可以操作,等待A操作完之後,才能繼續。

MYISAM支持表鎖,不支持行鎖;
InnoDB引擎支持行鎖和表鎖。

即:在MYISAM下如果要加鎖,無論怎麼加都會是表鎖。
    在InnoDB引擎支持下如果是基於索引查詢的數據則是行級鎖,否則就是表鎖。

在innodb引擎中,update、insert、delete的行爲內部都會先申請鎖(排它鎖),申請到之後才執行相關操作,最後再釋放鎖。

所以,當多個人同時像數據庫執行:insert、update、delete等操作時,內部加鎖後會排隊逐一執行。
而select則默認不會申請鎖
select * from xxx;

如果,你想要讓select去申請鎖,則需要配合 事務 + 特殊語法來實現。

  • for update,排它鎖,加鎖之後,其他不可以讀寫。
begin; 
    select * from L1 where name="飛某人" for update;    -- name列不是索引(表鎖)
commit;
begin; -- 或者 start transaction;
    select * from L1 where id=1 for update;              -- id列是索引(行鎖)
commit;

當行鎖是id(索引)時,其他事務也需要有單獨的索引纔不會被阻塞,否則數據會通過id列就導致了阻塞

  • lock in share mode,共享鎖,加鎖之後,其他可讀但不可寫。
begin; 
    select * from L1 where name="飛某人" lock in share mode;    -- 假設name列不是索引(表鎖)
commit;
begin; -- 或者 start transaction;
    select * from L1 where id=1 lock in share mode;           -- id列是索引(行鎖)
commit;

排它鎖

排它鎖( for update),加鎖之後,其他事務不可以讀寫。

應用場景:總共100件商品,每次購買一件需要讓商品個數減1 。

A: 訪問頁面查看商品剩餘 100
B: 訪問頁面查看商品剩餘 100

此時 A、B 同時下單,那麼他們同時執行SQL:
    update goods set count=count-1 where id=3
由於Innodb引擎內部會加鎖,所以他們兩個即使同一時刻執行,內部也會排序逐步執行。


但是,當商品剩餘 1個時,就需要注意了。
A: 訪問頁面查看商品剩餘 1
B: 訪問頁面查看商品剩餘 1

此時 A、B 同時下單,那麼他們同時執行SQL:
    update goods set count=count-1 where id=3
這樣剩餘數量就會出現 -1,很顯然這是不正確的,所以應該怎麼辦呢?


這種情況下,可以利用 排它鎖,在更新之前先查詢剩餘數量,只有數量 >0 纔可以購買,所以,下單時應該執行:
    begin; -- start transaction;
    select count from goods where id=3 for update;
    -- 獲取個數進行判斷
    if 個數>0:
        update goods set count=count-1 where id=3;
    else:
        -- 已售罄
    commit;

基於Python代碼示例:

import pymysql
import threading


def task():
    # 創建鏈接
    conn = pymysql.connect(host="localhost", port=3306, user="root", passwd="root", charset="utf8", db="db1")

    # 創建遊標
    # 參數的作用是將遊標接收到的數據整理成字典的形式
    cursor = conn.cursor(pymysql.cursors.DictCursor)

    # 開啓事務
    conn.begin()

    # 嘗試執行事務
    try:
        cursor.execute("select id,age from tran where id=1 for update")
        # fetchall    有參:( {"id":1,"age":10},{"id":2,"age":10}, )  無參獲取的結果:((1,10),(2,10))
        # fetchone    有參{"id":1,"age":10}    無參獲取的結果:(1,10)
        result = cursor.fetchone()
        print(result)
        current_age = result['age']
        if current_age > 0:
            cursor.execute("update tran set age=age-1 where id=1")
        else:
            print("已售罄")
    except Exception as e:
        print("異常,進行回滾")
        conn.rollback()
    else:
        print("提交事務")
        conn.commit()
    cursor.close()
    conn.close()


def fun():
    thread = threading.Thread(target=task)
    thread.start()


if __name__ == '__main__':
    for x in range(11):
        fun()

 

 

數據庫連接池

假設在用戶使用時,每個用戶都會獲取一個數據庫連接,如果有很多用戶同時申請獲取連接,然後使用後連接也都斷開了,如果又要連接......這樣的過程很明顯用上面的代碼是行不通的,數據庫頂不住啊!

這時候就需要一個數據庫連接池,來管理用戶對數據庫連接的操作。

 

在操作數據庫時需要使用數據庫連接池

pip3.9 install pymysql
pip3.9 install dbutils
# 導入連接池工具包
import threading
import pymysql
from dbutils.pooled_db import PooledDB

MYSQL_DB_POOL = PooledDB(
    creator=pymysql,  # 使用鏈接數據庫的模塊,因爲dbutils並不能鏈接數據庫,真正連接數據庫還是需要pymsql
    maxconnections=5,  # 連接池允許的最大連接數,0和None表示不限制連接數
    mincached=2,  # 初始化時,鏈接池中至少創建的空閒的鏈接,0表示不創建
    maxcached=3,  # 鏈接池中最多閒置的鏈接,0和None不限制
    blocking=True,  # 連接池中如果沒有可用連接後,是否阻塞等待。True,等待;False,不等待然後報錯
    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='root',
    database='shool',
    charset='utf8'
)


def task():
    # 獲取連接池中的連接
    conn = MYSQL_DB_POOL.connection()
    # 獲取連接中的遊標
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    # cursor = conn.cursor()

    # 執行命令
    cursor.execute("show tables")
    # 接受數據
    result = cursor.fetchall()
    print(result)
    cursor.close()

    # 把鏈接交還給數據庫連接池
    conn.close()


def run():
    for x in range(10):
        thread = threading.Thread(target=task)
        thread.start()


if __name__ == '__main__':
    run()

 

 

SQL工具類

因爲數據庫連接池只有一個,所以我們需要基於數據庫連接池開發一個公共的SQL操作,方便以後操作數據庫。

使用模塊的方法實現單例

Python 的模塊就是天然的單例模式,因爲模塊在第一次導入時,會生成 .pyc 文件,當第二次導入時,就會直接加載 .pyc 文件,而不會再次執行模塊代碼。因此,我們只需把相關的函數和數據定義在一個模塊中,就可以獲得一個單例對象了。如果我們真的想要一個單例類,可以考慮這樣做

# 數據庫連接池_單例.py
import pymysql
from dbutils.pooled_db import PooledDB


# 模塊的方式實現單例
class DBHelper(object):
    # 初始化數據庫連接池
    def __init__(self):
        # TODO 此處配置,可以去配置文件中讀取。
        self.pool = PooledDB(
            creator=pymysql,  # 使用鏈接數據庫的模塊
            maxconnections=5,  # 連接池允許的最大連接數,0和None表示不限制連接數
            mincached=2,  # 初始化時,鏈接池中至少創建的空閒的鏈接,0表示不創建
            maxcached=3,  # 鏈接池中最多閒置的鏈接,0和None不限制
            blocking=True,  # 連接池中如果沒有可用連接後,是否阻塞等待。True,等待;False,不等待然後報錯
            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='root',
            database='shool',
            charset='utf8'
        )

    # 獲取連接和遊標
    def get_conn_cursor(self):
        conn = self.pool.connection()
        cursor = conn.cursor(pymysql.cursors.DictCursor)
        return conn, cursor

    # 關閉遊標,把鏈接交還給連接池
    def close_conn_cursor(self, *args):
        for item in args:
            item.close()

    # 發送指令
    def exec(self, sql, **kwargs):
        conn, cursor = self.get_conn_cursor()
        cursor.execute(sql, kwargs)
        conn.commit()
        self.close_conn_cursor(conn, cursor)

    # 獲取一條數據
    def fetch_one(self, sql, **kwargs):
        conn, cursor = self.get_conn_cursor()
        cursor.execute(sql, kwargs)
        result = cursor.fetchone()
        self.close_conn_cursor(conn, cursor)
        return result

    # 獲取所有數據
    def fetch_all(self, sql, **kwargs):
        conn, cursor = self.get_conn_cursor()
        cursor.execute(sql, kwargs)
        result = cursor.fetchall()
        self.close_conn_cursor(conn, cursor)

        return result


db = DBHelper()
# 數據庫連接池測試.py
# 測試模塊的方式實現的單例
from 數據庫連接池_單例 import db

db.exec("insert into student(sname) values(%(name)s)", name="飛某人")

ret = db.fetch_one("select * from student")
print(ret)

ret = db.fetch_one("select * from student where sid=%(nid)s", nid=3)
print(ret)

ret = db.fetch_all("select * from student")
print(ret)

ret = db.fetch_all("select * from student where sid>%(nid)s", nid=2)
print(ret)

 

上下文管理的方式實現單例

with 獲取連接:
    執行SQL(執行完畢後,自動將連接交還給連接池)
# 數據庫連接池_單例.py
import pymysql
from dbutils.pooled_db import PooledDB
# 上下文管理方式實現單例
# 創建一個連接池
POOL = PooledDB(
    creator=pymysql,  # 使用鏈接數據庫的模塊,因爲dbutils並不能鏈接數據庫,真正連接數據庫還是需要pymsql
    maxconnections=5,  # 連接池允許的最大連接數,0和None表示不限制連接數
    mincached=2,  # 初始化時,鏈接池中至少創建的空閒的鏈接,0表示不創建
    maxcached=3,  # 鏈接池中最多閒置的鏈接,0和None不限制
    blocking=True,  # 連接池中如果沒有可用連接後,是否阻塞等待。True,等待;False,不等待然後報錯
    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='root',
    database='shool',
    charset='utf8'
)

class Connect(object):
    # 初始化數據庫連接與遊標
    def __init__(self):
        self.conn = POOL.connection()
        self.cursor = self.conn.cursor(pymysql.cursors.DictCursor)

    # 上文管理
    def __enter__(self):
        return self

    # 下文管理
    def __exit__(self, exc_type, exc_val, exc_tb):
        self.cursor.close()
        self.conn.close()

    # 發送指令
    def exec(self, sql, **kwargs):
        self.cursor.execute(sql, kwargs)
        self.conn.commit()

    # 獲取一條數據
    def fetch_one(self):
        result = self.cursor.fetchone()
        return result

    # 獲取所有數據
    def fetch_all(self):
        result = self.cursor.fetchall()
        return result
# 數據庫連接池測試.py
# 上下文管理測試

from 數據庫連接池_單例 import Connect

with Connect() as obj:
    obj.exec("select * from student")
    result = obj.fetch_one()
    print(result)
    result = obj.fetch_all()
    print(result)

    obj.exec("select * from student where sname=%(name)s or sid=%(tid)s", name="某人飛", tid=4)
    result = obj.fetch_all()
    print(result)

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章