第五章:数据访问 (Data Access) 第五章:数据访问 (Data Access) 5.1 数据访问的重要性与概述 任何现代Web应用程序几乎都离不开数据的存储和检索。数据是应用程序的生命线,它驱动着用户界面,支撑着业务逻辑,并最终为用户提供价值。在ASP.NET应用程序中,数据访问层负责应用程序与数据存储之间的所有交互,它扮演着至关重要的角色,主要体现在以下几个方面: 数据持久化: 将应用程序运行时产生的数据(例如用户输入、业务状态等)存储到持久化存储介质中,如数据库、文件系统等,以便在应用程序重启或用户会话结束后数据仍然能够保存。 数据检索与展示: 从数据存储中读取数据,并将其转换为应用程序可以处理和展示的格式,例如在网页上显示用户信息、产品列表等。
任何现代Web应用程序几乎都离不开数据的存储和检索。数据是应用程序的生命线,它驱动着用户界面,支撑着业务逻辑,并最终为用户提供价值。在ASP.NET应用程序中,数据访问层负责应用程序与数据存储之间的所有交互,它扮演着至关重要的角色,主要体现在以下几个方面:
数据持久化: 将应用程序运行时产生的数据(例如用户输入、业务状态等)存储到持久化存储介质中,如数据库、文件系统等,以便在应用程序重启或用户会话结束后数据仍然能够保存。
数据检索与展示: 从数据存储中读取数据,并将其转换为应用程序可以处理和展示的格式,例如在网页上显示用户信息、产品列表等。
数据操作与业务逻辑: 数据访问层不仅负责数据的CRUD(创建、读取、更新、删除)操作,还可以封装一部分业务逻辑,例如数据验证、数据转换等,从而提高代码的可维护性和复用性。
数据安全性: 数据访问层需要考虑数据的安全性,例如防止SQL注入、数据泄露等安全风险,确保数据在传输和存储过程中的安全。
性能优化: 高效的数据访问是应用程序性能的关键。数据访问层需要考虑性能优化策略,例如查询优化、缓存机制、连接池管理等,以提高应用程序的响应速度和吞吐量。
在ASP.NET中,我们可以使用多种技术来实现数据访问,主要包括:
ADO.NET (ActiveX Data Objects .NET): .NET Framework提供的基础数据访问技术,允许开发者直接与各种数据源进行交互。它提供了底层的API,具有灵活性和性能优势,但需要开发者编写更多的代码来处理数据访问的细节。
Entity Framework (EF) Core: 轻量级、可扩展、开源和跨平台的对象关系映射 (ORM) 框架,是ADO.NET的进化。EF Core 允许开发者使用.NET 对象来操作数据库,而无需编写大量的SQL代码,大大提高了开发效率。
Dapper: 轻量级的ORM框架,被称为“Micro-ORM”,它提供了接近ADO.NET的性能,同时又具备ORM的便利性,适合对性能有较高要求的场景。
NoSQL 数据库驱动: 对于需要处理非结构化数据的应用程序,ASP.NET也支持与各种NoSQL数据库(如MongoDB、Redis、Cosmos DB等)进行集成。
本章将重点介绍ADO.NET和Entity Framework Core,并简要提及其他相关技术。
ADO.NET是.NET Framework提供的用于访问数据源的一组类库。它允许开发者使用统一的方式访问各种不同的数据源,例如SQL Server、Oracle、MySQL、Access等。ADO.NET的核心组件包括:
Connection (连接): 用于建立与数据源的连接。不同的数据源有不同的Connection对象,例如SqlConnection用于连接SQL Server,MySqlConnection用于连接MySQL。
Command (命令): 用于执行SQL语句或存储过程。Command对象与Connection对象关联,并可以执行查询、插入、更新、删除等操作。
DataReader (数据读取器): 用于以只读、只进的方式从数据源中读取数据流。DataReader性能很高,适用于快速读取大量数据。
DataAdapter (数据适配器): 用于在DataSet和数据源之间进行数据交换。DataAdapter可以填充DataSet,也可以将DataSet中的数据更新到数据源。
DataSet (数据集): 一个内存中的数据缓存,可以包含多个DataTable,DataTable又可以包含多个DataRow和DataColumn。DataSet可以断开连接操作数据,适用于需要离线操作数据的场景。
以下代码示例演示了如何使用ADO.NET连接到SQL Server数据库,执行查询并使用DataReader读取数据:
using System.Data.SqlClient; public class DataAccessExample { public static void ReadDataFromDatabase() { string connectionString = "Server=localhost;Database=YourDatabaseName;User Id=YourUsername;Password=YourPassword;"; // 替换为你的连接字符串 using (SqlConnection connection = new SqlConnection(connectionString)) // 创建SqlConnection对象,using确保连接在使用完毕后被正确释放 { try { connection.Open(); // 打开数据库连接 Console.WriteLine("数据库连接已打开"); string sqlQuery = "SELECT ProductID, ProductName, UnitPrice FROM Products"; // 定义SQL查询语句 SqlCommand command = new SqlCommand(sqlQuery, connection); // 创建SqlCommand对象,关联连接和查询语句 using (SqlDataReader reader = command.ExecuteReader()) // 执行查询,获取SqlDataReader对象 { Console.WriteLine("产品数据:"); while (reader.Read()) // 循环读取DataReader中的每一行数据 { Console.WriteLine($"ID: {reader["ProductID"]}, Name: {reader["ProductName"]}, Price: {reader["UnitPrice"]}"); // 通过列名或索引访问数据 } } } catch (Exception ex) { Console.WriteLine($"发生错误: {ex.Message}"); // 异常处理 } finally { if (connection.State == System.Data.ConnectionState.Open) { connection.Close(); // 显式关闭数据库连接 (using 语句已经处理,这里作为示例) Console.WriteLine("数据库连接已关闭"); } } } } }
代码详解:
using System.Data.SqlClient;: 导入 System.Data.SqlClient 命名空间,该命名空间包含了用于连接SQL Server的类。
string connectionString = ...;: 定义数据库连接字符串。你需要根据你的数据库配置修改连接字符串中的服务器地址、数据库名称、用户名和密码。请务必妥善保管连接字符串,避免硬编码在代码中,推荐使用配置文件或密钥管理工具。
using (SqlConnection connection = new SqlConnection(connectionString)): 创建 SqlConnection 对象,并使用 using 语句。using 语句确保 SqlConnection 对象在使用完毕后会被自动释放,即使发生异常也能保证连接被关闭,避免资源泄漏。
connection.Open();: 打开数据库连接。这是与数据库建立物理连接的过程。
string sqlQuery = "SELECT ProductID, ProductName, UnitPrice FROM Products";: 定义SQL查询语句,这里查询了Products表的ProductID, ProductName, UnitPrice三列。
SqlCommand command = new SqlCommand(sqlQuery, connection);: 创建 SqlCommand 对象,将SQL查询语句和数据库连接关联起来。SqlCommand 对象负责执行SQL命令。
using (SqlDataReader reader = command.ExecuteReader()): 执行查询语句,并返回 SqlDataReader 对象。ExecuteReader() 方法用于执行返回多行数据的查询语句。同样使用 using 语句确保 SqlDataReader 对象在使用完毕后被正确释放。
while (reader.Read()): 循环读取 SqlDataReader 中的每一行数据。reader.Read() 方法读取下一行数据,如果成功读取到数据则返回 true,否则返回 false。
Console.WriteLine($"ID: {reader["ProductID"]}, Name: {reader["ProductName"]}, Price: {reader["UnitPrice"]}");: 在循环中,通过列名 (reader["ProductID"]) 或列索引 (reader[0]) 访问当前行的数据。
catch (Exception ex): 捕获可能发生的异常,例如数据库连接错误、SQL语句错误等,并输出错误信息。
finally: finally 块中的代码无论是否发生异常都会被执行。这里显式关闭数据库连接(虽然 using 语句已经处理了连接的释放,但作为示例展示)。
除了数据读取,ADO.NET也支持数据的插入、更新和删除操作。以下示例演示了如何使用SqlCommand执行插入、更新和删除操作:
using System.Data.SqlClient; public class DataManipulationExample { public static void InsertData() { string connectionString = "Server=localhost;Database=YourDatabaseName;User Id=YourUsername;Password=YourPassword;"; using (SqlConnection connection = new SqlConnection(connectionString)) { try { connection.Open(); string insertSql = "INSERT INTO Products (ProductName, UnitPrice) VALUES (@ProductName, @UnitPrice)"; // 使用参数化查询,防止SQL注入 SqlCommand insertCommand = new SqlCommand(insertSql, connection); insertCommand.Parameters.AddWithValue("@ProductName", "New Product"); // 添加参数 insertCommand.Parameters.AddWithValue("@UnitPrice", 19.99); int rowsAffected = insertCommand.ExecuteNonQuery(); // 执行非查询语句,返回受影响的行数 Console.WriteLine($"{rowsAffected} 行数据被插入"); } catch (Exception ex) { Console.WriteLine($"插入数据错误: {ex.Message}"); } } } public static void UpdateData(int productId, decimal newPrice) { string connectionString = "Server=localhost;Database=YourDatabaseName;User Id=YourUsername;Password=YourPassword;"; using (SqlConnection connection = new SqlConnection(connectionString)) { try { connection.Open(); string updateSql = "UPDATE Products SET UnitPrice = @UnitPrice WHERE ProductID = @ProductID"; SqlCommand updateCommand = new SqlCommand(updateSql, connection); updateCommand.Parameters.AddWithValue("@UnitPrice", newPrice); updateCommand.Parameters.AddWithValue("@ProductID", productId); int rowsAffected = updateCommand.ExecuteNonQuery(); Console.WriteLine($"{rowsAffected} 行数据被更新"); } catch (Exception ex) { Console.WriteLine($"更新数据错误: {ex.Message}"); } } } public static void DeleteData(int productId) { string connectionString = "Server=localhost;Database=YourDatabaseName;User Id=YourUsername;Password=YourPassword;"; using (SqlConnection connection = new SqlConnection(connectionString)) { try { connection.Open(); string deleteSql = "DELETE FROM Products WHERE ProductID = @ProductID"; SqlCommand deleteCommand = new SqlCommand(deleteSql, connection); deleteCommand.Parameters.AddWithValue("@ProductID", productId); int rowsAffected = deleteCommand.ExecuteNonQuery(); Console.WriteLine($"{rowsAffected} 行数据被删除"); } catch (Exception ex) { Console.WriteLine($"删除数据错误: {ex.Message}"); } } } }
代码详解:
ExecuteNonQuery() 方法: 用于执行 INSERT, UPDATE, DELETE 等不返回结果集的SQL语句。它返回受影响的行数。
参数化查询 (@ProductName, @UnitPrice, @ProductID): 在SQL语句中使用参数占位符,并通过 command.Parameters.AddWithValue() 方法为参数赋值。参数化查询是防止SQL注入攻击的关键措施。 它将SQL语句和参数值分开处理,避免恶意用户通过构造恶意的SQL注入代码。
Entity Framework Core (EF Core) 是一个现代的、轻量级的、可扩展的ORM框架,用于.NET应用程序。ORM (Object-Relational Mapping) 框架的主要作用是在对象模型和关系数据库之间建立映射关系,允许开发者使用面向对象的方式操作数据库,而无需编写大量的SQL代码。
架构组件详解:
DbContext: EF Core 的核心类,代表与数据库的会话。它管理数据库连接、事务、实体状态跟踪等。DbContext 派生类通常包含 DbSet<T> 属性,用于访问数据库表。
DbSet: 代表数据库中的一个表或视图。DbSet<T> 是一个泛型类,T 是实体类型。通过 DbSet<T> 可以进行CRUD操作和查询操作。
实体 (Entity): 表示应用程序中的业务对象,通常映射到数据库表。实体类是普通的C#类 (POCO - Plain Old CLR Object)。
模型 (Model): 描述应用程序的实体类型和它们之间的关系,以及如何映射到数据库。模型可以通过约定 (Conventions)、数据注解 (Data Annotations) 或 Fluent API 进行配置。
Change Tracker (变更跟踪器): DbContext 的组件,负责跟踪实体的状态变化 (Added, Modified, Deleted, Unchanged)。当调用 SaveChanges() 方法时,Change Tracker 会根据实体的状态变化生成相应的SQL语句并提交到数据库。
LINQ to Entities Provider (LINQ to Entities 提供程序): 将LINQ查询转换为数据库可以理解的SQL查询。
Query Provider (查询提供程序): 负责执行LINQ查询并返回结果。
Database Provider (数据库提供程序): EF Core 的插件,用于支持不同的数据库系统。例如,SQL Server Provider 用于连接SQL Server,MySQL Provider 用于连接MySQL。
EF Core 支持多种模型创建方式,其中 Code-First 模式是最常用的方式。Code-First 模式允许开发者先编写实体类,然后通过迁移 (Migrations) 功能自动创建数据库和表结构。
1. 定义实体类 (Entity Class):
public class Product { public int ProductID { get; set; } // 主键,约定为 "ClassNameID" 或 "ID" public string ProductName { get; set; } public decimal UnitPrice { get; set; } }
2. 创建 DbContext 类:
using Microsoft.EntityFrameworkCore; public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Product> Products { get; set; } // DbSet<Product> 代表 Products 表 protected override void OnModelCreating(ModelBuilder modelBuilder) { // 可选:使用 Fluent API 进行模型配置 // modelBuilder.Entity<Product>().ToTable("Tbl_Products"); // 修改表名 // modelBuilder.Entity<Product>().Property(p => p.ProductName).HasMaxLength(100).IsRequired(); // 配置属性约束 } }
3. 配置 DbContext (Startup.cs 或 Program.cs):
// Startup.cs (ASP.NET Core) 或 Program.cs (.NET 6+) public void ConfigureServices(IServiceCollection services) // 或 Program.cs 中的 builder.Services { // ... 其他服务配置 services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); // 使用 SQL Server 数据库,从配置文件读取连接字符串 }
4. 添加和应用迁移 (Migrations):
在 Package Manager Console 或终端中执行以下命令:
Add-Migration InitialCreate // 添加迁移,名称为 "InitialCreate" Update-Database // 应用迁移,创建数据库和表结构
5. CRUD 操作示例 (Controller 或 Service):
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Threading.Tasks; [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly ApplicationDbContext _context; public ProductsController(ApplicationDbContext context) { _context = context; } [HttpGet] public async Task<ActionResult<IEnumerable<Product>>> GetProducts() { return await _context.Products.ToListAsync(); // 读取所有产品 } [HttpGet("{id}")] public async Task<ActionResult<Product>> GetProduct(int id) { var product = await _context.Products.FindAsync(id); // 根据 ID 读取产品 if (product == null) { return NotFound(); } return product; } [HttpPost] public async Task<ActionResult<Product>> CreateProduct(Product product) { _context.Products.Add(product); // 添加实体到 DbContext await _context.SaveChangesAsync(); // 保存更改到数据库,执行 INSERT 操作 return CreatedAtAction(nameof(GetProduct), new { id = product.ProductID }, product); } [HttpPut("{id}")] public async Task<IActionResult> UpdateProduct(int id, Product product) { if (id != product.ProductID) { return BadRequest(); } _context.Entry(product).State = EntityState.Modified; // 标记实体为 Modified 状态 try { await _context.SaveChangesAsync(); // 保存更改到数据库,执行 UPDATE 操作 } catch (DbUpdateConcurrencyException) { if (!ProductExists(id)) { return NotFound(); } else { throw; } } return NoContent(); } [HttpDelete("{id}")] public async Task<IActionResult> DeleteProduct(int id) { var product = await _context.Products.FindAsync(id); if (product == null) { return NotFound(); } _context.Products.Remove(product); // 移除实体 await _context.SaveChangesAsync(); // 保存更改到数据库,执行 DELETE 操作 return NoContent(); } private bool ProductExists(int id) { return _context.Products.Any(e => e.ProductID == id); } }
代码详解:
ApplicationDbContext: 继承自 DbContext 的类,作为应用程序的数据库上下文。
DbSet<Product> Products: DbSet<Product> 属性允许通过 _context.Products 访问 Products 表。
ToListAsync(), FindAsync(), Add(), Remove(), SaveChanges(): EF Core 提供的用于查询和操作数据库的方法,例如 ToListAsync() 用于异步获取所有数据,FindAsync() 用于根据主键查找数据,Add() 用于添加实体,Remove() 用于删除实体,SaveChangesAsync() 用于将所有更改保存到数据库。
_context.Entry(product).State = EntityState.Modified;: 在更新操作中,需要显式标记实体的状态为 Modified,以便 EF Core 知道需要更新该实体。
DbUpdateConcurrencyException: 处理并发更新异常。
除了 ADO.NET 和 Entity Framework Core,ASP.NET 开发中还有其他一些数据访问技术:
Dapper: 一个轻量级的ORM框架,性能接近ADO.NET,但提供了ORM的便利性,例如对象映射。Dapper 扩展了 IDbConnection 接口,可以使用 connection.Query<T>(), connection.Execute() 等方法执行查询和命令。
NoSQL 数据库驱动: ASP.NET 应用可以集成各种 NoSQL 数据库,例如 MongoDB, Redis, Cosmos DB 等。这些数据库通常使用特定的驱动程序进行访问,例如 MongoDB C# Driver, StackExchange.Redis 等。NoSQL 数据库适用于处理非结构化数据、高并发、大数据量等场景。
连接字符串管理: 避免硬编码连接字符串在代码中。将连接字符串存储在配置文件 (e.g., appsettings.json) 或密钥管理工具 (e.g., Azure Key Vault) 中,并使用配置系统读取。
参数化查询: 始终使用参数化查询或存储过程来防止 SQL 注入攻击。
异常处理: 合理处理数据访问过程中可能发生的异常,例如数据库连接错误、SQL 语句错误、并发冲突等。使用 try-catch-finally 块进行异常处理,并记录错误日志。
事务管理: 对于需要保证数据一致性的操作,使用事务来包装多个数据库操作。ADO.NET 和 EF Core 都提供了事务管理功能。
性能优化:
查询优化: 编写高效的 SQL 查询,避免全表扫描,合理使用索引。
缓存: 对于频繁访问且不经常变化的数据,可以使用缓存 (例如内存缓存、分布式缓存) 来减少数据库访问次数。
连接池: ADO.NET 和 EF Core 默认使用连接池,可以重用数据库连接,减少连接建立的开销。
异步操作: 在ASP.NET Core 应用中,尽可能使用异步数据访问方法 (ToListAsync(), SaveChangesAsync(), ExecuteReaderAsync() 等) 避免阻塞线程,提高应用程序的响应性。
数据验证: 在数据访问层或业务逻辑层进行数据验证,确保数据的有效性和完整性。可以使用数据注解或 Fluent Validation 等工具进行数据验证。
代码组织: 将数据访问代码封装到单独的数据访问层或仓储 (Repository) 模式中,提高代码的可维护性和可测试性。
数据访问是ASP.NET应用程序开发的关键组成部分。本章详细介绍了ADO.NET和Entity Framework Core这两种主流的数据访问技术,并提供了代码示例和最佳实践。选择合适的数据访问技术取决于应用程序的具体需求,例如性能要求、开发效率、数据模型的复杂程度等。掌握数据访问技术,并遵循最佳实践,可以帮助开发者构建高效、安全、可维护的ASP.NET应用程序。