Как заставить EF использовать объединения вместо разделения сложного запроса?
У меня есть сложный IQueryable, который я бы хотел, чтобы EF заполнял один запрос к базе данных, чтобы я мог использовать его с отложенным выполнением. Пожалуйста, рассмотрите следующий пример.
Для этих моделей:
public enum AlphaState { Unknown = '\0', Good = 'G', Bad = 'B' }
[Table("MY_ALPHA")]
public class Alpha
{
[Key]
[Column("alpha_index")]
public long Index { get; set; }
[Column("alpha_id")] // user-editable field that users call ID
public string AlphaId { get; set; }
[Column("deleted")]
public char? Deleted { get; set; }
[Column("state")]
public AlphaState State { get; set; }
[InverseProperty("Alpha")]
public ICollection<Bravo> Bravos { get; set; }
}
[Table("MY_BRAVO")]
public class Bravo
{
[Key]
[Column("bravo_index")]
public long BravoIndex { get; set; }
[ForeignKey("Alpha")]
[Column("alpha_index")] // actually a 1:0..1 relationship
public long? AlphaIndex { get; set; }
public virtual Alpha Alpha { get; set; }
[InverseProperty("Bravo")]
public ICollection<Charlie> Charlies { get; set; }
}
[Table("MY_CHARLIE_VIEW")]
public class Charlie
{
[Key]
[Column("charlie_index")]
public int CharlieIndex { get; set; }
[Column("deleted")]
public char? Deleted { get; set; }
[Column("created_at")]
public DateTime CreatedAt { get; set; }
[ForeignKey("Bravo")]
[Column("bravo_index")]
public long BravoIndex { get; set; }
public virtual Bravo Bravo { get; set; }
[ForeignKey("Delta")]
[Column("delta_index")]
public long DeltaIndex { get; set; }
public virtual Delta Delta { get; set; }
[InverseProperty("Charlie")]
public virtual ICollection<Delta> AllDeltas { get; set; }
}
[Table("MY_DELTA")]
public class Delta
{
[Key]
[Column("delta_index")]
public long DeltaIndex { get; set; }
[ForeignKey("Charlie")]
[Column("charlie_index")]
public long CharlieIndex { get; set; }
public virtual Charlie Charlie { get; set; }
[InverseProperty("Delta")] // actually a 1:0..1 relationship
public ICollection<Echo> Echoes { get; set; }
}
public enum EchoType { Unknown = 0, One = 1, Two = 2, Three = 3 }
[Table("MY_ECHOES")]
public class Echo
{
[Key]
[Column("echo_index")]
public int EchoIndex { get; set; }
[Column("echo_type")]
public EchoType Type { get; set; }
[ForeignKey("Delta")]
[Column("delta_index")]
public long DeltaIndex { get; set; }
public virtual Delta Delta { get; set; }
}
... рассмотрим этот запрос:
IQueryable<Alpha> result = context.Alphas.Where(a => a.State == AlphaState.Good)
.Where(a => !a.Deleted.HasValue)
.Where(a => a.Bravos.SelectMany(b => b.Charlies)
.Where(c => !c.Deleted.HasValue)
.Where(c => c.Delta.Echoes.Any())
.OrderByDescending(c => c.CreatedAt).Take(1)
.Any(c => c.Delta.Echoes.Any(e => e.Type == EchoType.Two)))
var query = result as System.Data.Objects.ObjectQuery;
string queryString = query.ToTraceString();
ПРИМЕЧАНИЕ: Чарли на самом деле является видом на стол; У Delta есть FK для таблицы Чарли, но представление дает поддельный FK для самой последней Delta, связанной с этим Charlie, поэтому модель использует это, потому что планируется использовать EF только для запросов, а не для обновления.
Мне бы хотелось, чтобы этот запрос заполнялся одним запросом к базе данных, но как написано, это не то, что происходит. Как я могу изменить этот запрос, чтобы получить те же результаты, но EF просто встроить условие в results
IQueryable вместо предварительной выборки данных для него?
Как я знаю, что он использует два запроса
Я точно знаю, что он разбивается на несколько запросов, потому что по причинам, выходящим за рамки этого вопроса, я намеренно дал контексту неверную строку соединения. result
является IQueryable, поэтому он должен использовать отложенное выполнение и на самом деле не пытаться получить какие-либо данные до тех пор, пока они не будут использованы, но я получаю исключение из-за сбоя соединения, как только я его объявляю.
Фон
У нас есть существующая структура базы данных, уровень доступа к базе данных и несколько сотен тысяч строк кода с использованием указанной структуры и DAL. Мы хотели бы добавить пользовательский интерфейс, позволяющий пользователям создавать собственные сложные запросы, и EF выглядел как хороший способ построить базовую модель для этого. Тем не менее, мы никогда не использовали EF раньше, поэтому Powers That Be заявили, что он никогда не сможет подключиться к базе данных; мы должны использовать EF для генерации IQueryable, извлечь из него строку запроса и использовать существующий DAL для выполнения запроса.
3 ответа
Как я знаю, что он использует два запроса
То, что вы наблюдаете, не EF начинает выполнять ваш запрос. После того, как вы назначите запрос result
переменная, у вас все еще есть определение запроса, а не набор результатов. Если вы присоедините профилировщик к вашей базе данных, вы увидите, что нет SELECT
Заявление выполнено по вашему запросу.
Так почему же происходит соединение с базой данных? Причина в том, что при первом создании запроса для заданного производного DbContext
типа, EF создает и кэширует свою модель в памяти для этого типа. Это достигается путем применения различных соглашений к типам, свойствам и атрибутам, которые вы определили. Теоретически, этот процесс не должен соединяться с базой данных, но провайдер для SQL Server делает это в любом случае. Это делается для определения используемой версии SQL Server, чтобы можно было определить, могут ли он использовать более свежие функции SQL Server в модели, которую он создает.
Интересно, что эта модель кэшируется для типа, а не для экземпляра контекста. Вы можете увидеть это, избавившись от своих context
затем создайте новый и повторите строки кода, которые строят запрос. Во втором случае вы вообще не увидите соединения с базой данных, поскольку EF будет использовать свою кэшированную модель для вашего типа контекста.
"Силы, которые будут" заявили, что никогда не смогут подключиться к базе данных; мы должны использовать EF для генерации IQueryable, извлечь из него строку запроса и использовать существующий DAL для выполнения запроса.
Так как вам вообще не нужно подключать EF к базе данных, вы можете увидеть мой пост здесь, в котором содержится информация о том, как вы можете вместо этого предоставить эту информацию в коде.
Также имейте в виду, что есть еще одна причина, по которой EF может подключиться к вашему серверу при первом обращении к вашему серверу. DbContext
Тип: инициализация. Если вы не отключили инициализацию (с чем-то вроде Database.SetInitializer<MyContext>(null)
) он проверит, что база данных существует, и попытается создать ее, если нет.
Обратите внимание, что вы можете позвонить ToString()
непосредственно на первый запрос кода EF, чтобы получить запрос T-SQL. Вам не нужно проходить через промежуточный ObjectQuery.ToTraceString()
метод, который на самом деле является частью устаревшего EF API. Однако оба эти метода предназначены для отладки и ведения журнала. Довольно необычно использовать EF для построения запросов, но не для их выполнения. С этим подходом вы, вероятно, столкнетесь с проблемами - наиболее очевидно, когда EF определит, что он должен генерировать параметризованный запрос. Кроме того, нет гарантии, что разные версии EF будут генерировать одинаковый T-SQL для одного и того же ввода, поэтому ваш код может оказаться довольно хрупким при обновлении до новых версий EF. Убедитесь, что у вас есть много тестов!
Если вас беспокоит то, что пользователи будут подключаться к базе данных напрямую - и это абсолютно законная проблема безопасности - вы можете рассмотреть альтернативный подход. У меня нет большого опыта с этим, но кажется, что OData могла бы быть хорошей подгонкой. Это позволяет создавать запросы на клиенте, сериализовать их через удаленное соединение, а затем заново создавать их на вашем сервере. Затем на сервере вы можете выполнить их для своей базы данных. Вашему клиенту вообще ничего не нужно знать о базе данных.
Если вы решили (или получили указание) продолжать подход, который вы подробно изложили в своем вопросе, потратьте время на изучение того, как профилировать соединение с SQL Server. Это будет абсолютно необходимым инструментом для понимания того, как EF переводит ваши запросы.
context.Foos.Where(f => f.Bars.Any(b => b.SomeOtherData == "baz"));
Я попытался выполнить запрос, аналогичный вашему, в имеющейся у меня базе данных (используя LINQPad), и в результате я получил
SELECT
[Extent1].[Property1] AS [Property1]
-- other properties of Foo
FROM [dbo].[Foos] AS [Extent1]
WHERE EXISTS (SELECT
1 AS [C1]
FROM [dbo].[Bars] AS [Extent2]
WHERE ([Extent1].[FooId] = [Extent2].[FooId]) AND (N'baz' = [Extent2].[SomeOtherData])
)
... что определенно выглядит как один запрос для меня.
Выражения в функциях IQueryable не выполняются напрямую - они используются для генерации SQL, который затем используется для выполнения запроса, когда необходимо материализовать результаты.
Попробуйте использовать синтаксис запроса LINQ.
var result = (
from f in foo
join b in bar
on f.fooid equals b.fooid
where b.someotherdata = "baz"
select new { f.fooid, f.somedata }
).Distinct().ToEnumerable();
Это будет отложено до перечисления.