jk's notes
  • ch07 集成数据访问层

ch07 集成数据访问层

本章的主题是在 Minimal API 中引入 数据访问层, 主要使用两个技术:

  • 使用 EntityFramework
  • 使用 Dapper

本章结束后需要可以在项目中使用 EF 来处理数据, 以及 Dapper, 并能解释在什么情况下哪一种方法更好.

基本要求

创建 Minimal API 项目.

使用 Entity Framework

我们需要构建一个 API, 并使用数据进行交互. 有很多方法可以实现, 但是 EF 是非常友好且实用的.

然后简要介绍了一下 EFCore.

EFCore 支持很多数据库: SQLite, MySQL, Oracle, SQLServer, 以及 PostgreSQL.

同时还支持内存数据库, 在开发测试阶段会非常方便, 因为不需要真正的数据库.

设置项目

在项目根目录, 创建文件 Icecream.cs 文件, 并录入:

namespace Chapter07.Models;
public class Icecream {
  public int Id { get; set; }
  public string? Name { get; set; }
  public string? Description { get; set; }
}

该数据被称之为 数据模型 (data model). 下一节会将其映射到数据库的表上.

下面在项目中添加 EFCore 包:

dotnet add package Microsoft.EntityFrameworkCore.InMemory

将 EFCore 添加到项目中

在 Program.cs 文件底部添加下面代码:

class IcecreamDb: DbContext {
  public IcecreamDb(DbContextOptions options) :base(options) {}
  public DbSet<Icecream> Icecreams = { get; set; } = null!;
}

DbContext 用于描述数据库的连接, 所有的数据库操作与查询均有该对象来实现.

DbSet 用于表示数据库中的表.

在本例中, 我们只有一个数据库表, 即 Icecreams.

在构建项目之前, 需要添加下面代码 (来注册 EFCore):

builder.Services
  .AddDbContext<ICecreamDb>(
    options => options.UseInMemoryDatabase("icecreams"));

在项目中添加端点

基本上就是 CRUD 操作. 首先是添加数据:

app.MapPost("/icecreams", async (IcecreamDb db, Icecream icecream) => {
  await db.Icecreams.AddAsync(icecream);
  await db.SaveChangeAsync();
  return Results.Created($"/icecreams/{icecream.Id}", icrecream);
});

然后作者简要解释了一下代码的结构, 以及依赖注入. 如果不记得依赖注入可以参考 ch04.

同理, 可以添加端点来获得所有的 Icecream 数据:

app.MapGet('/icecreams', async (IcecreamDb db) => 
           await db.Icecreams.ToListAsync());

然后对该代码进行解释. 然后打开 Swagger 查看保存与查询.

image-20231211090551599

实际上是验证保存的必要手段, 这里仅仅使用内存数据库. 无法持久化数据进行查看.

然后是其他 CRUD 方法, 下面是按照 id 查询某个具体数据

app.MapGet("/icecreams/{id}", async (Icecream db, int id) => 
           await db.Icecreams.FindAsync(id));

然后使用 MapPut 方法实现更新:

app.MapPut("/icecreams/{id}", async (IcecreamDb db, Icecream updateicecream, int id) => {
  var icecream = await db.Icecreams.FirstAsync(id);
  if(icecream is null) return Results.NotFound();
  
  icecream.Name = updateicecream.Name;
  icecream.Description = updateicecream.Description;
  await db.SaveChangesAsync();
  return Results.NoContent();
});

然后依旧是对代码的说明. 并解释道若没有数据, 需要返回 Not Found, 若找到, 并在修改后返回 No Content.

最后是删除:

app.MapDelete("/icecreams/{id}", async (IcecreamDb db, int id) => {
  var icecream = await db.Icecreams.FinsAsync(id);
  if (icecream is null) {
    return Results.NotFound();
  }
  db.Icecreams.Remove(icecream);
  await db.SaveChangesAsync();
  return Results.Ok();
});

简单收尾, 然后准备 Dapper.

使用 Dapper

Dapper 是一个对象关系映射, 准确的讲它是一个小的 ORM. 使用 Dapper 的特点是可以直接编写 SQL 语句. 其优势便是性能, 它不会从实体模型中创建查询. 它扩展了 IDbConnection 对象, 并提供了很多查询方法. 这表示我们编写的查询与数据 provider 可共存.

方法支持异步和同步的, 异步方法使用 Async 作为后缀.

下面是 IDbConnection 接口所支持的方法:

  • Execute
  • Query
  • QueryFirst
  • QueryFirstDefault
  • QUerySingle
  • QuerySingleOrDefault
  • QUeryMultiple

配置项目

首先是创建数据库, 可以使用数据库管理工具, 在 LocalDB 中执行下面 SQL 语句:

CREATE TABLE [dbo].[Icecreams] (
  [Id] [int] IDENTITY(1,1) NOT NULL,
  [Name] [nvarchar](50) NOT NULL,
  [Description] [nvarchar](255) NOT NULL
)
GO

INSERT [dbo].[Icecreams] ([Name], [Description]) VALUES
('Icecream 1', 'Description 1')
INSERT [dbo].[Icecreams] ([Name], [Description]) VALUES
('Icecream 2', 'Description 2')
INSERT [dbo].[Icecreams] ([Name], [Description]) VALUES
('Icecream 3', 'Description 3')

image-20231211095255149

貌似还是没有办法完全用 VSCode 替代 MSSSMS.

在有了数据库之后就可以安装依赖了.

dotnet add package Dapper
dotnet add Microsoft.Data.SqlClient

Dapper: https://www.nuget.org/packages/Dapper

创建仓库模式

下面添加数据库交互代码, 采用数据仓库模式.

作者的意思是, 尽可能将代码简化.

  1. 添加对应数据库的实体类, 直接添加到 Program.cs 文件底部.
  2. 修改配置文件 appsettings.json, 添加连接字符串
  3. 在项目根目录下添加 DapperContext.cs 文件, 并添加 DapperContext 类, 在其构造函数中注入 Iconfiguration 对象.
  4. 在 Program.cs 文件底部, 创建接口, 并实现仓库. 下一节会在该接口的基础上实现仓库.
  5. 在 Program.cs 文件中添加服务.

详细步骤

添加实体类

public class Icecream {
  public int Id { get; set; }
  public string? Name { get; set; }
  public string? Description { get; set; }
}

连接字符串

"ConnectionStrings": {
  "SqlConnection": "Data Source=(localdb)\\MSSQLLocalDB; Initial Catalog=test_dapper; Integrated Security=True; Connect Timeout=30; Encrypt=False; TrustServerCertificate=False;"
}

DapperContext 类

using System.Data;
using Microsoft.Data.SqlClient;

public class DapperContext {
  private readonly IConfiguration _configuration;
  private readonly string _connectionString;
  public DapperContext(IConfiguration configuration) {
    _configuration = configuration;
    _connectionString = _configuration.GetConnectionString("SqlConnection");
  }
  public IDbConnection CreateConnection() => new SqlConnection(_connectionString);
}

数据仓库模式

public interface IIcecreamsRepository {

}

public class IcecreamsRepository: IIcecreamsRepository {
    private readonly DapperContext _context;
    public IcecreamsRepository(DapperContext context) {
        _context = context;
    }
}

注册服务

builder.Services.AddSingleton<DapperContext>();
builder.Services.AddScoped<IIcecreamsRepository, IcecreamsRepository>();

使用 Dapper 查询数据库

更新 IIcecreamsRepository 来添加方法:

Task<IEnumerable<Icecream>> GetIcecreams();

然后实现该方法:

public async Task<IEnumerable<Icecream>> GetIcecreams() {
  var query = "SELECT * FROM Icecreams";
  using (var connection = _context.CreateConnection()) {
    var result = await connection.QueryAsync<Icecream>(query);
    return result.ToList();
  }
}

记得导入命名空间 using Dapper;

然后作者解释了代码. 并展示了最终代码.

其逻辑与 ADO.NET 类似, Execute 方法执行非查询语句, Query 方法执行查询语句.

使用 Dapper 添加新实体

模式一样, 添加接口, 实现方法 (SQL 语句, 创建连接, 执行).

public interface IIcecreamsRepository {
  Task<IEnumerable<Icecream>> GetIcecreams();
  Task CreateIcecream(Icecream icecream);
}

实现

public async Task CreateIcecream(Icecream icecream) {
    var query = "INSERT INTO Icecreams(Name, Description) VALUES(@Name, @Description)";
    var parameters = new DynamicParameters();
    parameters.Add("Name", icecream.Name, System.Data.DbType.String);
    parameters.Add("Description", icecream.Description, System.Data.DbType.String);
    using (var connection = _context.CreateConnection()) {
        await connection.ExecuteAsync(query, parameters);
    }
}

然后对代码进行了解释. Execute 方法实际上会返回受影响行数, 这里没有使用该值, 如果需要可以将其返回.

在端点实现数据仓库

本质上还是利用依赖注入, 将 IIcecreamsRepository 注入到端点处理函数中.

app.MapPost("/icecreams", async (IIcecreamsRepository repository, Icecream icecream) => {
  await repository.CreateIcecream(icecream);
  return Results.Ok();
});
app.MapGet("/icecreams", async (IIcecreamsRepository repository) => 
  await repository.GetIcecreams());

然后对代码进行说明.

运行时需要注意, 在 .NET8 中需要配置 <InvariantGlobalization>false</InvariantGlobalization>

image-20231211114537885

小结(略)

Last Updated:
Contributors: jk