如何在 .NET Core 和 C# 中使用 ORM 来减少 SQL 语句 - Entity Framework
我们都喜欢 SQL,对吧?不是吗?有时候,一个好的 SQL 查询是最好的方法,但大多数时候,你需要对每个新实体执行相同的 CRUD 操作:创建、读取、更新、删除。使用 ORM(对象关系映射器),你可以使用代码定义数据库的结构。此外,你也可以使用代码进行查询。.Net 和 .Net Core 的主要 ORM 称为实体框架 (Entity Framework),这就是我们在本文中要介绍的内容。
我只想说一件非常重要的事情。像 ORM 这样的工具永远不应该取代学习 SQL。ORM 的存在是为了让你的生活更轻松,所以当你最终编写 SQL 时,它只用于一些重要的事情,比如报表查询或需要真正高性能的查询。ORM 的初衷是处理一些更简单的 SQL,比如创建表和执行简单的插入操作。如果你对 SQL 知识不甚了解,请先阅读这里并尝试掌握基础知识:
TLDR;这篇文章有点长,但它从一开始就教你实体框架,并涵盖了很多非常棒的主题,值得一读。
在本文中,我们将介绍:
- 为什么使用 ORM?我们总是需要问自己为什么要用它。如果你需要与数据库进行大量简单的交互,ORM 就能真正发挥作用。使用它,你真的可以加快你的操作速度。
- 它能帮你做什么。
- 安装和设置
- CRUD 演示。我们将介绍如何读取、创建、更新和删除数据
资源
-
数据库提供程序:
您可以使用 Entity Framework 来处理各种不同的数据库。其核心思想是采用一种不可知论的方法,理论上,您可以将一个数据库替换为另一个,而代码保持不变。我们都知道,我们几乎从不这样做,但这确实是一个好主意。 -
初学者 EF Core 文章
本文部分基于此文章,即使我们更进一步 -
预先加载
我们在文章中介绍了其工作原理的基础知识,但总有更多的东西需要学习。
为什么选择 ORM
使用 ORM 可以提高速度、提高效率,并且可以准确了解数据库中的内容。
那么我什么时候使用它,总是还是?
对于大多数简单的应用程序来说,它绝对是不错的选择。对于需要高性能查询的应用程序,它当然仍然可以使用,但你需要更加注意 ORM 生成的 SQL。有时它就足够了,有时你需要用 SQL 手写查询。通常情况下,我个人不会使用 ORM 来处理报告查询,因为它们通常很复杂,很难用代码表达。但每个人都不一样。我见过即使是复杂的查询,也用代码编写。
ORM 概况
.Net 的 ORM 选项不止一个。Entity Framework 是最著名的,但还有其他的。你需要决定哪一个适合你的项目。
-
如果单从语法来看, Linq 2 db
提供的体验与 Entity Framework 类似。有人说它的语法与实际的 SQL 非常接近。 -
Dapper
曾被描述为Object Mapper和Micro ORM -
NHibernate
是 Hibernate 的 .Net 版本,也是最古老的 ORM 之一。
还有更多的 ORM,但上述三个是众所周知的选择。
什么
大多数 ORM 允许你在代码中定义表结构,并且可以将类映射到表。列只是类的属性。根据 ORM 的不同,有几种可行的方法
- 模式优先,在这种情况下,你需要定义一个模式,包含你拥有的表,以及它们之间的关系,例如一对一、一对多、一对多等等。最终,你需要根据该模式生成代码。
- 代码优先,在这种方法中,你首先定义代码。每个表对应一个类,你可以用代码表达所有内容之间的关系。然后,你的 ORM 将检查你的代码并从中生成结构化 SQL。
迁移
许多 ORM 都带有一个称为迁移的概念。迁移只是一段脚本,它可以改变数据库的结构,或者运行一段影响数据的 SQL,例如,用一些初始数据填充数据库。这个想法是,每次对数据库进行更改时,都应该在迁移中捕获一个小的事务更改。然后可以将该迁移应用于数据库,从而以所需的方式更改数据库。例如,向数据库添加客户表将是一个迁移,应用后将在数据库中创建表。迁移可以用 SQL 或代码表示。
安装和设置
要开始使用 Entity Framework,我们需要一些 NuGet 包,还需要一个可以安装这些包的项目。因此,在本练习中,我们将执行以下操作:
- 创建解决方案
- 搭建控制台项目并添加对解决方案的引用
- 将所需的 NuGet 包安装到控制台项目
创建解决方案
这很简单。首先,我们需要一个目录。所以创建一个目录,你可以自己选择名称,这里只是举个例子。
mkdir demo
然后我们需要将自己放置在这样的目录中:
cd demo
搭建控制台项目
接下来我们需要创建控制台项目。你可以自行选择名称,但我们选择App
。输入以下内容:
dotnet new console -o App
console
这将创建一个名为的新项目App
。
最后我们将该项目添加到解决方案中,如下所示:
dotnet sln add App/App.csproj
安装和设置
为此,我们将安装 Entity Framework 的核心库,并支持 SqlLite 数据库类型。请注意,它支持不同的数据库,请查看此处支持的数据库完整列表:
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.cs
file 文件。我们只关心第一个。我们来看一下:
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()
Order
OrderItem
Product
Down()
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
的形式。如果要将某些内容保存到数据库,则需要调用。Product
OrderItem
db.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
从而保存所有内容。OrderItem
db.OrderItems
db.SaveChanges()
创建订单
现在数据库中已经有了 aProduct
和 an OrderItem
。那么如何创建包含这两个实体的订单呢?
创建订单不仅仅是创建订单,而是创建订单并将订单项与订单关联起来。
关联部分可以通过两种不同的方式完成:
- 将 OrderItem 添加到
order.Items
- 向我们的 OrderItem 添加一个外键并分配我们的订单 ID。
上述两种解决方案都要求我们对实体框架有更多的了解。
加载相关实体
让我们从第一种方法开始。为此,我们需要知道如何加载相关实体。
为什么?
嗯,当你有一个实例时,除非我们明确地告诉它需要填充某些内容,否则Order
它Items
就会一直存在。为了使这种方法有效,我们至少需要它是一个空列表,这样我们才能添加我们的。null
OrderItem
好的,我想你最好给我看看。
当然,请看下面的代码:
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()
属性。Items
Order
让我们运行这段代码:
此时我们的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