第五章:数据访问 (Data Access)


文档摘要

第五章:数据访问 (Data Access) 第五章:数据访问 (Data Access) 5.1 数据访问的重要性与概述 任何现代Web应用程序几乎都离不开数据的存储和检索。数据是应用程序的生命线,它驱动着用户界面,支撑着业务逻辑,并最终为用户提供价值。在ASP.NET应用程序中,数据访问层负责应用程序与数据存储之间的所有交互,它扮演着至关重要的角色,主要体现在以下几个方面: 数据持久化: 将应用程序运行时产生的数据(例如用户输入、业务状态等)存储到持久化存储介质中,如数据库、文件系统等,以便在应用程序重启或用户会话结束后数据仍然能够保存。 数据检索与展示: 从数据存储中读取数据,并将其转换为应用程序可以处理和展示的格式,例如在网页上显示用户信息、产品列表等。

第五章:数据访问 (Data Access)

第五章:数据访问 (Data Access)

5.1 数据访问的重要性与概述

任何现代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,并简要提及其他相关技术。

5.2 ADO.NET:基础数据访问技术详解

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可以断开连接操作数据,适用于需要离线操作数据的场景。

5.2.1 ADO.NET 代码实践:连接、命令与数据读取

以下代码示例演示了如何使用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("数据库连接已关闭"); } } } } }

代码详解:

  1. using System.Data.SqlClient;: 导入 System.Data.SqlClient 命名空间,该命名空间包含了用于连接SQL Server的类。

  2. string connectionString = ...;: 定义数据库连接字符串。你需要根据你的数据库配置修改连接字符串中的服务器地址、数据库名称、用户名和密码。请务必妥善保管连接字符串,避免硬编码在代码中,推荐使用配置文件或密钥管理工具。

  3. using (SqlConnection connection = new SqlConnection(connectionString)): 创建 SqlConnection 对象,并使用 using 语句。using 语句确保 SqlConnection 对象在使用完毕后会被自动释放,即使发生异常也能保证连接被关闭,避免资源泄漏。

  4. connection.Open();: 打开数据库连接。这是与数据库建立物理连接的过程。

  5. string sqlQuery = "SELECT ProductID, ProductName, UnitPrice FROM Products";: 定义SQL查询语句,这里查询了Products表的ProductID, ProductName, UnitPrice三列。

  6. SqlCommand command = new SqlCommand(sqlQuery, connection);: 创建 SqlCommand 对象,将SQL查询语句和数据库连接关联起来。SqlCommand 对象负责执行SQL命令。

  7. using (SqlDataReader reader = command.ExecuteReader()): 执行查询语句,并返回 SqlDataReader 对象。ExecuteReader() 方法用于执行返回多行数据的查询语句。同样使用 using 语句确保 SqlDataReader 对象在使用完毕后被正确释放。

  8. while (reader.Read()): 循环读取 SqlDataReader 中的每一行数据。reader.Read() 方法读取下一行数据,如果成功读取到数据则返回 true,否则返回 false

  9. Console.WriteLine($"ID: {reader["ProductID"]}, Name: {reader["ProductName"]}, Price: {reader["UnitPrice"]}");: 在循环中,通过列名 (reader["ProductID"]) 或列索引 (reader[0]) 访问当前行的数据。

  10. catch (Exception ex): 捕获可能发生的异常,例如数据库连接错误、SQL语句错误等,并输出错误信息。

  11. finally: finally 块中的代码无论是否发生异常都会被执行。这里显式关闭数据库连接(虽然 using 语句已经处理了连接的释放,但作为示例展示)。

5.2.2 ADO.NET 数据操作:插入、更新与删除

除了数据读取,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注入代码。

5.3 Entity Framework Core:ORM 数据访问框架

Entity Framework Core (EF Core) 是一个现代的、轻量级的、可扩展的ORM框架,用于.NET应用程序。ORM (Object-Relational Mapping) 框架的主要作用是在对象模型和关系数据库之间建立映射关系,允许开发者使用面向对象的方式操作数据库,而无需编写大量的SQL代码。

5.3.1 Entity Framework Core 架构与核心概念

graph TD A[ASP.NET Application] --> B(DbContext); B --> C{Change Tracker}; B --> D(DbSet<T>); D --> E[LINQ to Entities Provider]; E --> F[Query Provider]; F --> G[Database Provider]; G --> H[Database]; C --> E; subgraph Entity Framework Core Architecture B C D E F G end style B fill:#f9f,stroke:#333,stroke-width:2px style D fill:#ccf,stroke:#333,stroke-width:2px style H fill:#eee,stroke:#333,stroke-width:2px

架构组件详解:

  • 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。

5.3.2 Entity Framework Core 代码实践:Code-First 模式与 CRUD 操作

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: 处理并发更新异常。

5.4 其他数据访问技术简介

除了 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 数据库适用于处理非结构化数据、高并发、大数据量等场景。

5.5 数据访问最佳实践

  • 连接字符串管理: 避免硬编码连接字符串在代码中。将连接字符串存储在配置文件 (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) 模式中,提高代码的可维护性和可测试性。

5.6 总结

数据访问是ASP.NET应用程序开发的关键组成部分。本章详细介绍了ADO.NET和Entity Framework Core这两种主流的数据访问技术,并提供了代码示例和最佳实践。选择合适的数据访问技术取决于应用程序的具体需求,例如性能要求、开发效率、数据模型的复杂程度等。掌握数据访问技术,并遵循最佳实践,可以帮助开发者构建高效、安全、可维护的ASP.NET应用程序。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U