C# Web开发教程(二)
ORM(Object-Relational-Mapping)
- 常用框架
- EF core (稳定团队用),学习成本高
- SQLServer完美支持
- MySQL和PostgreSQL 有小坑,也可以解决
- Dapper (团队不稳定),低学习成本
开发环境搭建
- 流程
- 新建业务类
- 建立配置类
- 建立DbContext类
- 迁移生成数据库
- 安装
SqlServer
- 安装:
Install-Package Microsoft.EntityFrameworkCore -Version 5.0.17
Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 5.0.17
Install-Package Microsoft.EntityFrameworkCore.Tools -Version 5.0.17
- 踩坑: 如果安装的版本不兼容回滚的时候,调用方法的时候会出现各种无法识别的问题
- 比如: builder.ToTable("T_Books"); // ToTable()方法一直无法被识别
- 新建
业务类
// Book.cs
......
namespace ConsoleAppORM
{
class Book
{
public long Id { get; set; }
public string Title { get; set; }
public DateTime PubTime { get; set; }
public double Price { get; set; }
}
}
// Person.cs
......
namespace ConsoleAppORM
{
public class Person
{
public long Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}
- 新建
配置类
// BookConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ConsoleAppORM
{
class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
}
}
}
// PersonConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ConsoleAppORM
{
public class PersonConfig : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.ToTable("T_Persons");
}
}
}
- 新建
Context类
using Microsoft.EntityFrameworkCore;
namespace ConsoleAppORM
{
class MyDbContext:DbContext
{
public DbSet<Book> Books { get; set; }
public DbSet<Person> Persons { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
string connStr = "Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true";
optionsBuilder.UseSqlServer(connStr);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
}
- 迁移数据库,创建迁移记录,最后再更新一下数据库
PM> ADD-Migration InitialCreate
Build started...
Build succeeded.
PM> Update-Database
Build started...
Build succeeded.
......
Done.
- 查看
SqlServer
刚刚创建的表
- 新建连接
- 服务器名称 填 . 或 (local) 或 localhost
- 身份验证 选 Windows 身份验证
- 如果表的结构有变更,需重新创建迁移记录并更新数据库
......
namespace ConsoleAppORM
{
public class Person
{
......
public int Age { get; set; }
// 新增字段
public string BirthPlace { get; set; }
}
}
- 创建迁移记录并刷新数据库
// 给予此次的迁移记录取一个名字
PM> Add-Migration AddPersonBirthPlace
......
PM> Update-Database
......
Done.
PM>
- 表结构修改的又一示例
// Person.cs
......
namespace ConsoleAppORM
{
public class Person
{
......
public string BirthPlace { get; set; }
# 新增
public double? Salary { get; set; }
}
}
// Book.cs
......
namespace ConsoleAppORM
{
class Book
{
......
# 新增
public string AuthorName { get; set; }
}
}
// BookConfig.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ConsoleAppORM
{
class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
# 新增对字段的限制
builder.Property(b => b.Title).HasMaxLength(50).IsRequired();
builder.Property(b => b.AuthorName).HasMaxLength(20).IsRequired();
}
}
}
// Dog.cs 新创建
......
namespace ConsoleAppORM
{
public class Dog
{
public long Id { get; set; }
public string Name { get; set; }
public string Color { get; set; }
}
}
// DogConfig.cs 新创建
......
namespace ConsoleAppORM
{
public class DogConfig : IEntityTypeConfiguration<Dog>
{
public void Configure(EntityTypeBuilder<Dog> builder)
{
builder.ToTable("T_Dogs");
}
}
}
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
namespace ConsoleAppORM
{
class MyDbContext:DbContext
{
......
# 注册
public DbSet<Dog> Dogs { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
......
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
......
}
}
}
-
创建迁移记录并迁移(如果报错则不成功,根据异常来修复)
-
插入数据
- SaveChangesAsync() # 异步方法,推荐使用
- SaveChanges() # 同步方法
// 主程序
using System;
namespace ConsoleAppORM
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
Dog d1 = new Dog { Name = "Kitty" };
dbObject.Add(d1);
// db成功插入数据
dbObject.SaveChanges();
}
Console.WriteLine("数据处理完成");
}
}
}
// 主程序
using System;
using System.Threading.Tasks;
namespace ConsoleAppORM
{
class Program
{
// 异步方法
static async Task Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
Dog d1 = new Dog { Name = "Peter" };
dbObject.Add(d1);
// 调用异步方法
await dbObject.SaveChangesAsync();
}
Console.WriteLine("数据处理完成");
}
}
}
查询语法
演示: 结合Linq
模块,实现底层的SQL查询语法
,先插入测试数据
using System;
using System.Threading.Tasks;
namespace ConsoleAppORM
{
class Program
{
static async Task Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
//Dog d1 = new Dog { Name = "Peter" };
//dbObject.Add(d1);
var b1 = new Book
{
AuthorName = "杨中科",
Title = "零基础趣学C语言",
Price = 59.8,
PubTime = new DateTime(2019, 3, 1)
};
var b2 = new Book
{
AuthorName = "Robert Sedgewick",
Title = "算法(第4版)",
Price = 99,
PubTime = new DateTime(2012, 10, 1)
};
var b3 = new Book
{
AuthorName = "吴军",
Title = "数学之美",
Price = 69,
PubTime = new DateTime(2020, 5, 1)
};
var b4 = new Book
{
AuthorName = "杨中科",
Title = "程序员的SQL金典",
Price = 52,
PubTime = new DateTime(2008, 9, 1)
};
var b5 = new Book
{
AuthorName = "吴军",
Title = "文明之光",
Price = 246,
PubTime = new DateTime(2017, 3, 1)
};
dbObject.Books.Add(b1);
dbObject.Books.Add(b2);
dbObject.Books.Add(b3);
dbObject.Books.Add(b4);
dbObject.Books.Add(b5);
await dbObject.SaveChangesAsync();
}
Console.WriteLine("数据处理完成");
}
}
}
- 过滤字段实例演示
using System;
using System.Threading.Tasks;
using System.Linq;
namespace ConsoleAppORM
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
var books = dbObject.Books.Where(obj => obj.Price > 80);
foreach (var book in books)
{
// 99 246
Console.WriteLine(book.Price);
}
}
Console.WriteLine("数据处理完成");
}
}
}
var book = dbObject.Books.Single(obj => obj.Title == "数学之美");
Console.WriteLine(book.Title);
var books = dbObject.Books.OrderBy(obj => obj.Price).Where(obj=>obj.Price<60);
foreach (var book in books)
{
Console.WriteLine(book.Price);
}
更新数据
演示
using System;
using System.Threading.Tasks;
using System.Linq;
namespace ConsoleAppORM
{
class Program
{
static async Task Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
var book = dbObject.Books.Single(obj => obj.Title == "数学之美");
book.AuthorName = "吴666666";
await dbObject.SaveChangesAsync();
}
Console.WriteLine("数据处理完成");
}
}
}
- 删除数据
var book = dbObject.Books.Single(obj => obj.Id == 5);
dbObject.Books.Remove(book);
- 批量更新数据: 旧版本不支持直接的批量更新api,新版本不清楚
var books = dbObject.Books.Where(obj => obj.Price > 10);
foreach (var book in books)
{
book.Price += 1;
}
配置类
的两种配置方式
- FluentAPI: 目前在用的,用法复杂但功能强大(推荐)
- Data Annotation: 以C#特性的形式标注在"实体类"中,用法简单,上手快,但是项目后期坑多!
[Table("T_Books")]
public class Book{...}
- 注意事项: 不建议混用,虽然能...
- 使用
特性
配置示例
......
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
......
namespace ConsoleAppORM
{
// 使用"特性配置"
[Table("T_Cats")]
class Cat
{
public int Id { get; set; }
// 使用"特性配置"
[Required]
[MaxLength(22)]
public string Name { get; set; }
}
}
// MyDbContext 注册一下
......
namespace ConsoleAppORM
{
class MyDbContext:DbContext
{
......
public DbSet<Dog> Dogs { get; set; }
// 注册
public DbSet<Cat> Cats { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
......
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
......
}
}
}
- 创建迁移记录并更新db,测试效果
- 使用
FluentAPI
演示
- 某些业务类的字段,不写入db演示
// Book.cs
......
namespace ConsoleAppORM
{
class Book
{
......
public string AuthorName { get; set; }
public int Age1 { get; set; } // 存入这个
public int Age2 { get; set; } // 忽略这个
}
}
// BookConfig.cs
......
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ConsoleAppORM
{
class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
......
builder.Property(b => b.AuthorName).HasMaxLength(20).IsRequired();
// 新增
builder.Ignore(b => b.Age2);
}
}
}
- 创建迁移记录并测试效果
- 对列的属性进行配置演示
......
namespace ConsoleAppORM
{
class Book
{
......
public string Name2 { get; set; }
}
}
......
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ConsoleAppORM
{
class BookConfig : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
......
builder.Property(b => b.Name2).HasColumnName("nameTwo")
.HasColumnType("varchar(8)").HasMaxLength(20);
}
}
}
- 创建迁移记录并测试效果
- 若想设置主键: builder.HasKey(b=>b.Name2)
- 若想设置默认值: builder.Property(b => b.Name).HasDefaultValue("Hello");
- 若想设置索引和复合索引:
- builder.HasIndex(b=>b.Title).IsUnique();
- builder.HasIndex(b=> new {b.Name2,b.AuthorName});
- 以下两种写法的区别
- 写法一的优点(推荐): 类型安全、编译时检查、支持重构、智能感知!
bullder.Property(b => b.Title)
.IsRequired()
.HasMaxLength(200)
.HasColumnName("Blog_Title");
- 写法二的优点(动态类型的最好选择): 根据条件来动态决定要配置哪些属性时,字符串是唯一的选择!
entity.Property("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnName("Blog_Title");
主键说明
- 聚集索引: 一个非常核心且重要的数据库概念,尤其在使用如 MySQL (InnoDB)、SQL Server 等关系型数据库时。
一、核心定义:什么是聚集索引?
聚集索引决定了表中数据的物理存储顺序。
你可以把数据库表想象成一本书:
- 聚集索引就像是这本书的目录结构,同时目录的顺序也直接决定了书页内容的排列顺序。比如你按章节顺序(第1章、第2章...)写目录,那么书的内容也必须严格按照这个顺序来印刷和装订。
- 正因为数据和索引是绑定在一起的,一个表只能有一个聚集索引,就像一本书只能有一种物理页码顺序。
关键点: 在聚集索引中,索引的叶子节点(B+Tree的最底层)直接存储了完整的数据行,而不是指向数据的指针。
二、一个生动的比喻:字典
-
《新华字典》拼音版(聚集索引):
- 这本字典的目录是按拼音
a, b, c, ... z
的顺序排列的。 - 当你通过拼音目录查到一个字在第100页时,你会发现字典正文本身也是严格按照拼音顺序
a, b, c, ... z
来印刷的。 - 这里,拼音目录就是聚集索引,它直接决定了数据(汉字和解释)的物理存放位置。
- 这本字典的目录是按拼音
-
《新华字典》部首检字版(非聚集索引):
- 这本书前面有一个部首目录,告诉你某个部首的字在拼音目录的第几页。
- 比如,你查“氵”部,它告诉你在拼音目录的第200页开始找。然后你翻到第200页,再在这个按拼音排序的列表中精确找到“江”字。
- 这里,部首目录就是一个非聚集索引,它本身不决定数据的物理顺序,只是一个“指向”真正数据位置(拼音顺序)的二级目录。
三、技术细节与特点
- 物理排序:表中的行数据按照聚集索引键的顺序存储在磁盘上。
- 唯一性:通常,聚集索引会创建在唯一性的列上(如主键),以避免数据库引擎需要为重复的键值添加额外的“唯一标识符”来维护行的唯一性。虽然也可以建在非唯一列上,但不推荐。
- 高效范围查询:由于数据物理上按顺序存放,对于范围查询(如
BETWEEN
,>
,<
)和ORDER BY
排序操作效率极高。数据库可以连续读取磁盘块,而不需要来回跳转。 - 一个表只有一个:这是由数据的物理存储方式决定的,一张表无法同时以两种物理顺序存储数据。
四、聚集索引 vs. 非聚集索引
特性 | 聚集索引 | 非聚集索引 |
---|---|---|
数量 | 每表一个 | 每表多个 |
存储内容 | 叶子节点存储整个数据行 | 叶子节点存储索引键值 + 指向数据行的指针(或聚集索引键) |
数据物理顺序 | 与索引顺序一致 | 与索引顺序无关 |
查询速度 | 对于主键/范围查询极快 | 通常比聚集索引慢,需要二次查找(回表) |
比喻 | 拼音目录的《新华字典》 | 部首检字表的《新华字典》 |
“回表”操作:当使用非聚集索引查询时,如果所需的列不在索引中,数据库需要根据叶子节点中的指针(在 InnoDB 中就是主键值)再次到聚集索引中查找完整的行数据。这个额外步骤就是“回表”,会影响性能。
五、最佳实践(通常如何选择聚集索引?)
- 主键默认是聚集索引:在 SQL Server 和 MySQL InnoDB 引擎中,如果你定义了主键(PRIMARY KEY),它默认就会成为表的聚集索引。这是最常见的情况。
- 选择原则:
- 唯一性:理想的聚集索引键是唯一的(如自增ID、GUID)。
- 递增性:使用自增整数(IDENTITY/AUTO_INCREMENT) 是最好的选择之一。因为新数据总是追加到末尾,避免了插入新数据时导致的页分裂(数据库需要重新组织数据页以维持顺序,这是一个昂贵的操作)。
- 静态性:键值不应频繁更新。更新聚集索引键意味着整个数据行可能都需要移动到新的物理位置。
- 常用于范围查询和排序的列:如日期列、有序的类别ID等。
- 避免的选择:
- 频繁更新的列:会导致大量的数据移动。
- 宽列(如很长的字符串):因为所有非聚集索引的叶子节点都会包含聚集索引键,如果聚集索引键很大,会使得所有非聚集索引也变得庞大。
总结
聚集索引就是表的物理数据本身,按照某个特定列(通常是主键)的顺序进行组织和存储。它像一个强大的内置排序和检索系统,是数据库性能的基石。 理解它对于设计高效的数据表结构和编写高性能的SQL查询至关重要。
- 在MySQL的InnoDB引擎中,需要频繁插入数据的表格中,不要用Guid来作为主键(性能很低)
- 在SQLServer中,不要把Guid主键设置为"聚集索引"
其他方案
ID+Guid
(非复合主键)- 物理表中,
ID
作为主键
,把Guid
当做业务逻辑上的主键
- 通俗理解:
ID
作为傀儡皇帝,Guid
掌握大权
- 通俗理解:
- 物理表中,
迁移
的其他命令
- Update-Database xxx:
回滚
或者升级
数据库版本 - 注意事项: 此时的
迁移脚本
不会被动到!
PM> Update-Database AddBookAge1AndIgnoreAge2 // 回滚
...
Build succeeded.
PM> Update-Database AddBookName2 // 升级
...
Build succeeded.
Remove_migration
: 删除最后一次的迁移脚本
- 注意事项: 如果此时的迁移脚本已经被应用于数据库,那么要先执行 Update-Database AddBookAge1AndIgnoreAge2,回滚到上一个数据库版本,然后再执行此命令,不然会异常,导致这条命名执行失败
Script-Migration
: 创建从版本A
到版本B
的SQL脚本
(可以在DB中直接执行)
PM> Script-Migration AddBookAge1AndIgnoreAge2 AddBookName2
...
Build succeeded.
BEGIN TRANSACTION;
GO
ALTER TABLE [T_Books] ADD [nameTwo] varchar(20) NULL;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20250822012846_AddBookName2', N'5.0.17');
GO
COMMIT;
GO
EF Core逆向工程
- 项目和数据库之间,先后存在的关系有三种
- DBFirst(数据库先存在): 比如接手老旧的项目,在此基础上更新,前面爽,后期坑多
- 注意事项: 这种生成实体类的方式,只能用一次,如果用第二次,对文件所做的任何更改,都将丢失
- DBFirst这种方式,能不用就不用
- ModelFirst
- CodeFirst (复杂项目,现在一般是这种形式): 前面痛苦,后期爽
- 先有数据库,然后生成
实体类
的实例演示
- 新建demo2数据库,新建T_demo表
- Id,Name,Age
- PM终端执行如下命令: Scaffold-DbContext "Server=.;Database=demo2;Trusted_Connection=True;MultipleActiveResultSets=true" Microsoft.EntityFrameworkCore.SqlServer
- 查看结果: 依据"T_demo"表生成了两个cs文件
- TDemo.cs: 业务类
- demo2Context.cs: 把"配置类"和"Context类"的业务合并到一起
- 缺点很明显,数据表一多,简直就是大杂烩...
- 我们必须手动把demo2Context的逻辑,拆分成之前熟悉的样子
- [配置类]
- [Context类]
- 知识点拓展: 安装库的便捷方式
- 把之前的项目的安装包的配置方式拷贝过来,然后运行程序即可自动安装
// your_project.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
......
</PropertyGroup>
<ItemGroup>
// 拷贝以下安装包
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.17">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
- 底层原理
- 通俗理解: 顾客去餐厅吃饭,并不需要专业的厨师技能,只需把需求和服务员沟通即可,由服务员和厨师之间沟通,从而完成点餐的过程
- 应用程序(顾客,开发者写的)
- ADO.NET Core(服务员,桥梁,微软提供)
- 数据库(厨师)
- 再加上ORM框架(Dapper、EF Core和FreeSql)
- 应用程序 → (选择性地使用) ORM框架 → (依赖于) ADO.NET Core → (与) 数据库 (通信)
监听SQL语句工具
好的,在 SQL Server Management Studio (SSMS) 中捕获正在执行的 SQL 查询有两种主流且强大的方法:一种是使用传统的 SQL Server Profiler,另一种是使用更现代的 扩展事件 (Extended Events)。
我将详细讲解这两种方法。
方法一:使用 SQL Server Profiler(经典工具,直观易用)
SQL Server Profiler 提供了一个图形化界面来实时监控数据库引擎中的所有活动,非常直观。
-
打开 Profiler
- 在开始菜单中找到并启动 SQL Server Profiler(它随 SSMS 一起安装)。
- 或者,也可以在 SSMS 内部:从顶部菜单栏选择 工具 -> SQL Server Profiler。
-
创建新跟踪
- 打开后,Profiler 会立即提示你连接服务器。输入你的数据库服务器地址和认证信息(如服务器名称
localhost
,身份验证选择“Windows 身份验证”),然后点击连接。
- 打开后,Profiler 会立即提示你连接服务器。输入你的数据库服务器地址和认证信息(如服务器名称
-
配置跟踪属性
-
连接成功后,会弹出“跟踪属性”窗口。
-
常规 选项卡:
- 跟踪名称:给你的跟踪起个名字,比如
MyApp Trace
。 - 使用模板:可以选择一个预置模板,例如
Standard
(标准)就很好用。
- 跟踪名称:给你的跟踪起个名字,比如
-
事件选择 选项卡(这是最关键的一步):
- 勾选 “显示所有事件” 和 “显示所有列”。
- 在事件列表中,找到并展开
Stored Procedures
类别。- 勾选
SP:StmtStarting
和SP:Completed
(用于跟踪存储过程中的语句)。
- 勾选
- 找到并展开
TSQL
类别。- 勾选
SQL:BatchStarting
和SQL:BatchCompleted
(这是最常用的,用于捕获客户端应用程序发送的 SQL 查询批处理)。 - 勾选
SQL:StmtStarting
和SQL:StmtCompleted
(用于更细粒度地捕获批处理中的每一个单独语句)。
- 勾选
- 推荐最小配置:通常只勾选
SQL:BatchCompleted
和SP:StmtCompleted
就足够了,这样可以捕获到所有已完成查询的最终文本和执行时间,避免输出过于冗长。
-
列筛选器(极其重要!)
- 为了避免捕获到过多无关信息(尤其是系统本身的查询),必须设置筛选器。
- 点击 “列筛选器...” 按钮。
- 在左侧列表中找到
DatabaseName
(数据库名称)。 - 在右侧的筛选中,展开 “类似于”,输入你想要监控的数据库名称(例如
MyDatabase
)。 - 可选:还可以过滤
ApplicationName
如果你的应用程序设置了特定的应用程序名称(例如,在连接字符串中设置Application Name=MyWebApp
)。 - 点击 确定。
-
-
运行跟踪
- 配置完成后,点击 “运行”。
- Profiler 窗口会开始实时显示捕获到的所有符合筛选条件的 SQL 查询。
-
分析结果
- 你会看到类似表格的界面,每一行代表一个被捕获的事件。
- 重要的列:
TextData
:包含实际的 SQL 查询语句。ApplicationName
:是哪个应用程序发送的查询。NTUserName
:是哪个 Windows 用户执行的。LoginName
:是哪个 SQL 登录名执行的。CPU
:查询消耗的 CPU 时间。Reads
/Writes
:逻辑读写次数(性能关键指标)。Duration
:查询执行耗时(单位是毫秒,这是分析慢查询最重要的指标)。StartTime
/EndTime
:开始和结束时间。
方法二:使用扩展事件 (Extended Events)(更轻量,性能更好,推荐用于生产环境)
扩展事件是微软推崇的下一代性能监控系统,它比 Profiler 对服务器性能影响更小,功能更强大。
-
在 SSMS 中打开扩展事件
- 在 SSMS 的对象资源管理器中,连接到你的服务器。
- 展开 管理 -> 扩展事件。
- 在 会话 上右键单击,选择 新建会话...。
-
配置新会话
-
常规 页:输入一个会话名称,例如
Capture_Queries
。 -
事件 页:点击“添加”按钮。
- 在事件库中,搜索并添加以下关键事件:
sql_batch_completed
rpc_completed
(用于捕获存储过程调用等)
- 点击 确定。
- 在事件库中,搜索并添加以下关键事件:
-
数据存储 页:可以选择将事件数据存储在文件中(便于后期分析)或仅实时查看。
-
筛选器 页(同样关键!):
- 点击“添加”按钮添加筛选器。
- 选择字段
sqlserver.database_name
。 - 操作符选择
like
,值填入你的数据库名(如MyDatabase
)。 - 点击 确定。
-
-
运行会话并查看数据
- 配置完成后,可以选择 “在创建后立即启动事件会话”,然后点击 确定。
- 会话创建后,在 “会话” 文件夹下找到你的会话(如
Capture_Queries
),右键单击它,选择 “查看实时数据”。 - 一个新的选项卡会打开,像 Profiler 一样实时显示捕获到的 SQL 查询。
总结与建议
特性 | SQL Server Profiler | 扩展事件 (Extended Events) |
---|---|---|
易用性 | 非常容易,图形化界面直观 | 稍复杂,但 SSMS 提供了图形向导 |
性能影响 | 较大,不建议在生产环境长时间使用 | 非常小,是生产环境的推荐选择 |
功能强度 | 基础监控,足够常用 | 极其强大,可定制性极高 |
推荐场景 | 开发和测试环境的快速、临时性抓取 | 所有环境,尤其是生产环境的监控和性能诊断 |
给你的建议:
- 如果你是临时想看一下应用程序生成了什么 SQL,或者在做简单的调试,使用 SQL Server Profiler 最快最方便。
- 如果你需要在生产服务器上进行监控,或者需要更强大的分析功能,请务必学习并使用 扩展事件。
EF Core的坑
坑一
: 存在C#
语法正确,但是无法翻译成SQL语句
的情况,比如自定义方法
using System;
using System.Threading.Tasks;
using System.Linq;
namespace ConsoleAppORM
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
// 传入自定义方法
var books = dbObject.Books.Where(obj => test(obj.AuthorName));
foreach (var book in books)
{
Console.WriteLine(book);
}
}
Console.WriteLine("数据处理完成");
}
// 自定义方法
static bool test(string s)
{
return s.Contains("杨");
}
}
}
- 结果: 运行报错,EF会尝试将Lambda表达式转换为SQL语句,但EF的查询转换器无法识别自定义方法test(),因此抛出异常!
System.InvalidOperationException:“The LINQ expression 'DbSet<Book>()
.Where(b => Program.test(b.AuthorName))' could not be translated...
- 注意事项: 高阶的EF版本,可能会支持
- 以下代码,依然报错
var books = dbObject.Books.Where(obj => obj.AuthorName.PadLeft(5) == "杨xxxx");
foreach (var book in books)
{
Console.WriteLine(book);
}
- EF 只能转换有限的、有直接 SQL 对应的方法(如 Contains, StartsWith, EndsWith )等
用代码的方式输出SQL语句(不使用图形化工具)
- 标准日志配置Demo
// MyDbContext.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using System;
namespace ConsoleAppORM
{
class MyDbContext:DbContext
{
// 新增静态字段,生成日志对象(静态字段,意味着它在所有 MyDbContext 实例之间共享)
public static ILoggerFactory loggerFactory = LoggerFactory.Create(b => b.AddConsole());
public DbSet<Book> Books { get; set; }
......
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
......
// 启用日志记录
optionsBuilder.UseLoggerFactory(loggerFactory);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
}
// 主程序
......
namespace ConsoleAppORM
{
class Program
{
static void Main(string[] args)
{
using (MyDbContext dbObject = new MyDbContext())
{
var books = dbObject.Books.OrderBy(obj => obj.Price).Where(obj => obj.Price > 0);
foreach (var book in books)
{
Console.WriteLine(book.Price);
}
}
Console.WriteLine("数据处理完成");
}
}
}
- 执行结果
info: ......
SELECT [t].[Id], [t].[Age1], [t].[AuthorName], [t].[nameTwo], [t].[Price], [t].[PubTime], [t].[Title]
FROM [T_Books] AS [t]
WHERE [t].[Price] > 0.0E0
ORDER BY [t].[Price]
62
69.8
79
109
数据处理完成
- 使用
简单日志
的方式- 注意事项: 这种输入方式会非常啰嗦,最好加过滤...
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
......
// 简单输出
optionsBuilder.LogTo(msg=> {
if (!msg.Contains("CommandExecuting")) return;
Console.WriteLine(msg);
});
}
- 输出结果:
Security Warning: The negotiated TLS 1.0 is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later.
dbug: 2025/8/25 10:05:55.618 RelationalEventId.CommandExecuting[20100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [t].[Id], [t].[Age1], [t].[AuthorName], [t].[nameTwo], [t].[Price], [t].[PubTime], [t].[Title]
FROM [T_Books] AS [t]
WHERE [t].[Price] > 0.0E0
ORDER BY [t].[Price]
62
69.8
79
109
数据处理完成
ToQueryString()
方法:用于获取表示 LINQ 查询的 SQL 字符串,而无需实际执行查询。它返回 EF Core 将要发送到数据库的确切 SQL 命令
- 使用限制:
- 只能用于查询
- 对于已执行的查询:一旦查询已执行(如调用了 ToList(), First(), Count() 等),不能再使用 ToQueryString()
var executedQuery = dbContext.Books.ToList(); // 查询已执行
executedQuery.ToQueryString(); // 这会报错
- 非 IQueryable 对象:只能用于 IQueryable<T> 类型,不能用于 IEnumerable<T> 或已物化的集合
var enumerable = dbContext.Books.AsEnumerable(); // 转换为 IEnumerable
enumerable.ToQueryString(); // 这会报错
- 某些复杂查询:极少数非常复杂的查询可能无法转换为 SQL 字符串
......
var books = dbObject.Books.OrderBy(obj => obj.Price).Where(obj => obj.Price > 0);
Console.WriteLine(books.ToQueryString());
......
- 返回结果:
SELECT [t].[Id], [t].[Age1], [t].[AuthorName], [t].[nameTwo], [t].[Price], [t].[PubTime], [t].[Title]
FROM [T_Books] AS [t]
WHERE [t].[Price] > 0.0E0
ORDER BY [t].[Price]
数据处理完成...
小结:ToQueryString()
是一个非常有用的调试工具,适用于几乎所有类型的 EF Core 查询,只要这些查询尚未执行且仍然是 IQueryable
类型,它是开发和优化数据库查询的宝贵工具.
- 写测试性代码: 用简单日志
- 给DBA或者审核: 用标准日志
- 开发阶段,从繁琐的操作中想立即看到SQL,用ToQueryString()方法
数据库的方言
- 简而言之,不同的
数据库
语法不同(大体相同,某些细节不同) - 在同一个项目中,若想为不同的数据库生成不同的迁移脚本,可使用以下命令
- 注意事项: 一个项目中,用不同的数据库来存储,少见...
PM> Add-Migration -OutputDir
// 比如这句,SQLServer和MySQL的语法就不一样了
var books = dbObject.Books.OrderBy(obj => obj.Price).Where(obj => obj.Price > 0).Take(3);
foreach (var book in books)
{
Console.WriteLine(book.Price);
}
- MySQL演示
- 安装
MySQL驱动
- Install-Package Pomelo.EntityFrameworkCore.MySql -Version 5.0.4
......
namespace ConsoleAppORM
{
class MyDbContext:DbContext
{
......
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);
// string connStr = "Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true";
// optionsBuilder.UseSqlServer(connStr
// 使用MySQL
string connStr = "Server=localhost;Database=dot_net_tests;user=root;password=root";
var serverVersion = new MySqlServerVersion(new Version(5,7,22));
optionsBuilder.UseMySql(connStr,serverVersion);
optionsBuilder.LogTo(Console.WriteLine);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
......
}
}
}
- 结果:
......
SELECT `t`.`Id`, `t`.`Age1`, `t`.`AuthorName`, `t`.`nameTwo`, `t`.`Price`, `t`.`PubTime`, `t`.`Title`
FROM `T_Books` AS `t`
WHERE `t`.`Price` > 0.0
ORDER BY `t`.`Price`
LIMIT @__p_0 // 和SQLServer不同,它用Take
- 注意事项: 不管使用哪种
数据库
,只是DbContext
代码变了而已,其他部分一样的(暂时这么认为)
- 如果这里用的是"PostgreSQL",可以安装 Npgsql.EntityFrameworkCore.PostgreSQL
- optionsBuilder.UseNpgsql("Host=127.0.0.1;Database=dot_net_tests;user=root;password=root");