Skip to content

EF Core 8 中数组映射的初学者指南

Published: at 12:00 AM

EF Core 8 中数组映射的初学者指南

摘要

EF Core 8 引入了将简单值的类型数组映射到数据库列的支持,因此可以在从 LINQ 查询生成的 SQL 中使用映射的语义。


2024年6月4日

Entity Framework Core 博客帖子可能会变得非常复杂!在这篇文章中,我们将尝试保持基本内容,同时传授比简单信息更多的内容。EF Core 8 已发布六个月了,它的重要新功能之一是数组映射。让我们开始吧!

提示 此处展示的所有代码均可从 GitHub 下载。要开始使用 EF Core,请参阅 安装 Entity Framework Core

假设您想将以下 Post 类型的实例保存到关系数据库中:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Contents { get; set; }
    public string[] Tags { get; set; }
    public DateTime[] Visits { get; set; }
}

这应该很简单,对吧?是的,对于 IdTitleContents 属性来说确实如此。这些是简单的整数和字符串属性,因此我们可以只创建一个具有适当整数和字符串列类型的表。例如,在使用 Azure SQL 时,EF Core 8 默认将这些列映射为:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [Tags] ??? NOT NULL,
    [Visits] ??? NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);

但是 TagsVisits 怎么办呢?这些都是数组属性,而大多数关系数据库不支持数组类型。那么 EF Core 8 如何处理呢?对于支持数组类型的数据库(如 PostgreSQL),EF Core 8 会直接使用这些类型。但对于大多数不原生支持数组类型的数据库,EF Core 8 会自动使用 JSON 数组。这对使用 EF Core 8 的应用程序开发人员来说基本上是透明的;您只需要编写 LINQ 查询,EF Core 8 会为您正在使用的目标数据库生成最合适的翻译。

PostgreSQL 数组列

如果我们的数据库支持数组类型,那么 EF Core 8 会自动使用它们。例如,在 PostgreSQL 上,我们的 Posts 表的完整映射如下:

CREATE TABLE "Posts" (
    "Id" integer GENERATED BY DEFAULT AS IDENTITY,
    "Title" text NOT NULL,
    "Contents" text NOT NULL,
    "Tags" text[] NOT NULL,
    "Visits" timestamp with time zone[] NOT NULL,
    CONSTRAINT "PK_Posts" PRIMARY KEY ("Id")
);

注意 TitleContents 列的类型为 text,表明每行包含一个单独的“title”值或一个单独的“contents”值。而 Tags 列的类型为 text[],表明每行包含一个包含零到多个“tag”值的数组。下面是 Posts 表中的一些示例数据:

IdTitleContentsTagsVisits
1Arrays in EF Core 8Imagine you want…{EF Core,Entity Framework,.NET,Databases}{2024-05-13 12:41:36.957711,2024-05-12 12:41:36.957714}
2What’s new in Orleans 8Let’s take a look at …{Orleans,.NET}{2024-05-14 12:41:36.957779}
3.NET at BuildGet ready for a{.NET,ASP.NET Core}{2024-05-12 12:41:36.957780}

注意数组列每行可以包含多个值。

EF Core 8 然后会在查询翻译中使用这些数组列。例如,下面是一个 LINQ 查询,用于从 Tags 数组列中提取前两个标签:

var postTags = await context.Posts
    .Select(post => new
    {
        PostTitle = post.Title,
        FirstTag = post.Tags[0],
        SecondTag = post.Tags[1]
    }).ToListAsync();

在使用 PostgreSQL 时,EF Core 8 将此 LINQ 查询翻译为以下 SQL:

SELECT p."Title" AS "PostTitle",
       p."Tags"[1] AS "FirstTag",
       p."Tags"[2] AS "SecondTag"
FROM "Posts" AS p

注意 p."Tags"[1]p."Tags"[2] 是如何索引数组以提取前两项的。

另一个常见的 LINQ 查询是查找 Tags 属性包含给定标签值的所有 Post 实例:

var tag = "EF Core";
var posts = await context.Posts
    .Where(post => post.Tags.Contains(tag))
    .ToListAsync();

在使用 PostgreSQL 数组列时,EF Core 8 将此翻译为:

SELECT p."Id", p."Contents", p."Tags", p."Title", p."Visits"
FROM "Posts" AS p
WHERE p."Tags" @> ARRAY[@__tag_0]::text[]

PostgreSQL 的语法有些特殊,所以如果你完全不理解这段 SQL 也不用担心。重点是 WHERE p."Tags" @> ARRAY[@__tag_0]::text[] 通过在 Tags 数组中查找传入的 tag 参数值进行过滤。

JSON 数组

上述情况对于 PostgreSQL 用户非常好,但对于使用 Azure SQL、SQLite 或其他不原生支持数组的数据库系统用户来说如何呢?在这些情况下,EF Core 8 会自动使用 JSON 数组。这意味着在 Azure SQL 上,上述 Post 类型映射为以下表:

CREATE TABLE [Posts] (
     [Id] int NOT NULL IDENTITY,
     [Title] nvarchar(max) NOT NULL,
     [Contents] nvarchar(max) NOT NULL,
     [Tags] nvarchar(max) NOT NULL,
     [Visits] nvarchar(max) NOT NULL,
     CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);

对于 Azure SQL 而言,TagsVisits 列只是简单的字符串列。然而,EF Core 8 知道的更多。EF Core 8 知道这些列实际上包含 JSON 数组,因此可以翻译利用这些知识的查询。例如,下面是相同的 LINQ 查询,用于从 Tags 列中提取前两个值:

var postTags = await context.Posts
    .Select(post => new
    {
        PostTitle = post.Title,
        FirstTag = post.Tags[0],
        SecondTag = post.Tags[1]
    }).ToListAsync();

在 Azure SQL 上,EF Core 8 将此 LINQ 查询翻译为:

SELECT [p].[Title] AS [PostTitle],
       JSON_VALUE([p].[Tags], '$[0]') AS [FirstTag],
       JSON_VALUE([p].[Tags], '$[1]') AS [SecondTag]
FROM [Posts] AS [p]

这与 PostgreSQL 的翻译非常相似。不同之处在于首先使用 JSON_VALUE 函数告诉 SQL Tags 是一个 JSON 文档。然后使用 $[0]$[1] 选择 JSON 数组中第一和第二位置的标签值。

再来看上面的第二个 LINQ 查询:

var tag = "EF Core";
var posts = await context.Posts
    .Where(post => post.Tags.Contains(tag))
    .ToListAsync();

在使用 Azure SQL 时,此查询翻译为:

SELECT [p].[Id], [p].[Contents], [p].[Tags], [p].[Title], [p].[Visits]
FROM [Posts] AS [p]
WHERE @__tag_0 IN (
    SELECT [t].[value]
    FROM OPENJSON([p].[Tags]) WITH ([value] nvarchar(max) '$') AS [t]
)

在此情况下,查询使用 OPENJSONTags 列中的 JSON 数组转换为一种临时表。WHERE @__tag_0 IN 然后用于该临时表以查找包含给定 tag 参数值的行。

类型数组

目前所有示例都使用了字符串数组。然而,EF Core 8 可以处理任何简单类型的数组,包括数字、日期/时间、GUID 等。例如,上述 Post 类中的 Visits 属性是 DateTime 实例的数组。在 PostgreSQL 上,此属性映射为 timestamp with time zone[] 列。这里很明确这实际上是一个时间戳数组,而不是其他类型的数组。EF Core 8 会使用这些信息对数组中的值执行特定于时间戳类型的操作。例如,请考虑这个返回所有在给定年份访问过的帖子实例的 LINQ 查询:

var year = DateTime.UtcNow.Year;
var visited = await context.Posts
    .Where(post => post.Visits.Any(v => v.Year == year))
    .ToListAsync();

在 PostgreSQL 上,这将翻译为:

SELECT p."Id", p."Contents", p."Tags", p."Title", p."Visits"
FROM "Posts" AS p
WHERE EXISTS (
    SELECT 1
    FROM unnest(p."Visits") AS v(value)
    WHERE date_part('year', v.value AT TIME ZONE 'UTC')::int = @__year_0)

注意 date_part 函数如何用于从每个时间戳中提取年份。这只因为数组被认为包含 timestamp 值。

在 Azure SQL 中,类型不能包含在列定义中,这只是一个简单的字符串列:[Visits] nvarchar(max) NOT NULL。然而,EF Core 8 知道这是一个包含时间戳的 JSON 列并能适当使用这些信息进行查询翻译:

SELECT [p].[Id], [p].[Contents], [p].[Tags], [p].[Title], [p].[Visits]
FROM [Posts] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OPENJSON([p].[Visits]) WITH ([value] datetime2 '$') AS [v]
    WHERE DATEPART(year, [v].[value]) = @__year_0)

注意在 OPENJSON 语句中使用的 WITH ([value] datetime2 '$')。这再次告知 Azure SQL 创建一个包含 datetime2 列的临时表。EF Core 8 然后可以编写操作这些 datetime2 值的查询,例如使用 DATEPART 函数提取年份。这仅因为 EF Core 8 理解 JSON 列中存储的内容

数组的其他应用

EF Core 8 允许在大多数可以使用非数组简单类型的地方使用简单类型的数组。我们已经看到属性可以映射到数组列的示例。另一个例子是通过将多个值作为数组传递在一个参数中传递很多值。例如,假设我们希望返回标题以几个字符串之一开头的所有帖子。这里有一个 LINQ 查询来实现这一点:

var prefixes = new[] { "What's new", "Getting started", "Intro to" };
await context.Posts
    .Where(post => prefixes.Any(prefix => post.Title.StartsWith(prefix)))
    .ToListAsync();

查看 PostgreSQL 中此查询的 EF Core 8 日志,可以看到 prefixes 数组作为单个 PostgreSQL 数组参数传递:

info: 5/14/2024 14:34:20.970 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@__prefixes_0={ 'What's new', 'Getting started', 'Intro to' } (DbType = Object)], CommandType='Text', CommandTimeout='30']
      SELECT p."Id", p."Contents", p."Tags", p."Title", p."Visits"
      FROM "Posts" AS p
      WHERE EXISTS (
          SELECT 1
          FROM unnest(@__prefixes_0) AS p0(value)
          WHERE p0.value IS NOT NULL AND left(p."Title", length(p0.value)) = p0.value)

EF Core 8 然后会使用 PostgreSQL 的 unnest 函数应用所有传递到数组参数中的值的过滤器。

如果查看 Azure SQL 中的日志,我们会看到参数是一个 JSON 数组,包含在一个字符串参数中:

info: 5/14/2024 14:42:57.689 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (4ms) [Parameters=[@__prefixes_0='["What\u0027s new","Getting started","Intro to"]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Contents], [p].[Tags], [p].[Title], [p].[Visits]
      FROM [Posts] AS [p]
      WHERE EXISTS (
          SELECT 1
          FROM OPENJSON(@__prefixes_0) WITH ([value] nvarchar(max) '$') AS [p0]
          WHERE [p0].[value] IS NOT NULL AND LEFT([p].[Title], LEN([p0].[value])) = [p0].[value])

EF Core 8 生成的 SQL 使用 OPENJSON 创建一个临时表,不过这次是在参数值上。

将数组属性与数组参数组合使用允许翻译简短但强大的 LINQ 查询。例如,这个 LINQ 查询返回包含任何给定标签值的所有帖子:

var tags = new[] { ".NET", "ASP.NET Core" };
await context.Posts
    .Where(post => tags.Any(tag => post.Tags.Contains(tag)))
    .ToListAsync();

在具有原生数组类型的 PostgreSQL 上,此查询翻译为:

SELECT p."Id", p."Contents", p."Tags", p."Title", p."Visits"
FROM "Posts" AS p
WHERE @__tags_0 && p."Tags"

在使用 JSON 数组的 Azure SQL 上,SQL 如下:

SELECT [p].[Id], [p].[Contents], [p].[Tags], [p].[Title], [p].[Visits]
FROM [Posts] AS [p]
WHERE EXISTS (
    SELECT 1
    FROM OPENJSON(@__tags_0) WITH ([value] nvarchar(max) '$') AS [t]
    WHERE [t].[value] IN (
        SELECT [t0].[value]
        FROM OPENJSON([p].[Tags]) WITH ([value] nvarchar(max) '$') AS [t0]
    ))

在两种情况下,值都作为数组参数传递,然后这些值与从数组列中取出的值一起使用。

常见问题

以下是一些人们关于数组映射常见问题。

总结

EF Core 8 引入了将简单值的类型数组映射到数据库列的支持。如果可用,则使用本地数据库数组类型,否则 EF Core 8 使用包含 JSON 数组的字符串列。无论哪种情况,EF Core 8 都能理解映射的语义,从而可以执行依赖于数组中类型值的查询。简单值的数组还可以在其他地方使用,例如在单个参数中传递多个值给 EF Core。

了解更多

我们只触及了可以使用这些模式翻译的查询类型的表面。请查看以下资源以了解更深入的信息:

To learn more about EF Core 8, see Entity Framework Core 8 (EF Core 8) is available today announcement post.