Entity Framework 中表和属性的名称不区分大小写 7
Case insensitive name of tables and properties in Entity Framework 7
我使用 Entity Framework 7 和 Npgsql 适配器。 Sql 由 EF 生成似乎是
SELECT "r"."Id", "r"."Name" FROM "public"."Role" AS "r"
它在 Postgres 中不起作用,因为区分大小写的策略。为了让它工作,我需要编写 create table script
CREATE TABLE "Role" (
"Id" int,
"Name" varchar(200)
);
但是很丑。有没有办法让 EF 生成不带引号或小写命名风格的脚本?
为此,您需要将 SQL 生成服务替换为您自己的、无引用的小写版本。为此,您将需要了解 EF 如何使用 DI(尝试阅读 Understanding EF Services),并且需要替换生成 SQL 的服务。在 EF 中,这可能是 ISqlGenerationHelper
、IMigrationsSqlGenerator
或 IUpdateSqlGenerator
,具体取决于具体情况..
Npgsql 到处生成引号是有充分理由的 - 所以你绝对不应该删除它们(即使它在技术上是可行的,正如@natemcmaster 所说)。 PostgreSQL 会自动将不带引号的标识符转换为小写。 Entity Framework 需要能够将 C# 属性映射到数据库列,但 C# 属性区分大小写;所以如果你删除数据库区分大小写,你就是搬起石头砸自己的脚...
除非你有真正的问题(除了丑陋的感觉),否则你应该保持原样。
像这样覆盖 NpgsqlSqlGenerationHelper 中的 DelimitIdentifier:
public class SqlGenerationHelper : NpgsqlSqlGenerationHelper
{
public override string DelimitIdentifier(string identifier) => identifier.Contains(".") ? base.DelimitIdentifier(identifier) : identifier;
}
使用 ReplaceService 方法将 ISqlGenerationHelper 替换为您的 class:
public class MyContext : DbContext
{
public virtual DbSet<MyTable> MyTable { get; set; }
public MyContext(DbConnection connection) :
base(new DbContextOptionsBuilder().UseNpgsql(connection)
.ReplaceService<ISqlGenerationHelper, SqlGenerationHelper>()
.Options)
{ }
}
我真的不喜欢在我的 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>());
有了这个,我所有的 table 名称、列和约束都以 snake_case 命名,而不是 PascalCase,这意味着我不必担心在我的手动查询中使用带引号的标识符。我的实体 类 是我喜欢的方式,我的数据库名称也是我喜欢的方式。
YMMV,但它对我来说非常流畅。请务必注意,虽然这实际上并未从 EF 查询中删除引号,但它使手动查询的引号需求消失。
如您在 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)。现在有点简单,但我认为 EF Core 很快将提供一种定义自定义命名约定的方法。
仅供参考,Npgsql EF Core 提供程序的 2.1 版只会在需要时引用标识符(例如,当它们包含大写字母时)。
更重要的是,每个想要 snake case 列(或除当前行为之外的任何其他内容)的人都可以简单地使用 EF Core fluent API 手动指定他们想要的任何 table 和列名称。编写遍历所有实体和属性的代码也很容易,并通过应用 snake-case 转换或其他方式自动定义它们的数据库名称。
这比更改任何提供程序服务都要好,并且始终有效,而更改 SQL 生成服务(或任何其他服务)可能会很脆弱。
这是 .NET Core 的紧凑解决方案 3.X - Net5(可能在 Net6 中运行良好)。这将假定所有表和列都是小写和引用。如果有人命名 table/column 与保留关键字冲突(例如:“user”、“role”、“default”、“comment”等),您会发现无条件引用很有帮助。
/// <summary>A replacement for <see cref="NpgsqlSqlGenerationHelper"/>
/// to convert PascalCaseCsharpyIdentifiers to alllowercasenames.
/// So table and column names with no embedded punctuation
/// get generated with no quotes or delimiters.</summary>
public class NpgsqlSqlGenerationLowercasingHelper : NpgsqlSqlGenerationHelper
{
//Don't lowercase ef's migration table
const string dontAlter="__EFMigrationsHistory";
static string Customize(string input) => input==dontAlter? input : input.ToLower();
public NpgsqlSqlGenerationLowercasingHelper(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, NpgsqlSqlGenerationLowercasingHelper >();
我使用 Entity Framework 7 和 Npgsql 适配器。 Sql 由 EF 生成似乎是
SELECT "r"."Id", "r"."Name" FROM "public"."Role" AS "r"
它在 Postgres 中不起作用,因为区分大小写的策略。为了让它工作,我需要编写 create table script
CREATE TABLE "Role" (
"Id" int,
"Name" varchar(200)
);
但是很丑。有没有办法让 EF 生成不带引号或小写命名风格的脚本?
为此,您需要将 SQL 生成服务替换为您自己的、无引用的小写版本。为此,您将需要了解 EF 如何使用 DI(尝试阅读 Understanding EF Services),并且需要替换生成 SQL 的服务。在 EF 中,这可能是 ISqlGenerationHelper
、IMigrationsSqlGenerator
或 IUpdateSqlGenerator
,具体取决于具体情况..
Npgsql 到处生成引号是有充分理由的 - 所以你绝对不应该删除它们(即使它在技术上是可行的,正如@natemcmaster 所说)。 PostgreSQL 会自动将不带引号的标识符转换为小写。 Entity Framework 需要能够将 C# 属性映射到数据库列,但 C# 属性区分大小写;所以如果你删除数据库区分大小写,你就是搬起石头砸自己的脚...
除非你有真正的问题(除了丑陋的感觉),否则你应该保持原样。
像这样覆盖 NpgsqlSqlGenerationHelper 中的 DelimitIdentifier:
public class SqlGenerationHelper : NpgsqlSqlGenerationHelper { public override string DelimitIdentifier(string identifier) => identifier.Contains(".") ? base.DelimitIdentifier(identifier) : identifier; }
使用 ReplaceService 方法将 ISqlGenerationHelper 替换为您的 class:
public class MyContext : DbContext { public virtual DbSet<MyTable> MyTable { get; set; } public MyContext(DbConnection connection) : base(new DbContextOptionsBuilder().UseNpgsql(connection) .ReplaceService<ISqlGenerationHelper, SqlGenerationHelper>() .Options) { } }
我真的不喜欢在我的 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>());
有了这个,我所有的 table 名称、列和约束都以 snake_case 命名,而不是 PascalCase,这意味着我不必担心在我的手动查询中使用带引号的标识符。我的实体 类 是我喜欢的方式,我的数据库名称也是我喜欢的方式。
YMMV,但它对我来说非常流畅。请务必注意,虽然这实际上并未从 EF 查询中删除引号,但它使手动查询的引号需求消失。
如您在 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)。现在有点简单,但我认为 EF Core 很快将提供一种定义自定义命名约定的方法。
仅供参考,Npgsql EF Core 提供程序的 2.1 版只会在需要时引用标识符(例如,当它们包含大写字母时)。
更重要的是,每个想要 snake case 列(或除当前行为之外的任何其他内容)的人都可以简单地使用 EF Core fluent API 手动指定他们想要的任何 table 和列名称。编写遍历所有实体和属性的代码也很容易,并通过应用 snake-case 转换或其他方式自动定义它们的数据库名称。
这比更改任何提供程序服务都要好,并且始终有效,而更改 SQL 生成服务(或任何其他服务)可能会很脆弱。
这是 .NET Core 的紧凑解决方案 3.X - Net5(可能在 Net6 中运行良好)。这将假定所有表和列都是小写和引用。如果有人命名 table/column 与保留关键字冲突(例如:“user”、“role”、“default”、“comment”等),您会发现无条件引用很有帮助。
/// <summary>A replacement for <see cref="NpgsqlSqlGenerationHelper"/>
/// to convert PascalCaseCsharpyIdentifiers to alllowercasenames.
/// So table and column names with no embedded punctuation
/// get generated with no quotes or delimiters.</summary>
public class NpgsqlSqlGenerationLowercasingHelper : NpgsqlSqlGenerationHelper
{
//Don't lowercase ef's migration table
const string dontAlter="__EFMigrationsHistory";
static string Customize(string input) => input==dontAlter? input : input.ToLower();
public NpgsqlSqlGenerationLowercasingHelper(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, NpgsqlSqlGenerationLowercasingHelper >();