
零基础入门Python16MySQL安装、账号权限与数据迁移本篇图解数据从哪里来、到哪里去图中最重要的边界是事务只有成功路径才提交数据失败路径不能留下半条业务记录。一、上一篇课后练习讲解取消订单必须在同一事务中读取明细、恢复库存和修改状态defcancel_order(connection,order_id):try:connection.execute(BEGIN)orderconnection.execute(SELECT status FROM orders WHERE id ?,(order_id,),).fetchone()iforderisNone:raiseValueError(订单不存在)iforder[status]cancelled:raiseValueError(订单已经取消)itemsconnection.execute(SELECT product_id, quantity FROM order_items WHERE order_id ?,(order_id,),).fetchall()foriteminitems:connection.execute(UPDATE products SET stockstock? WHERE id?,(item[quantity],item[product_id]),)connection.execute(UPDATE orders SET statuscancelled WHERE id?,(order_id,),)connection.commit()exceptException:connection.rollback()raise重复取消被拒绝防止库存重复增加。生产并发环境还需要行锁Django项目会实际使用select_for_update。可执行验收答案将取消逻辑包进测试数据库并检查回滚deftest_cancel_success_restores_stock(db):ordercreate_order(db,statuspaid,items[(1,2)])beforedb.scalar(select stock from products where id1)cancel_order(db,order.id)afterdb.scalar(select stock from products where id1)assertafterbefore2try:cancel_order(db,order.id)exceptValueErrorasexc:assert已经取消instr(exc)assertdb.scalar(select stock from products where id1)after再把UPDATE products改成错误的 product id人为触发异常事务结束后订单仍为 paid、库存未增加。SQLite 测试用db.rollback()清理MySQL/Django 集成测试还要确认连接隔离级别和行锁行为。二、本篇成果在Windows或Docker中运行MySQL 8创建utf8mb4数据库和最小权限账号理解服务、客户端、数据库和连接字符串并把SQLite表结构迁移到MySQL。三、启动MySQL使用已安装MySQL服务时mysql--version mysql-uroot-p也可以用Dockerdocker run--name course-mysql-e MYSQL_ROOT_PASSWORDchange-root-password-p 3306:3306-d mysql:8.4等待容器启动后docker exec-it course-mysql mysql-uroot-proot只用于管理不给业务程序长期使用。四、字符集和业务账号CREATEDATABASEbackend_courseCHARACTERSETutf8mb4COLLATEutf8mb4_0900_ai_ci;CREATEUSERcourse%IDENTIFIEDBYreplace-with-strong-password;GRANTSELECT,INSERT,UPDATE,DELETE,CREATE,ALTER,INDEXONbackend_course.*TOcourse%;FLUSHPRIVILEGES;SHOWGRANTSFORcourse%;utf8mb4能保存完整Unicode。生产环境应把来源限制为应用服务器IP教学容器使用%便于连接。五、SQLite与MySQL差异SQLite是单文件嵌入式数据库MySQL是独立服务SQLite占位符常用问号MySQL驱动常用%sMySQL支持账号权限、并发和运维能力。不要把SQLite文件直接复制给MySQL迁移包含重建结构和导入数据。金额使用DECIMAL而不是FLOAT避免二进制小数误差。InnoDB支持事务和行级锁。六、MySQL表结构USEbackend_course;CREATETABLEproducts(idBIGINTPRIMARYKEYAUTO_INCREMENT,nameVARCHAR(120)NOTNULL,priceDECIMAL(10,2)NOTNULL,stockINTUNSIGNEDNOTNULLDEFAULT0,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,CONSTRAINTchk_products_priceCHECK(price0),INDEXidx_products_name(name))ENGINEInnoDB;CREATETABLEorders(idBIGINTPRIMARYKEYAUTO_INCREMENT,user_idBIGINTNOTNULL,statusVARCHAR(20)NOTNULLDEFAULTpending,total_amountDECIMAL(12,2)NOTNULLDEFAULT0,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP,INDEXidx_orders_user_created(user_id,created_at))ENGINEInnoDB;七、环境变量连接.envDATABASE_URLmysqlpymysql://course:password127.0.0.1:3306/backend_course?charsetutf8mb4.env必须加入.gitignore。安装驱动python-m pip install pymysql最小连接检查importosimportpymysql connectionpymysql.connect(hostos.getenv(MYSQL_HOST,127.0.0.1),portint(os.getenv(MYSQL_PORT,3306)),useros.environ[MYSQL_USER],passwordos.environ[MYSQL_PASSWORD],databaseos.getenv(MYSQL_DATABASE,backend_course),charsetutf8mb4,)try:withconnection.cursor()ascursor:cursor.execute(SELECT VERSION())print(MySQL版本,cursor.fetchone()[0])finally:connection.close()八、迁移核对清单在MySQL执行建表脚本从SQLite按确定字段导出JSON或CSV事务中参数化插入MySQL比较每张表行数抽查金额、中文、日期和关联应用先连接测试库再连接生产库。不要只看导入命令成功必须验证数据数量和内容。九、本篇验收MySQL 8服务可连接backend_course使用utf8mb4course账号不能管理其他用户表使用InnoDB和DECIMAL密码来自环境变量能解释迁移不等于复制SQLite文件。十、课后练习创建users、products、orders和order_items完整表结构给外键和常用查询建立索引使用course账号验证CRUD成功再尝试CREATE USER并确认被拒绝。下一篇会用SQLAlchemy统一SQLite和MySQL的Python访问方式。实战补充数据库连接配置实验开发环境使用 SQLite部署环境通常使用 PostgreSQL。连接串必须从环境变量读取代码不应该根据 if production 复制两套 Repository。importosfromsqlalchemyimportcreate_engine database_urlos.environ.get(DATABASE_URL,sqlite:///./dev.db)enginecreate_engine(database_url,pool_pre_pingTrue)测试时传入临时 SQLite 文件CI 用服务容器提供 PostgreSQL同一个模型和业务测试在两种数据库上运行差异通过测试暴露而不是靠猜。验收与课后练习分别设置 DATABASE_URL 运行健康检查故意删除环境变量时得到明确错误。课后记录 SQLite 和 PostgreSQL 在日期、布尔、唯一约束上的差异下一篇实现 Repository。本篇结束完整模块文件本节不是代码片段而是本篇结束时该模块的完整版本。请先备份旧文件再整体替换替换后重新运行本篇命令和测试。阅读时重点看本篇新增的函数、事务边界和错误处理未涉及的代码先不要自行删减。本篇完整示例CREATE DATABASE backend_course CHARACTER SET utf8mb4; CREATE USER courselocalhost IDENTIFIED BY change-me; GRANT ALL ON backend_course.* TO courselocalhost;