Я изо всех сил пытаюсь понять, как построить свой SQL-запрос с использованием Entity Framework 6 и SQL Server в моем проекте ASP.NET Web API v2.
Моя структура таблицы: таблица Computers - это моя корневая таблица для всех компьютеров, которые были инвентаризированы моим программным обеспечением. Он содержит столбец ComputerID, который используется в качестве внешнего ключа в других таблицах.
Я могу получить компьютер, включая всю имеющуюся на нем информацию, с помощью следующего запроса:
Computer computer = ComputersDbContext.Computers
... more includes....
.Include("Computer_Win_Installed_Software")
... more includes....
.Where(a => a.ComputerId == computerId)
.First(t => t.TenantId == tenantId);
Как видите, у меня есть еще одна таблица под названием Computer_Win_Installed_Software, в которой хранится все программное обеспечение, установленное в системе. Это выглядит так:
IdentityKey | ComputerID (FK) | Softwarename | EntryTimestamp
------------+-----------------+------------------+--------------
1 | 1 | Some Software | 1547241345
2 | 1 | Another Software | 1547241345
EntryTimestamp - это временная метка Unix, которая уникальна для каждого запуска инвентаризации и одинакова для всего программного обеспечения, обнаруженного при этом запуске. Теперь, если система снова будет инвентаризирована, таблица будет выглядеть так:
IdentityKey | ComputerID (FK) | Softwarename | EntryTimestamp
------------+-----------------+--------------------+---------------
1 | 1 | Some Software | 1547241345
2 | 1 | Another Software | 1547241345
3 | 1 | Some Software | 1886454564
4 | 1 | Another Software | 1886454564
5 | 1 | Even More Software | 1886454564
Я хочу сохранить исторические данные, поэтому мне нужно сохранить старые записи.
Моя проблема в том, что мой запрос EF сверху вернет ВСЕ эти записи в результирующем объекте.
Как мне изменить свой запрос, чтобы он возвращал только:
IdentityKey | ComputerID (FK) | Softwarename | EntryTimestamp
------------+-----------------+--------------------+---------------
3 | 1 | Some Software | 1886454564
4 | 1 | Another Software | 1886454564
5 | 1 | Even More Software | 1886454564
Я думал об использовании двух запросов:
EntryTimestampВторой запрос: примерно так:
Computer computer = ComputersDbContext.Computers
.Include("Computer_Win_Installed_Software")
.Where(a => a.ComputerId == computerId)
.Where(b => b.Computer_Win_Installed_Software.EntryTimestamp == EntryTimestamp)
.First(t => t.TenantId == tenantId);
Но Intellisense тут же кричит на меня: D
Я тоже думал только о выборе MAX() из колонки EntryTimestamp; но я даже не могу использовать b.Computer_Win_Installed_Software.EntryTimestamp в коде запроса.
Когда я пишу: .Where(b => b.Computer_Win_Installed_Software, он не перечисляет какие-либо столбцы как доступные варианты.
Я думаю, это потому, что класс Computer_Win_Installed_Software в EF относится к типу ICollection<Computer_Win_Installed_Software>. Это та же проблема с другими таблицами, которые все имеют отношение "1 ко многим" с таблицей Computers.
Другая таблица имеет отношение 1 к 1 к таблице Computers, и там я могу выбрать все столбцы.
Я очень запутался.
И да, я сделал Google, но я не смог найти ничего, что помогло бы мне. Какой правильный путь сюда?
Обновлено: добавленные модели
DBContext:
public class DbEntities : DbContext
{
public virtual DbSet<Agent> Agents { get; set; }
public virtual DbSet<Computer_Win_Installed_Software> ComputerWinInstalledSoftware { get; set; }
public DbSet<Computer> Computers { get; set; }
}
Модель EF для компьютера:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace ProjectName
{
using System;
using System.Collections.Generic;
public partial class Computer
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Computer()
{
this.Computer_Win_Installed_Software = new HashSet<Computer_Win_Installed_Software>();
}
public int ComputerId { get; set; }
public int TenantId { get; set; }
public virtual Agent Agent { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Computer_Win_Installed_Software> Computer_Win_Installed_Software { get; set; }
}
}
Модель EF для компьютера_вин_установленное_программное обеспечение:
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace KysoWebApi
{
using System;
using System.Collections.Generic;
public partial class Computer_Win_Installed_Software
{
public int ComputerId { get; set; }
public string SoftwareName { get; set; }
public Nullable<double> SoftwareVersion { get; set; }
public Nullable<bool> IsServerSoftware { get; set; }
public Nullable<int> SoftwareVendorId { get; set; }
public string SoftwareIs32or64bits { get; set; }
public int ComputerWinInstalledSoftwareEntryId { get; set; }
public string EntryTimestamp { get; set; }
public virtual Computer Computer { get; set; }
}
}
В базе данных это: varchar (10), а в моем коде это строка. Но я даже не могу выбрать b.Computer_Win_Installed_Software.EntryTimestamp. Похоже, то же самое со всеми таблицами, смоделированными как ICollection <something>. Они имеют отношение 1 ко многим к таблице компьютеров.
Можете ли вы предоставить свои модельные объекты?
Кстати, вы можете легко сделать это на стороне сервера: SELECT identitykey, computerid, softwarename, entrytimestamp FROM computer_win_installed_software WHERE entrytimestamp = (SELECT MAX(entrytimestamp) FROM computer_win_installed_software)
@Mauricio Atanache: добавлены модели EF. Надеюсь, вы имели в виду именно эти. Если вы имели в виду что-то другое, поясните, пожалуйста, и я с радостью предоставлю их
@Miamy: Спасибо! Я, конечно, мог бы это сделать, но для этого мне потребуется выполнить 2 запроса. Я уверен, что столкнусь с этой проблемой в нескольких местах: / Знаете ли вы решение, которое я могу интегрировать в свой текущий запрос?
Я нигде не вижу поля TenantId.
@JuanR: Блин, ты молодец! Мне очень жаль, что я удалил ссылки на другие таблицы из моделей и кода, чтобы улучшить читаемость, а также удалил их. Я добавил это снова. У каждого компьютера есть ссылка на арендатора, см. Модель компьютера.
Похоже, у вас есть три базы данных (это dbSet, а не dbTable) 1) Агенты 2) ComputerWinInstalledSoftware 3) Компьютеры Каждая база данных будет иметь DataTables. Каждая таблица будет объектом списка. Вы пытаетесь выполнить перечисление с помощью компьютеров базы данных, которые не являются списком (). Вам нужно найти имя таблицы в компьютерной базе данных, в которой есть информация.
Какого результата вы хотите в итоге? Если вы ищете строки ComputerWinInstalledSoftware, почему бы не запросить их?
@jdweng: Нет, у меня только одна база данных. В соответствии с моей Visual Studio IntelliSense, похоже, существуют сопли DbTable. Не могли бы вы показать мне ссылку на документацию? Я не мог найти ни одного.
@NetMage: я могу получить все строки из таблицы ComputerWinInstalledSoftware, но я хочу отфильтровать их внутри запроса как часть компьютерного запроса, чтобы я не запрашивал базу данных несколько раз
Можете ли вы использовать SQL Server Management Studio для проверки баз данных, таблиц и столбцов. Структура в сети может не соответствовать базе данных. Ваш файл конфигурации может нуждаться в обновлении.
«Intellisense немедленно кричит на меня». Лучше сказать, о чем он кричит. Думаю нужен Where(b => b.Computer_Win_Installed_Software.Any(wis => wis.EntryTimestamp == EntryTimestamp))
@GertArnold: Спасибо, что связались со мной! С сожалением сообщаю, что ваше решение не работает. Результат: Если нет строки со значением EntryId -> нет результата. Если есть одна строка с правильным значением EntryId -> возвращаются все строки с совпадающим ComputerId, независимо от того, что EntryId является правильным для всех этих строк. Он не проверяет каждую строку на наличие правильного EntryId.
Заменить Any на All.
К сожалению, когда я это делаю, я получаю исключение Sequence contains no element Exception





Пожалуйста, попробуйте это:
var computer = db.ComputerWinInstalledSoftware
.Include("Computer")
.First(c => c.Computer.ComputerId == computerId
&& c.Computer.TenantId == tenantId
&& c.EntryTimestamp == EntryTimestamp ).Computer;
Это то, что я пробовал. Ошибка: Ошибка CS1061 «ICollection <Computer_Win_Installed_Software>» не содержит определения для «EntryTimestamp» и не может быть найден доступный метод расширения «EntryTimestamp», принимающий первый аргумент типа «ICollection <Computer_Win_Installed_Software> (если вам не хватает использования директива или ссылка на сборку?)
Просто чтобы подтвердить, это отличается от кода, который вы разместили в строке «Включить». Вы пробовали это изменение?
Вы заметили, что у вас разные пространства имен в ваших сущностях? Это нарочно?
Попробуй это:
Computer computer = context.Computers
.Include(c => c.Computer_Win_Installed_Software.Join(
c.Computer_Win_Installed_Software.Where(a => a.ComputerId == computerId && a.Computer.TenantId == tenantId)
.GroupBy(s => s.Softwarename)
.Select(g => new { SoftwareName = g.Key, MaxDate = g.Max(r => r.EntryTimestamp) })
,o => o.EntryTimestamp
,i => i.MaxDate
, (o,i) => o))
.First(a => a.ComputerId == computerId && a.TenantId == tenantId);
Я не вижу столбца TenantId в определении вашей сущности, поэтому просто убедитесь, что вы указали правильное поле.
Что это делает, так это присоединение к записям по максимальной дате.
ОБНОВЛЕНИЕ: Я сделал небольшую поправку. Это должно быть сгруппировано по названию программного обеспечения. Измените группировку по своему вкусу, чтобы она правильно идентифицировала каждую часть программного обеспечения.
С сожалением вынужден сообщить, что это не работает. Все, что находится внутри .Include (), помечается как ошибка: Ошибка CS1660 Не удается преобразовать лямбда-выражение в тип «строка», потому что это не тип делегата.
@soomon: Я еще раз взглянул на вашу модель, и похоже, что EntryTimeStamp - это string (хотя это похоже на число). Попробуйте изменить оператор Max на r.EntryTimestamp.ToString().
Спасибо еще раз. Да, это строка. К сожалению, ошибка все еще существует. Я думаю, что ошибка не в вашем коде, а в том факте, что выражение lamda не разрешено в Include().
Хорошо, мне нужно пространство имен using System.Data.Entity, чтобы удалить ошибку. Также вы правы насчет того, что EntryTimestamp - это строка. Я изменил его на целое число. Код по-прежнему выдает ошибку System.ArgumentException: The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties. Parameter name: path at System.Data.Entity.QueryableExtensions.Include[T,TProperty](IQueryable`1 source, Expression`1 path)
Пробовал: .Include(c => c.Computer_Win_Installed_Software.Where(d => d.EntryTimestamp == 1540275242)) провести простой тест. Он компилируется, но дает ту же ошибку. Когда я нажимаю . после d, он фактически показывает мне свойства Computer_Win_Installed_Software. Итак, он знает, что я хочу их использовать, но ошибка остается той же
Итак, для данного ComputerICollection<Computer_Win_Installed_Software> Computer_Win_Installed_Software всегда будет включать все связанные строки со всеми EntryTimestampts. Вы не можете (легко) фильтр включенных сущностей.
Итак, вместо этого просто верните отфильтрованные объекты:
var computer = ComputersDbContext.Computers
.Where(a => a.ComputerId == computerId)
.First(t => t.TenantId == tenantId);
var cwis = computer
.Computer_Win_Installed_Software
.Where(b => b.EntryTimestamp == EntryTimestamp);
Конечно, я не уверен, что это то, что вы действительно хотите сделать, у вас может быть Проблема XY.
Можете ли вы попробовать не включать связанные объекты, а просто запрашивать их?
var computer = ComputersDbContext.Computers
.Where(a => a.ComputerId == computerId)
.First(t => t.TenantId == tenantId);
.Where(b => b.Computer_Win_Installed_Software.EntryTimestamp == EntryTimestamp);
Блин, это значит, что если таблица Computer_Win_Installed_Software содержит 1000 строк, мне всегда придется запрашивать их все и потом фильтровать?
Или не используйте Include и загрузите их позже. Кажется, что ваша база данных и ваши сущности расходятся - я не использую EF, но я предполагаю, что вам следует перенести старые записи в таблицу истории, чтобы ваша сущность ожидала, что все текущие строки будут объединены. При некоторых изменениях модели EF можно создать свойство EF, которое предназначено только для текущих строк и автоматически изменяет соединение для последних. Очевидно, метод EF обычно представляет собой два запроса с использованием методов Query и Load для ручной загрузки отфильтрованных объектов.
Я добавил возможность, когда вместо их включения вы просто запрашиваете отношение.
Хорошо. После долгого исследования я нашел несколько сообщений, в которых говорилось, что вы не можете фильтровать внутри включенных статистических данных и что это сделка типа «все или ничего». Фильтрация в конце все время дает странные результаты.
Источники: Entity Framework - выборочное условие для включенного свойства навигации
Включает ли загрузку всех связанных сущностей или указанных?
Мне удалось найти 2 решения: DynamicSelectExtensions: https://github.com/thiscode/DynamicSelectExtensions
Кажется, есть EntityFramework + (EF +). Я видел сообщения разработчиков на Stackoverflow, но пока не могу их найти. Его библиотека, кажется, может делать то, что я хочу, но нормальный EF - нет и, похоже, никогда не будет, поскольку запрос функции для этого датируется несколькими годами.
Спасибо всем за попытку помочь мне и за то, что потратили свое время. Сейчас я сделаю несколько запросов и, возможно, вернусь позже, надеюсь, с большим опытом: D
Вы терпите неудачу, потому что вы не соответствуете типу столбцов. Какой тип EntryTimestamp? Это тот же тип, что и в базе данных?