首先查询相关数据,结果有重复数据

First query with related data, result with duplicate data

我正在尝试执行一个查询,我必须按如下方式从数据库中提取信息

找到包含 assetsinventory 个数字 1

但是我在 assets 对象中得到了重复的结果,我不明白为什么。

查询:

[HttpGet("Search/")]
public async Task<ActionResult<DtoInventory>> SearhInventory()
{
    Inventory queryset = await context.Inventories.Include(i => i.Assets).FirstOrDefaultAsync(i => i.inventory_id == 1);
    DtoInventory dto = mapper.Map<DtoInventory>(queryset);
    return dto;
}

DbContext

using API.Models;
using Microsoft.EntityFrameworkCore;

namespace API.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        {
        }
        public DbSet<Requirement> Requirements { get; set; }
        public DbSet<Inventory> Inventories { get; set; }
        public DbSet<Asset> Assets { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            #region Inventory
            // Table name
            modelBuilder.Entity<Inventory>().ToTable("Inventories");
            // PK
            modelBuilder.Entity<Inventory>().HasKey(i => i.inventory_id);
            #endregion


            #region Asset
            // Table Name
            modelBuilder.Entity<Asset>().ToTable("Assets");
            // PK
            modelBuilder.Entity<Asset>().HasKey(i => i.asset_id);
            // Code
            modelBuilder.Entity<Asset>().Property(a => a.code)
                .HasColumnType("int");

            // Relationship
            modelBuilder.Entity<Asset>()
                .HasOne(i => i.Inventory)
                .WithMany(a => a.Assets)
                .HasForeignKey(a => a.inventory_id); //FK
            #endregion
        }
    }
}

库存模型

namespace API.Models
{
    public class Inventory
    {
        public int inventory_id { get; set; }
        public string name { get; set; }
        public string location { get; set; }
        public int status { get; set; }
        public DateTime? created_date { get; set; }
        public List<Asset> Assets { get; set; }

    }
}

DtoInventory

namespace API.Dtos
{
    public class DtoInventory
    {
        public int inventory_id { get; set; }
        public string name { get; set; }
        public string location { get; set; }
        public bool status { get; set; }
        public DateTime created_date { get; set; }
        public List<Asset> Assets { get; set; }
    }
}

预期结果:

{
    "inventory_id": 1,
    "name": "cellphones",
    "location": "usa",
    "status": true,
    "created_date": "0001-01-01T00:00:00",
    "assets": 
    [
      {
        "asset_id": 1,
        "code": 1,
        "name": "iphone x",
        "inventory_id": 1
      },
      {
        "asset_id": 2,
        "code": 2,
        "name": "samsung pro",
        "inventory_id": 1
      },
      {
        "asset_id": 3,
        "code": 3,
        "name": "alcatel ",
        "inventory_id": 1
      }
    ]
}

得到的结果:


{
    "inventory_id": 1,
    "name": "cellphones",
    "location": "usa",
    "status": true,
    "created_date": "0001-01-01T00:00:00",
    "assets": [
      {
        "asset_id": 1,
        "code": 1,
        "name": "iphone x",
        "inventory_id": 1,
        "inventory": {
          "inventory_id": 1,
          "name": "cellphones",
          "location": "usa",
          "status": 1,
          "created_date": null,
          "assets": [
            null,
            {
              "asset_id": 2,
              "code": 2,
              "name": "samsung pro",
              "inventory_id": 1,
              "inventory": null
            },
            {
              "asset_id": 3,
              "code": 3,
              "name": "alcatel ",
              "inventory_id": 1,
              "inventory": null
            }
          ]
        }
      },
      {
        "asset_id": 2,
        "code": 2,
        "name": "samsung pro",
        "inventory_id": 1,
        "inventory": {
          "inventory_id": 1,
          "name": "cellphones",
          "location": "usa",
          "status": 1,
          "created_date": null,
          "assets": [
            {
              "asset_id": 1,
              "code": 1,
              "name": "iphone x",
              "inventory_id": 1,
              "inventory": null
            },
            null,
            {
              "asset_id": 3,
              "code": 3,
              "name": "alcatel ",
              "inventory_id": 1,
              "inventory": null
            }
          ]
        }
      },
      {
        "asset_id": 3,
        "code": 3,
        "name": "alcatel ",
        "inventory_id": 1,
        "inventory": {
          "inventory_id": 1,
          "name": "cellphones",
          "location": "usa",
          "status": 1,
          "created_date": null,
          "assets": [
            {
              "asset_id": 1,
              "code": 1,
              "name": "iphone x",
              "inventory_id": 1,
              "inventory": null
            },
            {
              "asset_id": 2,
              "code": 2,
              "name": "samsung pro",
              "inventory_id": 1,
              "inventory": null
            },
            null
          ]
        }
      }
    ]
  }

实体 Asset

需要另一个 Dto DtoAsset
namespace API.Dtos
{
    public class DtoInventory
    {
        public int inventory_id { get; set; }
        public string name { get; set; }
        public string location { get; set; }
        public bool status { get; set; }
        public DateTime created_date { get; set; }

        // List of Dto Assets
        public List<DtoAsset> Assets { get; set; }
    }

    public class DtoAsset
    {
        public int asset_id { get; set; }
        public int code { get; set; }
        public string name { get; set; }
        public int inventory_id { get; set;}
    }
}

所以你有一个 table 和 Inventories 和一个 table 和 Assets。 Inventories 和 Assets 之间有一个直接的 one-to-many 关系:每个 Inventory 都有零个或多个 Assets,每个 Asset 恰好属于一个 Inventory,即外键所指的 Inventory。

间奏曲:还有改进的空间

您决定将数据库中的行与您与用户(= 软件,而不是操作员)的沟通方式分开。因此,您有单独的 类 InventoryInventoryDto。这种分离可能是一件好事。如果您希望数据库布局发生变化,则您的用户不必进行更改。但是,由于 Inventory 和 InventoryDto 之间的差异非常小,我不确定在这种情况下这种分离是否是一种增强。

  • 如果 status 确实是布尔值,为什么不将其保存为数据库中的布尔值?此外,status 是一个容易混淆的名称。真实状态是什么意思?
  • Inventory.CreatedDate 中可以为空。 InventoryDto.CreatedDate 不是。你为什么做出这样的改变?如果数据库中的 CreatedDate 为空,您就会遇到麻烦。您想要 InventoryDto 中的什么值?

此外,您决定偏离 Entity Framework naming conventions。当然你可以自由地这样做,但是这种偏差使得你必须做更多的编程,就像你在 OnModelCreating.

中所做的那样

如果您遵循惯例,您的库存和驴子 类 将是这样的:

public class Inventory
{
    public int Id { get; set; }
    public string name { get; set; }
    public string location { get; set; }
    public int status { get; set; }
    public DateTime? created_date { get; set; }

    // Every Inventory has zero or more Assets (one-to-many)
    public virtual ICollection<Asset> Assets { get; set; }
}

public class Asset
{
    public int Id {get; set;}
    ... // other properties

    // Every Asses belongs to exactly one Inventory, using foreign key
    public int InventoryId {get; set;}
    public virtual Inventory Inventory {get; set;}
}

主要区别在于,我使用 ICollection<Asset>,而不是列表。 Inventory.Asset[4] 对您有明确的含义吗?您会使用 Asset 是 List 这一事实吗?如果你使用ICollection,用户不能使用索引,这很好,因为你不能保证什么对象会有什么索引。此外,这一点更重要:您不会强制 entity framework 将获取的数据复制到列表中。如果 entity framework 决定将数据放入另一种格式会更有效,为什么要强制将其用作列表?

所以在 one-to-many 和 many-to-many 中始终坚持 ICollection<...> 这个界面有你需要的所有功能:你可以在库存中添加和删除资产,你可以 Count 库存,您可以枚举它们 one-by-one。您需要的所有功能。

In entity framework the columns in the tables are represented by non-virtual properties. The virtual properties represent the relations between the tables (one-to-many, many-to-many, ...)

外键是 table 中的列,因此它们是 non-virtual。每个资产都属于一个库存这一事实是 table 之间的关系,因此这个 属性 是虚拟的。

回到你的问题

Find the inventory number 1 that contains assets.

如果您遵循约定,查询将很容易:

int inventoryId = 1;
using (var dbContext = new WhareHouseDbContext(...))
{
    Inventory fetchedInventory = dbContext.Inventories
        .Where(inventory => inventory.Id == inventoryId)
        .Select(inventory => new
        {
            // select only the properties that you actually plan to use
            Name = inventory.Name,
            Location = inventory.Location,
            ...

            // The Assets of this Inventory
            Assets = inventory.Assets
                .Where (asset => ...)     // only if you don't want all Assets of this Inventory
                .Select(asset => new
                {
                    // again, only the properties that you plan to use
                    ...

                    // not needed, you already now the value:
                    // InventoryId = asset.InventoryId,
                })
                .ToList(),
      })

      // expect at utmost one Inventory
      .FirstOrDefault();          

      if (fetchedInventory != null)
      {
          ... // process the fetched data
      }
}

Entity framework 了解库存和资产之间的 one-to-many 关系,并会为您进行适当的(组-)加入。

数据库管理系统经过极度优化,可以合并 table 和 select 数据。较慢的部分之一是将 selected 数据从 DBMS 传输到本地进程。

除了你不想传输你无论如何都不会使用的数据,或者你已经知道其值的数据,比如外键,还有另一个原因不获取完整的对象,也不使用包括。

每个 DbContext 都有一个 ChangeTracker,用于检测调用 SaveChanges 时必须更新哪些值。每当您获取一个完整的对象(table 中的行)时,该对象以及一个副本都会被放入 ChangeTracker。您获得了对副本(或原件,无关紧要)的引用。如果您对现有参考进行更改,则副本也会更改。

当您调用 SaveChanges 时,ChangeTracker 中的原件将按值与副本进行比较。仅更新更改。

如果您在不使用 Select 的情况下获取大量数据,所有这些项目及其副本都将放入 ChangeTracker。一旦你调用 SaveChanges,所有这些获取的数据都必须与它们的原始数据进行比较,以检查是否有变化。如果您不将您不想更新的项目放入 ChangeTracker,这将是一个巨大的性能提升。

In entity framework always use Select, and select only the properties that you actually plan to use. Only fetch complete rows, only use Include if you plan to update the fetched data.

匿名类型与具体类型

在我的解决方案中,我使用了匿名类型,这让我可以自由地 Select 只有我计划以我想要的格式使用的属性(可为空或 non-nullable CreatedDate,status作为布尔值或作为 int)。

缺点是匿名类型只能在定义它的方法中使用。如果您确实需要在方法之外使用数据,例如在 return 值中使用它,请调整 Select:

.Select(inventory => new InventoryDto
{
    Id = inventory.Id,
    Name = inventory.Name,
    ...

    Assets = inventory.Assets.Select(asset => new AssetDto
    {
        Id = asset.Id,
        ...
    })
    .ToList(),
}

现在您可以在您的方法之外使用此对象。

自己做 GroupJoin

有些人不想使用 virtual ICollection<...>,或者他们使用不支持此功能的 entity framework 版本。在这种情况下,您必须自己加入 (Group-)

var fetchedInventories = dbContext.Inventories
    .Where(inventory => ...)

    // GroupJoin the Inventories with the Assets:
    .GroupJoin(dbContext.Assets,

    inventory => inventory.Id,    // from every inventory take the primary key
    asset => asset.InventoryId,   // from every asset take the foreign key

    // parameter resultSelector:
    // from every inventory, each with its zero or more Assets make one new
    (inventory, assetsOfThisInventory) => new
    {
        Id = inventory.Id,
        Name = inventory.Name,
        ...

        Assets = assetsOfThisInventory.Select(asset => new
        {
            Id = asset.Id,
            ...
        })
        .ToList(),
});

当然,如果需要,使用具体类型而不是匿名类型。

In a one-to-many relation, use GroupJoin and start at the "one-side" if you need to fetch the "items, each with their zero or more subItems". Us Join and start at the "many side" if you need the fetch "items, each with their one parent item".

因此,使用 GroupJoin 获取学校及其学生、客户及其订单、图书馆及其书籍,以及您的情况下的库存继承人资产。

使用 Join 获取 Students,每个 Student 与他就读的学校,Orders 与下订单的客户的数据,或 Assets,与此 Asset 所属的唯一库存。