Я пытаюсь выполнить запрос, в котором я должен извлечь информацию из базы данных следующим образом.
Найдите 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
]
}
}
]
}
Вам понадобится еще один 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 useInclude
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". UsJoin
and start at the "many side" if you need the fetch "items, each with their one parent item".
Так что используйте GroupJoin для получения школ с их учениками, клиентов с их заказами, библиотек с их книгами и, в вашем случае, инвентаря с их активами.
Используйте Присоединение для получения Учащихся, каждого Учащегося со Школой, которую он посещает, Заказов с данными Клиента, разместившего Заказ, или Активов с единственным Инвентарем, которому принадлежит этот Актив.
Противоположный комментарий: не делайте GroupJoin
, потому что EF Core обычно не может перевести этот оператор. Только простые случаи для LEFT JOIN.
Это потому, что вы используете
Asset
в DTO. Вы должны использовать dto дляAsset
, который не имеет обратной ссылки на Inventory. Как в этот ответ.