1. 项目缘起为什么用Python操作MySQL是必备技能最近在带几个刚入行的新人做项目发现一个挺普遍的现象很多人会用Python写业务逻辑也会用SQL在数据库客户端里查数据但一到需要用Python把这两者连接起来实现一个增删改查的自动化流程时就有点抓瞎了。要么是连接字符串写不对要么是SQL语句拼接出问题导致注入风险再不然就是事务处理不当数据一致性没法保证。这让我想起自己刚入门那会儿也是这么过来的。所以今天我就以一个老开发的身份把Python操作MySQL数据库——这个看似基础但细节巨多的活儿——从头到尾、掰开揉碎了讲一遍。这不是一个简单的API调用教程我会把这些年踩过的坑、总结的最佳实践以及那些官方文档里不会写的“潜规则”都分享出来。无论你是想快速上手完成课程设计还是希望在职场项目中写出更健壮的代码这篇文章都能给你直接的参考。说到Python操作数据库mysql-connector-python和PyMySQL是两个最主流的选择。网上教程很多但大多只告诉你怎么连接、怎么执行SQL很少深入讲连接池管理、异常处理、上下文管理这些真正影响生产代码稳定性的东西。我们这次就以PyMySQL为例因为它纯Python实现兼容性好安装简单生态也丰富。我会先带你快速搭建环境然后深入到四种核心操作查、增、改、删的每一个细节最后再聊聊事务、连接池和ORM这些进阶话题。保证你读完就能写出比大多数教程里更靠谱的数据库操作代码。2. 环境搭建与连接管理从安装到第一个连接工欲善其事必先利其器。在写代码之前我们得先把环境准备好。这里会涵盖从安装Python、MySQL到建立第一个稳健数据库连接的全过程并解释每一个选择背后的原因。2.1 Python与MySQL的安装与验证首先确保你有一个可用的Python环境。我强烈建议使用Python 3.8或更高版本因为很多现代库对低版本的支持已经逐步停止。你可以打开终端或命令提示符输入python --version或python3 --version来检查。如果没有安装去Python官网下载安装包记得勾选“Add Python to PATH”这个选项这是很多新手会忽略导致后续命令找不到的关键一步。接下来是MySQL。对于初学者我推荐直接安装MySQL Community Server它是完全免费的。在安装过程中它会提示你设置root用户的密码这个密码务必记住我们后续连接数据库要用。安装完成后你可以通过系统服务启动MySQL或者使用命令行mysql -u root -p来登录输入密码后能进入MySQL命令行客户端就说明安装成功了。注意如果你在安装MySQL时遇到问题或者只是想快速体验也可以考虑使用Docker来运行一个MySQL容器这样能避免很多本地环境冲突的问题。命令大致是docker run --name some-mysql -e MYSQL_ROOT_PASSWORDmy-secret-pw -d mysql:latest。2.2 驱动选择与PyMySQL库安装Python要连接MySQL需要一个驱动程序driver。PyMySQL是一个纯Python编写的MySQL客户端库这意味着它不需要像某些C扩展库那样编译在Windows、Linux、macOS上都能无缝安装兼容性极佳。安装它非常简单只需要一条pip命令pip install PyMySQL如果下载速度慢可以临时使用国内的镜像源比如清华源pip install PyMySQL -i https://pypi.tuna.tsinghua.edu.cn/simple。安装完成后我们可以在Python交互环境里验证一下import pymysql如果不报错就说明安装成功了。为什么选PyMySQL而不是mysql-connector除了纯Python的优势外PyMySQL的API设计与Python DB-API 2.0规范高度一致学会了它再学其他数据库如PostgreSQL的psycopg2会非常容易这是一种“一通百通”的投资。2.3 建立第一个稳健的数据库连接现在进入正题写代码连接数据库。很多教程会给你一个最简单的示例但那种代码是脆弱的无法用于实际项目。我们先来看一个基础但完整的连接示例import pymysql from pymysql.err import OperationalError def create_connection(): connection None try: connection pymysql.connect( hostlocalhost, # 数据库服务器地址 userroot, # 用户名 passwordyour_password_here, # 密码 databasetest_db, # 要连接的数据库名 port3306, # 端口默认3306 charsetutf8mb4, # 字符集非常重要 cursorclasspymysql.cursors.DictCursor # 让返回结果以字典形式呈现 ) print(连接成功) return connection except OperationalError as e: print(f连接数据库时发生错误: {e}) return None # 使用连接 conn create_connection() if conn: # ... 后续操作 conn.close() # 记住一定要关闭连接我来解释一下这几个关键参数host: 如果是连接本地的MySQL服务就是localhost或127.0.0.1。如果是远程服务器就填服务器的IP地址或域名。database: 直接指定要操作的数据库。你也可以不指定后续用USE database_name;SQL命令切换但在代码中直接指定更清晰。charsetutf8mb4: 这是重中之重。MySQL的utf8其实是阉割版的最大只支持3字节字符无法存储emoji表情或某些生僻字。utf8mb4才是完整的UTF-8编码。如果你建表时用的也是utf8mb4但连接时用了utf8就可能出现乱码。cursorclasspymysql.cursors.DictCursor: 这是一个非常实用的设置。默认情况下查询结果是一个元组列表你需要用数字索引来访问字段比如row[0]。设置为DictCursor后结果变成字典列表你可以用字段名来访问比如row[id]代码的可读性会大大提升。然而上面的代码还有一个明显的问题连接没有安全地关闭。如果后续操作发生异常conn.close()可能不会被执行导致连接泄露。所以我们应该使用try...except...finally语句块或者更Pythonic的方式——上下文管理器with语句。import pymysql def get_data(): # 使用 with 语句管理连接和游标 try: with pymysql.connect( hostlocalhost, userroot, passwordyour_password, databasetest_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) as connection: with connection.cursor() as cursor: sql SELECT id, name FROM users WHERE id %s cursor.execute(sql, (1,)) # 注意参数化查询的写法 result cursor.fetchall() for row in result: print(fID: {row[id]}, Name: {row[name]}) except pymysql.MySQLError as e: print(f数据库错误: {e}) # 调用函数 get_data()使用with语句后无论代码块是否发生异常在退出时连接和游标都会自动、正确地关闭资源管理变得非常省心。这是编写生产级代码的第一个好习惯。3. 核心操作一数据查询Read的艺术与陷阱查询是数据库操作中最频繁、也最可能出性能问题的部分。很多人觉得SELECT语句很简单但要把查询做得既安全又高效里面有不少门道。3.1 基础查询与结果集处理我们先从最基本的查询开始。假设我们有一个users表结构如下CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, email VARCHAR(100) UNIQUE, age INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );一个简单的查询所有用户的代码如下def get_all_users(): with get_connection() as conn: # 假设get_connection()是上面封装好的连接函数 with conn.cursor(pymysql.cursors.DictCursor) as cursor: sql SELECT id, name, email, age FROM users cursor.execute(sql) # 获取所有结果 users cursor.fetchall() for user in users: print(user) return users这里用到了fetchall()它会一次性将所有结果加载到内存中。如果表里有几万、几十万行数据这样做会消耗大量内存甚至导致程序崩溃。对于可能返回大量数据的查询我们应该使用fetchone()或fetchmany(size)。def get_users_in_batches(batch_size100): with get_connection() as conn: with conn.cursor(pymysql.cursors.DictCursor) as cursor: sql SELECT id, name FROM users cursor.execute(sql) while True: # 每次获取 batch_size 条记录 batch cursor.fetchmany(batch_size) if not batch: break for user in batch: process_user(user) # 处理每条记录 print(f已处理 {len(batch)} 条记录)fetchmany允许你控制每次从服务器取回的数据量非常适合数据导出、批量处理等场景。而fetchone则用在只需要一条结果的场景比如根据唯一键查询。3.2 参数化查询防止SQL注入的黄金法则这是安全领域的重中之重。永远不要使用字符串拼接的方式来构造SQL语句看一个反面教材# 危险SQL注入漏洞 user_id input(请输入用户ID: ) sql fSELECT * FROM users WHERE id {user_id} cursor.execute(sql)如果用户输入1 OR 11那么SQL就会变成SELECT * FROM users WHERE id 1 OR 11导致查询出所有用户数据。更危险的输入可能是1; DROP TABLE users; --后果不堪设想。正确的做法是使用参数化查询Parameterized Query。PyMySQL使用%s作为占位符无论参数是什么类型。# 安全参数化查询 user_id input(请输入用户ID: ) sql SELECT * FROM users WHERE id %s cursor.execute(sql, (user_id,)) # 参数以元组形式传入 # 多个参数也一样 name John min_age 20 sql SELECT * FROM users WHERE name %s AND age %s cursor.execute(sql, (name, min_age))数据库驱动会在底层对参数进行正确的转义和处理确保它们被当作数据而非SQL指令的一部分从而从根本上杜绝SQL注入。这是每个开发者必须养成的肌肉记忆。3.3 条件查询、排序、分页与模糊查询实际业务中查询很少是SELECT *。我们通常需要加上条件、排序和分页。def search_users(name_keywordNone, min_ageNone, page1, per_page10): 综合查询示例模糊搜索姓名年龄过滤结果分页 with get_connection() as conn: with conn.cursor(pymysql.cursors.DictCursor) as cursor: # 基础SQL sql FROM users WHERE 11 params [] # 动态添加条件 if name_keyword: sql AND name LIKE %s params.append(f%{name_keyword}%) # LIKE模糊查询 if min_age is not None: sql AND age %s params.append(min_age) # 首先获取符合条件的总记录数用于前端显示总页数 count_sql SELECT COUNT(*) as total sql cursor.execute(count_sql, params) total_count cursor.fetchone()[total] # 然后构建获取数据的SQL添加排序和分页 data_sql SELECT id, name, email, age, created_at sql data_sql ORDER BY created_at DESC # 按创建时间倒序 data_sql LIMIT %s OFFSET %s # 分页关键子句 # 计算分页偏移量 offset (page - 1) * per_page params.extend([per_page, offset]) # 将分页参数加入列表 cursor.execute(data_sql, params) users cursor.fetchall() return { total: total_count, page: page, per_page: per_page, data: users }这里有几个关键点WHERE 11技巧这是一个常见的SQL技巧为了让后续的AND条件能统一用添加避免判断第一个条件时是否需要写WHERE的复杂逻辑。模糊查询LIKE使用%通配符%keyword%表示包含关键字。注意参数化查询时通配符%是作为参数值的一部分传入的而不是写在SQL的%s占位符里。分页LIMIT和OFFSETLIMIT per_page OFFSET offset是实现分页的标准方式。OFFSET值过大时比如翻到很后面的页性能会急剧下降这是MySQL分页的经典问题。对于海量数据分页可以考虑使用“基于游标的分页”Cursor-based Pagination即用WHERE id last_id LIMIT per_page来代替。先查总数再查数据这是一个常见的业务需求。注意为了性能不要在一条SQL里既查数据又用SQL_CALC_FOUND_ROWSMySQL特性对于复杂查询分开查通常更清晰可控。4. 核心操作二数据插入Create的细节与批量优化插入数据看似简单但涉及到自增主键获取、批量插入性能、避免重复插入等实际问题。4.1 单条插入与获取自增ID向users表插入一条新记录def add_user(name, email, age): with get_connection() as conn: with conn.cursor() as cursor: sql INSERT INTO users (name, email, age) VALUES (%s, %s, %s) try: cursor.execute(sql, (name, email, age)) conn.commit() # 提交事务 print(插入成功) # 获取刚刚插入记录的自增ID new_id cursor.lastrowid print(f新用户的ID是: {new_id}) return new_id except pymysql.err.IntegrityError as e: # 处理唯一约束冲突比如email重复 if email in str(e): print(错误该邮箱已被注册。) conn.rollback() # 回滚事务 else: raise e关键点conn.commit(): 在PyMySQL中默认是自动提交autocommit关闭的这意味着执行INSERT、UPDATE、DELETE后必须显式调用conn.commit()更改才会真正持久化到数据库。这给了我们使用事务的机会。cursor.lastrowid: 这是一个非常实用的属性。在执行INSERT语句后它返回该连接下最后一条插入记录的自增主键值。这在插入后需要立即使用新ID的场景下非常方便。异常处理这里特别捕获了IntegrityError通常是由于违反了主键、唯一键约束。我们通过检查错误信息来判断是否是邮箱重复并给用户友好的提示同时执行rollback()回滚事务。4.2 批量插入大幅提升性能的关键如果需要插入大量数据比如导入一个CSV文件逐条执行INSERT语句的效率极低因为每次都要经历网络往返、SQL解析、事务处理等开销。正确的做法是使用executemany()方法。def batch_add_users(user_list): user_list: 一个包含多个用户字典的列表例如 [{name:A,email:aa.com,age:20}, ...] if not user_list: return with get_connection() as conn: with conn.cursor() as cursor: sql INSERT INTO users (name, email, age) VALUES (%s, %s, %s) # 准备参数列表 params [(user[name], user[email], user[age]) for user in user_list] try: # 使用 executemany affected_rows cursor.executemany(sql, params) conn.commit() print(f批量插入成功影响了 {affected_rows} 行。) except pymysql.err.IntegrityError as e: print(f批量插入失败可能存在重复数据: {e}) conn.rollback() except pymysql.err.DataError as e: print(f数据格式错误请检查字段长度或类型: {e}) conn.rollback()executemany()会将SQL语句和参数列表组合以更高效的方式发送给数据库服务器通常是一次网络通信多条数据。这比在循环中调用execute()要快几个数量级。注意虽然executemany很快但如果数据量巨大例如数十万条一次性插入仍可能造成大事务占用大量内存和日志空间。对于超大批量导入可以考虑分批次插入比如每次1000条或者在导入前临时关闭自动提交和索引导入后再重建。4.3 “插入或更新”操作ON DUPLICATE KEY UPDATE业务中常遇到这种需求如果记录不存在就插入如果存在通常根据唯一键判断就更新。一种笨办法是先查询再判断是INSERT还是UPDATE但这需要两次数据库交互且在高并发下可能产生竞态条件。MySQL提供了INSERT ... ON DUPLICATE KEY UPDATE语法能原子性地完成这个操作。假设email字段有唯一索引def insert_or_update_user(name, email, age): with get_connection() as conn: with conn.cursor() as cursor: sql INSERT INTO users (name, email, age) VALUES (%s, %s, %s) ON DUPLICATE KEY UPDATE name VALUES(name), age VALUES(age), updated_at CURRENT_TIMESTAMP # VALUES(column_name) 会引用INSERT语句中试图插入的值 cursor.execute(sql, (name, email, age)) conn.commit() if cursor.rowcount 2: print(记录已存在并执行了更新。) elif cursor.rowcount 1: print(插入了新记录。) # rowcount 对于此语句1表示插入2表示更新这个语句非常高效。cursor.rowcount的返回值在这里有特殊含义1表示插入了新行2表示更新了已存在的行。5. 核心操作三与四更新与删除的谨慎之道更新和删除操作是“危险”的因为它们会直接修改或销毁数据。因此编写相关代码时必须格外小心。5.1 数据更新Update与原子性保证更新操作的核心在于WHERE条件要精确否则可能误伤大量数据。def update_user_email(user_id, new_email): with get_connection() as conn: with conn.cursor() as cursor: # 先检查新邮箱是否已被他人使用业务逻辑检查 check_sql SELECT id FROM users WHERE email %s AND id ! %s cursor.execute(check_sql, (new_email, user_id)) if cursor.fetchone(): print(更新失败该邮箱已被其他用户使用。) return False # 执行更新 update_sql UPDATE users SET email %s WHERE id %s try: affected_rows cursor.execute(update_sql, (new_email, user_id)) conn.commit() if affected_rows 1: print(邮箱更新成功。) return True elif affected_rows 0: print(未找到该用户ID更新失败。) return False else: # 理论上id是主键affected_rows不应该大于1。如果发生说明逻辑有问题。 print(f警告更新了 {affected_rows} 行请检查数据一致性) conn.rollback() return False except pymysql.err.IntegrityError: print(更新失败违反唯一约束虽然已检查但并发情况下仍可能发生。) conn.rollback() return False这里展示了几个重要实践业务逻辑前置检查在更新前先检查新邮箱是否已被占用。这虽然不能完全避免并发问题需要靠数据库唯一约束兜底但能提前给用户友好提示。检查affected_rowscursor.execute()会返回受影响的行数。对于通过主键更新的语句这个值应该是1。如果是0说明没有找到对应的记录可能ID不存在。如果大于1那就要警惕了很可能WHERE条件写得不准确导致了批量误更新。并发安全即使做了前置检查在检查和更新之间仍可能有其他请求插队注册了同一个邮箱。所以最终的数据一致性必须依靠数据库的UNIQUE约束来保证。代码中捕获IntegrityError就是为了处理这种极端情况。5.2 数据删除Delete与软删除模式直接DELETE是物理删除数据不可恢复。在生产环境中对于重要业务数据我们通常采用“软删除”Soft Delete。物理删除示例慎用def delete_user_physically(user_id): # 通常需要极高的权限和审计 with get_connection() as conn: with conn.cursor() as cursor: # 极其重要的前置确认再次查询要删除的数据 select_sql SELECT * FROM users WHERE id %s FOR UPDATE cursor.execute(select_sql, (user_id,)) user_to_delete cursor.fetchone() if not user_to_delete: print(用户不存在。) return # 这里可以加入日志记录记录谁在什么时候删除了什么 log_deletion(user_to_delete) # 执行删除 delete_sql DELETE FROM users WHERE id %s affected_rows cursor.execute(delete_sql, (user_id,)) conn.commit() if affected_rows 1: print(用户已物理删除。) else: conn.rollback()软删除实现推荐软删除的本质是不真的删除数据而是通过一个标记字段如is_deleted、deleted_at来标识数据已删除。查询时默认加上WHERE is_deleted 0的条件。修改表结构添加标记字段ALTER TABLE users ADD COLUMN is_deleted TINYINT DEFAULT 0 COMMENT 0:未删除1:已删除; -- 或者添加删除时间戳 ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL DEFAULT NULL;执行软删除def delete_user_softly(user_id): with get_connection() as conn: with conn.cursor() as cursor: update_sql UPDATE users SET is_deleted 1, deleted_at NOW() WHERE id %s AND is_deleted 0 affected_rows cursor.execute(update_sql, (user_id,)) conn.commit() if affected_rows 1: print(用户已标记为删除软删除。) else: print(用户不存在或已被删除。)查询时排除已删除数据def get_active_users(): with get_connection() as conn: with conn.cursor(pymysql.cursors.DictCursor) as cursor: sql SELECT id, name FROM users WHERE is_deleted 0 # 关键在这里 cursor.execute(sql) return cursor.fetchall()软删除的好处显而易见数据可恢复有完整的操作历史避免了误操作带来的灾难性后果。代价是所有的查询语句都要记得加上软删除条件这可以通过在数据库建立视图View或者在ORM如SQLAlchemy中定义过滤器来统一管理。6. 进阶话题事务、连接池与ORM初探掌握了基本的CRUD之后我们需要关注更高级的话题以确保应用的健壮性和可维护性。6.1 事务处理保证数据一致性事务Transaction是数据库的核心特性它确保一系列操作要么全部成功要么全部失败。经典的例子是银行转账A账户扣钱和B账户加钱必须作为一个整体。def transfer_money(from_account_id, to_account_id, amount): 模拟转账业务 with get_connection() as conn: try: with conn.cursor() as cursor: # 1. 检查转出账户余额是否充足 check_sql SELECT balance FROM accounts WHERE id %s FOR UPDATE cursor.execute(check_sql, (from_account_id,)) from_balance cursor.fetchone() if not from_balance or from_balance[balance] amount: raise ValueError(转出账户余额不足或不存在。) # 2. 扣减转出账户余额 deduct_sql UPDATE accounts SET balance balance - %s WHERE id %s cursor.execute(deduct_sql, (amount, from_account_id)) # 3. 增加转入账户余额 add_sql UPDATE accounts SET balance balance %s WHERE id %s cursor.execute(add_sql, (amount, to_account_id)) # 4. 记录交易流水可选但很重要 log_sql INSERT INTO transactions (from_acc, to_acc, amount) VALUES (%s, %s, %s) cursor.execute(log_sql, (from_account_id, to_account_id, amount)) # 所有操作成功提交事务 conn.commit() print(转账成功) except Exception as e: # 任何一步出错回滚所有操作 conn.rollback() print(f转账失败已回滚: {e}) raise e关键点FOR UPDATE在查询余额时使用了FOR UPDATE这是悲观锁的一种。它会在事务期间锁定这行记录防止其他事务同时修改它避免出现“丢失更新”的并发问题。对于余额这种强一致性要求的场景通常需要加锁。conn.commit()和conn.rollback()它们控制事务的边界。在try块中所有操作执行成功后调用commit()使更改生效。如果在try块中发生任何异常except块会捕获并调用rollback()撤销该事务内所有未提交的更改。异常处理将业务逻辑检查如余额不足也通过抛出异常的方式触发回滚可以保证逻辑一致性。6.2 使用DBUtils管理数据库连接池在Web应用或高频后台任务中频繁地创建和关闭数据库连接开销很大。连接池Connection Pool可以预先创建并维护一组活跃的连接程序使用时从池中取用用完后归还避免了重复创建的开销。DBUtils是一个常用的数据库连接池工具包。安装pip install DBUtils。from dbutils.pooled_db import PooledDB import pymysql # 创建连接池 pool PooledDB( creatorpymysql, # 使用什么模块创建连接 maxconnections10, # 连接池允许的最大连接数 mincached2, # 初始化时创建的空闲连接数 maxcached5, # 池中空闲连接的最大数 blockingTrue, # 连接池耗尽时是否阻塞等待 hostlocalhost, userroot, passwordyour_password, databasetest_db, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor ) # 使用连接池 def get_user_from_pool(user_id): # 从池中获取一个连接 connection pool.connection() try: with connection.cursor() as cursor: sql SELECT * FROM users WHERE id %s cursor.execute(sql, (user_id,)) return cursor.fetchone() finally: # 不是关闭连接而是归还给连接池 connection.close() # 这里的close()被PooledDB重写为归还操作 # 在Web框架如Flask中通常会在应用启动时创建全局连接池 # 在每个请求开始时获取连接请求结束后归还。使用连接池后数据库连接的管理变得高效且自动化特别适合并发场景。6.3 从原生SQL到ORM以SQLAlchemy为例当项目变得复杂表结构繁多关系复杂时直接写原生SQL会变得难以维护。对象关系映射ORM工具应运而生它允许你用Python类来定义表用对象操作来代替SQL编写。SQLAlchemy是Python生态中最强大、最流行的ORM之一。安装pip install sqlalchemy pymysql一个简单的例子from sqlalchemy import create_engine, Column, Integer, String, DateTime from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from datetime import datetime # 定义基类 Base declarative_base() # 定义User模型对应users表 class User(Base): __tablename__ users id Column(Integer, primary_keyTrue, autoincrementTrue) name Column(String(100), nullableFalse) email Column(String(100), uniqueTrue, nullableFalse) age Column(Integer) created_at Column(DateTime, defaultdatetime.utcnow) is_deleted Column(Integer, default0) def __repr__(self): return fUser(name{self.name}, email{self.email}) # 创建数据库连接引擎 engine create_engine(mysqlpymysql://root:your_passwordlocalhost/test_db?charsetutf8mb4) # 创建所有表如果不存在 Base.metadata.create_all(engine) # 创建会话工厂 Session sessionmaker(bindengine) # 使用ORM进行CRUD def orm_crud_demo(): # 创建一个会话类似数据库连接 session Session() try: # 1. 创建Create new_user User(name李四, emaillisiexample.com, age25) session.add(new_user) session.flush() # 将数据刷到数据库获取id print(f新用户ID: {new_user.id}) # 2. 查询Read # 查询所有用户 users session.query(User).filter_by(is_deleted0).all() for u in users: print(u) # 条件查询 user session.query(User).filter(User.email lisiexample.com).first() if user: print(f找到用户: {user.name}) # 3. 更新Update if user: user.age 26 # session.add(user) # 对于已存在于session中的对象修改属性后会自动标记为脏数据无需显式add # 4. 删除Delete- 这里演示软删除 if user: user.is_deleted 1 # 提交事务 session.commit() print(所有操作已提交。) except Exception as e: session.rollback() print(f操作失败已回滚: {e}) raise e finally: session.close()ORM的优势在于代码更清晰用类和对象操作更符合Python哲学。避免SQL注入ORM内部使用参数化查询。便于重构修改表结构时通常只需修改模型类。关系处理方便定义表之间的关系一对一、一对多、多对多后ORM可以帮你轻松进行关联查询。但ORM也有代价学习成本、一定的性能开销、复杂查询可能不如原生SQL直观。对于大型项目通常是ORM和原生SQL混合使用简单操作用ORM复杂报表查询用原生SQL。从掌握原生SQL开始再学习ORM你会对底层有更深刻的理解也能更好地驾驭ORM。