ExecuteQuery 是一个强大的方法,它允许你执行一个 SQL 查询并直接将结果映射到自定义的对象集合中。核心优势在于它极大地简化了数据访问,并且通过参数化查询有效防止了 SQL 注入攻击。

(图片来源网络,侵删)
我们将重点讨论 参数化查询,这是传递参数最安全、最推荐的方式。
核心概念:为什么使用参数化查询?
- 安全性:直接拼接 SQL 字符串(
"SELECT * FROM Users WHERE Name = '" + userName + "'")是极其危险的。userName包含恶意代码(如' OR '1'='1),整个 SQL 逻辑就会被破坏,导致数据泄露(SQL 注入),参数化查询将 SQL 语句和数据(参数)分开处理,数据库引擎会将其视为纯粹的值,从而杜绝了注入风险。 - 性能:对于需要重复执行的查询(只是参数值不同),数据库可以缓存编译后的 SQL 计划,提高执行效率。
- 可读性和维护性:代码更清晰,SQL 语句结构一目了然,避免了复杂的字符串拼接。
参数化查询的通用语法
参数化查询在 SQL 语句中使用占位符来表示参数,不同的数据库和框架使用不同的占位符风格:
@paramName(SQL Server, C#):最常见的方式,使用 符号后跟参数名。- (OleDb, ODBC, Java JDBC):使用问号作为位置占位符,参数顺序必须和添加的顺序一致。
paramName(Oracle, SQLite):使用冒号后跟参数名。- 或 (MySql Connector/NET):支持两种风格,但推荐使用 。
在不同环境中的具体实现
下面我们以最常用的 @paramName 风格为例,展示在几种主流技术栈中的用法。
C# (ADO.NET)
这是最基础也是最经典的用法,通常使用 SqlCommand 和 SqlParameter。

(图片来源网络,侵删)
场景:查询指定 ID 的用户信息。
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
public class UserRepository
{
private readonly string _connectionString;
public UserRepository(string connectionString)
{
_connectionString = connectionString;
}
public User GetUserById(int userId)
{
// 1. 定义 SQL 查询,使用 @paramName 作为占位符
string sql = "SELECT Id, Name, Email FROM Users WHERE Id = @UserId AND IsActive = 1";
User user = null;
// 2. 使用 'using' 语句确保连接、命令和读取器被正确释放
using (SqlConnection connection = new SqlConnection(_connectionString))
using (SqlCommand command = new SqlCommand(sql, connection))
{
// 3. 添加参数
// - 第一个参数是 SQL 中的占位符名称
// - 第二个参数是要传入的值
command.Parameters.AddWithValue("@UserId", userId);
try
{
connection.Open();
using (SqlDataReader reader = command.ExecuteReader())
{
if (reader.Read())
{
user = new User
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Email = reader.GetString(2)
};
}
}
}
catch (Exception ex)
{
// 处理异常
Console.WriteLine($"数据库查询出错: {ex.Message}");
}
}
return user;
}
}
关键点:
command.Parameters.AddWithValue("@UserId", userId);是核心,它将变量userId的值安全地绑定到 SQL 语句的@UserId占位符上。AddWithValue方法会自动推断参数的数据类型,非常方便。
C# (Dapper - 一个轻量级 ORM)
Dapper 极大地简化了 ADO.NET 的操作,ExecuteQuery(在 Dapper 中通常叫 Query)是其核心功能之一。
场景:查询所有属于指定部门的员工。

(图片来源网络,侵删)
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using Dapper;
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int DepartmentId { get; set; }
}
public class EmployeeRepository
{
private readonly string _connectionString;
public EmployeeRepository(string connectionString)
{
_connectionString = connectionString;
}
public IEnumerable<Employee> GetEmployeesByDepartment(int departmentId)
{
// 1. 定义 SQL 查询
string sql = "SELECT EmployeeId, FirstName, LastName, DepartmentId FROM Employees WHERE DepartmentId = @DeptId";
// 2. 定义参数对象
// Dapper 会自动查找 SQL 中 @ 开头的属性名并与对象属性匹配
var parameters = new { DeptId = departmentId };
IEnumerable<Employee> employees = new List<Employee>();
// 3. 执行查询
using (IDbConnection connection = new SqlConnection(_connectionString))
{
try
{
// Query<T> 会自动将查询结果映射到 Employee 对象的集合中
employees = connection.Query<Employee>(sql, parameters);
}
catch (Exception ex)
{
Console.WriteLine($"Dapper 查询出错: {ex.Message}");
}
}
return employees;
}
}
关键点:
- 简洁:Dapper 的
Query<T>方法一行代码就完成了数据读取和对象映射。 - 匿名对象:
new { DeptId = departmentId }创建一个匿名对象,其属性名DeptId必须与 SQL 中的占位符@DeptId完全匹配(不区分大小写,但最好保持一致)。 - 强类型:查询结果被直接转换成
IEnumerable<Employee>集合,类型安全。
Java (JDBC)
Java 的 JDBC API 本身没有 ExecuteQuery,但 PreparedStatement 就是实现参数化查询的标准方式。
场景:根据用户名和密码查找用户。
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class UserDAO {
private final String jdbcUrl;
private final String jdbcUsername;
private final String jdbcPassword;
public UserDAO(String jdbcUrl, String jdbcUsername, String jdbcPassword) {
this.jdbcUrl = jdbcUrl;
this.jdbcUsername = jdbcUsername;
this.jdbcPassword = jdbcPassword;
}
public User findUser(String username, String password) throws SQLException {
// 1. 定义 SQL 查询,使用 ? 作为占位符
String sql = "SELECT id, username, email FROM users WHERE username = ? AND password = ?";
User user = null;
// 使用 try-with-resources 确保 Connection, PreparedStatement, ResultSet 被关闭
try (Connection conn = DriverManager.getConnection(jdbcUrl, jdbcUsername, jdbcPassword);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 2. 设置参数 (索引从 1 开始)
pstmt.setString(1, username);
pstmt.setString(2, password);
// 3. 执行查询
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
}
}
}
return user;
}
}
// 假设的 User 类
class User {
private int id;
private String username;
private String email;
// getters and setters...
}
关键点:
PreparedStatement是关键,它预编译了 SQL 语句。pstmt.setString(1, username);的第一个参数是占位符的位置(从 1 开始),第二个参数是值。- 必须按顺序设置所有参数。
Python (PyMySQL / sqlite3)
在 Python 中,使用数据库驱动(如 PyMySQL 或内置的 sqlite3)的 execute 方法。
场景:查找所有价格高于某个值的商品。
import sqlite3
# 假设我们有一个 SQLite 数据库
def find_products_above_price(min_price):
products = []
# 1. 定义 SQL 查询,使用 ? 作为占位符
sql = "SELECT id, name, price FROM products WHERE price > ?"
# 2. 使用 try-finally 或 with 语句确保连接关闭
try:
# with 语句会自动处理连接的关闭
with sqlite3.connect('my_database.db') as conn:
conn.row_factory = sqlite3.Row # 允许通过列名访问
cursor = conn.cursor()
# 3. 执行查询,参数作为元组传递
# 注意:即使只有一个参数,也必须写成 (value,) 的形式
cursor.execute(sql, (min_price,))
# 4. 获取所有结果
for row in cursor.fetchall():
products.append({
'id': row['id'],
'name': row['name'],
'price': row['price']
})
except sqlite3.Error as e:
print(f"数据库错误: {e}")
return products
# 示例调用
expensive_products = find_products_above_price(100.0)
for product in expensive_products:
print(product)
关键点:
cursor.execute(sql, (min_price,))是核心,参数必须作为一个元组传递。- 特别注意:如果只有一个参数,元组中必须有一个逗号,即
(value,),否则 Python 会将其视为单个值而不是元组。
总结与最佳实践
| 技术 | 核心类/方法 | 参数占位符 | 参数传递方式 |
|---|---|---|---|
| C# (ADO.NET) | SqlCommand + Parameters.AddWithValue |
@ParamName |
command.Parameters.AddWithValue("@Param", value); |
| C# (Dapper) | IDbConnection.Query |
@ParamName |
匿名对象 new { Param = value } |
| Java (JDBC) | PreparedStatement |
pstmt.setX(index, value); (索引从1开始) |
|
| Python (sqlite3) | cursor.execute |
元组 cursor.execute(sql, (value,)) |
黄金法则:
- 永远不要使用字符串拼接来构建 SQL 查询,除非你 100% 确保输入数据是绝对安全的(硬编码的数字)。
- 始终优先使用参数化查询,这是防御 SQL 注入攻击最有效的方法。
- 保持代码简洁:在 C# 中,优先考虑使用 Dapper 等 ORM 库;在 Java 中,考虑 MyBatis 或 JPA;在 Python 中,考虑 SQLAlchemy,它们能让你用更少的代码完成更复杂的数据库操作。
