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 身份验证

Sqlserver

  • 如果表的结构有变更,需重新创建迁移记录并更新数据库
......
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的最底层)直接存储了完整的数据行,而不是指向数据的指针。


二、一个生动的比喻:字典

  1. 《新华字典》拼音版(聚集索引)

    • 这本字典的目录是按拼音 a, b, c, ... z 的顺序排列的。
    • 当你通过拼音目录查到一个字在第100页时,你会发现字典正文本身也是严格按照拼音顺序 a, b, c, ... z 来印刷的
    • 这里,拼音目录就是聚集索引,它直接决定了数据(汉字和解释)的物理存放位置。
  2. 《新华字典》部首检字版(非聚集索引)

    • 这本书前面有一个部首目录,告诉你某个部首的字在拼音目录的第几页。
    • 比如,你查“氵”部,它告诉你在拼音目录的第200页开始找。然后你翻到第200页,再在这个按拼音排序的列表中精确找到“江”字。
    • 这里,部首目录就是一个非聚集索引,它本身不决定数据的物理顺序,只是一个“指向”真正数据位置(拼音顺序)的二级目录。

三、技术细节与特点

  1. 物理排序:表中的行数据按照聚集索引键的顺序存储在磁盘上。
  2. 唯一性:通常,聚集索引会创建在唯一性的列上(如主键),以避免数据库引擎需要为重复的键值添加额外的“唯一标识符”来维护行的唯一性。虽然也可以建在非唯一列上,但不推荐。
  3. 高效范围查询:由于数据物理上按顺序存放,对于范围查询(如 BETWEEN, >, <)和 ORDER BY 排序操作效率极高。数据库可以连续读取磁盘块,而不需要来回跳转。
  4. 一个表只有一个:这是由数据的物理存储方式决定的,一张表无法同时以两种物理顺序存储数据。

四、聚集索引 vs. 非聚集索引

特性 聚集索引 非聚集索引
数量 每表一个 每表多个
存储内容 叶子节点存储整个数据行 叶子节点存储索引键值 + 指向数据行的指针(或聚集索引键)
数据物理顺序 与索引顺序一致 与索引顺序无关
查询速度 对于主键/范围查询极快 通常比聚集索引慢,需要二次查找(回表)
比喻 拼音目录的《新华字典》 部首检字表的《新华字典》

“回表”操作:当使用非聚集索引查询时,如果所需的列不在索引中,数据库需要根据叶子节点中的指针(在 InnoDB 中就是主键值)再次到聚集索引中查找完整的行数据。这个额外步骤就是“回表”,会影响性能。


五、最佳实践(通常如何选择聚集索引?)

  1. 主键默认是聚集索引:在 SQL Server 和 MySQL InnoDB 引擎中,如果你定义了主键(PRIMARY KEY),它默认就会成为表的聚集索引。这是最常见的情况。
  2. 选择原则
    • 唯一性:理想的聚集索引键是唯一的(如自增ID、GUID)。
    • 递增性:使用自增整数(IDENTITY/AUTO_INCREMENT) 是最好的选择之一。因为新数据总是追加到末尾,避免了插入新数据时导致的页分裂(数据库需要重新组织数据页以维持顺序,这是一个昂贵的操作)。
    • 静态性:键值不应频繁更新。更新聚集索引键意味着整个数据行可能都需要移动到新的物理位置。
    • 常用于范围查询和排序的列:如日期列、有序的类别ID等。
  3. 避免的选择
    • 频繁更新的列:会导致大量的数据移动。
    • 宽列(如很长的字符串):因为所有非聚集索引的叶子节点都会包含聚集索引键,如果聚集索引键很大,会使得所有非聚集索引也变得庞大。

总结

聚集索引就是表的物理数据本身,按照某个特定列(通常是主键)的顺序进行组织和存储。它像一个强大的内置排序和检索系统,是数据库性能的基石。 理解它对于设计高效的数据表结构和编写高性能的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版本BSQL脚本(可以在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 → (与) 数据库 (通信)

deepseek_mermaid_20250822_e9d83d

监听SQL语句工具

好的,在 SQL Server Management Studio (SSMS) 中捕获正在执行的 SQL 查询有两种主流且强大的方法:一种是使用传统的 SQL Server Profiler,另一种是使用更现代的 扩展事件 (Extended Events)

我将详细讲解这两种方法。


方法一:使用 SQL Server Profiler(经典工具,直观易用)

SQL Server Profiler 提供了一个图形化界面来实时监控数据库引擎中的所有活动,非常直观。

  1. 打开 Profiler

    • 在开始菜单中找到并启动 SQL Server Profiler(它随 SSMS 一起安装)。
    • 或者,也可以在 SSMS 内部:从顶部菜单栏选择 工具 -> SQL Server Profiler
  2. 创建新跟踪

    • 打开后,Profiler 会立即提示你连接服务器。输入你的数据库服务器地址和认证信息(如服务器名称 localhost,身份验证选择“Windows 身份验证”),然后点击连接
  3. 配置跟踪属性

    • 连接成功后,会弹出“跟踪属性”窗口。

    • 常规 选项卡:

      • 跟踪名称:给你的跟踪起个名字,比如 MyApp Trace
      • 使用模板:可以选择一个预置模板,例如 Standard(标准)就很好用。
    • 事件选择 选项卡(这是最关键的一步):

      • 勾选 “显示所有事件”“显示所有列”
      • 在事件列表中,找到并展开 Stored Procedures 类别。
        • 勾选 SP:StmtStartingSP:Completed(用于跟踪存储过程中的语句)。
      • 找到并展开 TSQL 类别。
        • 勾选 SQL:BatchStartingSQL:BatchCompleted(这是最常用的,用于捕获客户端应用程序发送的 SQL 查询批处理)。
        • 勾选 SQL:StmtStartingSQL:StmtCompleted(用于更细粒度地捕获批处理中的每一个单独语句)。
      • 推荐最小配置:通常只勾选 SQL:BatchCompletedSP:StmtCompleted 就足够了,这样可以捕获到所有已完成查询的最终文本和执行时间,避免输出过于冗长。
    • 列筛选器(极其重要!)

      • 为了避免捕获到过多无关信息(尤其是系统本身的查询),必须设置筛选器。
      • 点击 “列筛选器...” 按钮。
      • 在左侧列表中找到 DatabaseName(数据库名称)。
      • 在右侧的筛选中,展开 “类似于”,输入你想要监控的数据库名称(例如 MyDatabase)。
      • 可选:还可以过滤 ApplicationName 如果你的应用程序设置了特定的应用程序名称(例如,在连接字符串中设置 Application Name=MyWebApp)。
      • 点击 确定
  4. 运行跟踪

    • 配置完成后,点击 “运行”
    • Profiler 窗口会开始实时显示捕获到的所有符合筛选条件的 SQL 查询。
  5. 分析结果

    • 你会看到类似表格的界面,每一行代表一个被捕获的事件。
    • 重要的列
      • TextData:包含实际的 SQL 查询语句。
      • ApplicationName:是哪个应用程序发送的查询。
      • NTUserName:是哪个 Windows 用户执行的。
      • LoginName:是哪个 SQL 登录名执行的。
      • CPU:查询消耗的 CPU 时间。
      • Reads / Writes:逻辑读写次数(性能关键指标)。
      • Duration:查询执行耗时(单位是毫秒,这是分析慢查询最重要的指标)。
      • StartTime / EndTime:开始和结束时间。

方法二:使用扩展事件 (Extended Events)(更轻量,性能更好,推荐用于生产环境)

扩展事件是微软推崇的下一代性能监控系统,它比 Profiler 对服务器性能影响更小,功能更强大。

  1. 在 SSMS 中打开扩展事件

    • 在 SSMS 的对象资源管理器中,连接到你的服务器。
    • 展开 管理 -> 扩展事件
    • 会话 上右键单击,选择 新建会话...
  2. 配置新会话

    • 常规 页:输入一个会话名称,例如 Capture_Queries

    • 事件 页:点击“添加”按钮。

      • 在事件库中,搜索并添加以下关键事件:
        • sql_batch_completed
        • rpc_completed (用于捕获存储过程调用等)
      • 点击 确定
    • 数据存储 页:可以选择将事件数据存储在文件中(便于后期分析)或仅实时查看。

    • 筛选器 页(同样关键!):

      • 点击“添加”按钮添加筛选器。
      • 选择字段 sqlserver.database_name
      • 操作符选择 like,值填入你的数据库名(如 MyDatabase)。
      • 点击 确定
  3. 运行会话并查看数据

    • 配置完成后,可以选择 “在创建后立即启动事件会话”,然后点击 确定
    • 会话创建后,在 “会话” 文件夹下找到你的会话(如 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");
posted @ 2025-08-13 11:50  清安宁  阅读(44)  评论(0)    收藏  举报