* 本文凡使用变量 httpContextAccessor 或 _httpContextAccessor 的地方都需要需要注入 Microsoft.AspNetCore.Http.IHttpContextAccessor
* 在视图(view)中应以 ViewContext 类引出
获取当前页面网址:
using Microsoft.AspNetCore.Http.Extensions;
string url = Request.GetDisplayUrl();获取当前访问的域名和端口:

获取来源:
Request.GetTypedHeaders().Referer
判断 Scheme 是否为 https:
Request.IsHttps获取客户端IP和端口、服务器IP和端口:

获取浏览器用户代理(UserAgent):
httpContextAccessor.HttpContext.Request.Headers[HeaderNames.UserAgent];获取当前请求的唯一标识:
httpContextAccessor.HttpContext.TraceIdentifier返回结果:80000564-0002-f700-b63f-84710c7967bb
用途:可作为生成随机数的种子。
获取客户端IP地址:
httpContextAccessor.HttpContext.Connection.RemoteIpAddress获取当前项目根目录磁盘路径:
AppContext.BaseDirectory // 以“\”结尾注意:此路径到项目根目录,而非网站根目录,网站根目录请自行追加,默认为 wwwroot\
编码/解码:(参数和返回值均为 string?)
using System.Net;
WebUtility.HtmlEncode(myString)
WebUtility.HtmlDecode(myString)
WebUtility.UrlEncode(myString)
WebUtility.UrlDecode(myString)更多:https://xoyozo.net/Blog/Index/Core
You could try using --skip-broken to work around the problem
……
curl-7.54.0-1.el7.centos.x86_64 is a duplicate with curl-7.29.0-35.el7.centos.x86_64
解决方法:
yum remove curl-7.29.0-35.el7.centos.x86_64
yum install curl
实测上面的方法无法安装 curl
思路:使用 SHOW PROCESSLIST 命令列出数据库当前的所有连接,筛选相关进程,使用 KILL 命令结束进程。
首先创建 SHOW PROCESSLIST 结果集的模型:
class ProcessItem
{
public int Id { get; set; }
public string User { get; set; }
public string db { get; set; }
public string Command { get; set; }
public int Time { get; set; }
}查询、筛选、执行:
using (var db = new dbEntities())
{
foreach (var p in db.Database.SqlQuery<ProcessItem>("SHOW PROCESSLIST").ToList())
{
if (p.Id > 0 && p.User == "用户名" && p.db == "库名" && p.Command == "Sleep" /*&& p.Time >= minSecondsToExpire*/)
{
pids.Add(p.Id);
}
}
foreach (var pid in pids)
{
db.Database.ExecuteSqlCommand("KILL " + pid);
}
}
前言:
ChatGPT 给了 3 条建议:
在应用程序中正确释放数据库连接。确保在使用完数据库连接后,将其关闭并将其返回到连接池中。您可以使用 using 语句来确保连接在使用完毕后被正确释放。
调整连接池的大小。默认情况下,连接池的最大大小为 100。如果您的应用程序需要更多的连接,则可以增加连接池的大小。您可以在连接字符串中设置 Max Pool Size 属性来调整连接池的大小。
调整连接池的超时时间。默认情况下,连接池中的连接在 30 秒钟内未使用时将被关闭。如果您的应用程序需要更长的连接时间,则可以增加连接池的超时时间。您可以在连接字符串中设置 Connection Lifetime 属性来调整连接池的超时时间。
亲测有效,尤其是第 3 条,原因是 Connection Lifetime 的默认值是 0,即没有超时限制。
—— 2023.5
一般地,我们使用 EF 连接数据库前会先初始化一个数据库上下文:
dbEntities db = new dbEntities();虽然 ASP.NET 会在查询完毕后自动关闭该连接,但是在什么情况下回收等都是不确定的,所以会导致 MySQL 中出现很多 Sleep 的连接(执行命令 SHOW FULL PROCESSLIST 可见),占用数据库的连接数,除非主动调用 Dispose():
db.Dispose();官方建议的写法是使用 using 语法:
using (dbEntities db = new dbEntities())
{
}using 会自动调用 Dispose()。这样对减少连接数是很有效的,但官方提示为了提高下一次连接的速度,并不会完全关闭所有连接。
C# 8 建议写法:
using dbEntities db = new dbEntities();在实际项目中(该项目有 500+处数据库连接)测试结果,在不执行 Dispose() 时稳定为 140 个左右连接数,使用 using 或 Dispose() 后稳定变为 40 个左右。
如果不小心在 using 外部或 Dispose() 后再次对该上下文执行查询操作会出现异常:
无法完成该操作,因为 DbContext 已释放。
或
此 ObjectContext 实例已释放,不可再用于需要连接的操作。
所以要避免出现这种情况。这里还有一种另类的解决方法(不建议),根据上下文的特性,只要在 using 内查询一次(譬如视图中需要用到的导航属性,即外键关联的表),就可以在外部使用这个属性。
(建议)在 ASP.NET MVC 或 Web API 项目中,如果一个控制器中仅在 Action 外部定义一个 DbContext,那么,只要重写该控制器的 Dispose() 方法即可:
private dbEntities db = new dbEntities();
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}上下文使用 private 修饰即可。
使用会话状态服务器(StateServer)管理会话状态,可防止网站发布后会话丢失,参 ASP.NET 网站每次发布后丢失 Session 怎么办?
查询数据库时,尽量使用 using 包裹 db 上下文,或者 db.Dispose(),或重写 Dispose(),可以减少数据库连接数(Sleep),参 如何减少 ASP.NET 连接 MySQL 时的 Sleep 查询(即时关闭数据库上下文)
发布时使用预编译功能,参 彻底告别 .NET 网站首次访问速度慢的问题
清理日志不要这样写(先读取再删除):
db.dt_log.RemoveRange(db.dt_log.Where(c => c.time < dt).OrderBy(c => c.time).Take(count));SQL Server 应该:
db.Database.ExecuteSqlCommand($"DELETE FROM {nameof(db.dt_log)} WHERE {nameof(l.id)} IN (SELECT TOP {count} {nameof(l.id)} FROM {nameof(db.dt_log)} WHERE {nameof(l.time)} < '{dt.ToString("yyyy-MM-dd HH:mm:ss")}' ORDER BY {nameof(l.time)})");注意时间可能需要使用 convert 函数转化,给 time 字段添加索引。
MySQL 应该:
db.Database.ExecuteSqlCommand($"DELETE FROM {nameof(db.dt_log)} WHERE {nameof(l.time)} < '{dt.ToString("yyyy-MM-dd HH:mm:ss")}' ORDER BY {nameof(l.time)} LIMIT {count}");给 time 字段添加索引。
未完待续
Discuz! 数据库加索引
待优化的 SQL:(pre_forum_thread 表有 150 万条数据)
SELECT * FROM pre_forum_thread WHERE `fid`='62' AND `displayorder` IN('0','1','2','3','4') ORDER BY displayorder DESC, dateline DESC LIMIT 20, 20加索引前,
EXPLAIN 结果 Extra 为:Using index condition; Using where; Using filesort
> 时间: 0.915s
加索引后:`fid`, `displayorder`, `dateline`
EXPLAIN 结果 Extra 为:Using where 或 Using index condition
> 时间: 0.001s
magapp 数据库加索引
待优化的 SQL:(mag_score_action_log 表有 200 万条数据)
SELECT COUNT(*) AS tp_count
FROM `mag_score_action_log`
WHERE action_id = 20
AND user_id = 650070
AND create_time >= 1534953600
AND create_time < 1535040000
LIMIT 1加索引前,
EXPLAIN 结果 Extra 为:?????
> 时间: 70s
加索引后:`action_id`, `user_id`, `create_time`
EXPLAIN 结果 Extra 为:Using where; Using index
> 时间: 0.073s
待优化的 SQL:(mag_score_mission_log 表有约 55 万条数据)
SELECT COUNT(*) AS tp_count
FROM `mag_score_mission_log`
WHERE mission_id = 7
AND user_id = 650070
AND create_time >= 1534953600
AND create_time < 1535040000
LIMIT 1加索引前,
EXPLAIN 结果 rows 为:549178
> 时间: 17.719s
加索引后:`mission_id`, `user_id`, `create_time`
EXPLAIN 结果 rows 为:1
> 时间: 0.025s
待优化的 SQL:(mag_score_mission_user 表有约 28 万条数据)
SELECT *
FROM `mag_score_mission_user`
WHERE `user_id` = 431779
AND `mission_id` = 5
AND `create_time` >= 1534953600
ORDER BY complete_count DESC
LIMIT 1不加索引
1SIMPLEmag_score_mission_userALL282436Using where; Using filesort
> 时间: 7.325s
`user_id`, `mission_id`
1SIMPLEmag_score_mission_userrefix_us_miix_us_mi10const,const7Using where; Using filesort
时间: 0.014s
`user_id`, `mission_id`, `create_time`
1SIMPLEmag_score_mission_userrangeix_us_miix_us_mi151Using index condition; Using filesort
时间: 0.023s
`user_id`, `mission_id`, `complete_count`
1SIMPLEmag_score_mission_userrefix_us_miix_us_mi10const,const7Using where
> 时间: 0.014s
`user_id`, `mission_id`, `complete_count`, `create_time`
1SIMPLEmag_score_mission_userrefix_us_miix_us_mi10const,const7Using where
> 时间: 0.028s
`user_id`, `mission_id`, `create_time`, `complete_count`
1SIMPLEmag_score_mission_userrangeix_us_miix_us_mi151Using index condition; Using filesort
> 时间: 0.025s
其它就不一一举例了,根据 SHOW FULL PROCESSLIST 的慢查询自行加索引就行了。
本文未完成,部分测试方法、条件或结果可能有误,请谨慎参考! :)
本文基于 MySQL 的 InnoDB BTREE 方法的索引进行测试。
以一张包含 2000 万条记录的表做实验:
CREATE TABLE `dt_read` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`time` datetime(0) NOT NULL,
`a_id` int(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
);这张表是用于记录文章点击量的,
`id` 为主键,int(11) 自增;
`time` 为非空 datetime,表示文章打开时间,测试数据是从 2017-03-11 至 2018-04-28;
`a_id` 为非空 int(11),表示文章 ID,在此表中不唯一,测试数据是从 1 至 260218。
体验“全表扫描”
首先来体验一下什么是全表扫描,执行下面语句:
SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' LIMIT 10
> 时间: 0.012s
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' LIMIT 10
> 时间: 7.317s
表中数据是按主键从小到大排列的,当查询条件为 `time` < '2020-1-1' 时,能很快地从表的前端找到 10 条满足条件的数据,所以不再继续判断后面的记录,立刻返回结果,耗时 0.012 秒;但当条件改为 `time` < '2000-1-1' 时,同样逐条判断,直到最后一条也没有找到,这种情况就是所谓的“全表扫描”,耗时 7 秒。
索引对 ORDER BY 的 ASC 和 DESC 的影响
我们给 `time` 建一个索引,同样执行刚才需要全表扫描的语句:
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' LIMIT 10
> 时间: 0.012s
创建 `time` 的索引后,相当于生成了一张按 `time` 字段排列的新表,这时 MySQL 就能够很快地定位并找到符合条件的记录,避免了全表扫描。
试试按 `time` 倒序排:
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `time` DESC LIMIT 10
> 时间: 0.013s
结论:索引对 ORDER BY 的顺序(ASC)和倒序(DESC)都是有效的。
索引字段的次序对 WHERE 和 ORDER BY 的影响
删除所有索引,创建一个新的索引,字段依次为 `time`, `a_id`。
分别执行以下查询:
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' AND `a_id` < 0 LIMIT 10
> 时间: 0.013s
SELECT * FROM `dt_read` WHERE `a_id` < 0 AND `time` < '2000-1-1' LIMIT 10
> 时间: 0.013s
结论:MySQL 会自动优化 WHERE 条件的次序来匹配最合适的索引。
但在 ORDER BY 中却不是这么回事了:
SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10
> 时间: 0.013s
SELECT * FROM `dt_read` ORDER BY `a_id`, `time` LIMIT 10
> 时间: 14.066s
原因也很好理解,对两个字段进行排序,先后次序肯定会影响结果集,因此只能以 SQL 语句指定的字段次序来 ORDER BY,这样,按索引的字段次序进行 ORDER BY 查询无疑是更快的。
索引中的字段必须依次使用
保持上例创建的索引不变,即 `time`, `a_id`。
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' AND `a_id` < 0 LIMIT 10
> 时间: 0.013s
SELECT * FROM `dt_read` WHERE `a_id` < 0 LIMIT 10
> 时间: 6.438s
上句合理利用了索引的字段,而下句跳过了 `time`,直接 WHERE 了 `a_id`,这是不受该索引支持的。
我们可以想象一下这张由索引生成的虚拟表,其实就是一张普通的平面二维表格,按索引指定的字段次序进行了排序,所以全表中仅仅是索引指定的第一个字段是按大小排列的,第二个字段是在第一个字段值相同的区域内按大小排列,后同。所以,跳过索引指定的第一个字段直接对第二个字段进行检索,是无法应用该索引的。这个结论也同样也体现在 ORDER BY 语句中:
SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10
> 时间: 0.013s
SELECT * FROM `dt_read` ORDER BY `a_id` LIMIT 10
> 时间: 29.566s
WHERE 和 ORDER BY 混合
保持上例创建的索引不变,即 `time`, `a_id`。
先来执行这两句:
SELECT * FROM `dt_read` ORDER BY `a_id` LIMIT 10
> 时间: 12.29s
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `a_id` LIMIT 10
> 时间: 0.013s
仅仅 WHERE 了一个 `time`,对 ORDER BY `a_id` 的效率却有质的提升,是因为 WHERE 中的 `time` 和 ORDER BY 中的 `a_id` 一起找到了索引吗?答案是否定的。
我们把时间改大,让它能马上找到符合条件的数据:
SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' ORDER BY `a_id` LIMIT 10
> 时间: 22.34s
为什么这个语句就不走索引了呢?
其实,一个简单的 SELECT 查询语句,首先执行 WHERE,然后 ORDER BY,最后是 LIMIT。每一步都独自去找了索引,而非 WHERE 和 ORDER BY 混在一起去找索引。必须保证每一步是快的,最终才是快的。
当 `time` < '2000-1-1' 时,WHERE 用到了索引,所以很快,ORDER BY 却没有用到索引,但为什么也很快呢?因为 WHERE 的结果集非常小(示例中为 0 条)。
当 `time` < '2020-1-1' 时,WHERE 也用到了索引,但其结果集非常大(示例中为所有记录),再 ORDER BY `a_id` 就非常慢了,因为我们没有创建以 `a_id` 开头的索引。
现在把索引改成只有 `time` 一个字段。
SELECT * FROM `dt_read` WHERE `time` < '2020-1-1' ORDER BY `a_id` LIMIT 10
> 时间: 6.033s
因为索引里有 `
SELECT * FROM `dt_read` WHERE `time` < '2000-1-1' ORDER BY `a_id` LIMIT 10
> 时间: 0.013s
SELECT * FROM `dt_read` WHERE `a_id` < 0 ORDER BY `time` LIMIT 10
> 时间: 6.033s
第二句先 WHERE `a_id`,后 ORDER BY `time` 是不能匹配所建的索引的。
索引中的字段越多越好
分别在创建索引(`time`)和索引(`time`, `a_id`)的情况下执行下面语句:
本例使用 ORDER BY 而不是 WHERE 来测试是因为,在 WHERE 的多个条件下,如果符合前一条件的筛选结果集过小会导致判断第二条件时数据量不足,无法判断索引是否起作用。
SELECT * FROM `dt_read` ORDER BY `time` LIMIT 10
仅创建索引(`time`)的情况下:
> 时间: 0.013s
仅创建索引(`time`, `a_id`)的情况下:
> 时间: 0.013s
SELECT * FROM `dt_read` ORDER BY `time`, `a_id` LIMIT 10
仅创建索引(`time`)的情况下:
> 时间: 15.015s
仅创建索引(`time`, `a_id`)的情况下:
> 时间: 0.014s
可以看到,在索引字段依次使用的前提下,索引字段数不少于查询字段数才能避免全表扫描。
虽然索引中的字段越多越好,但必须依次使用,否则也是无效索引。
索引对 INSERT / UPDATE / DELETE 的效率影响
分别在创建索引(`time`)和索引(`time`, `a_id`)的情况下执行下面语句:
INSERT INTO `dt_read` (`time`, `a_id`) VALUES ('2018-4-28', 260218)
不建索引的情况下:
> 时间: 0.01s
仅创建索引(`time`)的情况下:
> 时间: 0.01s
同时创建索引(`time`)和索引(`time`, `a_id`)的情况下:
> 时间: 0.01s
UPDATE `dt_read` SET `time` = '2018-4-28' WHERE `id` = 20000000(注:存在该 id 值的记录)
不建索引的情况下:
> 时间: 0.01s
仅创建索引(`time`)的情况下:
> 时间: 0.01s
同时创建索引(`time`)和索引(`time`, `a_id`)的情况下:
> 时间: 0.01s
虽然在 INSERT / UPDATE / DELETE 时数据库会更新索引,但从实测数据来看,索引对其效率的影响可忽略不计。
一些误区
“in 语法效率很低”?
in 语法也是应用索引的,网传 in 会比一个一个 WHERE OR 要慢得多的说法是不靠谱的。in 主键和 in 索引同理。
另外:
对于字符串类型,LIKE '%abc%' 是不能应用索引的,但 LIKE 'abc%' 可以。更多关于字符串类型的索引,请查阅全文索引(FULLTEXT)。
索引的字段是可以指定长度的,类似字符串索引指定前面若干唯一字符就可以优化效率。
本文系个人实践总结,欢迎批评指正!
在 NuGet 中安装
Microsoft.AspNetCore.Session和Newtonsoft.Json打开 Startup.cs,在 ConfigureServices 方法中加入(视情况配置相关属性)
services.AddSession(options => { // 设置了一个较短的时间,方便测试 options.IdleTimeout = TimeSpan.FromSeconds(10); options.CookieHttpOnly = true; });在 Configure 方法中加入
app.UseSession();添加 Extensions 文件夹并添加类 SessionExtensions.cs
using Microsoft.AspNetCore.Http; using Newtonsoft.Json; public static class SessionExtensions { public static void Set<T>(this ISession session, string key, T value) { session.SetString(key, JsonConvert.SerializeObject(value)); } public static T Get<T>(this ISession session, string key) { var value = session.GetString(key); return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value); } }使用方法
HttpContext.Session.Set("abc", 123); var v1 = HttpContext.Session.Get<int>("abc"); HttpContext.Session.Remove("abc"); var v2 = HttpContext.Session.Get<int>("abc"); // v1 = 123, v2 = 0
官方教程:Getting started with ASP.NET Core MVC and Entity Framework Core using Visual Studio
第一课:入门
Entity Framework Core NuGet 软件包
安装数据库提供程序(以 SQL Server 为例):其它数据库提供程序
Install-Package Microsoft.EntityFrameworkCore.SqlServer该软件包依赖 Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational
新建数据模型

文件夹 Models 存放实体类
默认以 ID 或 类名ID 为主键列,并且是标识(即自动递增),以下代码可以取消标识,允许自定义 ID 值
[DatabaseGenerated(DatabaseGeneratedOption.None)]导航属性:对于数据库来说就是外键,对一定义为实体类,对多定义为 ICollection<T>
字段类型可以是枚举 enum
创建数据库上下文
在 Data 文件夹中创建一个名为 SchoolContext.cs 的类,继承于 DbContext
使用依赖注入注册上下文
修改 Startup.cs 的 ConfigureServices 方法
在 appsettings.json 中配置数据库连接字符串
添加代码以使用测试数据初始化数据库
在 Data 文件夹中创建一个名为 DbInitializer.cs 的类
修改 Startup.cs 的 Configure 方法
新建一个控制器和视图
在 Controllers 文件夹上右键,添加
MVC 依赖项选择 Minimal Dependencies 即可
会自动安装 NuGet 包:Microsoft.EntityFrameworkCore.Design 和 Microsoft.EntityFrameworkCore.SqlServer.Design
公约
表名优先来自 DbSet 属性名,其次是类名
实体属性名将映射为列名
实体属性名 ID 或 类名ID 将默认被设为主键
如果属性被命名,它将当作外键(例如,导航属性 Student 对应的实体 Student 的主键是 ID,所以 StudentID 是外键);
公约可以被覆盖,例如,可以指定表名,设置列名,并将任何属性设置为主键或外键。
异步代码
第二课:新建、读取、更新和删除操作(CRUD)
自定义详细信息页面
Include() // 加载导航属性
ThenInclude() // 加载导航属性的导航属性
AsNoTracking() // 使该实体在当前上下文生命周期中不更新
SingleOrDefaultAsync() // 异步检索单个实体
路由数据
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>
<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>生成为
<a href="/Students/Edit/6">Edit</a>
<a href="/Students/Edit?studentID=6">Edit</a>改进“新建页面”
在 HttpPost 的 Create 方法中,因为 ID 是自动递增的,所以可以从 Bind 中删除
[ValidateAntiForgeryToken] 搭配 if (ModelState.IsValid),用于实现验证,防止 CSRF 攻击
改进“删除页面”
在 HttpGet 的 Delete 方法中增加参数用于传递报错内容
在 HttpPost 的 Delete 方法中删除失败则 RedirectToAction 到 Delete 页面并带上报错参数
用”初始化和附加“的方法实现删除
Student studentToDelete = new Student() { ID = id };
_context.Entry(studentToDelete).State = EntityState.Deleted;实例化要删除的实体(仅指定主键值),并将状态设置为 Deleted,即可在 SaveChanges 时实现删除,从而避免了删除必须先读取的情况。
处理交易
当 SaveChanges 时,EF 自动确保所有更改都成功或都失败,如果首先完成一些更改,然后发生错误,那么这些更改将自动回滚。
无追踪查询
什么情况下适用 AsNoTracking 方法:
在上下文中不需要更新任何实体,并且不需要加载导航属性(若需要导航属性,可以预先 Include 或稍后单独查询)
检索大量数据时,仅一小部分将被更新(稍后单独查询)
当前检索的实体需要在稍后以不同的目的附加(_context.Entry(*).State)
第三课:排序,过滤,分布和分组
添加列排序链接到学生索引页
将“搜索框”添加到“学生索引”页面
将分页功能添加到“学生索引”页面
PaginatedList 类的使用
创建一个显示学生统计信息的关于页面
创建视图模型:在 Models 文件夹中创建一个 SchoolViewModels 文件夹,添加一个类文件 EnrollmentDateGroup.cs
修改 HomeController 添加上下文
使用 group by 进行分组查询
第四课:迁移
用于迁移的 NuGet 软件包
要使用迁移,可以使用包管理器控制台(PMC)或命令行界面(CLI)。教程
CLI 的另一种打开方式:在项目上右键 - 在文件资源管理器中打开文件夹,在地址栏上键入 cmd 回车
删除数据库的 CLI 命令
dotnet ef database drop创建初始迁移
dotnet ef migrations add InitialCreate创建数据库的代码位于 Migrations 文件夹中,Up 方法用于创建,Down 方法用于回滚
这时只生成了创建数据库的代码,并没有真正的创建数据库
多次创建的迁移会按时间戳的次序展示在 Migrations 文件夹中,数据库中记录了最后一次迁移的版本
检查数据模型快照
迁移会在 Migrations/SchoolContextModelSnapshot.cs 中创建当前数据库模式的快照,因此 EF Core 不必与数据库进行交互以创建迁移。
要删除最后一次迁移,执行:
dotnet ef migrations remove将迁移应用于数据库
dotnet ef database update执行完成会创建数据库结构,但表中的初始数据将在第一次运行时被添加。
上次应用迁移之后创建的一个或多个新迁移,将在当前应用迁移时按顺序全部执行。
第五课:创建复杂的数据模型

通过使用属性自定义数据模型
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]DataType 用于指定比数据库固有类型更具体的数据类型,上例指定为日期,而不是日期和时间
DateType 支持:Date, Time, PhoneNumber, Currency, EmailAddress 等等
DateType 支持传达数据的语义到 HTML5
DisplayFormat 用于指定格式
ApplyFormatInEditMode 指定是否当值显示在文本框中进行编辑时也应用此格式
[StringLength(50)]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]StringLength 设置数据库中最大长度,并为客户端和服务端提供验证
StringLength 可以指定最小长度,但该值对数据库没有影响
StringLength 不会阻止用户输入空格,可以使用 RegularExpression 来限制
缩短最大长度会导致迁移时发出警告,为了保证顺利迁移,请先更改数据或重新选择合适的最大长度
[Column("FirstName")]
public string FirstMidName { get; set; }上例将属性 FirstMidName 映射到数据库列名 FirstName
[Required]必需属性,不能为空的类型将自动处理必填字段,无需重复指定。
[Display(Name = "Last Name")]显示属性,指定用于显示的名称
计算属性,只有一个 get 访问器,不会在数据库中生成列,例如:
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}可以将多个属性放在一行中:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]在一对一的关系中,副表的主键名称不以副表实体 classnameID 命名,而以主表实体 classnameID 命名时,用 Key 属性来指定主键:
[Key]或者需要以除 classnameID 或 ID 外的其它属性为主键,那么可以用 Key
[Range(0, 5)]指定数值范围
当有相关实体的导航属性时,EF 不要求在数据模型中添加外键属性。详情见教程
[DatabaseGenerated(DatabaseGeneratedOption.None)]主键值由用户提供,而不是由数据库生成。
[Column(TypeName = "money")]
public decimal Budget { get; set; }通常不需要列映射,此例将 decimal 类型的属性 Budget 映射为 money 类型的字段 Budget。
部门可能有负责人也可能没有,负责人必定是教师。因此 InstructorID 属性为教师实体的外键,并且可空,导航属性是被命名为 Administrator 的 Instructor 实体。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }通常,EF 允许对不可空的外键进行级联删除以久多对多关系。
例如,如果将 Department.InstructorID 属性定义为不可空,则 EF 将配置级联删除规则,以在删除部门时删除教师。如果我们想保留教师,但业务逻辑要求该 InstructorID 属性确实不可为空,则必须使用以下语句来禁用关系中的级联删除:
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)补充,现在设计业务逻辑时会尽量避免删除操作,而使用禁用来代替,以在数据库中永久保留记录。
[DisplayFormat(NullDisplayText = "No grade")]NullDisplayText 指定当值为 null 时的显示内容
联合主键
在多对多关系(教师到课程)的连接表(CourseAssignment)中,两个外键都不可为空,并且一同唯一标识表的每一行,因此不需要单独的主键,将 InstructorID 和 CourseID 作为一个联合主键。
定义联合主键不能通过使用属性来完成,唯一方法是使用 Fluent API,修改 Data/SchoolContext.cs 的 OnModelCreating 方法:
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });Fluent API 替代属性
protected override void OnModelCreating(ModelBuilder modelBuilder){
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}在本教程中,您只使用 Fluent API 进行数据库映射,而不能使用属性。您还可以使用 Fluent API 来指定可以使用属性执行的大多数格式化、验证和映射规则。一些属性(如 MinimumLength)不能应用 Fluent API,因为它不会更改数据库模式,它只适用于客户端和服务端验证。
一些开发人员喜欢专门使用 Fluent API,以保持实体类“干净”。当然可以混合使用属性和 Fluent API,但推荐只选择一种方式。如果混合使用,那么 Fluent API 会覆盖属性。
添加迁移
dotnet ef migrations add ComplexDataModel如果在此时尝试运行命令 database update(不要这样做),您将收到以下错误:
ALTER TABLE语句与FOREIGN KEY约束“FK_dbo.Course_dbo.Department_DepartmentID”冲突。冲突发生在数据库“ContosoUniversity”,表“dbo.Department”,“DepartmentID”列。
这是因为在课程 Course 表增加了一个 DepartmentID 外键,而且是不可空的,又由于课程表中已经存在数据,所以无法 AddColumn。
我们可以创建一个临时部门,然后将它的 DepartmentID 值赋给 Course.DepartmentID 作为默认值。
打开 _ComplexDataModel.cs 文件,注释将 DepartmentID 添加到课程表的代码行,注释之,并在创建 Department 表的代码之后添加创建默认部门和添加 DepartmentID 的代码,详见教程。
第六课:读取相关数据
预先加载、显式加载和延时加载
预先加载:当实体被读取时,相关数据随之被检索。即在单个连接查询中读取所有所需数据。通过 Include 和 ThenInclude 方法实现。
显式加载:当实体首次读取时,不检索相关数据。如果需要再检索相关数据。显式加载会导致多次数据库查询。
延时加载:当实体首次读取时,不检索相关数据。在首次尝试访问导航属性时,将自动检索该导航属性所需的数据。
* 从定义上看“显示加载”和“延时加载”比较类似,详细区别会在学完本课后有具体的了解。
创建一个显示部门名称的课程页面
在课程列表中,每个课程都显示部门名称,因此使用预先加载。
在控制器中使用 Include 和 AsNoTracking
在视图中使用 @Html.DisplayFor(modelItem => item.Department.Name)
创建一个显示课程和报名的教师页面
为教师页面创建一个视图模型 InstructorIndexData.cs
在控制器中多次使用 Include 和 ThenInclude 来预先加载相关数据
第一个 Include 加载办公室(一对零或一)
第二个 Include 加载课程(多对多)和报名课程的学生(多对多)
第三个 Include 加载课程(多对多)和课程所在部门(多对一)
本例中“课程的学生”和“课程的部门”是各自级联 ThenInclude 加载的,不能合并到同一个 Include 的课程中,因为 ThenInclude 不能读取多个分支的导航属性,每个分支都必须使用 Include 从教师实体调用。
在视图中给选择的教师添加 class 样式类
显式加载
去掉 Include 和 AsNoTracking,然后在需要的时候附加相关的实体(上下文.Entry())来实现显式加载,以减少首次读取实体时的数据量。
await 上下文.Entry(单个实体).Collection(x=>x.导航属性).LoadAsync(); // 一对多
await 上下文.Entry(单个实体).Reference(x=>x.导航属性).LoadAsync(); // 一对一
第七课:更新相关数据
课程必须与部门关联,在新建和编辑课程时提供部门的下拉列表。
ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);<select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
<option value="">-- Select Department --</option>
</select>SelectList 为下拉列表创建一个集合,允许指定默认选中项,并传递到 ViewBag。
将 .AsNoTracking() 添加到详细信息和删除方法
“一对零或一”关系:(本例是教师和办公室分配,抑或是文章和正文、学生和档案)
删除教师的办公室分配时,如果原先具有值,则删除办公室分配;
输入教师的办公室分配时,如果原先为空,则创建一个办公室分配;
更改教师的办公室分配时,更改现在有办公室分配值。
如何处理:
在 HttpGet 的 Edit 方法中,Include 办公室实体,并 AsNoTracking
在 HttpPost 的 Edit 方法中,移除除 id 以外的参数(为了不与 HttpGet 的 Edit 冲突,改名为 EditPost,并 ActionName("Edit"))
if (await TryUpdateModelAsync<Instructor>(
instructorToUpdate,
"",
i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))使用模型绑定器的值更新检索到的教师实体。TryUpdateModel 支持更新实体的导航属性。
我们可以直接设置实体的导航属性为 null,以便相关的行将被删除。
第八课:处理并发冲突
悲观并发,即锁定,性能差。
乐观并发:
[Timestamp]
public byte[] RowVersion { get; set; }添加一个 timestamp 类型的 RowVersion 字段
详见教程
第九课:继承
第十课:进阶专题
调用“返回实体”的查询
为了防止 SQL 注入,使用参数化查询:
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
var department = await _context.Departments
.FromSql(query, id)
.Include(d => d.Administrator)
.AsNoTracking()
.SingleOrDefaultAsync();
if (department == null)
{
return NotFound();
}
return View(department);
}调用“返回其他类型”的查询
编写 SQL,而非 LINQ:
public async Task<ActionResult> About()
{
List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
var conn = _context.Database.GetDbConnection();
try
{
await conn.OpenAsync();
using (var command = conn.CreateCommand())
{
string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
+ "FROM Person "
+ "WHERE Discriminator = 'Student' "
+ "GROUP BY EnrollmentDate";
command.CommandText = query;
DbDataReader reader = await command.ExecuteReaderAsync();
if (reader.HasRows)
{
while (await reader.ReadAsync())
{
var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
groups.Add(row);
}
}
reader.Dispose();
}
}
finally
{
conn.Close();
}
return View(groups);
}调用“更新”查询(批量更新)
使用参数化查询批量更新数据,返回受影响行数:
ViewData["RowsAffected"] =
await _context.Database.ExecuteSqlCommandAsync(
"UPDATE Course SET Credits = Credits * {0}",
parameters: multiplier);检查发送到数据库的 SQL
有时,可以看到发送到数据库的实际 SQL 查询很有帮助。
第一步:设置一个断点
第二步:调试模式运行程序
第三步:在“输出”窗口可以看到 SQL 查询语句
自动变化检测
EF 通过将实体的当前值与原始值进行比较来确定实体如何更改。
一些方法会导致自动更改检测:
DbContext.SaveChages
DbContext.Entry
ChangeTracker.Entries
如果您正在跟踪大量实体,并且您在循环中多次调用上述方法之一,则可以通过临时关闭 AutoDetectChangesEnabled 属性,能显著提升性能。
_context.ChangeTracker.AutoDetectChangesEnabled = false;从现有数据库反向工程
常见错误
问:ContosoUniversity.dll 由另一个进程使用
答:在 IIS Express 中停止站点。
问:迁移在 Up 和 Down 方法中没有代码
原因:在运行migrations add命令时有未保存的更改
答:运行migrations remove命令,保存代码更改并重新运行migrations add命令。
新建项目
使用 VS2017 / VS2015 新建项目 - ASP.NET Web 应用程序 - MVC - 个人身份验证
添加模型
右击 Models 文件夹,添加类 Movie
using System;
using System.Data.Entity;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
public class MovieDBContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
}
}Movie 类表示一部电影,一个对象实例对应数据库中一行,每个属性对应表中一列。
MovieDBContext 代表 EF 数据库上下文,处理抓取、存储、更新。
创建连接字符串和使用 SQL Server
打开 Web.config
在 <configuration /> 中的 <connectionStrings /> 中添加
<add name="MovieDBContext" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Movies.mdf;Initial Catalog=Movies;Integrated Security=True" providerName="System.Data.SqlClient" />
name 必须与 DbContext 类的名称匹配(MovieDBContext)
这步将会把数据库文件 Movies.mdf 创建到 App_Data 文件夹中,你也可以使用其它 SQL Server 数据库的连接字符串,简单的方法是:
在“服务器资源管理器”中添加连接
从右击属性中获取连接字符串
从控制器访问模型的数据
右击 Controllers 文件夹添加控制器,选择 包含视图的 MVC 5 控制器(使用 Entity Framework)
模型类:Movie (MvcMovie.Models)
数据上下文类:MovieDBContext (MvcMovie.Models)
F5 运行,访问 /Movies 可添加、查看、编辑、删除影片
添加新字段
设置模型更改的 Code First 迁移
在程序包管理器控制台窗口中,在 PM> 提示符下输入
Enable-Migrations -ContextTypeName MvcMovie.Models.MovieDBContext
在 Migrations 文件夹中新建了 Configurations.cs,打开并在 Seed 方法中加入
context.Movies.AddOrUpdate(i => i.Title,
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
}
);Seed() 将在每次迁移(PM>update-database)后被调用执行,AddOrUpdate 将执行 upsert 操作(有则 update,无则 insert)
AddOrUpdate 的第一个参数指定用于检查行是否已存在的属性
如果该属性值不唯,则出现异常
序列包含多个元素
创建迁移命令:
PM>add-migration Initial
名称“Initial”是任意的
执行迁移:
PM>update-database
F5 运行将显示种子数据
向 Movie 模型添加 Rating 属性
向 Movie 类添加属性,生成
public string Rating { get; set; }向 Create 和 Edit Action 方法的 Bind 属性添加 Rating
更改各视图支持新的 Rating 属性
此时 F5 运行将提示
System.InvalidOperationException:“支持“MovieDBContext”上下文的模型已在数据库创建后发生更改。请考虑使用 Code First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。”
要解决错误除了手动往数据库中添加 Rating 字段外可以利用 Code First 迁移:
更新 Migrations\Configuration.cs 给每个 Movie 对象添加一个 Rating 字段
PM>add-migration Rating
名称 Rating 是任意的
创建了 DbMigration 的派生类 Rating,可以看到添加新列的代码
PM>update-database
数据库自动完成了对新字段的添加,当然 update-database 也会把种子数据还原。
参考
Getting Started with ASP.NET MVC 5