MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的Entity Framework数据访问技术

2021-01-184.9k 阅读

什么是Entity Framework

1. 概述

Entity Framework(EF)是微软为.NET开发者提供的一款对象关系映射(Object Relational Mapping,简称ORM)框架。在传统的数据访问模式中,开发者需要手动编写大量的SQL语句来与数据库进行交互,包括查询、插入、更新和删除等操作。而ORM框架的出现,旨在将数据库中的表结构映射为面向对象编程语言中的类,使得开发者可以使用对象的方式来操作数据库,从而大大减少了数据访问层的代码量,提高开发效率。

Entity Framework允许开发者以一种抽象的、与数据库无关的方式来编写数据访问逻辑。这意味着,在使用EF时,开发者可以针对概念模型进行编程,而不必关心底层数据库的具体实现细节,如数据库类型(SQL Server、MySQL、Oracle等)、表结构以及SQL语法等。当应用程序需要部署到不同的数据库环境时,只需要更改少量的配置,而无需对数据访问层的代码进行大规模修改。

2. 工作原理

Entity Framework的核心工作原理基于三个主要组件:概念模型(Conceptual Model)、存储模型(Storage Model)和映射(Mapping)。

概念模型:这是一个用实体数据模型(EDM)定义的面向对象模型,它描述了应用程序中的实体(对应数据库中的表)以及实体之间的关系。例如,在一个简单的订单管理系统中,可能有“客户”、“订单”和“产品”等实体,以及“客户下订单”、“订单包含产品”等关系。概念模型使用Entity Data Model Schema Definition Language(EDM DSL)来定义,通常以.edmx文件的形式存在。

存储模型:存储模型描述了实际数据库的结构,包括表、列、存储过程等。它与具体的数据库类型紧密相关,例如,如果使用SQL Server,存储模型将反映SQL Server数据库中的表结构和存储过程定义。存储模型同样使用EDM DSL来定义,但侧重于数据库层面的描述。

映射:映射定义了概念模型中的实体和关系如何对应到存储模型中的表和关系。通过映射,Entity Framework能够将对概念模型的操作(如创建、查询、更新实体)转换为对实际数据库的操作。映射信息也包含在.edmx文件中。

当应用程序执行数据访问操作时,Entity Framework首先根据概念模型接收操作请求,然后通过映射将这些请求转换为针对存储模型的操作,最后生成相应的SQL语句并发送到数据库执行。数据库返回结果后,EF再将结果映射回概念模型中的对象,返回给应用程序。

Entity Framework的安装与配置

1. 安装Entity Framework

在使用Entity Framework之前,需要先将其安装到项目中。Entity Framework可以通过NuGet包管理器进行安装,这是一种在Visual Studio中管理项目依赖项的便捷方式。

打开Visual Studio,创建一个新的C#项目(例如控制台应用程序、ASP.NET Web应用程序等)。在解决方案资源管理器中,右键点击项目名称,选择“管理NuGet程序包”。在NuGet包管理器窗口中,搜索“Entity Framework”,然后点击“安装”按钮。NuGet会自动下载并安装Entity Framework及其相关依赖项到项目中。

2. 配置连接字符串

安装完成后,需要配置数据库连接字符串。连接字符串告诉Entity Framework如何连接到实际的数据库。在.NET项目中,连接字符串通常存储在项目的配置文件(如App.config或Web.config)中。

以下是一个连接到SQL Server数据库的示例连接字符串:

<connectionStrings>
  <add name="MyDbContext" connectionString="Data Source=YOUR_SERVER_NAME;Initial Catalog=YOUR_DATABASE_NAME;User ID=YOUR_USERNAME;Password=YOUR_PASSWORD" providerName="System.Data.SqlClient" />
</connectionStrings>

在上述示例中,Data Source指定了数据库服务器的名称,Initial Catalog指定了要连接的数据库名称,User IDPassword分别是登录数据库的用户名和密码。providerName指定了用于连接数据库的.NET数据提供程序,这里使用的是SQL Server的官方数据提供程序System.Data.SqlClient

如果使用的是其他类型的数据库,如MySQL,连接字符串的格式会有所不同。例如,连接到MySQL数据库的连接字符串可能如下:

<connectionStrings>
  <add name="MyDbContext" connectionString="server=YOUR_SERVER_NAME;database=YOUR_DATABASE_NAME;uid=YOUR_USERNAME;pwd=YOUR_PASSWORD;" providerName="MySql.Data.MySqlClient" />
</connectionStrings>

这里使用的是MySQL官方提供的.NET数据提供程序MySql.Data.MySqlClient

3. 创建DbContext

DbContext是Entity Framework的核心类之一,它代表了与数据库的会话。通过DbContext,开发者可以查询、插入、更新和删除数据。在项目中,需要创建一个继承自DbContext的自定义类。

以下是一个简单的示例:

using System.Data.Entity;

public class MyDbContext : DbContext
{
    public MyDbContext() : base("MyDbContext")
    {
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Order> Orders { get; set; }
}

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }
    public DateTime OrderDate { get; set; }
    public virtual Customer Customer { get; set; }
}

在上述代码中,MyDbContext继承自DbContext,并通过构造函数调用基类的构造函数,传入在配置文件中定义的连接字符串名称MyDbContextMyDbContext类还定义了两个DbSet属性,分别对应CustomerOrder实体。DbSet表示数据库中对应的表,通过它可以对实体进行各种操作。

CustomerOrder类定义了实体的属性,其中CustomerIdOrderId通常作为主键。Order类中的Customer属性是一个导航属性,用于表示OrderCustomer之间的关系。

使用Entity Framework进行数据查询

1. LINQ查询

Entity Framework支持使用Language-Integrated Query(LINQ)进行数据查询。LINQ是一种集成在C#语言中的查询语法,它允许开发者以一种类似于SQL的方式对各种数据源进行查询,包括数据库、集合等。

以下是一些常见的LINQ查询示例:

查询所有客户

using (var context = new MyDbContext())
{
    var customers = context.Customers.ToList();
    foreach (var customer in customers)
    {
        Console.WriteLine($"Customer ID: {customer.CustomerId}, Name: {customer.Name}, Email: {customer.Email}");
    }
}

在上述代码中,context.Customers表示Customer实体的集合,ToList()方法将查询结果转换为一个列表。

按条件查询客户

using (var context = new MyDbContext())
{
    var customers = context.Customers.Where(c => c.Email.EndsWith("@example.com")).ToList();
    foreach (var customer in customers)
    {
        Console.WriteLine($"Customer ID: {customer.CustomerId}, Name: {customer.Name}, Email: {customer.Email}");
    }
}

这里使用Where方法来筛选出邮箱以“@example.com”结尾的客户。

关联查询

using (var context = new MyDbContext())
{
    var orders = context.Orders
       .Include(o => o.Customer)
       .ToList();
    foreach (var order in orders)
    {
        Console.WriteLine($"Order ID: {order.OrderId}, Customer Name: {order.Customer.Name}, Order Date: {order.OrderDate}");
    }
}

在这个示例中,Include方法用于指定在查询Order实体时,同时加载与之关联的Customer实体。这样可以避免在访问order.Customer时产生额外的数据库查询(即所谓的“N + 1问题”)。

2. SQL查询

除了LINQ查询,Entity Framework也允许直接执行SQL查询。这在某些复杂的查询场景下非常有用,例如当LINQ无法表达特定的SQL逻辑时。

执行查询并返回实体

using (var context = new MyDbContext())
{
    var customers = context.Database.SqlQuery<Customer>("SELECT * FROM Customers WHERE Email LIKE '%@example.com'").ToList();
    foreach (var customer in customers)
    {
        Console.WriteLine($"Customer ID: {customer.CustomerId}, Name: {customer.Name}, Email: {customer.Email}");
    }
}

在上述代码中,context.Database.SqlQuery<Customer>方法执行一个SQL查询,并将结果映射为Customer实体的列表。

执行存储过程: 假设数据库中有一个名为GetCustomersByCity的存储过程,接受一个城市名称参数,并返回该城市的客户列表。可以这样调用:

using (var context = new MyDbContext())
{
    var city = "New York";
    var customers = context.Database.SqlQuery<Customer>("EXEC GetCustomersByCity @City", new SqlParameter("@City", city)).ToList();
    foreach (var customer in customers)
    {
        Console.WriteLine($"Customer ID: {customer.CustomerId}, Name: {customer.Name}, Email: {customer.Email}");
    }
}

这里通过SqlQuery方法执行存储过程,并传入参数。

使用Entity Framework进行数据插入、更新和删除

1. 插入数据

向数据库中插入新数据非常简单。只需创建一个新的实体对象,设置其属性值,然后将其添加到DbSet中,并调用SaveChanges方法即可。

以下是插入一个新客户的示例:

using (var context = new MyDbContext())
{
    var newCustomer = new Customer
    {
        Name = "John Doe",
        Email = "johndoe@example.com"
    };
    context.Customers.Add(newCustomer);
    context.SaveChanges();
    Console.WriteLine($"New customer added with ID: {newCustomer.CustomerId}");
}

在上述代码中,首先创建了一个Customer对象并设置其属性。然后通过context.Customers.Add方法将该对象添加到DbSet中。最后调用context.SaveChanges方法,Entity Framework会生成相应的SQL INSERT语句并执行,将新客户数据插入到数据库中。SaveChanges方法返回受影响的行数,并且在成功插入后,newCustomer.CustomerId会自动填充数据库生成的主键值。

2. 更新数据

更新数据也相对直观。首先从数据库中获取要更新的实体对象,修改其属性值,然后调用SaveChanges方法。

以下是更新客户邮箱的示例:

using (var context = new MyDbContext())
{
    var customerToUpdate = context.Customers.FirstOrDefault(c => c.CustomerId == 1);
    if (customerToUpdate != null)
    {
        customerToUpdate.Email = "newemail@example.com";
        context.SaveChanges();
        Console.WriteLine($"Customer email updated.");
    }
}

在上述代码中,通过FirstOrDefault方法获取CustomerId为1的客户。如果找到该客户,则修改其Email属性,并调用SaveChanges方法。Entity Framework会检测到实体对象的属性变化,并生成相应的SQL UPDATE语句,只更新发生变化的字段。

3. 删除数据

删除数据同样简单。获取要删除的实体对象,调用DbSetRemove方法,然后调用SaveChanges方法。

以下是删除一个客户的示例:

using (var context = new MyDbContext())
{
    var customerToDelete = context.Customers.FirstOrDefault(c => c.CustomerId == 1);
    if (customerToDelete != null)
    {
        context.Customers.Remove(customerToDelete);
        context.SaveChanges();
        Console.WriteLine($"Customer deleted.");
    }
}

在上述代码中,获取CustomerId为1的客户,调用context.Customers.Remove方法将其标记为删除状态,最后通过SaveChanges方法生成并执行SQL DELETE语句,从数据库中删除该客户记录。

Entity Framework中的数据关系处理

1. 一对一关系

一对一关系是指两个实体之间存在一一对应的关系。例如,一个用户可能有一个唯一的个人资料。

首先定义两个实体类:

public class User
{
    public int UserId { get; set; }
    public string Username { get; set; }
    public virtual UserProfile Profile { get; set; }
}

public class UserProfile
{
    public int UserProfileId { get; set; }
    public string Address { get; set; }
    public int UserId { get; set; }
    public virtual User User { get; set; }
}

User类中,Profile属性是一个导航属性,用于引用对应的UserProfile。在UserProfile类中,UserId是外键,User属性是反向导航属性。

配置一对一关系:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
       .HasOptional(u => u.Profile)
       .WithRequired(p => p.User);
}

在上述配置中,HasOptional表示User实体可以有一个可选的ProfileWithRequired表示UserProfile实体必须关联一个User

2. 一对多关系

一对多关系是最常见的关系类型。例如,一个客户可以有多个订单。

定义实体类:

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Order> Orders { get; set; }
}

public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }
    public DateTime OrderDate { get; set; }
    public virtual Customer Customer { get; set; }
}

Customer类中,Orders属性是一个导航属性,用于表示该客户的所有订单。在Order类中,CustomerId是外键,Customer属性是反向导航属性。

配置一对多关系:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
       .HasMany(c => c.Orders)
       .WithRequired(o => o.Customer)
       .HasForeignKey(o => o.CustomerId);
}

在上述配置中,HasMany表示Customer实体可以有多个OrderWithRequired表示Order实体必须关联一个CustomerHasForeignKey指定了外键字段。

3. 多对多关系

多对多关系表示两个实体之间存在多对多的关联。例如,一个学生可以选修多门课程,一门课程可以有多个学生选修。

定义实体类和中间表实体类(在EF Core中,有时可以不显示定义中间表实体类):

public class Student
{
    public int StudentId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<StudentCourse> StudentCourses { get; set; }
}

public class Course
{
    public int CourseId { get; set; }
    public string CourseName { get; set; }
    public virtual ICollection<StudentCourse> StudentCourses { get; set; }
}

public class StudentCourse
{
    public int StudentId { get; set; }
    public int CourseId { get; set; }
    public virtual Student Student { get; set; }
    public virtual Course Course { get; set; }
}

StudentCourse类中,都有一个StudentCourses导航属性,用于表示它们之间的多对多关系。StudentCourse类作为中间表实体类,包含两个外键和两个导航属性。

配置多对多关系:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<StudentCourse>()
       .HasKey(sc => new { sc.StudentId, sc.CourseId });

    modelBuilder.Entity<StudentCourse>()
       .HasRequired(sc => sc.Student)
       .WithMany(s => s.StudentCourses)
       .HasForeignKey(sc => sc.StudentId);

    modelBuilder.Entity<StudentCourse>()
       .HasRequired(sc => sc.Course)
       .WithMany(c => c.StudentCourses)
       .HasForeignKey(sc => sc.CourseId);
}

在上述配置中,首先为StudentCourse实体类定义复合主键。然后分别配置StudentCourseStudent以及StudentCourseCourse之间的关系。

Entity Framework的高级特性

1. 迁移(Migrations)

Entity Framework的迁移功能允许开发者在应用程序的生命周期内管理数据库架构的变化。随着项目的发展,数据库的结构可能需要不断更新,例如添加新表、修改列、删除约束等。手动管理这些变化可能会很繁琐,并且容易出错。迁移功能提供了一种自动化的方式来处理这些架构变更。

首先,确保已经安装了Entity Framework Tools包。在NuGet包管理器控制台中,可以使用以下命令安装:

Install - Package Microsoft.EntityFrameworkCore.Tools

启用迁移: 在项目的DbContext类所在的项目中,打开NuGet包管理器控制台,执行以下命令:

Add - Migration InitialCreate

上述命令中的InitialCreate是迁移的名称,可以根据实际情况命名。这个命令会在项目中创建一个迁移文件夹,并生成一个迁移类,该类记录了当前数据库架构的初始状态。

更新数据库: 执行以下命令将迁移应用到数据库:

Update - Database

这会根据迁移类中的定义,生成并执行相应的SQL脚本,创建数据库架构。

当数据库架构需要更新时,例如添加一个新表,可以再次执行Add - Migration命令,为新的架构变化创建一个新的迁移类,然后执行Update - Database命令将这些变化应用到数据库。

2. 延迟加载(Lazy Loading)

延迟加载是指当访问导航属性时,Entity Framework会自动从数据库中加载相关联的实体,而不是在查询主实体时就一并加载所有关联实体。这可以提高查询性能,尤其是在处理大型数据集或具有复杂关系的实体时。

默认情况下,Entity Framework启用延迟加载。例如,在前面的CustomerOrder关系示例中:

using (var context = new MyDbContext())
{
    var customer = context.Customers.FirstOrDefault(c => c.CustomerId == 1);
    // 此时,customer.Orders尚未从数据库加载
    foreach (var order in customer.Orders)
    {
        // 第一次访问customer.Orders时,EF会自动从数据库加载该客户的订单
        Console.WriteLine($"Order ID: {order.OrderId}, Order Date: {order.OrderDate}");
    }
}

在上述代码中,当获取Customer对象时,其Orders导航属性并没有立即从数据库加载。只有在第一次访问customer.Orders时,Entity Framework才会执行一个额外的数据库查询来加载该客户的订单。

然而,延迟加载也可能带来一些性能问题,例如“N + 1问题”。为了避免这种情况,可以使用Include方法进行急切加载,或者使用显式加载。

3. 缓存(Caching)

在某些情况下,为了提高应用程序的性能,可以对Entity Framework的查询结果进行缓存。缓存可以减少对数据库的重复查询,尤其是对于那些不经常变化的数据。

一种简单的缓存方式是使用.NET的内存缓存。以下是一个示例:

using Microsoft.Extensions.Caching.Memory;

public class CustomerService
{
    private readonly MyDbContext _context;
    private readonly IMemoryCache _cache;

    public CustomerService(MyDbContext context, IMemoryCache cache)
    {
        _context = context;
        _cache = cache;
    }

    public List<Customer> GetCustomers()
    {
        if (!_cache.TryGetValue("Customers", out List<Customer> customers))
        {
            customers = _context.Customers.ToList();
            var cacheEntryOptions = new MemoryCacheEntryOptions()
               .SetSlidingExpiration(TimeSpan.FromMinutes(10));
            _cache.Set("Customers", customers, cacheEntryOptions);
        }
        return customers;
    }
}

在上述代码中,CustomerService类依赖于MyDbContextIMemoryCacheGetCustomers方法首先尝试从缓存中获取客户列表,如果缓存中不存在,则从数据库查询,并将结果存入缓存中,设置缓存的滑动过期时间为10分钟。这样,在10分钟内再次调用GetCustomers方法时,将直接从缓存中获取数据,而无需查询数据库。

还可以使用其他缓存机制,如分布式缓存(如Redis),以适应更复杂的应用场景和多服务器环境。

4. 事务处理

在数据访问操作中,事务处理非常重要。它确保一组相关的数据操作要么全部成功,要么全部失败,以保证数据的一致性。

Entity Framework提供了简单的事务处理方式。例如,在一个涉及插入客户和订单的场景中:

using (var context = new MyDbContext())
{
    using (var transaction = context.Database.BeginTransaction())
    {
        try
        {
            var newCustomer = new Customer
            {
                Name = "Jane Smith",
                Email = "janesmith@example.com"
            };
            context.Customers.Add(newCustomer);
            context.SaveChanges();

            var newOrder = new Order
            {
                CustomerId = newCustomer.CustomerId,
                OrderDate = DateTime.Now
            };
            context.Orders.Add(newOrder);
            context.SaveChanges();

            transaction.Commit();
            Console.WriteLine("Customer and order added successfully.");
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            Console.WriteLine($"Error: {ex.Message}");
        }
    }
}

在上述代码中,通过context.Database.BeginTransaction方法开始一个事务。在try块中执行插入客户和订单的操作,如果所有操作都成功,则调用transaction.Commit方法提交事务。如果在任何操作过程中发生异常,catch块会捕获异常并调用transaction.Rollback方法回滚事务,确保数据库状态不会被部分修改。

通过合理运用这些高级特性,可以进一步提升Entity Framework在实际应用中的性能、可靠性和可维护性。