Первый запрос со связанными данными, результат с повторяющимися данными

Я пытаюсь выполнить запрос, в котором я должен извлечь информацию из базы данных следующим образом.

Найдите inventory число 1, которое содержит assets.

Но я получаю повторяющийся результат в моем объекте 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;
}

Дбконтекст

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

    }
}

Дтоинвентарь

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. Вы должны использовать dto для Asset, который не имеет обратной ссылки на Inventory. Как в этот ответ.

Gert Arnold 04.05.2022 15:06
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
38
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вам понадобится еще один Dto DtoAsset для Entity Asset

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;}
    }
}
Ответ принят как подходящий

Итак, у вас есть стол с Inventories и стол с Assets. Существует прямое отношение «один ко многим» между Инвентарями и Активами: каждый Инвентарь имеет ноль или более Активов, каждый Актив принадлежит ровно одному Инвентарю, а именно Инвентарю, на который ссылается внешний ключ.

Интермеццо: есть к чему стремиться

Вы решили отделить строки в своей базе данных от того, как вы общаетесь со своими пользователями (= программное обеспечение, а не операторы). Следовательно, у вас есть отдельные классы Inventory и InventoryDto. Это разделение может быть хорошей вещью. Если вы ожидаете изменений в макете вашей базы данных, вашим пользователям не придется меняться. Однако, поскольку различия между Inventory и InventoryDto очень малы, я не уверен, является ли это разделение в данном случае улучшением.

  • Если статус действительно является логическим значением, почему бы не сохранить его как логическое значение в базе данных? Кроме того, статус — сбивающее с толку название. Что означает настоящий статус?
  • В Inventory.CreatedDate можно обнулить. InventoryDto.CreatedDate нет. Почему вы сделали эту разницу? У вас проблемы, если CreatedDate в базе данных имеет значение null. Какое значение вы хотите в InventoryDto?

Кроме того, вы решаете отклониться от Соглашения об именовании Entity Framework. Конечно, вы можете это сделать, но это отклонение заставляет вас программировать намного больше, как вы делаете в OnModelCreating.

Если бы вы следовали соглашениям, ваши классы Inventory и Asses были бы такими:

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] определенное значение для вас? Будете ли вы когда-нибудь использовать тот факт, что Актив — это List? Если вы используете ICollection, пользователи не могут использовать индексацию, и это хорошо, потому что вы не можете обещать, какой объект будет иметь какой индекс. Кроме того, и это более важно: вы не будете заставлять фреймворк сущностей копировать полученные данные в список. Если инфраструктура сущностей решит, что было бы более эффективно поместить данные в другой формат, зачем заставлять ее использовать их как список?

Таким образом, в «один ко многим» и «многие ко многим» всегда придерживайтесь ICollection<...> Этот интерфейс имеет все необходимые вам функции: вы можете добавлять и удалять активы из инвентаря, а можете Count инвентари, и вы можете перечислять их один за другим. -один. Весь необходимый вам функционал.

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, ...)

Внешние ключи — это столбцы в ваших таблицах, поэтому они не виртуальные. Тот факт, что каждый Актив принадлежит ровно одному Инвентарю, является отношением между таблицами, поэтому это свойство является виртуальным.

Вернуться к вашему вопросу

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 знает отношение «один ко многим» между Inventory и Assets и выполнит для вас правильное (Group-)Join.

Системы управления базами данных чрезвычайно оптимизированы для объединения таблиц и выбора данных. Одной из самых медленных частей является передача выбранных данных из СУБД в ваш локальный процесс.

Кроме того, вы не хотите передавать данные, которые вы все равно не будете использовать, или данные, значение которых вы уже знаете, например, внешние ключи, есть еще одна причина не извлекать полные объекты и не использовать Include.

Каждый DbContext имеет ChangeTracker, который используется для определения того, какие значения должны быть обновлены, если вы вызываете SaveChanges. Всякий раз, когда вы извлекаете полный объект (строку в вашей таблице), объект помещается в 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.

Анонимные типы против конкретных типов

В своем решении я использовал анонимные типы, что дало мне свободу выбора только тех свойств, которые я планирую использовать в нужном мне формате (CreatedDate с нулевым или необнуляемым значением, статусом Boolean или int).

Недостатком является то, что анонимный тип можно использовать только в том методе, в котором он определен. Если вам действительно нужно использовать данные вне метода, например, использовать их в возвращаемом значении, настройте Select:

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

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

Теперь вы можете использовать этот объект вне вашего метода.

Присоединяйтесь к группе сами

Некоторые люди не хотят использовать virtual ICollection<...> или используют версию фреймворка сущностей, которая его не поддерживает. В этом случае вам придется выполнить (групповое) присоединение самостоятельно.

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 для получения школ с их учениками, клиентов с их заказами, библиотек с их книгами и, в вашем случае, инвентаря с их активами.

Используйте Присоединение для получения Учащихся, каждого Учащегося со Школой, которую он посещает, Заказов с данными Клиента, разместившего Заказ, или Активов с единственным Инвентарем, которому принадлежит этот Актив.

Противоположный комментарий: не делайте GroupJoin, потому что EF Core обычно не может перевести этот оператор. Только простые случаи для LEFT JOIN.

Svyatoslav Danyliv 04.05.2022 11:19

Другие вопросы по теме