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

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

.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类型作为单独实体的标识,因此关联的数据库表或视图、列、关系等都与之相关。可以从DbContext的许多泛型方法中看出,这些方法具有TEntity泛型类型参数,甚至包括像AttachAdd等非泛型方法,只接收对象实例,并使用GetType()来在模型中查找IEntityType,从而提供实体的CRUD服务。但最明显的是Set<TEntity>()方法,用于从相应的表中检索/查询实体数据。即使只有该方法的存在,也清楚地表明实体的CLR类型和表之间的关系是一对一的(从关系数据库的角度来说)。

现在,在最新版本中,理论上EF Core也支持所谓的共享类型实体,用于实现具有人工Dictionary<string, object>“实体”的多对多自动连接实体,但也可以与用户定义的类型一起使用。然而,问题是,它们需要特殊的配置来为实体提供名称,然后将该名称传递给不同的方法(包括上述的Set<TEntity>()),而一些其他只接收对象实例的方法根本不起作用。因此,我认为它们不太实用。

考虑到这一点,更好的方法是使用一个基类(带有或不带有任何EF Core数据库继承映射)和空的派生(但是独立的)类型。在你的例子中,你可以有一个基类Table和派生类Table1 : TableTable2 : Table等。这类似于你提到的解决方法,但不需要AutoMapper(或类似的映射器)。

最后,你的尝试有什么问题呢?嗯,从代码上来说,它是没问题的,而且也没有EF Core fluent映射的bug,那么问题是什么呢?

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

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");
}

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

那么关于

public virtual DbSet<Table> Table1 { get; set; }
public virtual DbSet<Table> 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.html
匿名

发表评论

匿名网友

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

确定