☰
Python操作数据库全解析:pymysql连接、事务与CRUD实战避坑指南
2026/10/3 10:50:03 网站建设 项目流程

简介:《Python从入门到精通》第14章“操作数据库”配套PPT课件,面向正在学习Python编程、希望掌握数据库交互技能的初学者和进阶者。内容围绕pymysql与MySQL、SQLite两种数据库展开,从连接参数(host、user、password、db、charset等)的配置,到Connection对象与Cursor游标对象的常用方法,系统讲解execute执行SQL、commit提交事务、rollback回滚,以及fetchone、fetchmany、fetchall等结果集获取方式,并配以实际代码示例展示数据插入、查询等操作。课件共1个PPT文件,大小约465KB,页面设计精简,知识点密度高,便于课堂教学或自学速览。目前已有1555人学习下载,是快速理解Python数据库编程核心概念的高性价比资料。通过学习,读者能够独立完成数据库连接、数据操作与事务处理的代码编写,为后续Web开发、爬虫等项目打下基础。

1. 操作数据库这章,到底能让你少踩几个坑

很多自学 Python 的人学到文件操作就停了,觉得数据库是「后端工程师的事」,拿到这份《Python 从入门到精通 第14章 操作数据库》课件时,第一反应多半是翻两页就搁置。但我拆完这 25 页 PPT 后想说:Python 操作数据库恰恰是入门阶段性价比最高的一章,因为它把「写代码」和「真实业务」之间的那道缝补上了。课件内容并不深,主线很清晰——先讲 pymysql 怎么连 MySQL,再讲 Connection 和 Cursor 这对核心对象各自干什么,最后用 SQLite 收尾演示一套完整 CRUD。适合刚学完语法、想把自己的数据持久化存下来的新手,也适合准备面试前快速过一遍数据库编程接口的求职者。我接下来会按课件顺序把每一块拆开,补上参数说明和实际跑代码时会遇到的那些坑。

2. 把数据库接进 Python:pymysql 连接参数与 Connection/Cursor 对象

2.1 connect() 参数不是照着填就完事,每个字段都有讲究

课件第一章就给了这么一段连接代码,看起来平平无奇:

import pymysql conn = pymysql.connect( host='localhost', user='user', password='passwd', db='test', charset='utf8', cursorclass=pymysql.cursors.DictCursor )

这里我建议你不要直接复制,而是逐个确认参数含义,因为每一行都对应一个实际的故障点。host填localhost表示连接本机 MySQL,如果你要连远程数据库,这里要改成服务器的 IP 或域名,而且大概率还要加一个port=3306参数——MySQL 默认端口是 3306,但很多云数据库实例用的是自定义端口,不写port连不上时你都不知道去哪查。user和password是数据库账号,注意这个账号的权限范围,新手常见的翻车现场是:本地 root 能连,换了应用账号就报Access denied,原因往往是账号只授权了某个库。

db参数指定你要操作的数据库名,课件里写的是test,你换成自己的库名即可。这里有个非常隐蔽的坑:db和database是等价的,pymysql 两个都认,但如果你在连接时不指定db,后面执行 SQL 就必须写成库名.表名的形式,否则会报No database selected。charset='utf8'是字符集设置,注意这里不要写成utf-8,带横线的写法 pymysql 不认。而且utf8在 MySQL 里实际是utf8mb3,如果你要存 emoji 表情或生僻字,得用utf8mb4,这是后话,避坑章节我会展开。

cursorclass=pymysql.cursors.DictCursor是容易被忽略但影响深远的一个参数。默认情况下,游标返回的数据是元组,你要通过row[0]、row[1]这样的下标访问字段;改成DictCursor后,每一行变成一个字典,你可以用row['id']、row['name']这种键名访问。课件选DictCursor是对的,代码可读性高很多,但你要记住:这改变了结果集的访问方式,以前写row[0]的代码在DictCursor下会直接报TypeError: tuple indices must be integers。所以连接参数不是抄一遍就完,它决定了你后面所有代码的写法。

2.2 Connection 对象是「连接」,不是「操作入口」

课件给 Connection 对象列了四个方法:cursor()、commit()、rollback()、close(),并配了这样一段使用示例:

conn = pymysql.connect( host='localhost', user='user', password='passwd', db='test', charset='utf8', cursorclass=pymysql.cursors.DictCursor ) cur = conn.cursor() cur.execute("INSERT INTO users (id, name) VALUES (1, 'mr')") cur.close() conn.commit() conn.close()

我拆这段代码时的第一感受是:顺序值得注意。很多人第一次写会先commit()再close(),这没问题,但容易漏掉的是cur.close()。你可能会想,连接都关了,游标还需要单独关吗?需要。游标是数据库会话里的独立资源,不关闭它在连接池场景下会造成游标泄漏,MySQL 服务端会有对应的临时资源一直挂着。规范顺序是:先关游标,再提交事务,最后关连接。严格说commit()放在cur.close()前后都能生效,因为这个事务属于连接而不是游标,但养成「用完先关游标」的习惯,在写复杂查询时能少很多资源方面的麻烦。

Connection对象本质上是客户端与 MySQL 服务器之间的一条会话通道。cursor()是这条通道上开启一个执行 SQL 的工作句柄,commit()把事务里的所有更改持久化,rollback()撤销自上次提交以来的所有未提交更改,close()释放通道。理解这层关系后你就明白:为什么commit()没调,数据就「离奇消失」了——因为 MySQL 默认开启事务,你的INSERT只是在会话里生效,没提交就断开连接,服务器端直接丢弃。课件把这四个方法列成表,看起来是背诵题,实际是让你建立「连接-游标-事务」三件套的肌肉记忆。

2.3 Cursor 对象的方法清单,哪些是高频、哪些是冷门

课件把 Cursor 对象的方法列了一张表,我帮你按实际使用频率排个序。最高频的是execute(),执行一条 SQL,可以带参数;其次是fetchall()、fetchone()、fetchmany(size)三个取数方法;然后是executemany(),批量执行;最后是callproc()和nextset(),这俩属于冷门——callproc()调存储过程,中小项目很少用;nextset()处理多个结果集,只有存储过程或批量 SQL 才可能碰到。

cur = conn.cursor() # 单条插入,注意 execute 返回的是受影响行数 affected = cur.execute("INSERT INTO users (id, name) VALUES (%s, %s)", (2, 'python')) # 批量插入,executemany 接收一条 SQL 和参数序列 data = [(3, 'a'), (4, 'b'), (5, 'c')] cur.executemany("INSERT INTO users (id, name) VALUES (%s, %s)", data) conn.commit() # 查询并取数 cur.execute("SELECT * FROM users") rows = cur.fetchall() for row in rows: print(row) cur.close() conn.close()

这里有个细节值得拎出来说:execute()的返回值是受影响行数,而不是查询结果。很多人第一次写result = cur.execute("SELECT ...")然后直接print(result),打印出来一个数字,以为查询失败了,实际这是命中的记录条数。真正要拿数据,必须用fetchone()、fetchmany(size)或fetchall()去游标里取。executemany()的第二个参数是一个可迭代对象,每个元素是一个参数元组,它底层是复用同一条 SQL 模板循环执行,比你在 Python 里自己写 for 循环逐条execute()快得多,尤其插入上千行时差距非常明显。

3. 把 SQL 真正跑起来:execute 执行、事务提交与数据抓取

3.1 查询数据的三种方式:fetchone、fetchmany、fetchall 怎么选

课件专门讲了查询数据的三种方式,原话很短,但展开说这里面的门道不少。fetchone()每次从结果集里取一条记录,游标指针自动下移,适合逐条处理、内存敏感的场景;fetchmany(size)一次取指定数量,适合分页或分批消费;fetchall()一次取全部,适合结果集很小、需要整体操作的场景。

cur.execute("SELECT id, name FROM users") # 方式一:逐条取,循环里处理 row = cur.fetchone() while row: print(row) row = cur.fetchone() # 方式二:按批取,每次取 2 条 while True: batch = cur.fetchmany(2) if not batch: break print(batch) # 方式三:全量取 rows = cur.fetchall() print(len(rows))

三个方法对应的场景差别很大。如果你用fetchall()去取一张十万行的表,Python 会把所有数据一次性加载进内存,机器差一点直接卡死;这时候应该用fetchmany(1000)分批消费,或者干脆fetchone()逐条处理。反过来,如果你只需要第一条记录,比如判断某个条件是否存在,用fetchone()就够了,拿fetchall()是浪费。还有一个容易忽略的点:游标是流式读取的,fetchone()之后再调fetchall(),拿到的只是剩余记录,而不是从头开始的全量数据。想重新取一遍,必须重新execute()。

3.2 事务边界:commit 和 rollback 是一对,谁也别丢

课件在 Connection 对象方法里列了commit()和rollback(),但没有强调它们的配对关系。实际项目中,事务的典型写法是:try 里执行一组 SQL,全部成功就commit();任何一步异常就rollback(),把前面的操作全部撤销。这张「后悔药」只有在事务范围内才有效——如果你每执行一条 SQL 就commit()一次,那rollback()就没有任何可回滚的内容了。

try: cur = conn.cursor() cur.execute("UPDATE users SET name = 'new' WHERE id = 1") cur.execute("INSERT INTO logs (action) VALUES ('update_user')") conn.commit() # 两条 SQL 一起生效 except Exception as e: conn.rollback() # 任何一条失败,两条都撤销 print("事务回滚:", e) finally: cur.close() conn.close()

这里有个实际业务里非常常见的决策点:到底是一条 SQL 一提交,还是一个事务包多條 SQL?我给出的判断标准是看业务一致性要求。像「更新用户资料后写一条操作日志」,这两步必须同时成功或同时失败,必须放进同一个事务;像「每插入一条商品记录」这种本身就是独立事件的,逐条提交反而更灵活,不会因为一条脏数据把整批操作全部回滚。课件里的示例代码是单条 INSERT 后直接commit(),那是为了演示最基本的写法,不要把它当作唯一正确的模式。

3.3 参数化查询:为什么不能把变量直接拼进 SQL 字符串

课件示例里写的是cur.execute("INSERT INTO users (id, name) VALUES (1, 'mr')"),值直接写在 SQL 里。入门阶段这样写没问题,但一旦数据来自用户输入,这就成了 SQL 注入的突破口。安全且规范的做法是参数化查询,用%s占位符,把实际值作为execute()的第二个参数传入:

cur = conn.cursor() # 错误示范:字符串拼接,用户输入 name 时可能注入恶意 SQL # cur.execute("INSERT INTO users (id, name) VALUES (1, '" + name + "')") # 正确做法:参数化,pymysql 会自动处理转义 name = "mr'; DROP TABLE users; --" cur.execute("INSERT INTO users (id, name) VALUES (%s, %s)", (1, name)) conn.commit() cur.close() conn.close()

参数化查询的价值不只是防注入,它还能帮你规避引号转义的麻烦。比如上面例子里的name变量如果包含单引号,拼接字符串的写法轻则语法错误,重则被恶意构造 SQL。pymysql 收到带参数的execute()后,会把参数值安全地转义再拼进 SQL 发给服务器,你完全不用手动处理引号。注意%s是 pymysql 的占位符,不要和 Python 字符串格式化的%搞混——这里你传的是一个元组(1, name),不是格式化后的字符串。

4. 操作数据库的四个高频坑:字符集、事务、批量与连接泄漏

4.1 字符集坑:utf8 存 emoji 直接报错

现象:往表里插入带 emoji 的数据,比如INSERT INTO users (name) VALUES ('程序员😄'),报错Incorrect string value: '\xF0\x9F\x98\x84',或者插入成功后查出来是乱码。

原因:课件里的连接参数charset='utf8',在 MySQL 里对应的是utf8mb3,这个字符集只支持最多 3 字节的 UTF-8 编码,而 emoji 是 4 字节编码。建表时如果字段也是utf8,存不下这类字符。

解决:连接参数改成charset='utf8mb4',同时把表的字符集也改掉。我的习惯是建表时显式指定:

CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(50) ) DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

如果表已经建好了,用ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;转换。注意这里有个连带问题:改了连接字符集后,之前用utf8存的乱码数据可能显示异常,这是因为数据本身已经损坏,不是连接参数能救回来的。

4.2 事务未提交:数据「消失」了

现象:代码执行了INSERT,程序不报错,但打开 Navicat 查表,数据不在;或者程序重启后再查,数据还是不在。

原因:pymysql 默认开启了事务,但很多教材示例里只做了execute()没做commit()。MySQL 服务器端的事务还挂着,连接一关就被回滚了。

解决:检查自己的代码是否调用了conn.commit()。我踩过这个坑之后养成了一个习惯——所有写操作(INSERT/UPDATE/DELETE)之后强制检查两条:一是有没有commit(),二是commit()的位置在不在所有写操作之后。另外注意,conn.close()不会自动提交未完成的事务,它只会回滚并释放连接。

4.3 DictCursor 与元组游标混用

现象:设置了cursorclass=pymysql.cursors.DictCursor后,代码里还用row[0]访问字段,报错TypeError: tuple indices must be integers;或者没设置DictCursor,代码里用row['name']访问,报错TypeError: string indices must be integers。

原因:游标类型决定了结果集里每一行的数据结构。默认是元组,DictCursor是字典,两种访问方式不能混用。

解决:先确认连接参数里有没有cursorclass,再统一全项目的访问风格。我一般全程用DictCursor,因为字段名访问的可读性比下标好得多,而且后续如果改了 SELECT 的字段顺序,下标访问的代码会静默出错——row[0]可能从id变成了name,代码不报错但逻辑错了,这种 bug 最难查。

4.4 批量插入用错方式:性能差到怀疑人生

现象:往表里插入几千行数据,用 for 循环逐条execute(),跑了十几秒甚至几十秒;换成executemany()后秒完成。

原因:execute()逐条执行,每次都要经过一次完整的「SQL 解析-执行-返回」流程,还伴随着网络往返;executemany()在底层做了优化,批量发送,减少了解析次数和网络开销。

解决:批量操作一律用executemany()。课件里没有展开讲这个方法,但它是实际项目里最常见的性能优化手段之一。

data = [(i, f'user_{i}') for i in range(10000)] cur.executemany("INSERT INTO users (id, name) VALUES (%s, %s)", data) conn.commit()

注意executemany()的第二个参数是「可迭代的元组序列」,不要传成单个元组,否则会报参数数量不匹配。我之前翻车就是这样——忘了外面套一层列表,直接把(1, 'a')传进去,pymysql 把它当成两条数据去绑定参数,直接报错。

5. 往 SQLite 扩展:用 sqlite3 快速验证你的 CRUD 功底

课件后半部分讲了 SQLite,它是 Python 内置sqlite3模块直接支持的轻量级数据库——不需要安装服务器,不需要账号密码,数据就是磁盘上的一个.db文件。操作 MySQL 的整套思路(连接、建表、增删改查、事务)在 SQLite 上完全通用,非常适合拿来练手和验证代码逻辑。

import sqlite3 # 连接:如果文件不存在会自动创建 conn = sqlite3.connect('test.db') cur = conn.cursor() # 建表 cur.execute(''' CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ) ''') # 增 cur.execute("INSERT INTO user (id, name) VALUES (?, ?)", (1, 'mr')) # 改 cur.execute("UPDATE user SET name = ? WHERE id = ?", ('python', 1)) # 查 cur.execute("SELECT * FROM user") print(cur.fetchall()) # 删 cur.execute("DELETE FROM user WHERE id = ?", (1,)) conn.commit() cur.close() conn.close()

这段代码和 pymysql 版本有两点差异值得注意。第一,占位符从%s变成了?,这是sqlite3模块的规定;第二,不用传字符集和游标类型,SQLite 默认就是 UTF-8,返回的行默认是元组。如果你用惯了DictCursor,在这里想让行变成字典,需要额外指定conn.row_factory = sqlite3.Row,这样就能用字段名访问了,但要转成真正的字典还得套一层dict(row)。

我的实际建议是:用 SQLite 做练习,用 MySQL 跑真业务。在 SQLite 上把建表和增删改查的流程跑通,理解游标、提交、回滚这些概念,然后切换到 pymysql 时只需要换掉连接代码和占位符风格。这样的学习路径能把「数据库编程」的核心逻辑和「具体数据库的方言」分开,思维负担小很多。

验证代码是否写对,我有一个习惯沿用至今:每个 CRUD 操作写完,都用一条独立的查询去核对数据。插入后查一次,更新后查一次,删除后再查一次。不要只看代码不报错就认为操作成功了——不报错只代表语法没毛病,不代表数据状态符合预期。就拿UPDATE来说,如果WHERE条件没匹配到任何行,pymysql 不会报错,但execute()返回的受影响行数是 0,你完全可以通过这个返回值来判断操作是否真正生效。

从那以后,我每次写完数据库相关代码,都强制走一遍「连接参数确认 → 事务边界确认 → 结果集确认 → 资源关闭确认」这四步,已经成了肌肉记忆。这套思路帮我少加了不少班,尤其在生产环境出问题时,排查顺序清晰,不会像无头苍蝇一样乱试。这份课件虽然只有 25 页,但把数据库操作的主干都覆盖到了,按我上面的路线拆开吃透,入门足够了。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询