.NET: Entity Framework 和 DbContext:在多个表中使用相同的模型/类

huangapple go评论85阅读模式
英文:

.NET: Entity Framework & DbContext: Use same model/class for multiple tables

问题

我正在在一个现有的数据库上创建一个新项目,但我不能修改数据库设计,不幸的是,数据库在某些方面设计得相当差。然而,这个数据库有八个具有相同结构的表,为了简化这个问题,假设有两个表,Table1 和 Table2,字段非常有限。这是一个Azure SQL数据库,如果有关系的话。

在我的数据库上下文中,看起来是这样的:

public virtual DbSet<Table1> Table1{ get; set; }
public virtual DbSet<Table2> Table2{ get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Table1>(entity =>
    {
        entity.HasKey(e => e.PkId);

        entity.ToTable("table1");

        entity.Property(e => e.PkId).HasColumnName("pk_id");

        entity.Property(e => e.Version1)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("version1");
    });

    modelBuilder.Entity<Table2>(entity =>
    {
        entity.HasKey(e => e.PkId);

        entity.ToTable("table2");

        entity.Property(e => e.PkId).HasColumnName("pk_id");

        entity.Property(e => e.Version2)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("version2");
    });
}

请注意,字段名称略有不同。现在,当我决定使用相同的模型来简化这段代码时,我认为自己非常聪明,因为这将大大简化我的代码。

public virtual DbSet<Table> Table1{ get; set; }
public virtual DbSet<Table> Table2{ get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Table>(entity =>
    {
        entity.HasKey(e => e.PkId);

        entity.ToTable("table1");

        entity.Property(e => e.PkId).HasColumnName("pk_id");

        entity.Property(e => e.Version)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("version1");
    });

    modelBuilder.Entity<Table>(entity =>
    {
        entity.HasKey(e => e.PkId);

        entity.ToTable("table2");

        entity.Property(e => e.PkId).HasColumnName("pk_id");

        entity.Property(e => e.Version)
            .IsRequired()
            .HasMaxLength(50)
            .HasColumnName("version2");
    });
}

一般来说,这似乎是有效的,但在运行 AddRangeAsync() 时,其中一个表会默默失败 - 在调用 SaveChangesAsync() 时不会写入任何数据到数据库中。

我通过使用 AutoMapper 找到了解决办法:基本上,我使用我的 Table 对象,在将它们添加到 DB 上下文之前,将它们映射到它们各自的目标对象,例如 Table1。这可以顺利运行,并且是解决这个问题的正确方法。

但是,我仍然很好奇为什么我的巧妙解决方案会默默失败 - 我曾相当确信你可以为不同的表使用相同的模型类。是 modelBuilder 无法处理多次使用相同实体类型吗?除了使用 AutoMapper 处理这种情况,还有更好的方法吗?我原本以为可以避免映射过程,出于性能原因和简化的考虑。

再次强调,我不能修改数据库设计,如果这是我的数据库,我会使用一个表并引入一个额外的 ID 列之类的东西,但我不能这样做 - 不要因为数据库设计不好而责怪我。

英文:

I am creating a new project on top of an existing database which I cannot modify - the database design unfortunately is rather poor in some aspects. However, amongst others this database has eight tables that have the same structure. For the sake of simplicity of this question, let's assume it is two tables, Table1 and Table2 with a very limited set of fields. It is an Azure SQL database in case that matters.

Note: This question was highlighted as a duplicate of this one but I don't believe it is. I am not interested in how to get around this problem or solve it (I have suggested a solution), I want to know why it appears in the first place.

In my database context, this looks like this:

    public virtual DbSet&lt;Table1&gt; Table1{ get; set; }
    public virtual DbSet&lt;Table2&gt; Table2{ get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Table1&gt;(entity =&gt;
        {
            entity.HasKey(e =&gt; e.PkId);

            entity.ToTable(&quot;table1&quot;);

            entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

            entity.Property(e =&gt; e.Version1)
                .IsRequired()
                .HasMaxLength(50)
                .HasColumnName(&quot;version1&quot;);
            }

        modelBuilder.Entity&lt;Table2&gt;(entity =&gt;
        {
            entity.HasKey(e =&gt; e.PkId);

            entity.ToTable(&quot;table2&quot;);

            entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

            entity.Property(e =&gt; e.Version2)
                .IsRequired()
                .HasMaxLength(50)
                .HasColumnName(&quot;version2&quot;);
            }
    }

Note that the field names are slightly different. Now I thought I was super smart when I decided to simplify this code by using the same model for these two classes - as this would simplify my code a lot.

    public virtual DbSet&lt;Table&gt; Table1{ get; set; }
    public virtual DbSet&lt;Table&gt; Table2{ get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity&lt;Table&gt;(entity =&gt;
        {
            entity.HasKey(e =&gt; e.PkId);

            entity.ToTable(&quot;table1&quot;);

            entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

            entity.Property(e =&gt; e.Version)
                .IsRequired()
                .HasMaxLength(50)
                .HasColumnName(&quot;version1&quot;);
            }

        modelBuilder.Entity&lt;Table&gt;(entity =&gt;
        {
            entity.HasKey(e =&gt; e.PkId);

            entity.ToTable(&quot;table2&quot;);

            entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

            entity.Property(e =&gt; e.Version)
                .IsRequired()
                .HasMaxLength(50)
                .HasColumnName(&quot;version2&quot;);
            }
    }

Generally, this seems to work, but when running AddRangeAsync() on one of the table, it silently fails - nothing is written to the database when calling SaveChangesAsync().

I found myself a way around this by using AutoMapper: Basically I work with my Table objects, and just before adding them to the DB Context I map them to their respective target objects, e.g. Table1. This works without any problems and is a proper way around this issue.

However, I'm still curious to know why my ingenious solution silently fails - I was quite convinced that you could use the same model class for different tables. Is it the modelBuilder that cannot handle the same entity type multiple times? Is there a better way than using AutoMapper to handle situations like that? I thought I could avoid the mapping process for performance reasons and the sake of simplicity.

Again, I cannot modify the database design, if it was my DB I would have used one table and introduced an additional ID column or sth like that, but I cannot do that - don't blame me for the poor DB design.

答案1

得分: 1

一般情况下,EF Core将实体类的CLR类型用作独立实体的标识,因此与相关的数据库表或视图、列、关系等相关联 - 请参阅Entity Types。可以从DbContext的许多通用方法中看到这一点,这些方法具有TEntity泛型类型参数,甚至非泛型方法如AttachAdd等只接收对象实例,并使用GetType()来查找模型中的IEntityType,然后提供实体CRUD服务。但最明显的是Set&lt;TEntity&gt;()方法,用于从相应的表中检索/查询实体数据。即使只是存在该方法也清楚地表明实体CLR类型和表的关系是一对一(从关系数据库术语来说)。

现在,在最新版本中,理论上EF Core还支持所谓的Shared-type entity types,这些类型用于实现多对多关系的自动关联实体,其中包括人工创建的Dictionary&lt;string, object&gt; "entity",但也可以与用户定义的类型一起使用。但问题在于,它们需要特殊的配置,提供实体的名称,然后将该名称传递给不同的方法(包括前面提到的Set&lt;TEntity&gt;()),而一些只接收对象实例的方法根本不起作用。因为所有这些原因,我认为它们不太实用。

考虑到这一点,更好的方法是使用一个基类(可以包含EF Core数据库继承映射,也可以不包含),以及空的派生(但是独立的)类型。在您的示例中,您将拥有一个基类Table和派生类Table1 : TableTable2 : Table等。这与您提到的解决方法类似,但不需要AutoMapper(或类似的映射工具)。

最后,关于您的尝试有什么问题。在代码方面,它是可以的,而且没有EF Core fluent映射错误,那么问题出在哪里呢?

问题在于EF Core的fluent API允许您多次对表/属性进行重新定义,后者会简单地覆盖先前的配置(后者胜出)。所以在这里:


modelBuilder.Entity&lt;Table&gt;(entity =&gt;
{
    entity.HasKey(e =&gt; e.PkId);

    entity.ToTable(&quot;table1&quot;);

    entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

    entity.Property(e =&gt; e.Version)
        .IsRequired()
        .HasMaxLength(50)
        .HasColumnName(&quot;version1&quot;);
    }

modelBuilder.Entity&lt;Table&gt;(entity =&gt;
{
    entity.HasKey(e =&gt; e.PkId);

    entity.ToTable(&quot;table2&quot;);

    entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

    entity.Property(e =&gt; e.Version)
        .IsRequired()
        .HasMaxLength(50)
        .HasColumnName(&quot;version2&quot;);
}

您实际上将实体Table映射到表"table2",将Version列映射到"version2"列等。基本上,第一个配置中的所有内容都被第二个配置忽略/替换。您可以通过从上述模型/流畅配置生成EF Core迁移来轻松验证这一点,您将看到只有一个CreateTable而不是您期望的2、3、4个。

至于

    public virtual DbSet&lt;Table&gt; Table1 { get; set; }
    public virtual DbSet&lt;Table&gt; Table2 { get; set; }

将它们视为(实际上是什么)两个指向同一列表/对象/定义的变量,在这种情况下 - 表。

英文:

In general EF Core uses the entity class CLR type as identity of a separate entity, thus the associated database table or view, columns, relationships etc. - see Entity Types. It can be seen from many generic methods of the DbContext having TEntity generic type argument, or even non generic methods like Attach, Add etc. just receiving object instance, and using the GetType() for finding IEntityType in the model, and in turn provide the entity CRUD services. But most noticeable is the Set&lt;TEntity&gt;() method used to retrieve/query the entity data from the corresponding table. Even only the presence of that method is clear indication that the entity CLR type and table relationship is one to one (if speaking in relational database terms).

Now, in latest versions in theory EF Core also supports the so called Shared-type entity types which are used for implementing the automatic join entity for many-to-many with artificial Dictionary&lt;string, object&gt; "entity", but could be be used with user defined types as well. The problem is though, they require special configuration providing a name for the entity, and then passing that name for different methods (including aforementioned Set&lt;TEntity&gt;()), and some other which receive just object instances do not work at all. Because of all that, I find them not very practical usable.

With that in mind, it's much better the use a base class (with or without any EF Core database inheritance mapping) and empty derived (but separate) types. In your example, you would have a base class Table and derived classes Table1 : Table, Table2 : Table etc. It's similar to workaround you mentioned, but does not require AutoMapper (or similar mapper).

Finally, what's wrong with your attempt. Well, code wise it's ok, also there is no EF Core fluent mapping bug, so what's the problem then.

The problem is that EF Core fluent API allow you multiple table/attribute redefinition, with later simply overriding the previous configuration (last wins). So here


modelBuilder.Entity&lt;Table&gt;(entity =&gt;
{
    entity.HasKey(e =&gt; e.PkId);

    entity.ToTable(&quot;table1&quot;);

    entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

    entity.Property(e =&gt; e.Version)
        .IsRequired()
        .HasMaxLength(50)
        .HasColumnName(&quot;version1&quot;);
    }

modelBuilder.Entity&lt;Table&gt;(entity =&gt;
{
    entity.HasKey(e =&gt; e.PkId);

    entity.ToTable(&quot;table2&quot;);

    entity.Property(e =&gt; e.PkId).HasColumnName(&quot;pk_id&quot;);

    entity.Property(e =&gt; e.Version)
        .IsRequired()
        .HasMaxLength(50)
        .HasColumnName(&quot;version2&quot;);
}

you are effectively mapping entity Table to table "table2" with Version column mapped to "version2" column etc. Essentially everything from the first configuration is ignored/replaced by the second. You can easily verify that by generating EF Core migration from the above model/fluent configuration, and you'll see that there would be just one CreateTable instead of 2,3,4 you expect.

What about

    public virtual DbSet&lt;Table&gt; Table1 { get; set; }
    public virtual DbSet&lt;Table&gt; Table2 { get; set; }

think of them (and what you'll get actually) as two variables pointing to one and the same list/object/definition, in this case - table.

huangapple
  • 本文由 发表于 2023年8月9日 14:28:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/76865106-2.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定