获得数据
现在, 我们已经在我们的数据库中添加了测试用数据. 下面我们来使用 Entity Framework Core 5 来进行查询并筛选数据. 要筛选数据, 我们既可以使用 LINQ 查询语法, 也可以使用方法语法. 我个人倾向于混合使用两种方法来检索我需要的数据. 我首先会介绍查询语法, 然后会说明方法语法. 在介绍基础的 LINQ 之后, 我会介绍如何基于数据库, 通过一些 NUnit 集成测试来测试 Entity Framework Core.
LINQ 查询
我先展示 LINQ
语法, 因为我认为它更容易理解. 一个 LINQ
查询看起来有点像 SQL
查询, 除了 "select"
子句紧跟的 "from"
子句之外. 例如, 使用 first name 为 "Susan" 来查询所有数据, 我们可以按下面的方式来使用查询语法:
var persons = from p in Persons
where p.FirstName == "Susan"
select p;
这个查询不会立即在数据库中执行, 直到晚一些, 一些方法语法, 例如 ToList()
, 执行时它才会被执行. "from"
子句告诉我们需要查询哪一个集合, 本例中, 就是 AppDbContext
中的 Persons
. "where"
子句是指定任意查询条件的地方. 本例中, 我们想要获得 "FirstName"
属性值为 "Susan"
的所有的 person 记录. "where"
子句中可以使用任何断言, 因此你可以在集合中含有更为复杂的过滤表达式. 最后, "select"
来指定我们想要从集合中显示的数据. 可以可以是完整的实体, 例如我们的查询示例. 你也可以选择单个属性值, 或创建一个具名或匿名类型的实例.
例如, 我们可以修改这个查询, 来仅仅显示所有 Person 的 "Id"
属性值, 条件是 FirstName
值为 "Susan"
. 例如:
var persons = from p in Persons
where p.FirstName == "Susan"
select p.Id;
本例中, 我们会获得一个整数值的集合, 而不是整个 Person 实体. 集合的类型是 IQueryable<T>
, 其中的 T
的数据类型是在 select
子句中指定数据的类型. 在上面的查询中, T
是 int
, 因为它是 Id
属性的类型.
查询匿名类型
如果你不想创建一个完整的类来获得从数据库中获得的数据, 你可以使用匿名类型来代替一个具名类型. 例如, 我们可以从 person 记录中只查询出 FirstName
和 LastName
. 就像是:
var persons = from p in Persons
where p.FirstName == "Susan"
select new { p.FirstName, p.LastName };
本例中, 将会得到一个 IQueryable<T>
类型的数据, 其中 T
是匿名类型. 匿名类型是一个应用程序中还未定义的类型, 并且它由编译器来处理. 如果你想知道它真正的工作方式, .NET 会创建一个你看不到的类. 如果你需要创建一个中间查询, 或需要在代码中不会使用, 导出到其他位置的数据, 选择匿名类型是很有用的. 大多数情况下, 我更愿意创建一个数据转换对象 (Data Transfer Object, DTO) 类, 如果该数据会被其他地方的代码所使用, 例如公开 API.
选择对象变换 (Select Object Transformation)
按照最后一个示例, 我们利用匿名类型查询 FirstName
和 LastName
属性. 如果想要将该数据导出到代码中的其他部分, 我们可以使用 DTO, 首先, 我们创建 DTO 类, 例如:
public class PersonDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
下一步, 我们创建一个方法来展示在 LINQ 查询中使用 DTO 变换来进行检索. 例如:
public IQueryable<PersonDto> GetDisplayPersons()
{
return from p in Persons
where p.FirstName == "Susan"
select new PersonDto
{
FirstName = p.FirstName,
LastName = p.LastName
};
}
最后, 我们得到的是同样的数据. 但是现在, 你的应用程序获得的是具名的强类型对象.
在 LINQ 中连接
如果你熟悉 SQL, 你就会知道, 有些时候需要从多个实体中获取数据. 通常这些实体之间存在一对多的关系, 它们一般使用主外键约束关联起来. 在我们的示例中, 我们有 person 和 address 记录. 其中每一个 person 通过 PersonId 属性关联到 Address 实体, 每一个 Person 含有一个或多个 Address. 在我们的案例中, 有两种方式同 person 来获得 address: 我们可以使用 LINQ 连接 (join) 来关联两个实体, 我们还可以使用我们的导航属性. 我首先会介绍如何使用 join, 然后给你展示如何使用导航属性来展示查询.
例如, 要根据 Susan 获得所有的地址, 我们需要使用下面的 LINQ 查询:
var personAddresses = from p in Persons
join a in Addresses on p.Id equals a.PersonId
where p.FirstName == "Susan"
select a;
如你所看, "join"
子句连接两个实体, 通过一个关联的属性值. 一般情况下, 它是数据库中的外键列. 在我们的案例中, 公共属性是 Person
实体的 Id
属性.
选择导航属性
我们可以使用, 在 Person 实体中起到连接作用的导航属性 Addresses, 从 join 案例中简化这个查询, 看下面代码:
var personAddresses = (from p in Persons
where p.FirstName == "Susan"
select p.Addresses).SelectMany(a => a);
这段代码通过 SelectMany
方法语法来匹配 join 示例.
数据排序
在 LINQ 查询中, 使用 order by
子句, 可以轻松的实现对数据的排序. 你可以对一个或多个列进行排序, 并且可以根据每列进行升序和降序排序. 例如, 利用 lastName 降序, 和 firstName 升序来对 person 记录来排序, 你可以执行下面查询:
var personAddresses = from p in Persons
orderby p.LastName descending, p.firstName
select p;
方法语法查询
fluent LINQ 语法使用方法链式编程来获取数据. 你可以使用扩展方法 Where()
来过滤集合, 可以将一段谓词判断传入该方法中. 谓词定义了一个过滤的标准. 例如, 从 person 集合中, 将 lastName 为 Smith 的所有记录取出来, 我们可以使用:
var persons = Persons.Where(x => x.LastName=="Smith").ToList();
最后添加的 ToList()
方法会在我们的数据库上运行. 要获得单条记录, 我们可以使用 Single()
, First()
, 以及 Last()
方法. 所有的这些方法功能如其名. 若检索未捕获到记录, 那么 Signle()
方法会抛出异常. 还有一些方法 SingleOrDefault()
, FirstOrDefault()
以及 LastOrDefault()
可以使用. 两组方法的不同点在于, Signle()
, First()
, 以及 Last()
方法在未检索到数据时会抛出异常. 而 OrDefault
版本的方法在没有检索到数据时会返回 null
.
若你仅仅想要获得数据的一部分 (subset
, 理解为数据投影会好点, 有点术语存在. 并非集合的子集, 而是应该理解为, 关系的子集, 数学术语浓厚.) 或将数据映射为新的数据类型, 那么可以选择使用 Select
方法. 例如, 仅仅获得 person 的 FirstName, 可以像下面这样使用 Select
方法:
var persons = Persons.Select(x => x.FirstName);
要对数据进行排序, 你可以使用 OrderBy
或 OrderByDescending()
方法. 若你需要对多个属性进行排序, 在 OrderBy()
或 OrderByDescending()
方法后跟上调用 ThenBy()
或 ThenByDescending()
方法. 例如, 要根据 LastName 降序, FirstName 升序对所有的 Person 记录进行排序, 你可以这么用:
var perspns = Persons
.OrderByDescending(x => x.LastName)
.ThenBy(x => x.FirstName);
如果你仅仅需要指定数据的一个范围 (这里才是指集合的子集的概念), 你可以使用 Skip()
和 Take()
方法. 它们通常用于实现服务端分页的逻辑. 例如, 如果你要实现一个一般的分页方法, 可以这么写:
public IEnumerable<T> GetPagedData<T>(IQueryable<T> data, int pageSize, int page)
where T: class, new() {
return data.Skip((page - 1) * pageSize).Take(pageSize).ToList();
}
然后, 要获得每页十条记录的第一页数据列表, 可以像下面一样调用该方法:
var persons = GetPagedData(Persons, 10, 1);
我们也可以很容易地使用方法语法, 对我们的 address 记录进行过滤. 例如, 获得 IL 州的所有地址, 我们可以使用下面查询:
var addresses = Addresses.Where(x => x.State == "IL").ToList();
如你所看到的, fluent API 非常的简洁, 并且容易达到目标. 我常常会使用方法语法, 除非需要对数据进行连接或分组.
测试我们的数据
此时, 我们已经简单的了解到如何查询数据了. 下面是时候使用一些 NUnit 测试项目, 来测试 Entity Framework Core 5 是否正确的连接到我们的 SQL Server 数据库了. 如我们这样直接对 SQL Server 数据库进行的测试, 一般被称为集成测试.
首先, 从更名开始. 将 DAL.Tests 项目中的 UnitTest1 类文件更名为 SelectTest. 删除其中的测试代码, 然后写入下面的代码:
using NUnit.Framework;
namespace EFCore5WebApp.DAL.Tests {
[TestFixture]
public class SelectTests {
}
}
为了连接到 SQL Server 数据库, 我们需要实例化 AppDbCOntext
类, 并传入我们的数据库连接字符串. 我们需要在 SetUp
方法中来做这个事情. 这个方法, 只会为 SelectTests
类中的所有测试调用一次. 我们只需要创建一个私有字段 _context
, 并在 SetUp
方法中进行初始化, 例如:
private AppDbCOntext _context;
[SetUp]
public void SetUp() {
_context = new AppDbContext(
new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfCore5WebApp;Trusted_Connection=True;MultipleActiveResultSets=true")
.Options
);
}
现在, 我们有了一个连接到我们数据库的对象, 我们可以使用 NUnit 集成测试来验证之前插入的种子数据是否正确. 作为第一个简单的测试, 我们首先确保创建了两条 person 记录. 使用单元测试或集成测试的一个简单模型是, 设置, 执行, 断言 (AAA, arrange, act, assert). 在我们第一个集成测试中, 我们在一个方法调用中合并了设置和执行的步骤, 我们会获得所有记录. 然后我们会断言我们有两条 person 记录. 代码如下:
[Test]
public void GetAllPersons() {
IEnumerable<Person> persons = _contex.Persons.ToList();
Assert.AreEqual(2, persons.Count());
}
现在, 你的 SelectTests
类文件看起应该是这样:
using EFCore5WebApp.Core.Entities;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
namespace EFCore5WebApp.DAL.Tests
{
public class Tests
{
private AppDbContext _context;
[SetUp]
public void Setup()
{
_context = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfCore5WebApp;Trusted_Connection=True;MultipleActiveResultSets=true")
.Options);
}
[Test]
public void GetAllPersons()
{
IEnumerable<Person> persons = _context.Persons.ToList();
Assert.AreEqual(2, persons.Count());
}
}
}
通过单元测试, 我们来验证第一个 person 有一个 address, 第二个 person 有两个 address. 代码如下:
[Test]
public void PersonsHaveAddresses()
{
List<Person> persons = _context.Persons.Include("Addresses").ToList();
Assert.AreEqual(1, persons[0].Addresses.Count);
Assert.AreEqual(2, persons[1].Addresses.Count);
}
这一段是我个人认为不合适的位置, 悄悄的带有
Include
方法, 对于入门级教程来说非常的糟糕.
最后, 我们创建一个单元测试, 来测试我们之前插入的 LookUp
记录是否成功. 我会验证我们有一个国家数据, 以及 51 个州. 你的 SelectTests
类应该看起来像这样:
using EFCore5WebApp.Core.Entities;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
namespace EFCore5WebApp.DAL.Tests
{
public class Tests
{
private AppDbContext _context;
[Test]
public void HaveLookUpRecords()
{
var lookUps = _context.LookUps.ToList();
var countties = lookUps.Where(x => x.LookUpType == LookUpType.Country).ToList();
var states = lookUps.Where(x => x.LookUpType == LookUpType.State).ToList();
Assert.AreEqual(1, countties.Count);
Assert.AreEqual(51, states.Count);
}
[SetUp]
public void Setup()
{
_context = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfCore5WebApp;Trusted_Connection=True;MultipleActiveResultSets=true")
.Options);
}
[Test]
public void GetAllPersons()
{
IEnumerable<Person> persons = _context.Persons.ToList();
Assert.AreEqual(2, persons.Count());
}
[Test]
public void PersonsHaveAddresses()
{
List<Person> persons = _context.Persons.Include("Addresses").ToList();
Assert.AreEqual(1, persons[0].Addresses.Count);
Assert.AreEqual(2, persons[1].Addresses.Count);
}
}
}
小结
本章中, 我介绍了如何使用查询语法. 然后, 介绍了一些方法语法的基础. 我们使用两种语法来查询, 筛选, 以及排序数据. 我们还演示了如何使用 Skip() 和 Take() 方法来实现分页. 最后, 我们使用 NUnit 集成测试来验证了我们之前插入的测试数据是否成功. 这个集成测试在测试迁移中是否成功创建数据非常有用. 下一章中, 我会介绍如何使用 Entity Framework Core 5 来插入数据.