PostgreSQL与EF Core无缝集成指南:从基础设置到高级封装

作者:微信公众号:【架构师老卢】
8-2 8:29
13

根据最新的Stack Overflow调查,PostgreSQL是目前最受欢迎的数据库。当然,作为一款多功能的ORM框架,EF Core与PostgreSQL能很好地配合使用。不过,要将这两者集成起来,还需要完成几个步骤,过程中也有一些注意事项。在本文中,我们将一起走完这些步骤,并实现几个辅助方法,让未来的集成工作更加简单。

如果你只想使用简化后的PostgreSQL连接方式,可以直接跳到文章末尾的TLDR部分。

项目设置:使用Docker Compose部署Postgres并建立初始原始连接

首先,让我们搭建一个本地的PostgreSQL实例。下面是一个简单的compose.yml文件,可以实现这一功能:

services:
  postgres:
    image: postgres
    environment:
      POSTGRES_DB: playground
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432

执行docker compose up -d命令,让数据库启动并运行后,我们来搭建一个测试项目。我们将使用最基本的Minimal API模板:

dotnet new web

我们还可以让日志输出更美观一些,并移除app.Run()调用,因为我们实际上并不需要一个运行中的主机:

builder.Logging.AddSimpleConsole(c => c.SingleLine = true); // 新增代码

// 移除的代码 -> app.Run()

最重要的是,让我们连接到数据库。目前,我们只需要一个包——Entity Framework Core的PostgreSQL提供程序:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

有了这个包,我们就应该能够连接到之前部署的数据库了。但首先,我们需要一个DbContext——让我们声明一个空的DbContext:

public class Db(DbContextOptions<Db> options) : DbContext(options) {
}

使用这个DbContext,我们可以在依赖注入容器中注册我们的数据库:

builder.Services.AddDbContext<Db>((sp, options) =>
{
  options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground");
});

最后,让我们通过记录连接结果来测试我们的设置。下面的代码可以实现这一点:

await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);

执行dotnet run后,控制台应该会打印出Can connect to database: True。这就是我们的设置——为了方便参考,下面是完整的Program.cs代码:

var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddSimpleConsole(c => c.SingleLine = true);

builder.Services.AddDbContext<Db>((sp, options) =>
{
    options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground");
});

var app = builder.Build();

await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);

public class Db(DbContextOptions<Db> options) : DbContext(options) {
}

连接数据库已经没问题了,但我们如何执行一些SQL操作呢?让我们在下一节中直接进入这个话题。

第一个查询:搭建数据库并发出示例请求

要执行一个合适的SQL查询,我们需要一个表。让我们搭建一个简单的表,名字就叫Records(记录):

public class Db(DbContextOptions<Db> options) : DbContext(options) {
    public DbSet<Record> Records { get; set; } = null!;
}

public class Record
{
    public int Id { get; set; }
    public required string Name { get; set; }
}

为了进行实验,我们需要一个全新的数据库,每次启动时都创建表。我们可以这样做:

友情提醒:不要在实际应用程序中使用EnsureDeleted或EnsureCreated。

await db.Database.EnsureDeletedAsync();
await db.Database.EnsureCreatedAsync();

有了架构之后,让我们执行最基本的添加和读取记录操作:

db.Add(new Record { Name = "Test" });

await db.SaveChangesAsync();

var records = await db.Records.ToListAsync();

现在,执行dotnet run后,我们应该能看到EF Core对数据库执行的命令:

INSERT INTO "Records" ("Name") VALUES (@p0) RETURNING "Id";
SELECT r."Id", r."Name" FROM "Records" AS r

执行查询的过程很顺利,不是吗?但是,查询看起来相当难看,每个表名和列名周围都有引号。让我们在下一节中解决这个问题!

蛇形命名法:让EF与PostgreSQL友好协作

PostgreSQL对蛇形命名法(snake_case)有相当强烈的偏好:如果列名或表名不是蛇形命名的,除非用引号括起来,否则PostgreSQL甚至不会识别它。另一方面,EF Core默认使用帕斯卡命名法(PascalCase)。当然,这种不一致会给PostgreSQL查询的工作带来一些麻烦。让我们通过使用一个命名约定包来解决这个问题:

dotnet add package EFCore.NamingConventions

安装好包后,我们可以更新DbContext的注册,应用蛇形命名法约定。这将确保所有表名和列名都会自动转换:

builder.Services.AddDbContext<Db>((sp, options) =>
{
    options.UseNpgsql("Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground")
        .UseSnakeCaseNamingConvention();
});

更改后,我们的查询将会是这样的:

INSERT INTO records (name) VALUES (@p0) RETURNING id;
SELECT r.id, r.name FROM records AS r

现在我们的查询看起来好多了,是时候解决下一个问题了。如果你和我一样,看到连接字符串硬编码应该已经开始觉得不舒服了。让我们在下一节中解决这个问题。

更好的注册方式:利用.NET配置系统并创建扩展方法

避免硬编码连接字符串很容易——我们将从配置中读取值:

builder.Services.AddDbContext<Db>((sp, options) =>
{
    var connectionString = builder.Configuration.GetConnectionString("Postgres");

    options.UseNpgsql(connectionString)
        .UseSnakeCaseNamingConvention();
});

显然,我们还需要设置这个值。无论使用什么配置源,我们的代码都不应该有太多变化,但正如我在这篇文章中解释的那样,我用于存储此类值的配置源是launchSettings.json,所以我们需要添加以下行:

"ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"

下面是launchSettings.json大致的样子:

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "applicationUrl": "http://localhost:5267",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"
      }
    }
  }
}

现在,执行dotnet run应该会得到和之前完全相同的结果。现在代码已经不错了,但我们能不能创建一个扩展方法,让PostgreSQL数据库的注册更加顺畅呢?让我们开始吧,将配置解析转移到一种更灵活的方法,避免依赖WebApplicationBuilder。

由于我们的连接字符串很可能是必需的,我们需要一个IConfiguration.GetRequiredValue方法。幸运的是,有一个包提供了这样的方法:

dotnet add package Confi

现在,我们将从依赖注入容器中解析IConfiguration,而不是依赖WebApplicationBuilder。代码如下:

var config = sp.GetRequiredService<IConfiguration>();
var connectionString = config.GetRequiredValue(configurationPath);

使用这种方法,我们应该能够组装:

using Confi;

// ...

builder.Services.AddPostgreDbContext<Db>();

// ...

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddPostgreDbContext<TContext>(this IServiceCollection services, string configurationPath = "ConnectionStrings:Postgres")
        where TContext : DbContext
    {
        return services.AddDbContext<TContext>((sp, options) => {
            var config = sp.GetRequiredService<IConfiguration>();
            var connectionString = config.GetRequiredValue(configurationPath);
            options.UseNpgsql(connectionString)
                .UseSnakeCaseNamingConvention();
        });
    }
}

同样,结果应该和之前完全相同,但注册只需要一行简单的代码。在最后一节中,你会找到文章的快速回顾,以及一些额外内容,让PostgreSQL集成更加容易。

另外,也欢迎你自己进行实验。你可以在GitHub上找到本文的完整代码。

TLDR(简要总结)

在本文中,我们实现了一个辅助方法,用于在.NET应用程序中无缝注册PostgreSQL数据库。你不必从头重新创建这个方法,可以使用Persic.EF包:

dotnet add package Persic.EF.Postgres

安装好包后,你只需一行代码就能附加数据库:

builder.Services.AddPostgres<Db>();

当然,你需要先部署数据库。下面是一个简单的compose.yml文件:

services:
  postgres:
    image: postgres
    environment:
      POSTGRES_DB: playground
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432

最后,不要忘记将连接字符串设置为ConnectionStrings:Postgres配置值。我的建议是利用launchSettings.json来设置:

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "applicationUrl": "http://localhost:5267",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "ConnectionStrings:Postgres" : "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=playground"
      }
    }
  }
}

下面是一个测试连接的简单代码片段:

await using var scope = app.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<Db>();
var canConnect = await db.Database.CanConnectAsync();
app.Logger.LogInformation("Can connect to database: {CanConnect}", canConnect);

我们的PostgreSQL集成之旅到此结束。本文以及Persic.EF.Postgres包都是persic项目的一部分,该项目包含各种与数据库相关的工具。

相关留言评论
昵称:
邮箱:
阅读排行