首页 文章详情

[035] 实战:使用MySQL+EF Core实现数据持久化

DotNet NB | 908 2021-12-01 16:57 0 0 0
UniSMS (合一短信)

本文来自『.NET大牛之路』星球的免费分享

在前面的文章中,书大师网站展示的书籍信息是硬编码的。虽然满足了基本需求,但不便于后面的信息更新。更新书籍信息还得修改代码,而且以往发布的书籍信息没有记录。所以我们需要用数据库来做数据的持久化,把数据和代码分离。

为使操作数据库的编程更加高效,我们的项目将使用 EF Core 作为 ORM 框架。

1升级为 .NET 6 正式版

上周 .NET 6 已经正式发布了,VS 2022 也同步发布了,相信很多人已经更新了。只要你升级 VS 2022 为最新版本,就会自动安装或升级 .NET 为 6.0。如果你用的是 MacOS 或 Linux 系统,则可能需要去官网手动下载 SDK。

同样,我们的生产环境也做相应的升级,由原来的 RC2 版本升级到正式版。升级很简单,直接按照官网文档操作就行。我的服务器是 CentOS 8,对应的 .NET 6 安装文档:

https://docs.microsoft.com/en-us/dotnet/core/install/linux-centos

注意,如果你生产环境和我一样是 CentOS 8,请按 CentOS 7 的方式安装 ASP.NET Core 运行时,因为:

官网表示 .NET 6 不支持 CentOS 8 了,但 CentOS 8 包的安装是兼容 CentOS 7 的,所以按 CentOS 7 的方式安装即可。执行文档中的两个命令即可:

$ sudo rpm -Uvh https://packages.microsoft.com/config/centos/7/packages-microsoft-prod.rpm
$ sudo yum install aspnetcore-runtime-6.0

安装完成后,需要修改一下我们的 bookist.service 服务文件:

然后使用 systemctl daemon-reload 重新加载服务。

2安装 MySQL

我们这个项目使用的是 MySQL 数据库,版本是 8.x。由于我们使用的是 EF Core ORM 框架,对于同一套代码,不同数据库,API 都是一样的,所以你也可以使用任何其它你熟悉的数据库。

为了简单起见,开发和生产环境我直接使用同一数据库,加上大多数个人没有条件为一个小网站设一个专门的数据库服务器,所以我把 MySQL 数据库直接安装在应用生产环境。

CentOS 8 安装 MySQL 8 很简单,直接运行下面命令:

$ dnf -y install @mysql

稍等片刻即可完成安装,使用下面命令验证一下版本:

$ mysql -V
mysql Ver 8.0.26 for Linux on x86_64 (Source distribution)

安装完成后,我们还需要把 MySQL 服务开启,并设为开机启动,运行如下两行命令:

# 开启 MySQL 服务
$ systemctl start mysqld
# 使 MySQL 开机启动
$ systemctl enable mysqld
Created symlink /etc/systemd/system/multi-user.target.wants/mysqld.service → /usr/lib/systemd/system/mysqld.service.

我们还需要设置一下数据库用户 root 的访问密码,以便我们程序能够连接。使用如下命令进入 MySQL 的安全向导:

$ mysql_secure_installation

运行该脚本后,会让你选择配置安全选项,比如密码强度设置,这个根据自己需要选择。其中,有两个选项需要注意,一是允许 root 远程访问,二是配置 root 用户密码:

为了更方便远程管理数据库,我们需要开户 MySQL 的远程登录,所以建议设置高强度的密码。如果你使用的是云服务器,需要先到安全组配置中添加 3306 端口的入站规则。以腾讯云为例:

注意,如果是重要项目,请关闭 root 远程登录。如果需要远程登录,则创建权限受限的数据库帐号。

最后,需要配置一下允许远程访问数据库的 IP 地址,这里配置为所有 IP(因为家庭宽带 IP 一般是动态的):

$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.26 Source distribution

mysql> CREATE USER 'root'@'%' IDENTIFIED BY '这里是你的密码';
Query OK, 0 rows affected (0.01 sec)

mysql> GRANT ALL ON *.* TO 'root'@'%';
Query OK, 0 rows affected (0.01 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)

mysql> exit

使用任一 MySQL 客户端(我用的是 Navicat),测试一下在本地是否可以正常连接数据库。

3添加 EF Core 包

Entity Framework Core 是微软提供的一个非常强大的 ORM(Object Relational Mapper )框架。下面在我们的项目中使用该框架。

切换到 Bookist.Web 目录,使用如下命令安装两个 Nuget 包:

$ dotnet add package Microsoft.EntityFrameworkCore.Design
$ dotnet add package Pomelo.EntityFrameworkCore.MySql

第一个是 EF Core 的设计库,包含 EF Core 基本库和工具库;第二个是一个第三方的 MySql 支持库,不同的数据库需要使用对应的支持库,它们实现了使用 EF Core 用统一的接口访问数据库。安装完后会在 Bookist.Web.csproj 文件中生成如下代码:

...
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.0" />
</ItemGroup>
...

如果你用的是 VS,可以用 Nuget 包管理器搜索 EntityFrameworkCore 安装这两个包。

4设计实体类

要使 ORM 框架能把数据表映射为 C# 对象,则需要把 C# 类设计成和数据库实体一致,EF Core 会按照约定俗成的规则(比如默认类的属性应对数据表的字段)自动处理。

我们把之前的 BookVM 类重命名为 Book,属性基本保持不变,只是加上了主键和一些用于描述字段定义的特性:

using System.ComponentModel.DataAnnotations;

namespace Bookist.Web.Models;

public class Book
{
public int Id { get; set; }

[Required, MaxLength(100)]
public string Title { get; set; }

[Required, MaxLength(255)]
public string Cover { get; set; }

[MaxLength(100)]
public string Author { get; set; }

public DateOnly PubDate { get; set; }

[MaxLength(2000)]
public string Description { get; set; }

[Required, MaxLength(50)]
public string Format { get; set; }

[Required, MaxLength(100)]
public string FetchUrl { get; set; }

[MaxLength(10)]
public string FetchCode { get; set; }
}

这里的 MaxLength 将告诉 EF Core 为字符串类型的字段指定最大长度,比如 Titlevarchar(100)

我们不需要编写任何 SQL 脚本,通过使用 EF 工具可以自动为我们生成脚本,并可自动执行到数据库,创建对应的 Book 数据表,下文将介绍如何操作。

5编写 DbContext

对于 EF Core 来说,一般一个数据库对应一个 DbContext 类,它对应数据库操作的上下文,所有的数据库操作都是在这个上下文中进行。

Bookist.Web/Models 目录中添加一个类,名为 BookistDbContext,代码如下:

using Microsoft.EntityFrameworkCore;

namespace Bookist.Web.Models;

public class BookistDbContext : DbContext
{
public BookistDbContext(DbContextOptions<BookistDbContext> options)
: base(options)
{
}

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<Book>();
}
}

注意,自定义的 DbContext 类一定要和上面这样写一个带有 DbContextOptions<T> 参数的构造函数。

BookistDbContext 类中覆盖 OnModelCreating() 方法,在该方法中通过 builder.Entity<T>() 方法指定要映射的实体类。

DbContext 很强大,基本上所有的数据库定义和操作都可以通过它来完成,比如定义数据修改的勾子,后面我们还会经常接触这个类。

6配置 EF Core

要使我们的应用程序能连上数据库,自然少不了数据库连接。在 appsettings.json 中添加数据库连接字符串,如下:

{
"ConnectionStrings": {
"BookistConnection""server=你的服务器IP;port=3306;database=bookist;uid=root;pwd=你的数据库密码"
},
"Logging": {
"LogLevel": {
"Default""Information",
"Microsoft.AspNetCore""Warning"
}
},
"AllowedHosts""*"
}

然后我们还需要注册 EF Core 服务,编辑 Program.cs 文件如下:

using Bookist.Web.Models;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// 注册服务
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<BookistDbContext>(opt =>
{
var conStr = config.GetConnectionString("BookistConnection");
opt.UseMySql(conStr, ServerVersion.Parse("8.0"));
});

var app = builder.Build();

// 注册中间件
app.UseStaticFiles();
app.MapDefaultControllerRoute();

app.Run();

这里通过 AddDbContext 方法注册了相关服务并指定了 BookistDbContext 的数据库连接字符串。

7使用 EF 工具构建数据库

.NET CLI 提供了 EF 相关的工具,代号为 dotnet-ef。它可以帮助我们自动生成脚本、更新数据库结构等。第一次使用需要使用如下命令安装:

$ dotnet tool install --global dotnet-ef
可使用以下命令调用工具: dotnet-ef
已成功安装工具“dotnet-ef”(版本“6.0.0”)。

如果你已经安装过,也可以执行该命令将 dotnet-ef 升级到最新版本。

Bookist.Web 目录下,使用如下命令给当前实体类创建一个数据库变更版本:

$ dotnet ef migrations add 001

为了简单起见,我这里直接使用数字序号代表每次数据库的变更,你也可以使用更友好的描述性的词。执行该命令后,会在目录下生成一个 Migrations 文件夹和对应的数据库修改描述文件,*.cs*.Designer.cs

第一次创建数据库,我们可以直接使用如下命令(不推荐这种方式):

$ dotnet ef database update

这个命令会自动生成数据库升级脚本,并自动执行。此时,我们的数据库已经创建好了,包括一个 Books 表。

但是直接自动执行升级脚本是非常不安全的。稍大点的开发团队是有专门的 DBA 来修改数据库结构的,一般我们需要把数据库升级脚本给到 DBA 去执行。而且,有时候数据库升级不仅仅是结构发生变化,也有可能数据也需要调整,比如删除一个字段的同时把这个字段的值移到另一个表,这时候我们需要增加手写脚本了。

所以推荐的做法是,使用如下命令先生成数据库升级脚本:

$ dotnet ef migrations script -o ./Scripts/001.sql

检查生成的脚本文件 001.sql,确认无误后再把脚本复制到服务器上执行。

由于我们还没有做后台书籍管理的功能,所以暂时需要手动添加数据,执行如下脚本创建初始数据:

INSERT INTO `bookist`.`Book` (`Title`, `Cover`, `Author`, `PubDate`, `Description`, `Format`, `FetchUrl`, `FetchCode`)
VALUES ('CLR via C# (4th Edition)', 'https://img.geekgist.com/Foc1d-NbacAQ6D1WSQ_3UndhaOuR-w2h3', 'Jeffrey Richter', '2012-10-01', 'Dig deep and master the intricacies of the CLR, C#, and .NET development. You’ll gain pragmatic insights for building robust, reliable, and responsive apps and components.', 'PDF', 'https://url19.ctfile.com/f/15677019-228693113-e89578', 'bookist');

8从数据库读取数据

数据和 EF Core 配置都准备好了,现在只需要使用 EF Core 把数据从数据库读取出来展示到页面上就行。

原来我们是把数据硬编码到 Controller 中返回给 View 的,这次改为从数据库中读取,视图代码不动。这里就体现了 MVC 分层的优点,业务代码修改了不会影响到视图。

修改 HomeController 代码如下:

using Bookist.Web.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Bookist.Web.Controllers;

public class HomeController : Controller
{
private readonly BookistDbContext _context;

public HomeController(BookistDbContext context)
{
_context = context;
}

public async Task<ViewResult> Index()
{
var book = await _context.Set<Book>()
.OrderBy(x => x.Id).LastOrDefaultAsync();
return View(book);
}
}

运行起来,浏览器查看一下确保程序没有问题。到这,我们就成功使用 EF Core + MySQL 实现了数据持久化。

完整代码已同步更新到 GitHub:

https://github.com/liamwang/bookist.cc

9保护敏感信息

前文我们在 appsettings.json 中配置了数据库连接字符串,请千万不要把这种敏感信息提交到公开的 Git 仓库。

在 .NET 中,我们可以使用 user-secrets 工具来管理敏感信息,这个工具可以将敏感信息保存在一个 secrets.json 文件中,它不在项目文件夹下,而是存放在另外的地方。对于三种操作系统,它的位置是:

Windows: %APPDATA%/Microsoft/UserSecrets/<UserSecretsId>/secrets.json
Linux : ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json
Mac : ~/.microsoft/usersecrets/<UserSecretsId>/secrets.json

一个 .NET 应用对应一个唯一的 UserSecretsId,一般是一个 GUID。

现在我们要把数据库链接字符串存在项目之外的 secrets.json 文件中,避免它随项目代码一起提交到 Git 仓库。

首先使用如下命令初给我们的项目始化一个 UserSecretsId

$ dotnet user-secrets init
Set UserSecretsId to 'adfa2b88-1fe4-438e-b567-03296ac46b2d' for MSBuild project 'D:\bookist\Bookist.Web\Bookist.Web.csproj'.

打开 Bookist.Web.csproj 文件,可以看到生成的 UserSecretsId

<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>adfa2b88-1fe4-438e-b567-03296ac46b2d</UserSecretsId>
</PropertyGroup>
...

然后使用如下命令在 secrets.json 中设置我们想要保护的敏感信息,比如数据库连接字符串:

$ dotnet user-secrets set "ConnectionStrings:BookistConnection" "server=******;port=3306;database=bookist;uid=root;pwd=******"
Successfully saved ConnectionStrings:BookistConnection = server=******;port=3306;database=bookist;uid=root;pwd=****** to the secret store.

你可以使用 dotnet user-secrets list 命令查看已经存储的敏感信息列表。

如果你使用的是 VS,则操作 user-secrets 更简单。直接右击项目,在菜单中选择 Manage User Secrets 选项:

它会直接帮你创建好 secrets.json 文件,并自动打开,你直接编辑该文件就行,比使用命令行方便很多。

如果本机 secrets.jsonappsettings.json 有相同的配置项,程序会优先读取前者的值。

对于 secrets.json 的所有配置项,最好也在 appsettings.json 保留相应的占位,比如数据库连接字符串:

{
"ConnectionStrings": {
"BookistConnection""<here is your database connection string>"
}
}

appsettings.json 中的使用占位可以告诉其它开发者有这样一个配置,这对于开源项目十分有用。

10发布

和前两篇文章一样,执行发布命令生成发布文件并复制到服务器上,然后重启服务即可,这里不再赘述。

不一样的是,这次我们生成发布文件后,配置文件中并没有包含真实的连接字符串。为了使生产环境拥有独立的配置项(比如生产环境的连接字符串和本机不一样),避免每次替换把生产环境的配置文件覆盖掉,我们在生产环境中创建一个名为 appsettings.Production.json 的文件,其内容和 appsettings.josn 文件一致,只修改其中数据库连接字符串:

{
"ConnectionStrings": {
"BookistConnection""server=localhost;database=bookist;uid=root;pwd=***"
}
}

生产环境运行的是 Release 模式,程序会优先读取 *.Production.json 配置文件。这个 appsettings.Production.json 文件只在生产服务器上有,所以即使我们复制全部文件替换到服务器也没有关系。

推荐阅读:
Kubernetes全栈架构师(Kubeadm高可用安装k8s集群)--学习笔记
.NET 云原生架构师训练营(模块一 架构师与云原生)--学习笔记
.NET Core开发实战(第1课:课程介绍)--学习笔记

点击下方卡片关注DotNet NB

一起交流学习

▲ 点击上方卡片关注DotNet NB,一起交流学习

请在公众号后台


回复 【路线图】获取.NET 2021开发者路线图
回复 【原创内容】获取公众号原创内容
回复 【峰会视频】获取.NET Conf开发者大会视频
回复 【个人简介】获取作者个人简介
回复 【年终总结】获取作者年终总结
回复 加群加入DotNet NB 交流学习群

长按识别下方二维码,或点击阅读原文。和我一起,交流学习,分享心得。


good-icon 0
favorite-icon 0
收藏
回复数量: 0
    暂无评论~~
    Ctrl+Enter