解锁C# EF/EF Core:从入门到进阶的技术飞跃
一、EF/EF Core 初相识
在.NET 开发的广阔天地中,Entity Framework (EF) 及其轻量级、可扩展、跨平台的版本 Entity Framework Core (EF Core),犹如两颗璀璨的明星,照亮了数据访问层开发的道路。它们是开源的对象关系映射器(ORM),赋予开发者以面向对象的优雅方式,与数据库进行交互,彻底告别繁琐的 SQL 语句编写。
EF 诞生于微软的技术实验室,自问世以来,便在.NET 生态系统中占据着举足轻重的地位。它为开发者提供了一种强大的抽象层,使得数据库操作变得如同操作对象一般自然流畅。随着技术的不断演进,EF Core 应运而生,它继承了 EF 的优秀基因,同时针对现代开发需求进行了全面升级。凭借其跨平台特性,EF Core 能够轻松适配多种操作系统,包括 Windows、Linux 和 macOS,为开发者提供了更加灵活的开发环境。
与其他 ORM 框架相比,EF/EF Core 拥有诸多显著优势。其强大的代码生成能力,可根据数据模型自动生成数据库表结构,反之亦然,极大地提高了开发效率。EF/EF Core 对 LINQ 查询的完美支持,使得开发者能够使用统一的语法进行数据查询,无论数据源是内存集合还是数据库,都能轻松应对。这种一致性不仅降低了学习成本,还使得代码更加简洁易读,易于维护。
在性能优化方面,EF/EF Core 也毫不逊色。通过合理的配置和优化策略,能够显著提升数据库访问的性能。例如,通过启用延迟加载,只有在真正需要时才会从数据库中加载相关数据,避免了不必要的性能开销;而预先加载技术则可以一次性获取所有相关数据,减少了数据库查询的次数,提高了查询效率。EF/EF Core 还支持多种缓存机制,如内存缓存、分布式缓存等,进一步提升了应用程序的性能和响应速度。
EF/EF Core 在事务处理方面也表现出色。它提供了简洁而强大的事务管理功能,确保在一系列数据库操作中,要么所有操作都成功提交,要么所有操作都回滚,从而保证了数据的一致性和完整性。无论是简单的单表操作,还是复杂的多表关联事务,EF/EF Core 都能轻松胜任。
在数据迁移方面,EF/EF Core 更是提供了便捷的工具和方法。通过代码优先或数据库优先的方式,开发者可以轻松地对数据库结构进行版本控制和迁移。无论是新增表、修改字段,还是删除数据,都可以通过简单的命令或代码实现,极大地简化了数据库的管理和维护工作。
EF/EF Core 凭借其强大的功能、卓越的性能和良好的可扩展性,成为了.NET 开发者进行数据访问层开发的首选框架。在接下来的内容中,我们将深入探索 EF/EF Core 的高级特性和应用场景,带您领略其无穷的魅力。
二、EF/EF Core 快速搭建
2.1 环境准备
要在项目中顺利使用 EF/EF Core,首先得打造一个适配的开发环境。Visual Studio 作为广大.NET 开发者的首选 IDE,其重要性不言而喻。请确保你安装的是最新版本的 Visual Studio,它不仅能为我们提供丰富的开发工具和功能,还能确保对最新的.NET 技术和框架的完美支持。
在安装 Visual Studio 时,要特别留意工作负载的选择。对于 EF/EF Core 开发,建议勾选 “ASP.NET和 Web 开发” 以及 “.NET 桌面开发” 等相关工作负载,这些工作负载将为我们提供开发所需的各种工具和库,大大提高开发效率。
别忽视了.NET SDK 的安装。它是.NET 开发的基石,为我们提供了运行和开发.NET 应用程序所必需的工具和库。同样,安装最新版本的.NET SDK 是明智之举,新版本通常会带来性能的提升、功能的增强以及对新特性的支持。
2.2 项目初始化
当开发环境准备就绪,就可以着手创建新项目了。在 Visual Studio 中,创建新项目的过程非常简单。点击 “文件” 菜单,选择 “新建”,再点击 “项目”。在弹出的 “新建项目” 对话框中,你可以根据实际需求选择创建 “Console App” 或 “Web App” 项目。
若你想创建一个简单的控制台应用程序来测试 EF/EF Core 的功能,那么 “Console App” 项目是个不错的选择。它轻量级、简洁,能让你快速专注于 EF/EF Core 的核心操作。若你计划开发一个 Web 应用程序,无论是基于ASP.NET Core 的 Web API,还是传统的 MVC 应用,“Web App” 项目则更为合适。
以创建 “Console App” 项目为例,在 “新建项目” 对话框中,搜索 “Console App”,选择对应的模板后,点击 “下一步”。在接下来的页面中,你可以为项目命名、选择项目的存储位置,并根据需要设置其他选项。完成设置后,点击 “创建” 按钮,一个全新的控制台应用项目就创建好了。
项目创建完成后,还需要为其添加 EF/EF Core 包。这一步至关重要,它将为项目引入 EF/EF Core 的核心功能和依赖。在 Visual Studio 中,右键点击项目名称,在弹出的菜单中选择 “管理 NuGet 包”。在 NuGet 包管理器中,搜索 “Microsoft.EntityFrameworkCore” 和 “Microsoft.EntityFrameworkCore.SqlServer”(若使用 SQL Server 数据库)。找到对应的包后,点击 “安装” 按钮,Visual Studio 会自动下载并安装这些包及其依赖项。
安装完成后,你可以在项目的 “依赖项” 中看到刚刚安装的 EF/EF Core 相关包。这表明你的项目已经成功集成了 EF/EF Core,接下来就可以开始使用它们来构建强大的数据访问层了。
三、EF/EF Core 实战进阶
3.1 精巧设计数据模型
在 EF/EF Core 的世界里,数据模型是与数据库交互的基石。以一个简单的博客系统为例,我们通常会有User和Post两个实体类。
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// 导航属性,用于表示用户与文章的关系,一个用户可以有多篇文章
public ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
// 外键,用于关联用户表
public int UserId { get; set; }
// 导航属性,用于表示文章的作者,一篇文章属于一个用户
public User Author { get; set; }
}
在上述代码中,User类的Posts属性和Post类的Author属性就是导航属性。导航属性的作用至关重要,它不仅能清晰地表达实体之间的关系,还能在查询数据时发挥巨大的作用。通过导航属性,我们可以轻松地获取一个用户所发表的所有文章,或者一篇文章的作者信息,而无需编写复杂的 SQL 连接语句。
3.2 高效配置 DbContext
DbContext 是 EF/EF Core 的核心类之一,它就像是一个桥梁,连接着我们的应用程序和数据库。定义和配置 DbContext 时,我们需要考虑多个方面。
public class MyDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 设置数据库连接字符串,这里使用SQL Server
optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=MyDb;Trusted_Connection=True;");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 使用Fluent API配置实体关系
modelBuilder.Entity<User>()
.HasMany(u => u.Posts)
.WithOne(p => p.Author)
.HasForeignKey(p => p.UserId);
base.OnModelCreating(modelBuilder);
}
}
在OnConfiguring方法中,我们通过optionsBuilder.UseSqlServer方法设置了数据库连接字符串。这个连接字符串包含了数据库服务器的地址、数据库名称以及连接方式等重要信息。在OnModelCreating方法中,我们使用 Fluent API 对实体关系进行了配置。这里定义了User和Post之间的一对多关系,即一个用户可以拥有多篇文章,而一篇文章只属于一个用户。
3.3 稳固连接数据库
在ASP.NET Core 应用程序中,我们通常在Startup.cs文件中添加 DbContext,以确保数据库连接的可靠性。
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
}
在上述代码中,AddDbContext方法用于将MyDbContext注册到依赖注入容器中。通过options.UseSqlServer方法,我们再次指定了数据库连接字符串,这里的连接字符串是从配置文件中读取的,这样可以提高配置的灵活性和可维护性。
3.4 灵活迁移数据库
数据模型在开发过程中难免会发生变化,这时候就需要迁移数据库来同步这些变化。EF/EF Core 提供了强大的迁移工具,让我们可以轻松应对这种情况。
# 添加迁移
dotnet ef migrations add InitialCreate
# 更新数据库
dotnet ef database update
dotnet ef migrations add命令用于添加一个新的迁移,它会根据数据模型的变化生成相应的迁移脚本。在执行这个命令时,我们需要给迁移起一个有意义的名字,比如InitialCreate,这个名字将用于标识这次迁移。dotnet ef database update命令则用于将这些迁移脚本应用到数据库中,从而更新数据库的结构,使其与数据模型保持一致。
3.5 流畅 CRUD 操作
增、删、改、查(CRUD)操作是数据库操作的核心。在 EF/EF Core 中,实现这些操作非常简单。
public async Task AddUser(User user)
{
using (var db = new MyDbContext())
{
await db.Users.AddAsync(user);
await db.SaveChangesAsync();
}
}
public async Task<List<User>> GetAllUsers()
{
using (var db = new MyDbContext())
{
return await db.Users.ToListAsync();
}
}
public async Task<User> GetUserById(int id)
{
using (var db = new MyDbContext())
{
return await db.Users.FindAsync(id);
}
}
public async Task UpdateUser(User user)
{
using (var db = new MyDbContext())
{
db.Entry(user).State = EntityState.Modified;
await db.SaveChangesAsync();
}
}
public async Task DeleteUser(User user)
{
using (var db = new MyDbContext())
{
db.Users.Remove(user);
await db.SaveChangesAsync();
}
}
在上述代码中,AddUser方法用于向数据库中添加一个新用户,GetAllUsers方法用于获取所有用户,GetUserById方法用于根据用户 ID 获取单个用户,UpdateUser方法用于更新用户信息,DeleteUser方法用于删除用户。这些方法都使用了MyDbContext来进行数据库操作,并通过await关键字实现了异步操作,提高了应用程序的性能和响应性。
3.6 强大的 LINQ 查询
LINQ(Language Integrated Query)是 EF/EF Core 中进行数据查询的强大工具。它允许我们使用类似 SQL 的语法来查询数据,同时又保持了.NET 语言的特性。
public async Task<List<User>> GetUsersWithPosts()
{
using (var db = new MyDbContext())
{
return await db.Users.Include(u => u.Posts).ToListAsync();
}
}
public async Task<List<User>> GetUsersWithEmailDomain(string domain)
{
using (var db = new MyDbContext())
{
return await db.Users.Where(u => u.Email.EndsWith(domain)).ToListAsync();
}
}
在GetUsersWithPosts方法中,我们使用Include方法来加载用户及其相关的文章,这是一种预加载技术,可以避免在后续访问文章数据时产生额外的数据库查询。在GetUsersWithEmailDomain方法中,我们使用Where方法来筛选出邮箱地址以指定域名结尾的用户,实现了条件查询。
3.7 巧用包含关系
在查询数据时,我们经常需要获取相关联的数据。EF/EF Core 提供了Include和ThenInclude方法,让我们可以轻松实现数据的深度加载。
public async Task<List<User>> GetUsersWithPostsAndComments()
{
using (var db = new MyDbContext())
{
return await db.Users
.Include(u => u.Posts)
.ThenInclude(p => p.Comments)
.ToListAsync();
}
}
在上述代码中,Include(u => u.Posts)用于加载用户的文章,ThenInclude(p => p.Comments)则用于在加载文章的基础上,进一步加载每篇文章的评论。通过这种方式,我们可以一次性获取用户及其文章和评论的所有数据,大大提高了查询效率。
3.8 复杂查询的艺术
除了基本的查询操作,EF/EF Core 还支持复杂查询,以满足复杂业务需求。
public async Task<List<User>> GetUsersWithMoreThanFivePosts()
{
using (var db = new MyDbContext())
{
return await db.Users
.Where(u => u.Posts.Count > 5)
.ToListAsync();
}
}
在这个例子中,我们使用Where方法结合Posts.Count > 5条件,筛选出拥有超过五篇文章的用户。这是一个简单的聚合查询示例,展示了 EF/EF Core 在处理复杂查询时的强大能力。通过这种方式,我们可以根据具体的业务需求,灵活编写各种复杂的查询逻辑。
3.9 存储过程的运用
在某些情况下,使用存储过程可以提高数据处理效率。在 EF/EF Core 中,调用存储过程也很方便。
public async Task<List<User>> ExecuteStoredProcedure()
{
using (var db = new MyDbContext())
{
var users = await db.Set<User>()
.FromSqlInterpolated($"EXEC dbo.GetUsers")
.ToListAsync();
return users;
}
}
在上述代码中,FromSqlInterpolated方法用于执行存储过程dbo.GetUsers,并将结果映射到User实体集合中。通过这种方式,我们可以充分利用存储过程的优势,如提高性能、增强安全性等。
3.10 事务处理的奥秘
事务是确保数据一致性和完整性的重要手段。在 EF/EF Core 中,实现事务操作也非常简单。
public async Task<bool> PerformTransaction()
{
using (var db = new MyDbContext())
{
try
{
var user = new User { Name = "Alice", Email = "alice@example.com" };
await db.Users.AddAsync(user);
await db.SaveChangesAsync();
var post = new Post { Title = "First Post", Content = "Content of the first post", Author = user };
await db.Posts.AddAsync(post);
await db.SaveChangesAsync();
return true;
}
catch (Exception ex)
{
await db.Database.RollbackTransactionAsync();
return false;
}
}
}
在PerformTransaction方法中,我们首先创建了一个新用户,并将其添加到数据库中。接着,我们创建了一篇文章,并将其与刚才创建的用户关联起来,也添加到数据库中。这两个操作都在一个事务中进行。如果在执行过程中发生任何异常,catch块中的代码会捕获异常,并调用RollbackTransactionAsync方法回滚事务,确保数据库状态不会被不一致地修改。如果所有操作都成功完成,事务将被自动提交,数据将被持久化到数据库中。通过这种方式,我们可以确保在一系列相关的数据库操作中,要么所有操作都成功执行,要么所有操作都被回滚,从而保证数据的一致性和完整性。
四、EF/EF Core 高级特性
4.1 配置文件的运用
在实际项目中,数据库连接字符串等配置信息经常需要根据不同的环境进行调整。使用配置文件可以将这些配置信息集中管理,提高项目的可维护性。在ASP.NET Core 项目中,我们通常使用appsettings.json文件来存储配置信息。
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDb;Trusted_Connection=True;"
}
}
在Startup.cs文件中,我们可以通过以下方式读取连接字符串:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<MyDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
}
}
通过这种方式,当我们需要切换数据库服务器或者更改数据库名称时,只需要修改appsettings.json文件中的连接字符串,而无需修改代码。
4.2 Code First Migrations 的深入
Code First Migrations 是 EF/EF Core 中非常强大的功能,它允许我们通过代码来管理数据库的迁移。在前面的基础操作中,我们已经了解了如何使用dotnet ef migrations add和dotnet ef database update命令来进行简单的迁移。实际上,Code First Migrations 还有更多的高级用法。
我们可以自定义迁移脚本。在添加迁移时,EF/EF Core 会自动生成一个迁移类,其中包含Up和Down方法。Up方法用于将数据库更新到新的状态,而Down方法则用于将数据库回滚到上一个状态。我们可以手动编辑这些方法,以实现更复杂的数据库更改。
假设我们需要在数据库中添加一个新的表,并且在表中插入一些初始数据。我们可以在迁移类的Up方法中添加如下代码:
public partial class AddNewTable : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.NewTable",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String()
})
.PrimaryKey(t => t.Id);
// 插入初始数据
Sql("INSERT INTO dbo.NewTable (Name) VALUES ('Data 1')");
Sql("INSERT INTO dbo.NewTable (Name) VALUES ('Data 2')");
}
public override void Down()
{
DropTable("dbo.NewTable");
}
}
在上述代码中,我们首先使用CreateTable方法创建了一个新的表NewTable,然后使用Sql方法插入了两条初始数据。在Down方法中,我们使用DropTable方法删除了这个表,以实现回滚操作。
4.3 Fluent API 的强大功能
Fluent API 是 EF/EF Core 中用于配置实体和关系的一种强大方式。除了前面介绍的基本用法外,Fluent API 还支持许多高级功能。
我们可以使用 Fluent API 来配置主键、索引和唯一约束。在OnModelCreating方法中,我们可以通过以下方式进行配置:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasKey(u => u.Id);
modelBuilder.Entity<User>()
.HasIndex(u => u.Email)
.IsUnique();
modelBuilder.Entity<User>()
.Property(u => u.Name)
.HasMaxLength(100);
}
在上述代码中,HasKey方法用于配置主键,HasIndex方法用于创建索引,并且通过IsUnique方法设置为唯一索引,Property方法用于配置属性的最大长度。通过这些配置,我们可以更精确地控制数据库表的结构和约束。
4.4 数据注解的妙用
数据注解是另一种配置实体和关系的方式,它通过在实体类的属性上添加特性来实现。常用的数据注解有[Required]、[EmailAddress]、[StringLength]等。
public class User
{
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
[EmailAddress]
public string Email { get; set; }
public ICollection<Post> Posts { get; set; }
}
在上述代码中,[Required]表示该属性是必需的,不能为空;[StringLength(50)]表示该属性的最大长度为 50 个字符;[EmailAddress]表示该属性必须是一个有效的电子邮件地址。这些数据注解不仅可以在数据库层面添加约束,还可以在应用程序中用于数据验证。
4.5 种子数据的填充
在数据库初始化时,填充一些种子数据可以方便我们进行开发和测试。在 EF/EF Core 中,我们可以在OnModelCreating方法中使用HasData方法来填充种子数据。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().HasData(
new User { Id = 1, Name = "Alice", Email = "alice@example.com" },
new User { Id = 2, Name = "Bob", Email = "bob@example.com" }
);
}
在上述代码中,我们为User表添加了两条种子数据。当我们执行数据库迁移时,这些数据会自动插入到数据库中。
4.6 异步编程的实践
在 EF/EF Core 中,许多操作都支持异步方法。使用异步方法可以提高应用程序的性能和响应性,特别是在处理 I/O 密集型操作时。
public async Task<List<User>> GetAllUsersAsync()
{
using (var db = new MyDbContext())
{
return await db.Users.ToListAsync();
}
}
在上述代码中,ToListAsync方法是一个异步方法,它会在后台线程中执行数据库查询操作,而不会阻塞主线程。这样,在查询数据库的同时,应用程序可以继续处理其他任务,提高了整体的性能和用户体验。
4.7 数据库视图的应用
数据库视图是一种虚拟的表,它是基于一个或多个表的查询结果。在 EF/EF Core 中,我们可以创建和使用数据库视图来简化复杂的查询。
首先,我们在数据库中创建一个视图:
CREATE VIEW vw_UsersWithPostCount AS
SELECT u.Id, u.Name, COUNT(p.Id) AS PostCount
FROM Users u
LEFT JOIN Posts p ON u.Id = p.UserId
GROUP BY u.Id, u.Name;
然后,在 EF/EF Core 中,我们可以将这个视图映射到一个实体类:
public class UserWithPostCount
{
public int Id { get; set; }
public string Name { get; set; }
public int PostCount { get; set; }
}
在DbContext中,我们可以通过以下方式配置这个视图:
public class MyDbContext : DbContext
{
public DbSet<UserWithPostCount> UserWithPostCounts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<UserWithPostCount>()
.ToView("vw_UsersWithPostCount");
}
}
通过这种方式,我们可以像查询普通实体类一样查询这个视图:
public async Task<List<UserWithPostCount>> GetUsersWithPostCount()
{
using (var db = new MyDbContext())
{
return await db.UserWithPostCounts.ToListAsync();
}
}
4.8 自定义函数的实现
在某些情况下,我们可能需要在数据库中使用自定义函数来实现特定的业务逻辑。在 EF/EF Core 中,调用自定义函数也很方便。
假设我们在数据库中定义了一个自定义函数dbo.fn_GetPostCount,用于获取某个用户的文章数量。我们可以在实体类中添加一个属性来映射这个函数:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
// 映射自定义函数
public int PostCount { get; set; }
}
在DbContext的OnModelCreating方法中,我们可以通过以下方式配置这个自定义函数:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.Property(u => u.PostCount)
.HasComputedColumnSql("dbo.fn_GetPostCount(Id)", stored: true);
}
通过这种方式,当我们查询User实体时,PostCount属性会自动通过调用自定义函数来获取值。
4.9 数据库触发器的运用
数据库触发器是一种特殊的存储过程,它在数据库表上的特定事件发生时自动执行。在 EF/EF Core 中,虽然不能直接通过代码创建触发器,但我们可以在数据库中创建触发器,并在应用程序中使用它们。
假设我们在Users表上创建一个触发器,当用户信息更新时,自动更新用户的最后登录时间:
CREATE TRIGGER tr_UpdateLastLogin
ON Users
AFTER UPDATE
AS
BEGIN
UPDATE Users
SET LastLogin = GETDATE()
WHERE Id IN (SELECT inserted.Id FROM inserted);
END;
在 EF/EF Core 中,当我们更新User实体时,这个触发器会自动执行,更新LastLogin字段的值。这样,我们可以通过数据库触发器来实现一些自动的数据更新和业务逻辑处理。
五、EF/EF Core 性能优化
5.1 性能优化策略
在 EF/EF Core 的应用中,性能优化是一个关键环节。减少数据加载量是提升性能的重要策略之一。我们可以通过投影操作,只选择需要的列,而不是加载整个实体。假设我们有一个Product实体,包含Id、Name、Description、Price等多个属性,但在某个查询中,我们只需要Id和Name属性,那么可以使用以下代码:
var products = await dbContext.Products
.Select(p => new { p.Id, p.Name })
.ToListAsync();
在这个例子中,Select方法指定了只投影Id和Name属性,这样数据库查询返回的数据量就大大减少,从而提高了查询性能。
优化查询语句同样重要。避免在内存中进行过滤操作,尽量在数据库层面完成过滤和排序。例如,以下是一个低效的查询示例:
var products = await dbContext.Products
.ToListAsync();
var filteredProducts = products.Where(p => p.Price > 100).ToList();
在这个例子中,首先将所有Product数据从数据库加载到内存,然后在内存中进行过滤。这种方式会消耗大量的内存和时间,尤其是当数据量较大时。我们可以将其优化为直接在数据库中进行过滤:
var products = await dbContext.Products
.Where(p => p.Price > 100)
.ToListAsync();
这样,数据库只返回价格大于 100 的产品数据,减少了数据传输和内存占用。
5.2 查询过滤器的使用
查询过滤器是 EF/EF Core 中一个强大的功能,它可以实现数据的自动过滤。在处理多租户应用时,我们可能需要为每个租户的数据添加一个租户标识,并确保每个租户只能访问自己的数据。我们可以通过查询过滤器来实现这一功能。
假设我们有一个Tenant实体和一个Product实体,Product实体包含一个TenantId属性用于关联Tenant实体。我们可以在DbContext的OnModelCreating方法中配置查询过滤器:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p => p.TenantId == _currentTenantId);
}
在这个例子中,_currentTenantId表示当前租户的标识。通过这个查询过滤器,所有对Product实体的查询都会自动添加p.TenantId == _currentTenantId这个条件,从而实现了租户数据的隔离。
5.3 查询拆分的技巧
查询拆分是指将一个复杂的查询拆分成多个简单的查询,以提高查询性能。在涉及多表连接的复杂查询中,查询拆分尤为有效。例如,当我们需要查询一个用户及其所有订单和订单明细时,如果使用传统的一次性查询,可能会生成非常复杂的 SQL 语句,影响查询性能。
假设我们有User、Order和OrderDetail三个实体,它们之间存在关联关系。我们可以使用查询拆分来优化查询:
var users = await dbContext.Users
.AsSplitQuery()
.Include(u => u.Orders)
.ThenInclude(o => o.OrderDetails)
.ToListAsync();
在这个例子中,AsSplitQuery方法告诉 EF/EF Core 将查询拆分成多个子查询。这样,每个子查询只处理一部分数据,避免了一次性加载大量数据带来的性能问题。查询拆分还可以减少数据库锁的持有时间,提高并发性能。
5.4 数据库分区的实现
数据库分区是提高数据存储和查询效率的重要手段。对于大型数据库,将数据按照一定的规则进行分区,可以显著提升查询性能。例如,我们可以根据时间对数据进行分区,将不同时间段的数据存储在不同的分区中。
假设我们有一个Sales表,记录了销售数据,其中包含一个SaleDate字段。我们可以使用 SQL Server 的分区功能对Sales表进行分区:
-- 创建分区函数
CREATE PARTITION FUNCTION SalesPF (datetime)
AS RANGE RIGHT FOR VALUES ('2023-01-01', '2024-01-01');
-- 创建分区方案
CREATE PARTITION SCHEME SalesPS
AS PARTITION SalesPF
TO ([PRIMARY], [PRIMARY], [PRIMARY]);
-- 创建分区表
CREATE TABLE Sales (
SaleId INT PRIMARY KEY,
SaleDate DATETIME,
Amount DECIMAL(10, 2)
) ON SalesPS (SaleDate);
在 EF/EF Core 中,我们可以像操作普通表一样操作分区表,数据库会自动根据查询条件选择合适的分区进行查询,从而提高查询效率。例如:
var recentSales = await dbContext.Sales
.Where(s => s.SaleDate >= new DateTime(2023, 1, 1) && s.SaleDate < new DateTime(2024, 1, 1))
.ToListAsync();
在这个例子中,数据库会只查询包含 2023 年销售数据的分区,而不会扫描整个表。
5.5 自定义值转换器的运用
在某些情况下,我们需要在数据库和实体之间进行自定义的数据类型转换,这时候就可以使用自定义值转换器。假设我们有一个DateTimeOffset类型的属性,但数据库中存储的是字符串类型的时间数据。我们可以创建一个自定义值转换器来实现两者之间的转换:
public class DateTimeOffsetConverter : ValueConverter<DateTimeOffset, string>
{
public DateTimeOffsetConverter()
: base(
v => v.ToString("yyyy-MM-dd HH:mm:sszzz"),
s => DateTimeOffset.Parse(s)
)
{
}
}
然后,在DbContext的OnModelCreating方法中应用这个值转换器:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<MyEntity>()
.Property(e => e.MyDateTimeOffsetProperty)
.HasConversion(new DateTimeOffsetConverter());
}
通过这个自定义值转换器,EF/EF Core 在将MyEntity实体保存到数据库时,会将MyDateTimeOffsetProperty属性的值转换为字符串格式存储;在从数据库读取数据时,又会将字符串转换回DateTimeOffset类型。
六、总结与展望
通过这篇高级进阶指南,我们深入探索了 EF/EF Core 的诸多方面。从基础的快速搭建,到复杂的实战操作和高级特性运用,再到关键的性能优化,EF/EF Core 展现出了强大的功能和灵活性。在实际项目开发中,它能极大地提高开发效率,减少数据访问层的代码量,使我们能够更加专注于业务逻辑的实现。
EF/EF Core 也在不断发展和演进。未来,随着技术的进步,我们可以期待它在性能、功能和跨平台支持等方面取得更大的突破。它可能会更好地与新兴的数据库技术和开发理念相结合,为开发者提供更加便捷、高效的开发体验。
希望各位读者能够将所学知识运用到实际项目中,不断实践和探索。在使用 EF/EF Core 的过程中,你可能会遇到各种问题和挑战,但这也是提升技术能力的宝贵机会。你可以通过查阅官方文档、参考优秀的开源项目、参与技术社区的讨论等方式,不断积累经验,解决遇到的问题。
相信通过持续的学习和实践,你一定能够熟练掌握 EF/EF Core,成为一名优秀的.NET 开发者,为构建更加高效、稳定的应用程序贡献自己的力量。