当关键列具有不同名称时,如何进行实体拆分?

12
我正在使用Entity Framework 4.3.1 Code-First,并且需要将实体分割成两个表。这两个表共用一个主键,是一对一关系,但是每个表上的列名不相同。
我无法控制数据布局,也不能请求任何更改。
例如,SQL表可能如下:

SQL data tables

这将是我的实体...

public class MyEntity
{
    public int Id {get; set;}
    public string Name {get;set}
    public string FromAnotherTable {get;set;}
}

这是我拥有的映射。

public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
    public MyEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        this.Property(e => e.Name).HasColumnName("MyDatabaseName");
        this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
        this.Map(m =>
            {
                m.Properties(e =>
                     {
                         e.Id,
                         e.Name
                     });
                m.ToTable("MainTable");
            });
        this.Map(m =>
            {
                m.Properties(e =>
                     {
                         e.Id,
                         e.FromAnotherTable
                     });
                m.ToTable("ExtendedTable");
            });
}

由于它们之间共享的密钥具有不同的列名,我不确定如何进行映射。这种映射将编译,但在运行时失败,因为EF发出的SQL寻找“ExtendedTable”表上的“ThePrimaryKeyId”列,而该列不存在。
编辑 为澄清起见,如果“ExtendedTable”上的PK遵循命名约定,则上述定义可以(并且确实)起作用。 但是它没有,我无法更改模式。
基本上,我需要EF发出类似以下的SQL语句:
SELECT
    [e1].*,   /*yes, wildcards are bad. doing it here for brevity*/
    [e2].*
FROM [MainTable] AS [e1]
INNER JOIN [ExtendedTable] AS [e2]  /*Could be left join, don't care. */
    ON  [e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]

但它似乎只想发出的是 <\p>
 SELECT
        [e1].*,
        [e2].*
    FROM [MainTable] AS [e1]
    INNER JOIN [ExtendedTable] AS [e2]
        ON  [e1].[ThePrimaryKeyId] = [e2].[ThePrimaryKeyId] /* this column doesn't exist */

编辑 我按照NSGaga的建议再次尝试了一对一的方法。但是没有成功,以下是结果。 实体

public class MyEntity
{
    public int Id { get; set; }
    public int Name { get; set; }
    public virtual ExtEntity ExtendedProperties { get; set; }
}
public class ExtEntity
{
    public int Id { get; set; }
    public string AnotherTableColumn { get; set; }
    public virtual MyEntity MainEntry { get; set; }
}

这里是映射类

public class MyEntityMapping : EntityTypeConfiguration<MyEntity>
{
    public MyEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        this.Property(e => e.Name).HasColumnName("MyDatabaseName");
        this.ToTable("MainTable");
        this.HasKey(e => e.Id);
        this.HasRequired(e => e.ExtendedProperties).WithRequiredPrincipal(f => f.MainEntry);
    }
}

public class ExtEntityMapping : EntityTypeConfiguration<ExtEntity>
{
    public ExtEntityMapping()
    {
        this.Property(e => e.Id).HasColumnName("NotTheSameName");
        this.Property(e => e.AnotherTableColumn).HasColumnName("AnotherTableColumn");
        this.ToTable("ExtendedTable");
        this.HasKey(e => e.Id);
        this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties);
    }
}

这个设置获取消息

"Column or attribute 'MyEntity_ThePrimaryKeyId' is not defined in 'ExtendedTable'"

更改最终地图行为:
this.HasRequired(e => e.MainEntry).WithRequiredDependent(f => f.ExtendedProperties).Map(m => M.MapKey("NotTheSameName"));

返回此消息
"Each property name in a type must be unique. property name 'NotTheSameName' was already defined."

将映射键更改为使用父表中的列,MapKey("ThePrimaryKeyId")。会返回此消息。
"Column or attribute 'ThePrimaryKeyId' is not defined in 'ExtendedTable'"

ExtEntity类中删除Id属性会导致错误,因为实体没有定义键。

啊!所以你正在创建的架构中没有“ExtendedTable”;如果你只是通过DropCreateDatabaseAlways来清除并创建一个新的数据库,那么就无法映射到不存在的表;唯一的方法是将此表作为架构的一部分包含在内,或者有一个预设的已经有“ExtendedTable”的数据库,并使用EF 4.3代码迁移来修改数据库,而不是创建一个新的。 - Ricky Gummadi
我不确定我理解你的意思,但这是一个现有的数据库架构。事实上,这是映射到主机上的DB2数据库。我不希望EF尝试删除或创建任何东西。 - Josh
1
嗨,Josh。我有同样的问题,并在EF论坛这里上发布了它,但没有太多运气解决它。你听到EF团队的任何消息了吗? - TheDuke
1
你确定一定要将实体“split”成两个吗?只是问一下——将两个实体(1对1-必需:必需)关联在一起是完全可定制的。映射/属性依赖于匿名类型,并需要指向属性的函数/表达式(以及它如何获得名称)——这是将其拆分为两个表的相当“敏感”的要求。EF的这个“途径”一直有些问题。你可以尝试更改迁移脚本以重命名列名,但在这种情况下,它似乎是一种黑客行为,不确定是否有效。 - NSGaga-mostly-inactive
1
我对C#代码拥有完全的控制权,但对数据库却没有任何控制权。 - Josh
显示剩余7条评论
8个回答

3

我已经花了几天时间解决这个问题,最终的解决方法是在映射片段的上下文中设置Id字段的列名这样,您可以为Id(或依赖于Id的外键)指定与主表的Id不同的名称。

this.Map(m =>
    {
        m.Property(p => p.Id).HasColumnName("NotTheSameName");
        m.Properties(e =>
             {
                 e.Id,
                 e.FromAnotherTable
             });
        m.ToTable("ExtendedTable");
    });

如果您运行并调试此代码,您会发现它会输出您所需的结果:

[e1].[ThePrimaryKeyId] = [e2].[NotTheSameName]

只是好奇,你使用的 EF 版本是什么? - Josh
@Josh 最新的6.1.3版本。 - Luke T O'Brien
@Josh,本来想建议将此标记为答案,但刚看到原始问题是针对EF 4.3的。希望你已经升级版本并解决了问题! - Marc L.

2

将 HasColumnName 移至映射内部:

this.Property(e => e.FromAnothertable).HasColumnName("AnotherTableColumn");
this.Map(m =>
    {
        m.Properties(e => new
             {
                 e.Id,
                 e.Name
             });
             m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
             m.Property(e => e.Name).HasColumnName("MyDatabaseName");

           m.Property(e => e.Id).HasColumnName("ThePrimaryKeyId");
        m.ToTable("MainTable");
    });
this.Map(m =>
    {
        m.Properties(e => new
             {
                 e.Id,
                 e.FromAnotherTable
             });
        m.ToTable("ExtendedTable");
    });
}

几乎,他在Id列上使用了两个名称,第一个映射片段具有一个Id名称,但第二个映射片段没有。 在第二个映射片段中为Id命名并使用m.Property(p => p.Id).HasColumnName("NotTheSameName")即可解决此问题 :) - Luke T O'Brien

2

我找不到任何明确说明列名必须在两个表中相同的内容;但是我也找不到任何内容说明它不需要相同,或者解释如何映射这种情况。我能找到的每个示例都将关键字在两个表中使用相同的名称。在我的看来,这是DbContext设计中的一个漏洞。


是的,那也是我现在倾向的方向。 - Josh

1

我建议使用一些类似这样的数据注释:

MainTable
---------
MainTableId
DatabaseName

ExtendedTable
----------
NotTheSameName
AnotherColumn

public class MainTable
{
 [Key]
 public int MainTableId { get; set; }
 public string DatabaseName { get; set; }

 [InverseProperty("MainTable")]
 public virtual ExtendedTable ExtendedTable { get; set; }
}

public class ExtendedTable
{
 [Key]
 public int NotTheSameName { get; set; }

 public string AnotherColumn { get; set; }

 [ForeignKey("NotTheSameName")]
 public virtual MainTable MainTable { get; set; }
}

谢谢你的建议,但在这种情况下,数据注释对我们来说不是一个选项。此外,微软表示使用Fluent API可以实现数据注释所能实现的一切,并且有些事情你可以用Fluent API做到,而注释则做不到。 - Josh
@Josh - 在我看来,流畅的API可能会变得乏味,并且不像数据注释那样直接。这是API,但是反向属性定义相当模糊:http://msdn.microsoft.com/en-us/library/hh295843(v=vs.103).aspx。很遗憾您不能使用数据注释,它们使使用EF更容易阅读和编码。接受的答案“在我看来,这是DbContext设计中的一个漏洞。”并不正确。使用流畅的API或数据注释都可以实现反向属性映射。 - Travis J
1
我不同意你的观点,即Fluent API很繁琐或不够直接。我也不同意注释更容易阅读。但在这件事上我的意见并不重要。我们的问题是使用Data Annotations会将Entity Framework的依赖泄漏到实体对象中,这意味着依赖项目也必须有EF引用。通过分离实体映射类,我们已经将EF依赖隔离到实现DbContext和消费映射的项目中。此外,我们正在将相同的实体映射到两个不同的数据存储中,这需要不同的映射。 - Josh
如果您提供了所问问题的答案,那么我会修改答案。仅仅简单地说“不是真的”而没有展示出一个答案是毫无用处的。 - Josh
1
@Josh - 如果您希望仅以流畅的API答案形式获得答案,则应明确说明要求。我演示了一种技术,显示EF支持此类型的共享数据,因此说“在DbContext设计中看起来像这是一个漏洞”而没有支持链接甚至示例的答案对于其他试图解决此问题的人来说是无用的。请参见此链接以获取流畅的API方法和反对数据注释的论点:https://dev59.com/LG025IYBdhLWcg3w964W#5693427 - Travis J

1

很遗憾,该链接不再可用。 - Mikkk

1

我找不到像.HasConstraint()这样的方法。你能给我指一下文档或参考资料吗?谢谢! - Josh
似乎 HasConstraint() 函数在 CTP 版本中存在,但在正式发布版中不存在。我还没有找到相应的替代方法。我正在使用 4.3.1 版本。 - Josh
顺便提一下,实体拆分将是首选解决方案。1对1是一个不太理想的替代方案。我认为EF不喜欢它,因为它希望在另一个表上看到每个表的PK,而这是一个FK和PK来强制执行它。 - Josh

1

只是为了提供(如我所承诺的)一对一(两个实体,两个表)映射,就值得一试。
以下是适用于我的情况并且应该适用于你的情况...

public class MainTable
{
    public int ThePrimaryKeyId { get; set; }
    public string Name { get; set; }
}
public class ExtendedTable
{
    public int NotTheSameNameID { get; set; }
    public string AnotherTableColumn { get; set; }
    public MainTable MainEntry { get; set; }
}
public class MainDbContext : DbContext
{
    public DbSet<MainTable> MainEntries { get; set; }
    public DbSet<ExtendedTable> ExtendedEntries { get; set; }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MainTable>()
            .HasKey(x => new { x.ThePrimaryKeyId });

        modelBuilder.Entity<ExtendedTable>()
            .HasKey(x => new { x.NotTheSameNameID });

        // Extended To Main 1 on 1
        modelBuilder.Entity<ExtendedTable>()
            .HasRequired(i => i.MainEntry)
            .WithRequiredDependent();
    }
}

...以及一个类似于测试代码的东西...

using (var db = new UserDbContext())
{
    foreach (var userid in Enumerable.Range(1, 100))
    {
        var main = new MainTable { Name = "Main" + userid };
        db.MainEntries.Add(main);

        var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, MainEntry = main };
        db.ExtendedEntries.Add(extended);
    }
    int recordsAffected = db.SaveChanges();
    foreach (var main in db.MainEntries)
        Console.WriteLine("{0}, {1}", main.Name, main.ThePrimaryKeyId);
    foreach (var extended in db.ExtendedEntries)
        Console.WriteLine("{0}, {1}, {2}, {3}", extended.AnotherTableColumn, extended.NotTheSameNameID, extended.MainEntry.Name, extended.MainEntry.ThePrimaryKeyId);
}

这将创建以下SQL脚本,表...

CREATE TABLE [MainTables] (
    [ThePrimaryKeyId] [int] NOT NULL IDENTITY,
    [Name] [nvarchar](4000),
    CONSTRAINT [PK_MainTables] PRIMARY KEY ([ThePrimaryKeyId])
)
CREATE TABLE [ExtendedTables] (
    [NotTheSameNameID] [int] NOT NULL,
    [AnotherTableColumn] [nvarchar](4000),
    CONSTRAINT [PK_ExtendedTables] PRIMARY KEY ([NotTheSameNameID])
)
CREATE INDEX [IX_NotTheSameNameID] ON [ExtendedTables]([NotTheSameNameID])
ALTER TABLE [ExtendedTables] ADD CONSTRAINT [FK_ExtendedTables_MainTables_NotTheSameNameID] FOREIGN KEY ([NotTheSameNameID]) REFERENCES [MainTables] ([ThePrimaryKeyId])

顺便提一下,根据我们之前的讨论...
这不是“分离” - 但
(a)在我看来,Code First 不允许任何这样的操作(我首先尝试了这个并手动修改迁移,但它“内部”基于预期的列名称相同,并且似乎没有绕过它的方法,至少对于 EF 的这个版本来说。
(b)表结构方面 - 表可以被制作成与您所需完全相同的外观(正如我之前所说,我将其用于将现有的 aspnet 成员资格表(我无法更改)关联到我的用户表中,该表具有指向外部/aspnet 表和 id 的自己的用户 id)。
当然,您不能使用一个 C# 模型类来实现它 - 但 C# 的灵活性更大,如果您可以控制 C#,那么至少应该给出相同的效果(就像在测试中一样,您始终可以通过扩展实体访问它,扩展和主要列都匹配1对1并保持“同步”。
希望这能帮到您
注意:您不必担心 fk id 等 - 只需始终通过 MainEntry 访问和添加主条目,id-s 就会很好。

编辑:
你也可以采用以下方法,以获得只需处理一个类的外观(即一种分割)

public class ExtendedTable
{
    public int NotTheSameNameID { get; set; }
    public string AnotherTableColumn { get; set; }

    public string Name { get { return MainEntry.Name; } set { MainEntry.Name = value; } }
    // public int MainID { get { return MainEntry.ThePrimaryKeyId; } set { MainEntry.ThePrimaryKeyId = value; } }
    internal MainTable MainEntry { get; set; }

    public ExtendedTable()
    {
        this.MainEntry = new MainTable();
    }
}

...然后像这样使用它...

var extended = new ExtendedTable { AnotherTableColumn = "Extended" + userid, Name = "Main" + userid };  

...你也可以通过使用WithRequiredPrincipal而不是dependent来反转fk的方向。
(如果您需要一对一关系,则所有引用都必须没有'virtual')
(并且MainTable可以像这里一样被设置为“internal”,因此它在外部不可见-它不能嵌套,因为EF不允许-被视为NotMapped)
...好吧,这就是我能做到的最好的了:)


尝试过这个,但是出现了错误{"无效的列名 'ExtendedTable_ThePrimaryKeyId'."}。 - Josh
我进一步研究了这个问题,我认为问题的一部分在于我们使用的实体提供程序。默认的SQL表现如你所描述的那样,但这是来自IBM的DB2提供程序。 - Josh
你尝试过“清理”数据库和迁移吗?我正在使用EF 4.3,最新版本 - 我不得不启用迁移 - 每次通常都会从项目中删除数据库和迁移,然后重新开始(请查看此帖子以获取详细信息https://dev59.com/cWDVa4cB1Zd3GeqPfaVC#9745125)。如果需要,我可以发送给您工作项目 - 只是不确定如何在SO中执行该操作。 - NSGaga-mostly-inactive
这对我来说是新鲜事(我看你提到了)- 不管提供商如何 - 你的迁移文件应该看起来相同,我想 - 你可以检查一下那里是否有你应该像我的SQL/tables一样的列。迁移有点棘手 - 我正在使用 SQL CE,仍然是微软的。 - NSGaga-mostly-inactive

0

我遇到了这个问题,并通过添加Column属性来匹配两个列名来解决它。 [Key] [Column("Id")] public int GroupId { get; set; }


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接