Новые возможности EF Core 9
EF Core 9 (EF9) — следующий выпуск после EF Core 8 и запланирован на выпуск в ноябре 2024 года.
EF9 доступен как ежедневные сборки , которые содержат все последние функции EF9 и настройки API. Примеры, приведенные здесь, используют эти ежедневные сборки.
Совет
Вы можете выполнить и выполнить отладку в примерах, скачав пример кода с GitHub. Каждый раздел ниже ссылается на исходный код, характерный для этого раздела.
EF9 предназначен для .NET 8 и, следовательно, может использоваться с .NET 8 (LTS) или предварительной версией .NET 9.
Совет
Новые документы обновляются для каждой предварительной версии. Все примеры настроены для использования ежедневных сборок EF9, которые обычно имеют несколько дополнительных недель завершенной работы по сравнению с последней предварительной версией. Мы настоятельно рекомендуем использовать ежедневные сборки при тестировании новых функций, чтобы вы не выполняли тестирование с устаревшими битами.
Azure Cosmos DB for NoSQL
EF 9.0 обеспечивает существенное улучшение поставщика EF Core для Azure Cosmos DB; Значительные части поставщика были перезаписаны, чтобы обеспечить новые функциональные возможности, разрешить новые формы запросов и лучше выровнять поставщика с рекомендациями Azure Cosmos DB. Ниже перечислены основные улучшения высокого уровня. Полный список см. в этой эпической проблеме.
Предупреждение
В рамках улучшений, которые вносятся в поставщик, необходимо вносить ряд критически важных изменений; Если вы обновляете существующее приложение, внимательно ознакомьтесь с разделом критических изменений.
Улучшение запросов с помощью ключей секций и идентификаторов документов
Каждый документ, хранящийся в базе данных Azure Cosmos DB, имеет уникальный идентификатор ресурса. Кроме того, каждый документ может содержать "ключ секции", который определяет логическую секционирование данных таким образом, чтобы база данных была эффективно масштабирована. Дополнительные сведения о выборе ключей секций можно найти в разделении и горизонтальном масштабировании в Azure Cosmos DB.
В EF 9.0 поставщик Azure Cosmos DB значительно лучше определяет сравнения ключей секций в запросах LINQ и извлекает их, чтобы убедиться, что запросы отправляются только в соответствующую секцию; Это может значительно повысить производительность запросов и сократить расходы на ЕЗ. Например:
var sessions = await context.Sessions
.Where(b => b.PartitionKey == "someValue" && b.Username.StartsWith("x"))
.ToListAsync();
В этом запросе поставщик автоматически распознает сравнение PartitionKey
по; если мы рассмотрим журналы, мы увидим следующее:
Executed ReadNext (189.8434 ms, 2.8 RU) ActivityId='8cd669ed-2ca5-4f2b-8923-338899071361', Container='test', Partition='["someValue"]', Parameters=[]
SELECT VALUE c
FROM root c
WHERE STARTSWITH(c["Username"], "x")
Обратите внимание, что WHERE
предложение не содержит PartitionKey
: сравнение было "снято" и используется для выполнения запроса только в соответствующей секции. В предыдущих версиях сравнение было оставлено в WHERE
предложении во многих ситуациях, что привело к выполнению запроса ко всем секциям, что привело к увеличению затрат и снижению производительности.
Кроме того, если запрос также предоставляет значение для свойства идентификатора документа и не включает другие операции запроса, поставщик может применить дополнительную оптимизацию:
var somePartitionKey = "someValue";
var someId = 8;
var sessions = await context.Sessions
.Where(b => b.PartitionKey == somePartitionKey && b.Id == someId)
.SingleAsync();
В журналах показано следующее для этого запроса:
Executed ReadItem (73 ms, 1 RU) ActivityId='13f0f8b8-d481-47f0-bf41-67f7deb008b2', Container='test', Id='8', Partition='["someValue"]'
Здесь запрос SQL не отправляется вообще. Вместо этого поставщик выполняет чрезвычайно эффективное чтение точек (ReadItem
API), которое напрямую извлекает документ с помощью ключа секции и идентификатора. Это наиболее эффективный и экономичный вид чтения, который можно выполнить в Azure Cosmos DB; Дополнительные сведения о точках чтения см. в документации по Azure Cosmos DB.
Дополнительные сведения о запросах с помощью ключей секций и операций чтения точек см. на странице документации по запросу.
Ключи иерархических разделов
Совет
Код, показанный здесь, поступает из HierarchicalPartitionKeysSample.cs.
Azure Cosmos DB изначально поддерживает один ключ секции, но с тех пор расширяет возможности секционирования, чтобы также поддерживать подсекцию с помощью спецификации до трех уровней иерархии в ключе секции. EF Core 9 обеспечивает полную поддержку иерархических ключей секций, что позволяет воспользоваться преимуществами повышения производительности и экономии затрат, связанных с этой функцией.
Ключи секций задаются с помощью API сборки модели, как правило, в DbContext.OnModelCreating. Для каждого уровня ключа секции должен быть сопоставлено свойство в типе сущности. Например, рассмотрим тип сущности UserSession
:
public class UserSession
{
// Item ID
public Guid Id { get; set; }
// Partition Key
public string TenantId { get; set; } = null!;
public Guid UserId { get; set; }
public int SessionId { get; set; }
// Other members
public string Username { get; set; } = null!;
}
Следующий код задает трехуровневый ключ секции с помощью TenantId
и UserId
SessionId
свойства:
modelBuilder
.Entity<UserSession>()
.HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });
Совет
Это определение ключа секции следует примеру, приведенному в разделе "Выбор ключей иерархических разделов" из документации по Azure Cosmos DB.
Обратите внимание, как, начиная с EF Core 9, свойства любого сопоставленного типа можно использовать в ключе секции. Для bool
и числовых типов, таких как int SessionId
свойство, значение используется непосредственно в ключе секции. Другие типы, такие как Guid UserId
свойство, автоматически преобразуются в строки.
При выполнении запросов EF автоматически извлекает значения ключа секции из запросов и применяет их к API запросов Azure Cosmos DB, чтобы убедиться, что запросы ограничены соответствующим количеством возможных секций. Например, рассмотрим следующий запрос LINQ, который предоставляет все три значения ключа секции в иерархии:
var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");
var sessions = await context.Sessions
.Where(
e => e.TenantId == tenantId
&& e.UserId == userId
&& e.SessionId == sessionId
&& e.Username.Contains("a"))
.ToListAsync();
При выполнении этого запроса EF Core извлекает значения tenantId
userId
параметров и sessionId
параметров и передает их в API запросов Azure Cosmos DB в качестве значения ключа секции. Например, ознакомьтесь с журналами выполнения приведенного выше запроса:
info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))
Обратите внимание, что сравнения ключей секций были удалены из WHERE
предложения и вместо этого используются в качестве ключа секции для эффективного выполнения: ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]
Дополнительные сведения см. в документации по запросу с помощью ключей секций.
Значительно улучшены возможности запросов LINQ
В EF 9.0 возможности перевода LINQ поставщика Azure Cosmos DB были значительно расширены, и поставщик теперь может выполнять значительно больше типов запросов. Полный список улучшений запросов слишком длинный для списка, но ниже приведены основные моменты.
- Полная поддержка примитивных коллекций EF, что позволяет выполнять запросы LINQ для коллекций, например ints или строк. Дополнительные сведения см . в статье "Новые возможности EF8: примитивные коллекции ".
- Поддержка произвольных запросов по немитивным коллекциям.
- Теперь поддерживаются множество дополнительных операторов LINQ: индексирование в коллекции,
Length
/Count
, ,Contains
ElementAt
и многие другие. - Поддержка статистических операторов, таких как
Count
иSum
. - Дополнительные переводы функций (см . документацию по сопоставлениям функций для полного списка поддерживаемых переводов):
- Переводы элементов
DateTime
иDateTimeOffset
элементов компонента (DateTime.Year
...DateTimeOffset.Month
). EF.Functions.IsDefined
иEF.Functions.CoalesceUndefined
теперь разрешать работу со значениямиundefined
.string.Contains
иStartsWith
EndsWith
теперь поддерживаетсяStringComparison.OrdinalIgnoreCase
.
- Переводы элементов
Полный список улучшений запросов см . в следующей статье:
Улучшенная модель, согласованная со стандартами Azure Cosmos DB и JSON
EF 9.0 сопоставляется с документами Azure Cosmos DB более естественными способами для базы данных документов на основе JSON и помогает взаимодействовать с другими системами, обращаюющимися к документам. Хотя это влечет за собой критические изменения, API существуют, которые позволяют вернуться к поведению до 9.0 во всех случаях.
Упрощенные id
свойства без дискриминации
Во-первых, предыдущие версии EF вставляют дискриминационные значения в свойство JSON id
, создавая такие документы, как:
{
"id": "Blog|1099",
...
}
Это было сделано, чтобы разрешить документам различных типов (например, блог и запись) и одному значению ключа (1099) существовать в одной секции контейнера. Начиная с EF 9.0, id
свойство содержит только значение ключа:
{
"id": 1099,
...
}
Это более естественный способ сопоставления с JSON и упрощает взаимодействие внешних средств и систем с документами JSON, созданными EF; такие внешние системы обычно не знают о дискриминационных значениях EF, которые по умолчанию являются производными от типов .NET.
Обратите внимание, что это критическое изменение, так как EF больше не сможет запрашивать существующие документы с старым id
форматом. Api был представлен, чтобы вернуться к предыдущему поведению, см . примечание о критических изменениях и документацию для получения дополнительных сведений.
Дискриминированное свойство, переименованное в $type
Ранее было названо Discriminator
дискриминационные свойства по умолчанию. EF 9.0 изменяет значение по умолчанию $type
:
{
"id": 1099,
"$type": "Blog",
...
}
Это следует за новым стандартом для полиморфизма JSON, что позволяет лучше взаимодействовать с другими инструментами. Например. System.Text.Json в NET также поддерживает полиморфизм, используя $type
в качестве имени свойства по умолчанию (docs).
Обратите внимание, что это критическое изменение, так как EF больше не сможет запрашивать существующие документы с старым именем дискриминационных свойств. Дополнительные сведения о том, как вернуться к предыдущей именовании, см. в заметке о критических изменениях.
Поиск сходства векторов (предварительная версия)
Azure Cosmos DB теперь предлагает предварительную версию поддержки поиска сходства векторов. Векторный поиск является основной частью некоторых типов приложений, включая ИИ, семантический поиск и другие. Azure Cosmos DB позволяет хранить векторы непосредственно в документах вместе с остальными данными, что означает, что вы можете выполнять все запросы к одной базе данных. Это может значительно упростить архитектуру и удалить необходимость дополнительного выделенного решения векторной базы данных в стеке. Дополнительные сведения о поиске векторов Azure Cosmos DB см. в документации.
После правильной настройки контейнера Azure Cosmos DB использование векторного поиска с помощью EF является простым вопросом добавления свойства вектора и его настройки:
public class Blog
{
...
public float[] Vector { get; set; }
}
public class BloggingContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Embeddings)
.IsVector(DistanceFunction.Cosine, dimensions: 1536);
}
}
После этого используйте функцию EF.Functions.VectorDistance()
в запросах LINQ для выполнения поиска сходства векторов:
var blogs = await context.Blogs
.OrderBy(s => EF.Functions.VectorDistance(s.Vector, vector))
.Take(5)
.ToListAsync();
Дополнительные сведения см. в документации по поиску векторов.
Поддержка разбивки на страницы
Поставщик Azure Cosmos DB теперь позволяет выполнять разбивку по результатам запроса с помощью маркеров продолжения, что гораздо эффективнее и экономично, чем традиционное использование Skip
и Take
:
var firstPage = await context.Posts
.OrderBy(p => p.Id)
.ToPageAsync(pageSize: 10, continuationToken: null);
var continuationToken = firstPage.ContinuationToken;
foreach (var post in page.Values)
{
// Display/send the posts to the user
}
Новый ToPageAsync
оператор возвращает CosmosPage
маркер продолжения, который можно использовать для эффективного возобновления запроса на более позднюю точку, извлекая следующие 10 элементов:
var nextPage = await context.Sessions.OrderBy(s => s.Id).ToPageAsync(10, continuationToken);
Дополнительные сведения см. в разделе документации по разбивке на страницы.
FromSql для более безопасного запроса SQL
Поставщик Azure Cosmos DB разрешил sql-запрос через FromSqlRaw. Однако этот API может быть подвержен атакам внедрения SQL, если предоставленные пользователем данные интерполируются или объединяются в SQL. В EF 9.0 теперь можно использовать новый FromSql
метод, который всегда интегрирует параметризованные данные в качестве параметра за пределами SQL:
var maxAngle = 8;
_ = await context.Blogs
.FromSql($"SELECT VALUE c FROM root c WHERE c.Angle1 <= {maxAngle}")
.ToListAsync();
Дополнительные сведения см. в разделе документации по разбивке на страницы.
Ролевой доступ
Azure Cosmos DB для NoSQL включает встроенную систему управления доступом на основе ролей (RBAC). Теперь эта поддержка поддерживается EF9 для всех операций плоскости данных. Однако пакет SDK Azure Cosmos DB не поддерживает RBAC для операций плоскости управления в Azure Cosmos DB. Используйте API управления Azure вместо EnsureCreatedAsync
RBAC.
Синхронный ввод-вывод теперь заблокирован по умолчанию
Azure Cosmos DB для NoSQL не поддерживает синхронные (блокирующие) API из кода приложения. Ранее EF маскировал это, блокируя для вас асинхронные вызовы. Однако это поощряет синхронное использование ввода-вывода, что является плохой практикой и может привести к взаимоблокировкам. Поэтому, начиная с EF 9, исключение возникает при попытке синхронного доступа. Например:
Синхронный ввод-вывод можно использовать сейчас, настроив уровень предупреждения соответствующим образом. Например, в OnConfiguring
типе DbContext
:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));
Обратите внимание, что мы планируем полностью удалить поддержку синхронизации в EF 11, поэтому начните обновление, чтобы использовать асинхронные методы, как ToListAsync
и SaveChangesAsync
как можно скорее!
AOT и предварительно скомпилированные запросы
Как упоминалось в вводном представлении, существует много работы, чтобы разрешить EF Core работать без JIT-компиляции. Вместо этого EF компилируется заранее (AOT) все необходимое для выполнения запросов в приложении. Эта компиляция AOT и связанная обработка будут выполняться в рамках создания и публикации приложения. На этом этапе в выпуске EF9 существует не так много доступных приложений, которые можно использовать разработчиком приложений. Однако для тех, кто заинтересован, завершенные проблемы в EF9, поддерживающие AOT и предварительно скомпилированные запросы, являются следующими:
- Скомпилированная модель: используйте статическую привязку вместо отражения для свойств и полей
- Скомпилированная модель: создание лямбда-кодов, используемых в отслеживании изменений
- Отслеживание изменений и конвейер обновления, совместимый с AOT/trimming
- Использование перехватчиков для перенаправления запроса в предварительно скомпилированные коды
- Сделать все узлы выражений SQL квотой
- Создание скомпилированной модели во время сборки
- Автоматическое обнаружение скомпилированной модели
- Создание объекта ParameterExtractingExpressionVisitor с возможностью извлечения путей для оценки фрагментов в дереве
- Создание деревьев выражений в скомпилированных моделях (фильтры запросов, преобразователи значений)
- Сделать LinqToCSharpSyntaxTranslator более устойчивым к нескольким объявлениям одной переменной в вложенных областях
- Оптимизация ParameterExtractingExpressionVisitor
Ознакомьтесь с примерами использования предварительно скомпилированных запросов по мере объединения взаимодействия.
Перевод LINQ и SQL
Как и при каждом выпуске, EF9 включает большое количество улучшений в возможностях запросов LINQ. Новые запросы можно преобразовать, и многие переводы SQL для поддерживаемых сценариев были улучшены как для повышения производительности, так и для удобства чтения.
Слишком большое количество улучшений, чтобы перечислить их здесь. Ниже выделены некоторые из более важных улучшений; См . эту проблему , чтобы получить более полный список выполненных работ в версии 9.0.
Мы хотели бы обратиться к Андреа Канчиани (@ranma42) за его многочисленные, высококачественные вклады в оптимизацию SQL, которая создается EF Core!
Сложные типы: поддержка GroupBy и ExecuteUpdate
GroupBy
Совет
Код, показанный здесь, поступает из ComplexTypesSample.cs.
EF9 поддерживает группировку по сложному экземпляру типа. Например:
var groupedAddresses = await context.Stores
.GroupBy(b => b.StoreAddress)
.Select(g => new { g.Key, Count = g.Count() })
.ToListAsync();
EF преобразует это как группирование по каждому элементу сложного типа, который соответствует семантике сложных типов в качестве объектов значений. Например, в SQL Azure:
SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]
ExecuteUpdate
Совет
Код, показанный здесь, поступает из ExecuteUpdateSample.cs.
Аналогичным образом, в EF9 ExecuteUpdate
также было улучшено принятие свойств сложного типа. Однако каждый элемент сложного типа должен быть явно указан. Например:
var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");
await context.Stores
.Where(e => e.Region == "Germany")
.ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));
Это создает SQL, который обновляет каждый столбец, сопоставленный с сложным типом:
UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
[s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
[s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
[s].[StoreAddress_Line2] = NULL,
[s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'
Ранее необходимо вручную вывести список различных свойств сложного типа в ExecuteUpdate
вызове.
Ненужимые элементы из SQL
Ранее EF иногда создавал SQL, содержащий элементы, которые на самом деле не нужны; В большинстве случаев они, возможно, были необходимы на более ранней стадии обработки SQL и были оставлены позади. EF9 теперь обрезает большинство таких элементов, что приводит к более компактным и, в некоторых случаях, более эффективному SQL.
Обрезка таблицы
В первом примере SQL, создаваемый EF, иногда содержал joIN в таблицы, которые фактически не нужны в запросе. Рассмотрим следующую модель, которая использует сопоставление наследования таблиц на тип (TPT):
public class Order
{
public int Id { get; set; }
...
public Customer Customer { get; set; }
}
public class DiscountedOrder : Order
{
public double Discount { get; set; }
}
public class Customer
{
public int Id { get; set; }
...
public List<Order> Orders { get; set; }
}
public class BlogContext : DbContext
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().UseTptMappingStrategy();
}
}
Если затем выполнить следующий запрос, чтобы получить всех клиентов по крайней мере с одним заказом:
var customers = await context.Customers.Where(o => o.Orders.Any()).ToListAsync();
EF8 создал следующий SQL:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
LEFT JOIN [DiscountedOrders] AS [d] ON [o].[Id] = [d].[Id]
WHERE [c].[Id] = [o].[CustomerId])
Обратите внимание, что запрос содержал соединение с DiscountedOrders
таблицей, даже если на нее не ссылались столбцы. EF9 создает обрезаемый SQL без соединения:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE EXISTS (
SELECT 1
FROM [Orders] AS [o]
WHERE [c].[Id] = [o].[CustomerId])
Прорезка проекции
Аналогичным образом давайте рассмотрим следующий запрос:
var orders = await context.Orders
.Where(o => o.Amount > 10)
.Take(5)
.CountAsync();
В EF8 этот запрос создал следующий SQL:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) [o].[Id]
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [t]
Обратите внимание, что проекция [o].[Id]
не требуется в подзапросе, так как внешнее выражение SELECT просто подсчитывает строки. EF9 создает следующее:
SELECT COUNT(*)
FROM (
SELECT TOP(@__p_0) 1 AS empty
FROM [Orders] AS [o]
WHERE [o].[Amount] > 10
) AS [s]
... и проекция пуста. Это может показаться не так много, но это может значительно упростить SQL в некоторых случаях; Вы можете прокрутить некоторые изменения SQL в тестах , чтобы увидеть эффект.
Переводы с участием GREATEST/LEAST
Совет
Код, показанный здесь, поступает из LeastGreatestSample.cs.
Были введены несколько новых переводов, использующих GREATEST
функции и LEAST
функции SQL.
Внимание
Функции GREATEST
были представлены в базах данных SQL Server или Azure SQL в версии 2022 года.LEAST
Visual Studio 2022 устанавливает SQL Server 2019 по умолчанию. Мы рекомендуем установить SQL Server Developer Edition 2022 , чтобы попробовать эти новые переводы в EF9.
Например, запросы с помощью Math.Max
или Math.Min
теперь переводятся для SQL Azure, используя GREATEST
и LEAST
соответственно. Например:
var walksUsingMin = await context.Walks
.Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
.ToListAsync();
Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:
SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
SELECT COUNT(*)
FROM OPENJSON([w].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b])) >
Math.Min
также Math.Max
можно использовать для значений примитивной коллекции. Например:
var pubsInlineMax = await context.Pubs
.SelectMany(e => e.Counts)
.Where(e => Math.Max(e, threshold) > top)
.ToListAsync();
Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:
SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1
Наконец, RelationalDbFunctionsExtensions.Least
и RelationalDbFunctionsExtensions.Greatest
можно использовать для непосредственного Least
вызова или Greatest
функции в SQL. Например:
var leastCount = await context.Pubs
.Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
.ToListAsync();
Этот запрос преобразуется в следующий SQL при использовании EF9 для SQL Server 2022:
SELECT LEAST((
SELECT COUNT(*)
FROM OPENJSON([p].[Counts]) AS [c]), (
SELECT COUNT(*)
FROM OPENJSON([p].[DaysVisited]) AS [d]), (
SELECT COUNT(*)
FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]
Принудительное или запретить параметризацию запросов
Совет
Код, показанный здесь, поступает из QuerySample.cs.
За исключением некоторых особых случаев, EF Core параметризует переменные, используемые в запросе LINQ, но включает константы в созданном SQL. Например, рассмотрим следующий метод запроса:
async Task<List<Post>> GetPosts(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == id)
.ToListAsync();
Это преобразуется в следующие параметры SQL и параметров при использовании SQL Azure:
Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0
Обратите внимание, что EF создал константу в SQL для блога .NET, так как это значение не изменится с запроса на запрос. Использование константы позволяет проверять это значение ядром СУБД при создании плана запроса, что может привести к более эффективному запросу.
С другой стороны, значение id
параметризуется, так как один и тот же запрос может выполняться с множеством различных значений.id
Создание константы в этом случае приведет к загрязнением кэша запросов с большим количеством запросов, которые отличаются только значениями id
. Это очень плохо для общей производительности базы данных.
Как правило, эти значения по умолчанию не должны быть изменены. Однако EF Core 8.0.2 представляет EF.Constant
метод, который заставляет EF использовать константу, даже если параметр будет использоваться по умолчанию. Например:
async Task<List<Post>> GetPostsForceConstant(int id)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
.ToListAsync();
Теперь перевод содержит константу для id
значения:
Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1
Метод EF.Parameter
EF9 вводит EF.Parameter
метод, чтобы сделать противоположное. То есть принудительно ef использовать параметр, даже если значение является константой в коде. Например:
async Task<List<Post>> GetPostsForceParameter(int id)
=> await context.Posts
.Where(e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
.ToListAsync();
Перевод теперь содержит параметр для строки блога .NET:
Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1
Параметризованные примитивные коллекции
EF8 изменил способ преобразования некоторых запросов, использующих примитивные коллекции. Если запрос LINQ содержит параметризованную примитивную коллекцию, EF преобразует его содержимое в JSON и передает его в виде одного значения параметра запроса:
async Task<List<Post>> GetPostsPrimitiveCollection(int[] ids)
=> await context.Posts
.Where(e => e.Title == ".NET Blog" && ids.Contains(e.Id))
.ToListAsync();
Это приведет к следующему переводу в SQL Server:
Executed DbCommand (5ms) [Parameters=[@__ids_0='[1,2,3]' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (
SELECT [i].[value]
FROM OPENJSON(@__ids_0) WITH ([value] int '$') AS [i]
)
Это позволяет иметь один и тот же SQL-запрос для разных параметризованных коллекций (только изменения значения параметра), но в некоторых ситуациях это может привести к проблемам с производительностью, так как база данных не может оптимально планировать запрос. Этот EF.Constant
метод можно использовать для возврата к предыдущему переводу.
Следующий запрос используется EF.Constant
для этого эффекта:
async Task<List<Post>> GetPostsForceConstantCollection(int[] ids)
=> await context.Posts
.Where(
e => e.Title == ".NET Blog" && EF.Constant(ids).Contains(e.Id))
.ToListAsync();
Результирующий SQL выглядит следующим образом:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] IN (1, 2, 3)
Кроме того, EF9 вводит TranslateParameterizedCollectionsToConstants
параметр контекста, который можно использовать для предотвращения параметризации примитивов для всех запросов. Мы также добавили дополнение TranslateParameterizedCollectionsToParameters
, которое заставляет параметризацию примитивных коллекций явно (это поведение по умолчанию).
Совет
Метод EF.Parameter
переопределяет параметр контекста. Если вы хотите предотвратить параметризацию примитивных коллекций для большинства запросов (но не все), можно задать параметр TranslateParameterizedCollectionsToConstants
контекста и использовать EF.Parameter
для запросов или отдельных переменных, которые требуется параметризировать.
Встроенные вложенные запросы без корректных
Совет
Код, показанный здесь, поступает из QuerySample.cs.
В EF8 объект IQueryable, на который ссылается другой запрос, может выполняться в виде отдельной обходной схемы базы данных. Например, рассмотрим следующий запрос LINQ:
var dotnetPosts = context
.Posts
.Where(p => p.Title.Contains(".NET"));
var results = dotnetPosts
.Where(p => p.Id > 2)
.Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
.Skip(2).Take(10)
.ToArray();
В EF8 запрос dotnetPosts
выполняется в виде одного кругового пути, а затем окончательные результаты выполняются в качестве второго запроса. Например, в SQL Server.
SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY
В EF9 встраиваются в dotnetPosts
структуру, что приводит к тому, IQueryable
что одна база данных круговая поездка:
SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
SELECT COUNT(*)
FROM [Posts] AS [p0]
WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
Агрегатные функции по вложенным запросам и агрегатам в SQL Server
EF9 улучшает перевод некоторых сложных запросов с помощью агрегатных функций, состоящих из вложенных запросов или других агрегатных функций. Ниже приведен пример такого запроса:
var latestPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
LatestPostRating = x.Posts.OrderByDescending(xx => xx.PublishedOn).FirstOrDefault().Rating
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.LatestPostRating))
.ToListAsync();
Во-первых, Select
вычисляется LatestPostRating
для каждого Post
из них, для которого требуется вложенный запрос при переводе в SQL. Далее в запросе эти результаты агрегируются с помощью Average
операции. Результирующий SQL выглядит следующим образом при запуске в SQL Server:
SELECT AVG([s].[Rating])
FROM [Blogs] AS [b]
OUTER APPLY (
SELECT TOP(1) [p].[Rating]
FROM [Posts] AS [p]
WHERE [b].[Id] = [p].[BlogId]
ORDER BY [p].[PublishedOn] DESC
) AS [s]
GROUP BY [b].[Language]
В предыдущих версиях EF Core создаст недопустимый SQL для аналогичных запросов, пытаясь применить агрегатную операцию непосредственно над вложенным запросом. Это не допускается в SQL Server и приводит к исключению. Тот же принцип применяется к запросам, использующим агрегатную обработку по другому агрегату:
var topRatedPostsAverageRatingByLanguage = await context.Blogs.
Select(x => new
{
x.Language,
TopRating = x.Posts.Max(x => x.Rating)
})
.GroupBy(x => x.Language)
.Select(x => x.Average(xx => xx.TopRating))
.ToListAsync();
Примечание.
Это изменение не влияет на Sqlite, который поддерживает агрегаты по вложенным запросам (или другим агрегатам) и не поддерживает LATERAL JOIN
(APPLY
). Ниже приведен SQL для первого запроса, запущенного в Sqlite:
SELECT ef_avg((
SELECT "p"."Rating"
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId"
ORDER BY "p"."PublishedOn" DESC
LIMIT 1))
FROM "Blogs" AS "b"
GROUP BY "b"."Language"
Запросы с помощью count != 0 оптимизированы
Совет
Код, показанный здесь, поступает из QuerySample.cs.
В EF8 был переведен следующий запрос LINQ для использования функции SQL COUNT
:
var blogsWithPost = await context.Blogs
.Where(b => b.Posts.Count > 0)
.ToListAsync();
EF9 теперь создает более эффективный перевод с помощью EXISTS
:
SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
SELECT 1
FROM "Posts" AS "p"
WHERE "b"."Id" = "p"."BlogId")
Семантика C# для операций сравнения со значениями, допускаемыми значением NULL
В EF8 сравнения между элементами, допускаемыми значением NULL, не выполнялись правильно для некоторых сценариев. В C#, если одно или оба операнда имеют значение NULL, результат операции сравнения имеет значение false; в противном случае сравниваются содержащиеся значения операндов. В EF8 мы использовали для преобразования сравнений с использованием семантики null базы данных. Это приведет к получению результатов, отличных от аналогичного запроса с помощью LINQ to Objects. Кроме того, мы создадим различные результаты при сравнении в фильтре и проекции. Некоторые запросы также будут создавать различные результаты между Sql Server и Sqlite/Postgres.
Например, запрос:
var negatedNullableComparisonFilter = await context.Entities
.Where(x => !(x.NullableIntOne > x.NullableIntTwo))
.Select(x => new { x.NullableIntOne, x.NullableIntTwo }).ToListAsync();
приведет к возникновению следующего КОДА SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE NOT ([e].[NullableIntOne] > [e].[NullableIntTwo])
который фильтрует сущности, для которых NullableIntOne
NullableIntTwo
задано значение NULL.
В EF9 мы производим:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo]
FROM [Entities] AS [e]
WHERE CASE
WHEN [e].[NullableIntOne] > [e].[NullableIntTwo] THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(1 AS bit)
Аналогичное сравнение, выполняемое в проекции:
var negatedNullableComparisonProjection = await context.Entities.Select(x => new
{
x.NullableIntOne,
x.NullableIntTwo,
Operation = !(x.NullableIntOne > x.NullableIntTwo)
}).ToListAsync();
привело к следующему SQL:
SELECT [e].[NullableIntOne], [e].[NullableIntTwo], CASE
WHEN NOT ([e].[NullableIntOne] > [e].[NullableIntTwo]) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Operation]
FROM [Entities] AS [e]
значение , возвращающее false
сущности, для которых NullableIntOne
NullableIntTwo
задано значение NULL (а не true
ожидалось в C#). Выполнение того же сценария в Sqlite, созданном:
SELECT "e"."NullableIntOne", "e"."NullableIntTwo", NOT ("e"."NullableIntOne" > "e"."NullableIntTwo") AS "Operation"
FROM "Entities" AS "e"
это приводит к Nullable object must have a value
исключению, так как перевод создает null
значение для случаев, когда NullableIntOne
или NullableIntTwo
имеет значение NULL.
EF9 теперь правильно обрабатывает эти сценарии, создавая результаты, согласованные с объектами LINQ to Objects и различными поставщиками.
Это улучшение было внесено @ranma42. Спасибо!
Order
Перевод операторов и OrderDescending
операторов LINQ
EF9 обеспечивает перевод операций упрощенного упорядочивания LINQ (Order
и OrderDescending
). Эти действия аналогичны OrderBy
/OrderByDescending
, но не требуют аргумента. Вместо этого они применяют упорядочение по умолчанию для сущностей, это означает упорядочение на основе значений первичного ключа и для других типов, упорядочение на основе самих значений.
Ниже приведен пример запроса, который использует преимущества упрощенных операторов упорядочивания:
var orderOperation = await context.Blogs
.Order()
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderDescending().ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).Order().ToList()
})
.ToListAsync();
Этот запрос эквивалентен следующему:
var orderByEquivalent = await context.Blogs
.OrderBy(x => x.Id)
.Select(x => new
{
x.Name,
OrderedPosts = x.Posts.OrderByDescending(xx => xx.Id).ToList(),
OrderedTitles = x.Posts.Select(xx => xx.Title).OrderBy(xx => xx).ToList()
})
.ToListAsync();
и создает следующий SQL:
SELECT [b].[Name], [b].[Id], [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Rating], [p].[Title], [p].[PromoText], [p].[Metadata], [p0].[Title], [p0].[Id]
FROM [Blogs] AS [b]
LEFT JOIN [Posts] AS [p] ON [b].[Id] = [p].[BlogId]
LEFT JOIN [Posts] AS [p0] ON [b].[Id] = [p0].[BlogId]
ORDER BY [b].[Id], [p].[Id] DESC, [p0].[Title]
Примечание.
Order
и OrderDescending
методы поддерживаются только для коллекций сущностей, сложных типов или скаляров. Они не будут работать над более сложными проекциями, например коллекции анонимных типов, содержащих несколько свойств.
Это улучшение было внесено в состав команды EF @bricelam. Спасибо!
Улучшен перевод оператора логического отрицания (!)
EF9 приносит много оптимизаций вокруг SQL CASE/WHEN
, COALESCE
отрицания и различных других конструкций; большинство из них были внесли Андреа Канчиани (@ranma42) - большое спасибо за все это! Ниже мы рассмотрим лишь несколько этих оптимизаций вокруг логического отрицания.
Давайте рассмотрим следующий запрос:
var negatedContainsSimplification = await context.Posts
.Where(p => !p.Content.Contains("Announcing"))
.Select(p => new { p.Content }).ToListAsync();
В EF8 мы создадим следующий SQL:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE NOT (instr("p"."Content", 'Announcing') > 0)
В EF9 мы "заталкиваем" NOT
операцию в сравнение:
SELECT "p"."Content"
FROM "Posts" AS "p"
WHERE instr("p"."Content", 'Announcing') <= 0
Другим примером, применимым к SQL Server, является отрицаемая условная операция.
var caseSimplification = await context.Blogs
.Select(b => !(b.Id > 5 ? false : true))
.ToListAsync();
В EF8 используется для выполнения вложенных CASE
блоков:
SELECT CASE
WHEN CASE
WHEN [b].[Id] > 5 THEN CAST(0 AS bit)
ELSE CAST(1 AS bit)
END = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
В EF9 мы удалили вложение:
SELECT CASE
WHEN [b].[Id] > 5 THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END
FROM [Blogs] AS [b]
В SQL Server при проецирование незначаемого логическое свойство:
var negatedBoolProjection = await context.Posts.Select(x => new { x.Title, Active = !x.Archived }).ToListAsync();
EF8 создаст CASE
блок, так как сравнения не могут отображаться в проекции непосредственно в запросах SQL Server:
SELECT [p].[Title], CASE
WHEN [p].[Archived] = CAST(0 AS bit) THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END AS [Active]
FROM [Posts] AS [p]
В EF9 этот перевод был упрощен и теперь использует побитовую не (~
):
SELECT [p].[Title], ~[p].[Archived] AS [Active]
FROM [Posts] AS [p]
Улучшена поддержка AZURE SQL и Azure Synapse
EF9 обеспечивает большую гибкость при указании типа ЦЕЛЕВОго SQL Server. Вместо настройки EF UseSqlServer
с помощью теперь можно указать UseAzureSql
или UseAzureSynapse
.
Это позволяет EF создавать лучшие SQL при использовании Azure SQL или Azure Synapse. EF может воспользоваться преимуществами конкретных функций базы данных (например , выделенного типа для JSON в Azure SQL) или обойти свои ограничения (например ESCAPE
, предложение недоступно при использовании LIKE
в Azure Synapse).
Другие улучшения запросов
- Примитивные коллекции, запрашивающие поддержку в EF8 , были расширены для поддержки всех
ICollection<T>
типов. Обратите внимание, что это относится только к параметрам и встроенным коллекциям — примитивные коллекции, которые являются частью сущностей, по-прежнему ограничены массивами, списками и в EF9 также массивами и списками только для чтения. - Новые
ToHashSetAsync
функции, возвращающие результаты запроса как aHashSet
(#30033, внесенные @wertzui). TimeOnly.FromDateTime
иFromTimeSpan
теперь переведены на SQL Server (No 33678).ToString
Перекрещенные перечисления теперь переводятся (#33706, внесенные @Danevandy99).string.Join
теперь преобразуется в CONCAT_WS в нерегларегированном контексте в SQL Server (#28899).EF.Functions.PatIndex
теперь преобразуется в функцию SQL ServerPATINDEX
, которая возвращает начальную позицию первого вхождения шаблона (#33702, @smnsht).Sum
иAverage
теперь работает для десятичных разрядов в SQLite (#33721, внося свой вклад в @ranma42).- Исправления и оптимизации в
string.StartsWith
иEndsWith
(#31482). Convert.To*
Теперь методы могут принимать аргумент типаobject
(#33891, внесенный @imangd).- Операция exclusive-Or (XOR) теперь преобразуется в SQL Server (#34071, внося в @ranma42).
- Оптимизация вокруг допустимости значений NULL для операций
AT TIME ZONE
(#34263, вносимая @ranma42).COLLATE
- Оптимизация для
DISTINCT
операций overEXISTS
IN
и set (#34381, вносимая @ranma42).
Выше были только некоторые из более важных улучшений запросов в EF9; См . эту проблему для более полного описания.
Миграции
Улучшены миграции темпоральных таблиц
Миграция, созданная при изменении существующей таблицы в темпоральную таблицу, сократилась в размере EF9. Например, в EF8 создание одной существующей таблицы в темпоральной таблице приводит к следующей миграции:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "SiteUri",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Blogs",
type: "nvarchar(max)",
nullable: false,
oldClrType: typeof(string),
oldType: "nvarchar(max)")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Blogs",
type: "int",
nullable: false,
oldClrType: typeof(int),
oldType: "int")
.Annotation("SqlServer:Identity", "1, 1")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
.OldAnnotation("SqlServer:Identity", "1, 1");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}
В EF9 та же операция теперь приводит к значительно меньшей миграции:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterTable(
name: "Blogs")
.Annotation("SqlServer:IsTemporal", true)
.Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
.Annotation("SqlServer:TemporalHistoryTableSchema", null)
.Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
.Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
migrationBuilder.AddColumn<DateTime>(
name: "PeriodEnd",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodEndColumn", true);
migrationBuilder.AddColumn<DateTime>(
name: "PeriodStart",
table: "Blogs",
type: "datetime2",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}
Защита от параллельной миграции
EF9 представляет механизм блокировки для защиты от нескольких выполнений миграции одновременно, так как это может оставить базу данных в поврежденном состоянии. Это не происходит при развертывании миграции в рабочую среду с помощью рекомендуемых методов, но может произойти, если миграции применяются во время выполнения с помощью DbContext.Database.Migrate()
метода. Рекомендуется применять миграции во время развертывания, а не как часть запуска приложения, но это может привести к более сложным архитектурам приложений (например , при использовании проектов .NET Aspire.
Примечание.
Если вы используете базу данных Sqlite, ознакомьтесь с потенциальными проблемами, связанными с этой функцией.
Предупреждение о том, что не удается выполнить несколько операций миграции внутри транзакции
Большинство операций, выполняемых во время миграции, защищены транзакцией. Это гарантирует, что если по какой-то причине миграция завершается ошибкой, база данных не находится в поврежденном состоянии. Однако некоторые операции не упаковываются в транзакцию (например, операции с оптимизированными для памяти таблицами SQL Server или операции изменения базы данных, например изменение параметров сортировки базы данных). Чтобы избежать повреждения базы данных в случае сбоя миграции, рекомендуется выполнить эти операции в изоляции с помощью отдельной миграции. EF9 теперь обнаруживает сценарий, когда миграция содержит несколько операций, одна из которых не может быть упакована в транзакцию и выдает предупреждение.
Улучшено начальное значение данных
EF9 представил удобный способ заполнения данных, который заполняет базу данных начальными данными. DbContextOptionsBuilder
теперь содержит и методыUseSeeding
, которые выполняются при инициализации DbContext (в составе EnsureCreatedAsync
).UseAsyncSeeding
Примечание.
Если приложение запущено ранее, база данных уже может содержать примеры данных (которые были бы добавлены при первой инициализации контекста). Таким образом, следует проверить, UseSeeding
UseAsyncSeeding
существуют ли данные перед попыткой заполнения базы данных. Это можно сделать, выполнив простой запрос EF.
Ниже приведен пример использования этих методов.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFDataSeeding;Trusted_Connection=True;ConnectRetryCount=0")
.UseSeeding((context, _) =>
{
var testBlog = context.Set<Blog>().FirstOrDefault(b => b.Url == "http://test.com");
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
context.SaveChanges();
}
})
.UseAsyncSeeding(async (context, _, cancellationToken) =>
{
var testBlog = await context.Set<Blog>().FirstOrDefaultAsync(b => b.Url == "http://test.com", cancellationToken);
if (testBlog == null)
{
context.Set<Blog>().Add(new Blog { Url = "http://test.com" });
await context.SaveChangesAsync(cancellationToken);
}
});
Дополнительные сведения см. здесь.
Построение модели
Автоматически скомпилированные модели
Совет
Приведенный здесь код получен из примера NewInEFCore9.CompiledModels .
Скомпилированные модели могут улучшить время запуска для приложений с большими моделями, то есть количество типов сущностей в 100-х или 1000-х годах. В предыдущих версиях EF Core необходимо создать скомпилированную модель вручную с помощью командной строки. Например:
dotnet ef dbcontext optimize
После выполнения команды необходимо добавить строку, например, .UseModel(MyCompiledModels.BlogsContextModel.Instance)
чтобы OnConfiguring
сообщить EF Core использовать скомпилированную модель.
Начиная с EF9, эта .UseModel
строка больше не требуется, если тип приложения DbContext
находится в том же проекте или сборке, что и скомпилированная модель. Вместо этого скомпилированная модель будет обнаружена и используется автоматически. Это можно увидеть, имея журнал EF всякий раз, когда он создает модель. При запуске простого приложения ef показано создание модели при запуске приложения:
Starting application...
>> EF is building the model...
Model loaded with 2 entity types.
Выходные данные запуска в dotnet ef dbcontext optimize
проекте модели:
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize
Build succeeded in 0.3s
Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model>
Обратите внимание, что выходные данные журнала указывают на то, что модель была создана при выполнении команды. Если теперь мы снова запустите приложение, после перестроения, но без внесения изменений в код, выходные данные:
Starting application...
Model loaded with 2 entity types.
Обратите внимание, что модель не была создана при запуске приложения, так как скомпилированная модель была обнаружена и использована автоматически.
Интеграция MSBuild
При приведенном выше подходе скомпилированная модель по-прежнему должна создаваться вручную при изменении типов сущностей или DbContext
конфигурации. Однако EF9 поставляется с пакетом MSBuild и целевым пакетом, который может автоматически обновлять скомпилированную модель при создании проекта модели! Чтобы приступить к работе, установите пакет NuGet Microsoft.EntityFrameworkCore.Tasks . Например:
dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-preview.4.24205.3
Совет
Используйте версию пакета в приведенной выше команде, которая соответствует используемой версии EF Core.
Затем включите интеграцию, задав свойству EFOptimizeContext
.csproj
файл. Например:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>
Существуют дополнительные, необязательные свойства MSBuild для управления сборкой модели, эквивалентные параметрам, переданным в командной строке dotnet ef dbcontext optimize
. Например:
Свойство MSBuild | Description |
---|---|
EFOptimizeContext | Установите для true включения автоматически скомпилированных моделей. |
DbContextName | Используемый класс DbContext. Имя класса только или полное имя с пространствами имен. Если этот параметр опущен, EF Core найдет класс контекста. Если существует несколько классов контекста, этот параметр является обязательным. |
EFStartupProject | Относительный путь к запуску проекта. Значение по умолчанию — текущая папка. |
EFTargetNamespace | Пространство имен, используемое для всех созданных классов. По умолчанию создается из корневого пространства имен и выходного каталога, а также скомпилированныхModels. |
В нашем примере необходимо указать проект запуска:
<PropertyGroup>
<EFOptimizeContext>true</EFOptimizeContext>
<EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>
Теперь, если мы создадим проект, мы видим ведение журнала во время сборки, указывающее, что скомпилированная модель строится:
Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
--additionalprobingpath G:\packages
--additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages"
--runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\
--namespace NewInEfCore9
--suffix .g
--assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.dll
--project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model
--root-namespace NewInEfCore9
--language C#
--nullable
--working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App
--verbose
--no-color
--prefix-output
При запуске приложения показано, что скомпилированная модель обнаружена, поэтому модель не создается снова:
Starting application...
Model loaded with 2 entity types.
Теперь, когда модель изменяется, скомпилированная модель будет автоматически перестроена сразу после создания проекта.
Примечание.
Мы работаем над некоторыми проблемами производительности с изменениями, внесенными в скомпилированную модель в EF8 и EF9. Дополнительные сведения см . в статье о проблеме 33483# .
Примитивные коллекции только для чтения
Совет
Код, показанный здесь, поступает из PrimitiveCollectionsSample.cs.
EF8 представила поддержку сопоставлений массивов и изменяемых списков примитивных типов. Это было развернуто в EF9, чтобы включить коллекции и списки только для чтения. В частности, EF9 поддерживает коллекции, типизированные как IReadOnlyList
, IReadOnlyCollection
или ReadOnlyCollection
. Например, в следующем коде DaysVisited
будет сопоставляться с соглашением как примитивная коллекция дат:
public class DogWalk
{
public int Id { get; set; }
public string Name { get; set; }
public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}
Коллекция только для чтения может быть поддерживается обычной, изменяемой коллекцией при необходимости. Например, в следующем коде DaysVisited
можно сопоставить как примитивную коллекцию дат, позволяя коду в классе управлять базовым списком.
public class Pub
{
public int Id { get; set; }
public string Name { get; set; }
public IReadOnlyCollection<string> Beers { get; set; }
private List<DateOnly> _daysVisited = new();
public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
}
Затем эти коллекции можно использовать в запросах обычным образом. Например, следующий LINQ-запрос:
var walksWithADrink = await context.Walks.Select(
w => new
{
WalkName = w.Name,
PubName = w.ClosestPub.Name,
Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
TotalCount = w.DaysVisited.Count
}).ToListAsync();
Который преобразуется в следующий SQL в SQLite:
SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
SELECT COUNT(*)
FROM json_each("w"."DaysVisited") AS "d"
WHERE "d"."value" IN (
SELECT "d0"."value"
FROM json_each("p"."DaysVisited") AS "d0"
)) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"
Указание коэффициента заполнения для ключей и индексов
Совет
Код, показанный здесь, поступает из ModelBuildingSample.cs.
EF9 поддерживает спецификацию коэффициента заполнения SQL Server при использовании миграций EF Core для создания ключей и индексов. Из документации SQL Server "При создании или перестроении индекса значение коэффициента заполнения определяет процент пространства на каждой странице конечного уровня, который будет заполнен данными, резервируя оставшуюся часть на каждой странице в качестве свободного места для будущего роста".
Коэффициент заполнения можно задать для одного или составного первичного и альтернативного ключей и индексов. Например:
modelBuilder.Entity<User>()
.HasKey(e => e.Id)
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasAlternateKey(e => new { e.Region, e.Ssn })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Name })
.HasFillFactor(80);
modelBuilder.Entity<User>()
.HasIndex(e => new { e.Region, e.Tag })
.HasFillFactor(80);
При применении к существующим таблицам это приведет к изменению коэффициента заполнения на ограничение:
ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];
ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);
Это улучшение было внесено @deano-охотником. Спасибо!
Сделать существующие соглашения по созданию моделей более расширяемыми
Совет
Код, показанный здесь, поступает из CustomConventionsSample.cs.
В EF7 были представлены соглашения о создании общедоступных моделей для приложений. В EF9 мы облегчили расширение некоторых существующих соглашений. Например, код для сопоставления свойств по атрибуту в EF7 :
public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: base(dependencies)
{
}
public override void ProcessEntityTypeAdded(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionContext<IConventionEntityTypeBuilder> context)
=> Process(entityTypeBuilder);
public override void ProcessEntityTypeBaseTypeChanged(
IConventionEntityTypeBuilder entityTypeBuilder,
IConventionEntityType? newBaseType,
IConventionEntityType? oldBaseType,
IConventionContext<IConventionEntityType> context)
{
if ((newBaseType == null
|| oldBaseType != null)
&& entityTypeBuilder.Metadata.BaseType == newBaseType)
{
Process(entityTypeBuilder);
}
}
private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
{
foreach (var memberInfo in GetRuntimeMembers())
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
entityTypeBuilder.Property(memberInfo);
}
else if (memberInfo is PropertyInfo propertyInfo
&& Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
{
entityTypeBuilder.Ignore(propertyInfo.Name);
}
}
IEnumerable<MemberInfo> GetRuntimeMembers()
{
var clrType = entityTypeBuilder.Metadata.ClrType;
foreach (var property in clrType.GetRuntimeProperties()
.Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
{
yield return property;
}
foreach (var property in clrType.GetRuntimeFields())
{
yield return property;
}
}
}
}
В EF9 это можно упростить до следующих:
public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
: PropertyDiscoveryConvention(dependencies)
{
protected override bool IsCandidatePrimitiveProperty(
MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
{
if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
{
if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
{
return true;
}
structuralType.Builder.Ignore(memberInfo.Name);
}
mapping = null;
return false;
}
}
Обновление ApplyConfigurationsFromAssembly для вызова недоступных конструкторов
В предыдущих версиях EF Core ApplyConfigurationsFromAssembly
метод создает только экземпляры типов конфигурации с открытыми конструкторами без параметров. В EF9 мы улучшили сообщения об ошибках, созданные при сбое, а также включили создание экземпляров неогласованным конструктором. Это полезно при совместном поиске конфигурации в частном вложенном классе, который никогда не должен быть создан кодом приложения. Например:
public class Country
{
public int Code { get; set; }
public required string Name { get; set; }
private class FooConfiguration : IEntityTypeConfiguration<Country>
{
private FooConfiguration()
{
}
public void Configure(EntityTypeBuilder<Country> builder)
{
builder.HasKey(e => e.Code);
}
}
}
В стороне, некоторые люди думают, что этот шаблон является боминацией, потому что он связывает тип сущности с конфигурацией. Другие люди считают, что это очень полезно, так как он совместно находит конфигурацию с типом сущности. Давайте не обсудим это здесь. :-)
Идентификатор иерархии SQL Server
Совет
Код, показанный здесь, поступает из HierarchyIdSample.cs.
Создание пути Sugar для HierarchyId
Добавлена поддержка первого класса для типа SQL Server HierarchyId
в EF8. В EF9 добавлен метод сахара, чтобы упростить создание дочерних узлов в структуре дерева. Например, следующий код запрашивает существующую сущность со свойством HierarchyId
:
var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");
Это HierarchyId
свойство можно использовать для создания дочерних узлов без явной обработки строк. Например:
var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");
Если daisy
имеет /4/1/3/1/
HierarchyId
значение , то child1
получит HierarchyId
"/4/1/3/1/1/" и child2
получит HierarchyId
"/4/1/1/1/2/".
Чтобы создать узел между этими двумя дочерними элементами, можно использовать дополнительный подуровневый элемент. Например:
var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");
При этом создается узел с элементом HierarchyId
/4/1/3/1/1.5/
, помещающим его между child1
и child2
.
Это улучшение было внесено @Rezakazemi890. Спасибо!
Средства
Меньше перестроек
Средство dotnet ef
командной строки по умолчанию создает проект перед выполнением средства. Это связано с тем, что не перестроение перед запуском средства является общим источником путаницы, когда вещи не работают. Опытные разработчики могут использовать --no-build
этот вариант, чтобы избежать этой сборки, которая может быть медленной. Однако даже этот --no-build
параметр может привести к повторному построению проекта при следующем построении за пределами средств EF.
Мы считаем, что вклад сообщества из @Suchiman исправил это. Тем не менее, мы также осознаем, что изменения вокруг поведения MSBuild имеют тенденцию иметь непредвиденные последствия, поэтому мы просим людей, как вы, чтобы попробовать это и сообщить о любых негативных опытах у вас есть.