# 模块二 基础巩固 EF Core 查询

## 2.4.5 EF Core -- 查询 <a href="#id-245efcore-cha-xun" id="id-245efcore-cha-xun"></a>

* 关联数据加载
* 客户端与服务端运算
* 跟踪与不跟踪
* 复杂查询运算
* 原生 SQL 查询
* 全局查询筛选器

### 关联数据加载 <a href="#guan-lian-shu-ju-jia-zai" id="guan-lian-shu-ju-jia-zai"></a>

学员和助教都在项目分组中，调整模型，删除 Assistant

ProjectGroup 添加 Member 列表

```
public List<Member> Members { get; set; }
```

Member 添加 是否助教判断，分组信息

```
public bool IsAssistant { get; set; }

public string GroupId { get; set; }

public ProjectGroup Group { get; set; }
```

Task 添加 学员信息

```
public Member Member { get; set; }
```

接下来为每一个表添加一个控制器

一个 Project 对应多个 ProjectGroup

ProjectGroup

```
namespace LighterApi.Controller
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProjectGroupController : ControllerBase
    {
        private readonly LighterDbContext _lighterDbContext;

        public ProjectGroupController(LighterDbContext lighterDbContext)
        {
            _lighterDbContext = lighterDbContext;
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] ProjectGroup group)
        {
            _lighterDbContext.ProjectGroups.Add(group);
            await _lighterDbContext.SaveChangesAsync();

            return StatusCode((int) HttpStatusCode.Created, group);
        }
        
        [HttpGet]
        [Route("{id}")]
        public async Task<IActionResult> GetAsync(string id, CancellationToken cancellationToken)
        {
            var project = await _lighterDbContext.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
            return Ok(project);
        }
    }
}
```

迁移

```
dotnet ef migrations add RefactoryProjectEntities

dotnet ef database update
```

Entity 主键添加自动生成

```
/// <summary>
/// 主键Id
/// </summary>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
```

启动程序，Postman 访问

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2FWGYFI4pKctfrc20lpBKq%2F204.jpg?alt=media\&token=1ccbab1c-29df-4c5e-ab4b-077604a0ed6c)

ProjectController

```
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetAsync(string id, CancellationToken cancellationToken)
{
    var project = await _lighterDbContext.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
    return Ok(project);
}
```

查询项目信息，发现分组信息 groups 为空

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2F3DgtvNelx6qEo7en0IWo%2F205.jpg?alt=media\&token=ecedd99b-5f1d-47c6-b42f-349ea1a4a969)

因为 EF 默认不会查询关联数据，所以需要实现一下

ProjectController 获取项目时使用 Include

```
[HttpGet]
[Route("{id}")]
public async Task<IActionResult> GetAsync(string id, CancellationToken cancellationToken)
{
    var project = await _lighterDbContext.Projects.Include(p => p.Groups)
        .FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
    return Ok(project);
}
```

由于项目中有分组引用，分组中有项目引用，所以需要在序列化的时候处理循环引用

Startup

```
services.AddControllers()
        .AddNewtonsoftJson(x=>x.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore);
```

这样就可以查到项目信息

![](https://3083743005-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F8gwpNo3eyzHkX0O40HRA%2Fuploads%2FHplBV0dztSt162ADyxuS%2F206.jpg?alt=media\&token=62f513c1-b022-4fb8-8e01-d183d18b0308)

EF Core 为我们提供了三种加载数据的方式

* 预先加载
* 显式加载
* 延迟加载

加载相关数据：<https://docs.microsoft.com/zh-cn/ef/core/querying/related-data/>

#### 预先加载 <a href="#yu-xian-jia-zai" id="yu-xian-jia-zai"></a>

预先加载表示从数据库中加载关联数据，作为初始查询的一部分。

在以下示例中，结果中返回的blogs将使用关联的posts填充其 Posts 属性。

```
using (var context = new BloggingContext())
{
    var blogs = context.Blogs
        .Include(blog => blog.Posts)
        .ToList();
}
```

#### 显式加载 <a href="#xian-shi-jia-zai" id="xian-shi-jia-zai"></a>

显式加载表示稍后从数据库中显式加载关联数据。

可以通过 DbContext.Entry(...) API 显式加载导航属性。

```
using (var context = new BloggingContext())
{
    var blog = context.Blogs
        .Single(b => b.BlogId == 1);

    context.Entry(blog)
        .Collection(b => b.Posts)
        .Load();

    context.Entry(blog)
        .Reference(b => b.Owner)
        .Load();
}
```

ProjectController

```
// 显式加载
var project = await _lighterDbContext.Projects.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
await _lighterDbContext.Entry(project).Collection(p => p.Groups).LoadAsync(cancellationToken);
```

#### 延迟加载 <a href="#yan-chi-jia-zai" id="yan-chi-jia-zai"></a>

延迟加载表示在访问导航属性时，从数据库中以透明方式加载关联数据。

使用延迟加载的最简单方式是通过安装 Microsoft.EntityFrameworkCore.Proxies 包，并通过调用 UseLazyLoadingProxies 来启用该包。

```
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseLazyLoadingProxies()
        .UseSqlServer(myConnectionString);
```

或在使用 AddDbContext 时：

```
.AddDbContext<BloggingContext>(
    b => b.UseLazyLoadingProxies()
          .UseSqlServer(myConnectionString));
```

EF Core 接着会为可重写的任何导航属性（即，必须是 virtual 且在可被继承的类上）启用延迟加载。 例如，在以下实体中，Post.Blog 和 Blog.Posts 导航属性将被延迟加载。

```
public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public virtual Blog Blog { get; set; }
}
```

Project

```
public virtual ICollection<ProjectGroup> Groups { get; set; }
```

ProjectController

```
// 延迟加载
project.Groups// 引用到属性时才加载
```

### 客户端与服务端运算 <a href="#ke-hu-duan-yu-fu-wu-duan-yun-suan" id="ke-hu-duan-yu-fu-wu-duan-yun-suan"></a>

客户端与服务端运算：<https://docs.microsoft.com/zh-cn/ef/core/querying/client-eval>

由于 SQL Server 提供程序不了解此方法的实现方式，因此无法将其转换为 SQL。 查询的所有其余部分是在数据库中评估的，但通过此方法传递返回的 URL 却是在客户端上完成。

```
var blogs = context.Blogs
    .OrderByDescending(blog => blog.Rating)
    .Select(blog => new
    {
        Id = blog.BlogId,
        Url = StandardizeUrl(blog.Url)// 服务端转换SQL，不了解客户端方法实现
    })
    .ToList();

public static string StandardizeUrl(string url)
{
    url = url.ToLower();

    if (!url.StartsWith("http://"))
    {
        url = string.Concat("http://", url);
    }

    return url;
}
```

需要区分数据运算最终在客户端，还是服务端运行

循环中获取分组会导致多次查询数据库

```
foreach (var project in _lighterDbContext.Projects)
{
    project.Groups// 多次查询数据库
}
```

应该一次性查询

```
var projects = _lighterDbContext.Projects.ToList();
```

### 跟踪与不跟踪 <a href="#gen-zong-yu-bu-gen-zong" id="gen-zong-yu-bu-gen-zong"></a>

跟踪与不跟踪：<https://docs.microsoft.com/zh-cn/ef/core/querying/tracking>

默认情况下，跟踪返回实体类型的查询。 这表示可以更改这些实体实例，然后通过 SaveChanges() 持久化这些更改。

非跟踪查询

```
var blogs = context.Blogs
    .AsNoTracking()
    .ToList();
```

还可以在上下文实例级别更改默认跟踪行为：

```
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

var blogs = context.Blogs.ToList();
```

### 复杂查询运算 <a href="#fu-za-cha-xun-yun-suan" id="fu-za-cha-xun-yun-suan"></a>

复杂查询运算：<https://docs.microsoft.com/zh-cn/ef/core/querying/complex-query-operators>

联接

```
var query = from photo in context.Set<PersonPhoto>()
            join person in context.Set<Person>()
                on photo.PersonPhotoId equals person.PhotoId
            select new { person, photo };
```

GroupJoin

```
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.PostId into grouping
            select new { b, grouping };
```

SelectMany

```
var query = from b in context.Set<Blog>()
            from p in context.Set<Post>()
            select new { b, p };
```

GroupBy

```
var query = from p in context.Set<Post>()
            group p by p.AuthorId into g
            select new
            {
                g.Key,
                Count = g.Count()
            };
```

Left Join

```
var query = from b in context.Set<Blog>()
            join p in context.Set<Post>()
                on b.BlogId equals p.BlogId into grouping
            from p in grouping.DefaultIfEmpty()
            select new { b, p };
```

### 原生 SQL 查询 <a href="#yuan-sheng-sql-cha-xun" id="yuan-sheng-sql-cha-xun"></a>

原生 SQL 查询：<https://docs.microsoft.com/zh-cn/ef/core/querying/raw-sql>

```
var blogs = context.Blogs
    .FromSqlRaw("SELECT * FROM dbo.Blogs")
    .ToList();
```

### 全局查询筛选器 <a href="#quan-ju-cha-xun-shai-xuan-qi" id="quan-ju-cha-xun-shai-xuan-qi"></a>

全局查询筛选器：<https://docs.microsoft.com/zh-cn/ef/core/querying/filters>

```
modelBuilder.Entity<Blog>().HasQueryFilter(b => EF.Property<string>(b, "_tenantId") == _tenantId);
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted);
```

所有实体都继承了基类 Entity，所以这样会把过滤器添加在所有查询上面

LighterDbContext

```
modelBuilder.Entity<Entity>().HasQueryFilter(x => x.TenantId == "");
```

### GitHub源码链接： <a href="#github-yuan-ma-lian-jie" id="github-yuan-ma-lian-jie"></a>

<https://github.com/MingsonZheng/ArchitectTrainingCamp/tree/main/LighterApi>
