如何在 .NET Core 和 C# 中使用 ORM 来减少 SQL 语句 - Entity Framework

2025-06-08

如何在 .NET Core 和 C# 中使用 ORM 来减少 SQL 语句 - Entity Framework

我们都喜欢 SQL,对吧?不是吗?有时候,一个好的 SQL 查询是最好的方法,但大多数时候,你需要对每个新实体执行相同的 CRUD 操作:创建、读取、更新、删除。使用 ORM(对象关系映射器),你可以使用代码定义数据库的结构。此外,你也可以使用代码进行查询。.Net 和 .Net Core 的主要 ORM 称为实体框架 (Entity Framework),这就是我们在本文中要介绍的内容。

我只想说一件非常重要的事情。像 ORM 这样的工具永远不应该取代学习 SQL。ORM 的存在是为了让你的生活更轻松,所以当你最终编写 SQL 时,它只用于一些重要的事情,比如报表查询或需要真正高性能的查询。ORM 的初衷是处理一些更简单的 SQL,比如创建表和执行简单的插入操作。如果你对 SQL 知识不甚了解,请先阅读这里并尝试掌握基础知识:

https://www.w3schools.com/sql/

TLDR;这篇文章有点长,但它从一开始就教你实体框架,并涵盖了很多非常棒的主题,值得一读。

在本文中,我们将介绍:

  • 为什么使用 ORM?我们总是需要问自己为什么要用它。如果你需要与数据库进行大量简单的交互,ORM 就能真正发挥作用。使用它,你真的可以加快你的操作速度。
  • 它能帮你做什么。
  • 安装和设置
  • CRUD 演示。我们将介绍如何读取、创建、更新和删除数据

 资源

为什么选择 ORM

使用 ORM 可以提高速度、提高效率,并且可以准确了解数据库中的内容。

那么我什么时候使用它,总是还是?

对于大多数简单的应用程序来说,它绝对是不错的选择。对于需要高性能查询的应用程序,它当然仍然可以使用,但你需要更加注意 ORM 生成的 SQL。有时它就足够了,有时你需要用 SQL 手写查询。通常情况下,我个人不会使用 ORM 来处理报告查询,因为它们通常很复杂,很难用代码表达。但每个人都不一样。我见过即使是复杂的查询,也用代码编写。

ORM 概况

.Net 的 ORM 选项不止一个。Entity Framework 是最著名的,但还有其他的。你需要决定哪一个适合你的项目。

还有更多的 ORM,但上述三个是众所周知的选择。

什么

大多数 ORM 允许你在代码中定义表结构,并且可以将类映射到表。列只是类的属性。根据 ORM 的不同,有几种可行的方法

  • 模式优先,在这种情况下,你需要定义一个模式,包含你拥有的表,以及它们之间的关系,例如一对一、一对多、一对多等等。最终,你需要根据该模式生成代码。
  • 代码优先,在这种方法中,你首先定义代码。每个表对应一个类,你可以用代码表达所有内容之间的关系。然后,你的 ORM 将检查你的代码并从中生成结构化 SQL。

迁移

许多 ORM 都带有一个称为迁移的概念。迁移只是一段脚本,它可以改变数据库的结构,或者运行一段影响数据的 SQL,例如,用一些初始数据填充数据库。这个想法是,每次对数据库进行更改时,都应该在迁移中捕获一个小的事务更改。然后可以将该迁移应用于数据库,从而以所需的方式更改数据库。例如,向数据库添加客户表将是一个迁移,应用后将在数据库中创建表。迁移可以用 SQL 或代码表示。

安装和设置

要开始使用 Entity Framework,我们需要一些 NuGet 包,还需要一个可以安装这些包的项目。因此,在本练习中,我们将执行以下操作:

  1. 创建解决方案
  2. 搭建控制台项目并添加对解决方案的引用
  3. 将所需的 NuGet 包安装到控制台项目

创建解决方案

这很简单。首先,我们需要一个目录。所以创建一个目录,你可以自己选择名称,这里只是举个例子。

mkdir demo

然后我们需要将自己放置在这样的目录中:

cd demo

搭建控制台项目

接下来我们需要创建控制台项目。你可以自行选择名称,但我们选择App。输入以下内容:

dotnet new console -o App

console这将创建一个名为的新项目App

最后我们将该项目添加到解决方案中,如下所示:

dotnet sln add App/App.csproj

安装和设置

为此,我们将安装 Entity Framework 的核心库,并支持 SqlLite 数据库类型。请注意,它支持不同的数据库,请查看此处支持的数据库完整列表:

https://docs.microsoft.com/en-us/ef/core/providers/

SqlLite 是一个非常简单的数据库,它只将结构和数据存储在硬盘上的文件中。

但是我正在使用一个真实的数据库,那我该怎么办呢?我会从这篇文章中受益吗?

是的,我们展示的是无论数据库类型如何都可广泛应用的通用知识。

好的,那么让我们首先导航到我们的控制台应用程序目录,如下所示:

cd App

然后安装所需的 NuGet 库:

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design

这将添加对项目的引用。打开它App.csproj,你应该会看到类似这样的内容:

<ItemGroup>
  <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />
  <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
</ItemGroup>

现在我们需要实际安装库,我们使用以下命令来完成:

dotnet restore

 CRUD 演示

我们将展示如何执行完整的 CRUD、创建、读取、更新和删除。

在这里我们将尝试以下操作:

  • 创建数据库
  • 创建代表数据库结构的迁移,然后应用它来创建数据库
  • 从数据库读取
  • 写入数据库
  • 使用初始数据填充数据库

创建数据库

首先我们需要一个数据库,所以让我们创建一个。

我们将创建一个名为的文件,Database.cs其内容如下:

// Database.cs

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;

namespace App
{
  public class DatabaseContext : DbContext
  {
    public DbSet<Product> Products { get; set; }

    public DbSet<OrderItem> OrderItems { get; set; }

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

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
      optionsBuilder.UseSqlite("Data Source=database.db");
    }
  }

  public class Order
  {
    public int OrderId { get; set; }

    public DateTime? Created { get; set; }

    public ICollection<OrderItem> Items { get; set; }
  }

  public class OrderItem
  {
    public int OrderItemId { get; set; }
    public int Quantity { get; set; }
    public virtual Product Product { get; set; }
  }

  public class Product 
  {
    public int ProductId { get; set; }
    public double Price { get; set; }
    public string Description { get; set; }
  }
}

从上面的代码可以看出我们有以下类:

  • Order,这是一个代表订单的类。
  • OrderItem,一个 Order 有许多 OrderItems ,每个 OrderItem 都有一个Quantity属性和对Product
  • 产品,这代表我们要订购的产品。它包含Price和 等信息Description

让我们评论一下代码中的一些有趣的结构。

一对多

我们通过类上的以下构造来表达一对多关系Order

public ICollection<OrderItem> Items { get; set; }

上面我们说了我们在订单上列出了订单项。

外键

我们还表达了另一个数据库概念,即外键。在OrderItem实体中,我们说我们有一个对产品的引用。在代码中,我们这样写:

public virtual Product Product { get; set; }

DbContext 和 DbSet

我们也来评论一下 first DbContext。当我们想要一个新的数据库时,我们应该像这样继承这个类:

public class DatabaseContext : DbContext

DbSet表示数据库中的表。它是一个泛型,以一个type作为模板参数,如下所示:

public DbSet<OrderItem> OrderItems { get; set; }

创建迁移

现在我们已经保存了文件Database.cs。是时候创建数据库了。为此,我们需要做两件事:

  • 生成迁移,这会对代码的当前状态进行快照,并将其与任何先前的快照进行比较。如果没有先前的快照,生成迁移只会创建初始迁移

  • 应用迁移,这将运行迁移。根据迁移的内容,它将创建数据库、影响数据库结构或更改数据。

生成迁移

让我们使用以下命令创建迁移:

dotnet ef migrations add InitialCreate

最后一个参数是迁移的名称,我们可以随意称呼它,但最好给它一个描述性的名称,例如InitialCreate

运行该命令将在终端中产生以下结果:

正如您在上面看到的,它很好地告诉我们如何使用命令撤消ef migrations remove我们刚刚执行的操作。

这为我们创建了一些文件,具体如下:

上面可以看到我们成功获取了迁移文件InitialCreate,但文件名称前面添加了时间戳。这是为了让 Entity Framework 知道要运行什么以及运行顺序。我们还可以看到该文件有两个版本:一个.cs文件和一个Designer.csfile 文件。我们只关心第一个。我们来看一下:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace App.Migrations
{
    public partial class InitialCreate : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "Orders",
                columns: table => new
                {
                    OrderId = table.Column<int>(nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Created = table.Column<DateTime>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Orders", x => x.OrderId);
                });

            migrationBuilder.CreateTable(
                name: "Products",
                columns: table => new
                {
                    ProductId = table.Column<int>(nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Price = table.Column<double>(nullable: false),
                    Description = table.Column<string>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Products", x => x.ProductId);
                });

            migrationBuilder.CreateTable(
                name: "OrderItems",
                columns: table => new
                {
                    OrderItemId = table.Column<int>(nullable: false)
                        .Annotation("Sqlite:Autoincrement", true),
                    Quantity = table.Column<int>(nullable: false),
                    ProductId = table.Column<int>(nullable: true),
                    OrderId = table.Column<int>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_OrderItems", x => x.OrderItemId);
                    table.ForeignKey(
                        name: "FK_OrderItems_Orders_OrderId",
                        column: x => x.OrderId,
                        principalTable: "Orders",
                        principalColumn: "OrderId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_OrderItems_Products_ProductId",
                        column: x => x.ProductId,
                        principalTable: "Products",
                        principalColumn: "ProductId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_OrderItems_OrderId",
                table: "OrderItems",
                column: "OrderId");

            migrationBuilder.CreateIndex(
                name: "IX_OrderItems_ProductId",
                table: "OrderItems",
                column: "ProductId");
        }

        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "OrderItems");

            migrationBuilder.DropTable(
                name: "Orders");

            migrationBuilder.DropTable(
                name: "Products");
        }
    }
}

我们首先看到的是,我们继承自类Migration。其次,我们有两个方法Up()Down()Up()当我们想要应用某些操作时运行。当我们想要撤消迁移Down()时运行。查看我们的方法,我们可以看到,我们为每个表调用了一次。我们还可以看到它定义了所有需要的外键。方法调用撤消表的创建。Up()CreateTable()OrderOrderItemProductDown()DropTable()

应用迁移

好的,我们有了迁移,让我们应用它。我们使用以下命令执行此操作:

dotnet ef database update

如果需要,这将首先创建数据库,然后应用迁移。

我们可以在文件结构中看到一个新文件被创建了database.db。我们可以使用 SQLite 客户端,或者也可以编写一些代码来连接它。:)

从数据库读取

好的,现在我们想看看是否可以连接到数据库并读取一些数据。打开Program.cs并转到方法Main()并添加以下内容:

using (var db = new DatabaseContext())
{
}

这将建立与数据库的连接。要从数据库中读取数据,我们只需要像这样:

using (var db = new DatabaseContext())
{
  var orders = db.Orders;
  foreach(var order in orders)
  {
    Console.WriteLine("Order: order.Created");
  }
}

我们来尝试一下吧?

好的,我们没有收到订单 :(。

嗯,这是意料之中的,我们没有往数据库中存任何东西。我们能不能改一下?

写入数据库

好的,我们知道了如何连接数据库。那么如何写入数据库呢?

为了创建一个,我们首先需要一些数据,至少以“1”和“1”Order的形式。如果要将某些内容保存到数据库,则需要调用ProductOrderItemdb.SaveChanges()

我们需要逐步采取所有这些措施,因为其中有一些活动部件。

创建产品

首先,我们将创建一个Product

让我们添加以下代码:

using (var db = new DatabaseContext())
{

    var product = new Product(){ Price = 100, Description = "Movie" };
    db.Products.Add(product);
    db.SaveChanges();

    foreach(var p in db.Products) 
    {
        Console.WriteLine("{0} {1} {2}", p.ProductId, p.Description, p.Price);
    }

}

上述代码将创建我们的Product,并通过调用db.SaveChanges()确保将其持久化到数据库中。

运行代码会导致

订单项

好的,这部分已经完成了。那么创建一个怎么样OrderItem?这很简单,我们只需要以下代码:

using (var db = new DatabaseContext())
{
    var product = db.Products.SingleOrDefault();

    if(product != null)
    {
        var item = new OrderItem
        {
            Quantity = 1,
            Product = product
        };
        db.OrderItems.Add(item);
        db.SaveChanges();

        Console.WriteLine("{0} {1} Product: {2}", item.OrderItemId, item.Quantity, item.Product.Description);
    }
}

让我们尝试突出重要的部分。

上面我们可以看到,我们首先从数据库中读取了product。接下来,我们将相同的产品赋值给Product上的属性。然后,我们将 添加到并调用 ,OrderItem从而保存所有内容OrderItemdb.OrderItemsdb.SaveChanges()

 创建订单

现在数据库中已经有了 aProduct和 an OrderItem。那么如何创建包含这两个实体的订单呢?

创建订单不仅仅是创建订单,而是创建订单并将订单项与订单关联起来。

关联部分可以通过两种不同的方式完成:

  1. 将 OrderItem 添加到order.Items
  2. 向我们的 OrderItem 添加一个外键并分配我们的订单 ID。

上述两种解决方案都要求我们对实体框架有更多的了解。

加载相关实体

让我们从第一种方法开始。为此,我们需要知道如何加载相关实体。

为什么?

嗯,当你有一个实例时,除非我们明确地告诉它需要填充某些内容,否则OrderItems就会一直存在。为了使这种方法有效,我们至少需要它是一个空列表,这样我们才能添加我们的nullOrderItem

好的,我想你最好给我看看。

当然,请看下面的代码:

var item = db.OrderItems.SingleOrDefault();
var order = new Order() { Created = DateTime.Now };
db.Orders.Add(order);
db.SaveChanges();

这样就创建了一个订单。那么如何添加我们的呢item?不过,我们遇到了一个问题:

item如果我们在保存Order第 49 行之后尝试加载at order.Items,则会null抛出运行时异常。为了解决这个问题,我们需要使用方法Include()。该方法接受一个 lambda 表达式,我们需要在其中指出要加载的内容。在本例中,我们需要加载上的Include()属性ItemsOrder

让我们运行这段代码:

此时我们的order.Items是一个空数组,我们可以添加我们的,OrderItem而不会导致代码崩溃,正如您所见,因为我们已到达第 54 行。

向 OrderItem 添加外键

在幕后,我们已经获得了一个外键OrderItem。我们可以看到,如果我们打开迁移:

我们现在的问题是它不存在于我们的属性中OrderItem,那么我们该如何解决这个问题呢?

好吧,我们只需在类定义中添加:

然后,因为我们有一个与 OrderItem 关联的现有订单,所以实际上填充了以下内容item.OrderId

如果我们想要在 Order 和 OrderItem 之间建立联系,而目前还没有建立联系,我们可以轻松地使用以下代码来实现:

using(var db = new DatabaseContext()) 
{
  var order = db.Orders.SingleOrDefault();
  var item = db.OrderItems.SingleOrDefault();
  item.OrderId = order.OrderId;
  db.SaveChanges();
}

更新

更新操作非常简单,就像我们之前创建订单的第二个场景一样。也就是读取实体,设置属性,然后调用db.SaveChanges()。像这样:

using(var db = new DatabaseContext()) 
{
  var item = db.OrderItems.SingleOrDefault();
  item.Quantity++;
  db.SaveChanges();
}

删除

删除就像从列表中移除某个元素一样简单。如果我们想删除某个元素,Product只需执行以下操作:

using(var db = new DatabaseContext()) 
{
  var product = db.Products.SingleOrDefault();
  db.Products.Remove(product);
  db.SaveChanges();
}

需要注意的是,如果您的产品是其中的一部分,则OrderItem您需要先删除该连接,如下所示:

using(var db = new DatabaseContext()) 
{

  var item = db.OrderItems.Include(i => i.Product)SingleOrDefault();

  item.Product = null;
  db.SaveChanges();

  var product = db.Products.SingleOrDefault();
  db.Products.Remove(product);
  db.SaveChanges();
}

概括

我们就到此为止吧。如果我们从零开始,这篇文章已经学到了很多东西。

我们了解到:

  • 什么是 ORM
  • 定义我们的数据库结构
  • 创建迁移并应用它
  • 读取数据
  • 创建数据
  • 更新数据
  • 删除数据
  • 加载相关实体
  • 外键

一篇文章就讲这么多了。希望你现在感兴趣并想了解更多。如果你想学习更高级的概念以及如何处理不同类型的数据库,请查看参考资料部分了解更多信息。

链接:https://dev.to/dotnet/how-you-can-use-an-orm-in-net-core-and-c-to-type-less-sql-starring-entity-framework-49ka
PREV
了解如何使用 .NET Core、C# 和 JavaScript 构建和使用 Blazor 应用程序
NEXT
使用 .NET 6 的精简 API 开始使用