实体框架7中

时间:2016-03-10 10:59:26

标签: c# entity-framework postgresql entity-framework-core npgsql

我将实体框架7与Npgsql适配器一起使用。 EF生成的Sql似乎是

SELECT "r"."Id", "r"."Name" FROM "public"."Role" AS "r"

并且它在Postgres中不起作用,因为区分大小写的策略。为了使它工作,我需要编写创建表脚本

CREATE TABLE "Role" (
    "Id" int,
    "Name" varchar(200)
);

但它很难看。有没有办法让EF生成没有引号或小写命名风格的脚本?

7 个答案:

答案 0 :(得分:3)

  1. 在NpgsqlSqlGenerationHelper中覆盖DelimitIdentifier,如下所示:

    public class SqlGenerationHelper : NpgsqlSqlGenerationHelper
    {
        public override string DelimitIdentifier(string identifier) => identifier.Contains(".") ? base.DelimitIdentifier(identifier) : identifier;
    }
    
  2. 使用ReplaceService方法将ISqlGenerationHelper替换为您的类:

    public class MyContext : DbContext
    {
        public virtual DbSet<MyTable> MyTable { get; set; }
    
        public MyContext(DbConnection connection) :
               base(new DbContextOptionsBuilder().UseNpgsql(connection)
                                                 .ReplaceService<ISqlGenerationHelper, SqlGenerationHelper>()
                                                 .Options) 
        { }
    }
    

答案 1 :(得分:2)

为此,您需要使用自己的无引号小写版本替换SQL生成服务。为此,您需要了解EF如何使用DI(尝试阅读Understanding EF Services),并且需要替换生成SQL的服务。在EF中,这可能是ISqlGenerationHelperIMigrationsSqlGeneratorIUpdateSqlGenerator,具体取决于具体情况。

答案 2 :(得分:2)

我真的不喜欢在PostgreSql数据库中使用PascalCase标识符,因为我直接对数据库进行了大量的手动查询,所以对于我的新.NET Core解决方案,我有点极端改变它。

首先,我使用PascalCase实体类定义了我的标准ApplicationDbContext并将其标记为抽象,然后我专门为Postgres实现创建了一个PgDbContext。

接下来,我创建了一个类似的辅助方法:

    public static string FromPascalCaseToSnakeCase(this string str)
    {
        return string.IsNullOrWhiteSpace(str) ? str : string.Concat(str.Select((x, i) => i > 0 && char.IsUpper(x) ? "_" + x.ToString() : x.ToString())).ToLower();
    }

然后我通过实现一些与Npgsql相关的类来覆盖一些关键方法:

public class LowercaseSqlGenerationHelper : RelationalSqlGenerationHelper
{
    public LowercaseSqlGenerationHelper(RelationalSqlGenerationHelperDependencies dependencies) : base(dependencies)
    {
    }

    public override void DelimitIdentifier(StringBuilder builder, string identifier)
    {
        base.DelimitIdentifier(builder, identifier.FromPascalCaseToSnakeCase());
    }

    public override void DelimitIdentifier(StringBuilder builder, string name, string schema)
    {
        base.DelimitIdentifier(builder, name.FromPascalCaseToSnakeCase(), schema.FromPascalCaseToSnakeCase());
    }

    public override string DelimitIdentifier(string identifier)
    {
        return base.DelimitIdentifier(identifier.FromPascalCaseToSnakeCase());
    }

    public override string DelimitIdentifier(string name, string schema)
    {
        return base.DelimitIdentifier(name.FromPascalCaseToSnakeCase(), schema.FromPascalCaseToSnakeCase());
    }
}

public class LowercaseQuerySqlGenerator : NpgsqlQuerySqlGenerator
{
    public LowercaseQuerySqlGenerator(QuerySqlGeneratorDependencies dependencies, RelationalSqlGenerationHelperDependencies rSGenDep, SelectExpression selectExpression) : 
        base(
            new QuerySqlGeneratorDependencies(dependencies.CommandBuilderFactory, 
                new LowercaseSqlGenerationHelper(rSGenDep), 
                dependencies.ParameterNameGeneratorFactory, 
                dependencies.RelationalTypeMapper)
            , selectExpression)
    {
    }
}

public class LowercaseHistoryRepository:NpgsqlHistoryRepository
{
    public LowercaseHistoryRepository(HistoryRepositoryDependencies dependencies) : base(dependencies)
    {
    }

    protected override string ExistsSql
    {
        get
        {
            var builder = new StringBuilder();

            builder.Append("SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace WHERE ");

            if (TableSchema != null)
            {
                builder
                    .Append("n.nspname='")
                    .Append(SqlGenerationHelper.EscapeLiteral(TableSchema.FromPascalCaseToSnakeCase()))
                    .Append("' AND ");
            }

            builder
                .Append("c.relname='")
                .Append(SqlGenerationHelper.EscapeLiteral(TableName.FromPascalCaseToSnakeCase()))
                .Append("');");

            return builder.ToString();
        }
    }
}

最后,如下所示连接IServiceCollection配置:

        services.AddDbContext<PgDbContext>(
            options =>
            {
                options.UseNpgsql(config.GetSection("ConnectionStrings:ApplicationContext").Value)
                    .ReplaceService<ISqlGenerationHelper, LowercaseSqlGenerationHelper>()
                    .ReplaceService<IQuerySqlGenerator, LowercaseQuerySqlGenerator>()
                    .ReplaceService<IHistoryRepository, LowercaseHistoryRepository>();
            },
            ServiceLifetime.Scoped);
        services.AddScoped<ApplicationDbContext>(di => di.GetService<PgDbContext>());

有了这个,我的所有表名,列和约束都以snake_case而不是PascalCase命名,这意味着我不必担心在我的手动查询中使用带引号的标识符。我的实体类以我喜欢的方式加载,我的数据库名称也是我喜欢的方式。

YMMV,但它对我来说很漂亮。重要的是要注意虽然这实际上并没有从EF查询中删除引号,但它需要手动查询的引号消失。

答案 3 :(得分:2)

正如您在NpgsqlSqlGenerationHelper.cs中看到的那样:

static bool RequiresQuoting(string identifier)
{
        var first = identifier[0];
        if (!char.IsLower(first) && first != '_')
            return true;

Npgsql认为以大写字母开头的标识符需要引用。经过一番思考后,我实现了https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql/中描述的解决方案(将所有PascalCase标识符转换为snake-case)。它现在有点简单,但我很快就会提供一种定义自定义命名约定的方法。

答案 4 :(得分:1)

Npgsql在任何地方生成引号都是有充分理由的 - 所以你绝对不应该删除它们(即使它在技术上可能像@natemcmaster所说)。 PostgreSQL会自动将没有引号的标识符转换为小写。实体框架需要能够将C#属性映射到数据库列,但C#属性区分大小写;因此,如果你删除了数据库区分大小写,那么你就是在脚下射击......

除非你有一个真正的问题(除了感觉到的丑陋),否则你应该保留原样。

答案 5 :(得分:1)

Npgsql EF Core提供商的FYI 2.1版仅在需要时引用标识符(例如,当它们包含大写字母时)。

更重要的是,每个想要蛇案例列(或当前行为之外的任何其他内容)的人都可以使用EF Core fluent API手动指定他们想要的任何表和列名称。编写遍及所有实体和属性的代码也很容易,并通过应用snake-case转换或其他方式自动定义其数据库名称。

这比更改任何提供程序服务更好,并且总是有效,而更改SQL生成服务(或任何其他服务)可能很脆弱。

答案 6 :(得分:0)

这是 .NET Core 3.X 的紧凑解决方案(不确定是否可以在 5.X 中工作)。这将假设所有表和列都是小写并引用。如果有人命名与保留关键字(例如:“user”、“role”、“default”、“comment”等)冲突的表/列,您会发现无条件引用很有帮助。

    public class CustomNameSqlGenerationHelper : RelationalSqlGenerationHelper
    {
        private static string Customize(string input) => input.ToLower();
        public CustomNameSqlGenerationHelper([NotNull] RelationalSqlGenerationHelperDependencies dependencies) : base(dependencies) { }
        public override string DelimitIdentifier(string identifier) => base.DelimitIdentifier(Customize(identifier));
        public override void DelimitIdentifier(StringBuilder builder, string identifier) => base.DelimitIdentifier(builder, Customize(identifier));
    }

插入它很简单:

optionsBuilder.UseNpgsql(...)
  .ReplaceService<ISqlGenerationHelper, CustomNameSqlGenerationHelper>();