Entity Framework 核心迁移问题
Entity Framework Core migration issue
EF Core 3.1.16 中的多对多关系存在问题。
相当基本的概念:用户/user_role/角色。
使用 MS Identity,因此依赖于 IdentityUserRole class。由于 class 已经具有 UserId 和 RoleId 属性,因此 EF 正尝试按照约定生成这些字段。我还没有想出一种方法来告诉 EF 将 MS UserId 和 RoleId 字段视为与我想要在 UserRole 实体 [=69 上的对象中的 User.Id 和 Role.Id 字段相同的字段=].我们使用 IEntityTypeConfiguration 实例来创建 EF 使用的数据库架构,而不是基于约定的方法。生成模式的代码如下。
用户
public class User : IdentityUser<int>, IEntityCustom
{
// Custom field for testing purpose only...wouldn't actually store this.
public int Age { get; set; }
public List<UserRole> UserRoles { get; set; }
}
public class UserTypeConfiguration : BaseTypeConfiguration<User>, IEntityTypeConfiguration<User>
{
public UserTypeConfiguration() : base("permissions", "user") { }
public override void Configure(EntityTypeBuilder<User> builder)
{
base.Configure(builder);
#region "Hiding other properties"
// Other fields are here that I omitted as they are just lowercasing only
// and dont have relation to what is going on in the question. user_name
// field below is same as all the code omitted just with that field name pair.
#endregion
builder.Property(x => x.UserName).HasColumnName("user_name");
builder.HasMany(x => x.UserRoles).WithOne(x => x.User).HasForeignKey("user_id");
builder.HasData(new User
{
Id = 1,
AccessFailedCount = 0,
Age = 12,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Email = "test@yopmail.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "test@yopmail.com",
NormalizedUserName = "test",
PasswordHash = "hash",
PhoneNumber = "5551231234",
PhoneNumberConfirmed = true,
SecurityStamp = Guid.NewGuid().ToString(),
TwoFactorEnabled = false,
UserName = "test"
});
}
}
USER_ROLE
public class UserRole : IdentityUserRole<int>, IEntityCustom
{
public int Id { get; set; }
public User User { get; set; }
public Role Role { get; set; }
public UserRole() { }
public UserRole(User user, Role role)
{
User = user;
Role = role;
}
}
public class UserRoleTypeConfiguration : BaseTypeConfiguration<UserRole>, IEntityTypeConfiguration<UserRole>
{
public UserRoleTypeConfiguration() : base("permissions", "user_role") { }
public override void Configure(EntityTypeBuilder<UserRole> builder)
{
base.Configure(builder);
// Gens the right schema, but fails at runtime on
// insert to user_role table with duplicate "role_id" column exists on table "user_role"
builder.Property<int>("UserId").HasColumnName("user_id");
builder.Property<int>("RoleId").HasColumnName("role_id");
// These 2 lines of code seem dumb, but they fix the schema
// Only way I could find to accomplish it tho...
builder.Property<int>("user_id").HasColumnName("user_id");
builder.Property<int>("role_id").HasColumnName("role_id");
builder.HasOne(x => x.User).WithMany(x => x.UserRoles).HasForeignKey("user_id");
builder.HasOne(x => x.Role).WithMany(x => x.UserRoles).HasForeignKey("role_id");
#region Region with all the ways I tried that didnt work.
// Removed all the stuff that I tried that didn't get the model right.
// ie....duplicate fields, etc.
#endregion
}
}
角色
public class Role : IdentityRole<int>, IEntityCustom
{
// Another custom field for testing. eg. Meaning = "SYSTEMADMIN"
public string Meaning { get; set; }
// I really dont want this navigation and my real code doesnt have it currently.
// I couldnt get it working without, so I finally just added to see if I could get it
// working as full many to many relation and would remove later.
public List<UserRole> UserRoles { get; set; }
}
public class RoleTypeConfiguration : BaseTypeConfiguration<Role>, IEntityTypeConfiguration<Role>
{
public RoleTypeConfiguration() : base("permissions", "role") { }
public override void Configure(EntityTypeBuilder<Role> builder)
{
base.Configure(builder);
builder.Property(x => x.Name).HasColumnName("name");
builder.Property(x => x.ConcurrencyStamp).HasColumnName("concurrency_stamp");
builder.Property(x => x.NormalizedName).HasColumnName("normalized_name");
builder.Property(x => x.Meaning).HasColumnName("meaning");
builder.HasMany(x => x.UserRoles).WithOne(x => x.Role).HasForeignKey("role_id");
builder.HasData(new Role
{
Id = 1,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Meaning = "SYSADMIN",
Name = "System Admin",
NormalizedName = "system admin"
});
builder.HasData(new Role
{
Id = 2,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Meaning = "CONTENTADMIN",
Name = "Content Admin",
NormalizedName = "content admin"
});
}
}
基本类型配置
public class BaseTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class, IEntityCustom
{
private readonly string _schemaName;
private readonly string _tableName;
public BaseTypeConfiguration(string schemaName, string tableName)
{
_schemaName = schemaName;
_tableName = tableName;
}
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
Console.WriteLine("schema:" + _schemaName);
Console.WriteLine("table:" + _tableName);
builder.Metadata.SetSchema(_schemaName);
builder.ToTable(_tableName);
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedOnAdd();
}
}
IENTITYCUSTOM
public interface IEntityCustom
{
int Id { get; set; }
}
输出
在下面的输出部分中,是迁移过程创建的内容。此示例在模型上为 user_role 生成正确的字段(即...单个 user_id 和 role_id 字段,再加上核心 id 字段),但是当您查看 FK refs它生成,两个字段都是重复的。我试过 运行 宁这个场景,它会 运行 迁移并会正确创建 tables,但是当你尝试 运行 插入 user_role table, EF抛出如下异常:
"42701: 列 role_id 指定了不止一次。"
migrationBuilder.CreateTable(
name: "user_role",
schema: "permissions",
columns: table => new
{
id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
user_id = table.Column<int>(nullable: false),
role_id = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_user_role", x => x.id);
table.ForeignKey(
name: "FK_user_role_role_role_id",
column: x => x.role_id,
principalSchema: "permissions",
principalTable: "role",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_user_user_id",
column: x => x.user_id,
principalSchema: "permissions",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_role_role_id1",
column: x => x.role_id,
principalSchema: "permissions",
principalTable: "role",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_user_user_id1",
column: x => x.user_id,
principalSchema: "permissions",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
已创建数据库
您应该尝试像这样配置您的实体:
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasMany(x => x.UserRoles)
.WithOne(x => x.User)
.HasForeignKey(x => x.UserId);
}
}
public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.HasMany(x => x.UserRoles)
.WithOne(x => x.Role)
.HasForeignKey(x => x.RoleId);
}
}
public void Configure(EntityTypeBuilder<UserRole> builder)
{
builder.Property(x => x.RoleId).HasColumnName("role_id");
builder.HasKey(x => new {x.UserId, x.RoleId});
}
当您指定外键时,您应该使用模型属性而不是指定像“role_id”或“user_id”这样的名称。由于您将 属性 定义为例如“role_id”在您的 table 中,EF 将在内部处理所有其他事情并正确映射您的外键。但是在您提供的示例中,EF 无法理解它应该映射到哪个属性,这就是它生成额外列的原因。
您还应该考虑使用它来重命名数据库级别的所有属性:Naming Conventions for Entity Framework Core Tables and Columns。
UserRole
class 中的 Id
字段也是多余的,因为您只需要一个复合键 {UserId, RoleId}
EF Core 3.1.16 中的多对多关系存在问题。
相当基本的概念:用户/user_role/角色。
使用 MS Identity,因此依赖于 IdentityUserRole class。由于 class 已经具有 UserId 和 RoleId 属性,因此 EF 正尝试按照约定生成这些字段。我还没有想出一种方法来告诉 EF 将 MS UserId 和 RoleId 字段视为与我想要在 UserRole 实体 [=69 上的对象中的 User.Id 和 Role.Id 字段相同的字段=].我们使用 IEntityTypeConfiguration 实例来创建 EF 使用的数据库架构,而不是基于约定的方法。生成模式的代码如下。
用户
public class User : IdentityUser<int>, IEntityCustom
{
// Custom field for testing purpose only...wouldn't actually store this.
public int Age { get; set; }
public List<UserRole> UserRoles { get; set; }
}
public class UserTypeConfiguration : BaseTypeConfiguration<User>, IEntityTypeConfiguration<User>
{
public UserTypeConfiguration() : base("permissions", "user") { }
public override void Configure(EntityTypeBuilder<User> builder)
{
base.Configure(builder);
#region "Hiding other properties"
// Other fields are here that I omitted as they are just lowercasing only
// and dont have relation to what is going on in the question. user_name
// field below is same as all the code omitted just with that field name pair.
#endregion
builder.Property(x => x.UserName).HasColumnName("user_name");
builder.HasMany(x => x.UserRoles).WithOne(x => x.User).HasForeignKey("user_id");
builder.HasData(new User
{
Id = 1,
AccessFailedCount = 0,
Age = 12,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Email = "test@yopmail.com",
EmailConfirmed = true,
LockoutEnabled = false,
NormalizedEmail = "test@yopmail.com",
NormalizedUserName = "test",
PasswordHash = "hash",
PhoneNumber = "5551231234",
PhoneNumberConfirmed = true,
SecurityStamp = Guid.NewGuid().ToString(),
TwoFactorEnabled = false,
UserName = "test"
});
}
}
USER_ROLE
public class UserRole : IdentityUserRole<int>, IEntityCustom
{
public int Id { get; set; }
public User User { get; set; }
public Role Role { get; set; }
public UserRole() { }
public UserRole(User user, Role role)
{
User = user;
Role = role;
}
}
public class UserRoleTypeConfiguration : BaseTypeConfiguration<UserRole>, IEntityTypeConfiguration<UserRole>
{
public UserRoleTypeConfiguration() : base("permissions", "user_role") { }
public override void Configure(EntityTypeBuilder<UserRole> builder)
{
base.Configure(builder);
// Gens the right schema, but fails at runtime on
// insert to user_role table with duplicate "role_id" column exists on table "user_role"
builder.Property<int>("UserId").HasColumnName("user_id");
builder.Property<int>("RoleId").HasColumnName("role_id");
// These 2 lines of code seem dumb, but they fix the schema
// Only way I could find to accomplish it tho...
builder.Property<int>("user_id").HasColumnName("user_id");
builder.Property<int>("role_id").HasColumnName("role_id");
builder.HasOne(x => x.User).WithMany(x => x.UserRoles).HasForeignKey("user_id");
builder.HasOne(x => x.Role).WithMany(x => x.UserRoles).HasForeignKey("role_id");
#region Region with all the ways I tried that didnt work.
// Removed all the stuff that I tried that didn't get the model right.
// ie....duplicate fields, etc.
#endregion
}
}
角色
public class Role : IdentityRole<int>, IEntityCustom
{
// Another custom field for testing. eg. Meaning = "SYSTEMADMIN"
public string Meaning { get; set; }
// I really dont want this navigation and my real code doesnt have it currently.
// I couldnt get it working without, so I finally just added to see if I could get it
// working as full many to many relation and would remove later.
public List<UserRole> UserRoles { get; set; }
}
public class RoleTypeConfiguration : BaseTypeConfiguration<Role>, IEntityTypeConfiguration<Role>
{
public RoleTypeConfiguration() : base("permissions", "role") { }
public override void Configure(EntityTypeBuilder<Role> builder)
{
base.Configure(builder);
builder.Property(x => x.Name).HasColumnName("name");
builder.Property(x => x.ConcurrencyStamp).HasColumnName("concurrency_stamp");
builder.Property(x => x.NormalizedName).HasColumnName("normalized_name");
builder.Property(x => x.Meaning).HasColumnName("meaning");
builder.HasMany(x => x.UserRoles).WithOne(x => x.Role).HasForeignKey("role_id");
builder.HasData(new Role
{
Id = 1,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Meaning = "SYSADMIN",
Name = "System Admin",
NormalizedName = "system admin"
});
builder.HasData(new Role
{
Id = 2,
ConcurrencyStamp = Guid.NewGuid().ToString(),
Meaning = "CONTENTADMIN",
Name = "Content Admin",
NormalizedName = "content admin"
});
}
}
基本类型配置
public class BaseTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class, IEntityCustom
{
private readonly string _schemaName;
private readonly string _tableName;
public BaseTypeConfiguration(string schemaName, string tableName)
{
_schemaName = schemaName;
_tableName = tableName;
}
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
Console.WriteLine("schema:" + _schemaName);
Console.WriteLine("table:" + _tableName);
builder.Metadata.SetSchema(_schemaName);
builder.ToTable(_tableName);
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasColumnName("id")
.ValueGeneratedOnAdd();
}
}
IENTITYCUSTOM
public interface IEntityCustom
{
int Id { get; set; }
}
输出
在下面的输出部分中,是迁移过程创建的内容。此示例在模型上为 user_role 生成正确的字段(即...单个 user_id 和 role_id 字段,再加上核心 id 字段),但是当您查看 FK refs它生成,两个字段都是重复的。我试过 运行 宁这个场景,它会 运行 迁移并会正确创建 tables,但是当你尝试 运行 插入 user_role table, EF抛出如下异常:
"42701: 列 role_id 指定了不止一次。"
migrationBuilder.CreateTable(
name: "user_role",
schema: "permissions",
columns: table => new
{
id = table.Column<int>(nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
user_id = table.Column<int>(nullable: false),
role_id = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_user_role", x => x.id);
table.ForeignKey(
name: "FK_user_role_role_role_id",
column: x => x.role_id,
principalSchema: "permissions",
principalTable: "role",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_user_user_id",
column: x => x.user_id,
principalSchema: "permissions",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_role_role_id1",
column: x => x.role_id,
principalSchema: "permissions",
principalTable: "role",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_user_role_user_user_id1",
column: x => x.user_id,
principalSchema: "permissions",
principalTable: "user",
principalColumn: "id",
onDelete: ReferentialAction.Cascade);
});
已创建数据库
您应该尝试像这样配置您的实体:
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasMany(x => x.UserRoles)
.WithOne(x => x.User)
.HasForeignKey(x => x.UserId);
}
}
public class RoleConfiguration : IEntityTypeConfiguration<Role>
{
public void Configure(EntityTypeBuilder<Role> builder)
{
builder.HasMany(x => x.UserRoles)
.WithOne(x => x.Role)
.HasForeignKey(x => x.RoleId);
}
}
public void Configure(EntityTypeBuilder<UserRole> builder)
{
builder.Property(x => x.RoleId).HasColumnName("role_id");
builder.HasKey(x => new {x.UserId, x.RoleId});
}
当您指定外键时,您应该使用模型属性而不是指定像“role_id”或“user_id”这样的名称。由于您将 属性 定义为例如“role_id”在您的 table 中,EF 将在内部处理所有其他事情并正确映射您的外键。但是在您提供的示例中,EF 无法理解它应该映射到哪个属性,这就是它生成额外列的原因。
您还应该考虑使用它来重命名数据库级别的所有属性:Naming Conventions for Entity Framework Core Tables and Columns。
UserRole
class 中的 Id
字段也是多余的,因为您只需要一个复合键 {UserId, RoleId}