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)

 

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