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 查看保存与查询.
实际上是验证保存的必要手段, 这里仅仅使用内存数据库. 无法持久化数据进行查看.
然后是其他 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')
貌似还是没有办法完全用 VSCode 替代 MSSSMS.
在有了数据库之后就可以安装依赖了.
dotnet add package Dapper
dotnet add Microsoft.Data.SqlClient
Dapper: https://www.nuget.org/packages/Dapper
创建仓库模式
下面添加数据库交互代码, 采用数据仓库模式.
作者的意思是, 尽可能将代码简化.
- 添加对应数据库的实体类, 直接添加到
Program.cs
文件底部. - 修改配置文件
appsettings.json
, 添加连接字符串 - 在项目根目录下添加
DapperContext.cs
文件, 并添加DapperContext
类, 在其构造函数中注入Iconfiguration
对象. - 在
Program.cs
文件底部, 创建接口, 并实现仓库. 下一节会在该接口的基础上实现仓库. - 在
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>