Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
EF Core 6.0 выпущен на платформе NuGet. Эта страница содержит обзор интересных изменений, представленных в этом выпуске.
Подсказка
Вы можете запустить и выполнить отладку в приведенных ниже примерах, скачав пример кода с GitHub.
Темпоральные таблицы SQL Server
Проблема GitHub: #4693.
Темпоральные таблицы SQL Server автоматически отслеживают все данные, которые когда-либо хранятся в таблице, даже после обновления или удаления этих данных. Это достигается путем создания параллельной таблицы истории, в которую сохраняются исторические данные с отметками времени при каждом изменении основной таблицы. Это позволяет запрашивать исторические данные, такие как аудит или восстановление, например восстановление после случайной мутации или удаления.
EF Core теперь поддерживает следующее:
- Создание темпоральных таблиц с помощью миграций
- Преобразование существующих таблиц в темпоральные таблицы снова с помощью миграций
- Запрос исторических данных
- Восстановление данных из некоторой точки в прошлом
Настройка темпоральной таблицы
Построитель моделей может быть использован для настройки таблицы как временной. Рассмотрим пример.
modelBuilder
.Entity<Employee>()
.ToTable("Employees", b => b.IsTemporal());
При использовании EF Core для создания базы данных новая таблица будет настроена как временная таблица с параметрами SQL Server по умолчанию для меток времени и таблицы истории. Например, рассмотрим тип сущности Employee
:
public class Employee
{
public Guid EmployeeId { get; set; }
public string Name { get; set; }
public string Position { get; set; }
public string Department { get; set; }
public string Address { get; set; }
public decimal AnnualSalary { get; set; }
}
Созданная темпоральная таблица будет выглядеть следующим образом:
DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
[EmployeeId] uniqueidentifier NOT NULL,
[Name] nvarchar(100) NULL,
[Position] nvarchar(100) NULL,
[Department] nvarchar(100) NULL,
[Address] nvarchar(1024) NULL,
[AnnualSalary] decimal(10,2) NOT NULL,
[PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
[PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');
Обратите внимание, что SQL Server создает два скрытых столбца datetime2
, которые называются PeriodEnd
и PeriodStart
. Эти "столбцы периода" представляют диапазон времени, в течение которого существовали данные в строке. Эти столбцы сопоставляются с теневыми свойствами в модели EF Core, что позволяет использовать их в запросах, как показано ниже.
Это важно
Время в этих столбцах всегда совпадает с временем UTC, созданным SQL Server. Время UTC используется для всех операций с темпоральными таблицами, например в запросах, приведенных ниже.
Обратите внимание, что связанная таблица истории, называемая EmployeeHistory
, создается автоматически. Имена столбцов периодов и таблицы истории можно изменить с помощью дополнительной настройки в построителе моделей. Рассмотрим пример.
modelBuilder
.Entity<Employee>()
.ToTable(
"Employees",
b => b.IsTemporal(
b =>
{
b.HasPeriodStart("ValidFrom");
b.HasPeriodEnd("ValidTo");
b.UseHistoryTable("EmployeeHistoricalData");
}));
Это отражается в таблице, созданной SQL Server:
DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
[EmployeeId] uniqueidentifier NOT NULL,
[Name] nvarchar(100) NULL,
[Position] nvarchar(100) NULL,
[Department] nvarchar(100) NULL,
[Address] nvarchar(1024) NULL,
[AnnualSalary] decimal(10,2) NOT NULL,
[ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
[ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');
Использование темпоральных таблиц
В большинстве случаев темпоральные таблицы используются так же, как и любая другая таблица. То есть SQL Server прозрачно обрабатывает столбцы периода и исторические данные таким образом, что приложение может игнорировать их. Например, новые сущности можно сохранить в базе данных обычным образом:
context.AddRange(
new Employee
{
Name = "Pinky Pie",
Address = "Sugarcube Corner, Ponyville, Equestria",
Department = "DevDiv",
Position = "Party Organizer",
AnnualSalary = 100.0m
},
new Employee
{
Name = "Rainbow Dash",
Address = "Cloudominium, Ponyville, Equestria",
Department = "DevDiv",
Position = "Ponyville weather patrol",
AnnualSalary = 900.0m
},
new Employee
{
Name = "Fluttershy",
Address = "Everfree Forest, Equestria",
Department = "DevDiv",
Position = "Animal caretaker",
AnnualSalary = 30.0m
});
await context.SaveChangesAsync();
Затем эти данные можно запрашивать, обновлять и удалять обычным образом. Рассмотрим пример.
var employee = await context.Employees.SingleAsync(e => e.Name == "Rainbow Dash");
context.Remove(employee);
await context.SaveChangesAsync();
Кроме того, после обычного запроса отслеживания значения из столбцов периодов текущих данных можно получить из отслеживаемых сущностей. Рассмотрим пример.
var employees = await context.Employees.ToListAsync();
foreach (var employee in employees)
{
var employeeEntry = context.Entry(employee);
var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;
Console.WriteLine($" Employee {employee.Name} valid from {validFrom} to {validTo}");
}
Это печатается:
Starting data:
Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
Обратите внимание, что ValidTo
столбец (по умолчанию называется PeriodEnd
) содержит максимальное datetime2
значение. Это всегда относится к текущим строкам в таблице. Столбцы ValidFrom
(по умолчанию называются PeriodStart
) содержат время в формате UTC, в которое была вставлена строка.
Запрос исторических данных
EF Core поддерживает запросы, которые включают исторические данные через несколько новых операторов запросов:
-
TemporalAsOf
: возвращает строки, активные (текущие) в заданное время в формате UTC. Это одна строка из текущей таблицы или таблицы журнала для заданного первичного ключа. -
TemporalAll
: возвращает все строки в исторических данных. Это обычно много строк из таблицы истории и (или) текущей таблицы для определенного первичного ключа. -
TemporalFromTo
: возвращает все строки, активные между двумя данными временными отметками UTC. Это может быть множество строк из таблицы истории и/или текущей таблицы для заданного первичного ключа. -
TemporalBetween
: то же самое, что иTemporalFromTo
, за исключением того, что включены строки, которые стали активными на верхней границе. -
TemporalContainedIn
: возвращает все строки, которые начали быть активными и перестали быть таковыми между двумя заданными моментами времени UTC. Это может быть множество строк из таблицы истории и/или текущей таблицы для заданного первичного ключа.
Замечание
Дополнительные сведения о том, какие строки включены для каждого из этих операторов, см. в документации по темпоральным таблицам SQL Server .
Например, после внесения некоторых обновлений и удаления данных мы можем выполнить запрос, используя TemporalAll
для просмотра исторических данных:
var history = await context
.Employees
.TemporalAll()
.Where(e => e.Name == "Rainbow Dash")
.OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
.Select(
e => new
{
Employee = e,
ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
ValidTo = EF.Property<DateTime>(e, "ValidTo")
})
.ToListAsync();
foreach (var pointInTime in history)
{
Console.WriteLine(
$" Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}
Обратите внимание, как метод EF.Property можно использовать для доступа к значениям из столбцов периода. Это используется в OrderBy
предложении для сортировки данных, а затем в проекции для включения этих значений в возвращаемые данные.
Этот запрос возвращает следующие данные:
Historical data for Rainbow Dash:
Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
Обратите внимание, что последняя строка, возвращенная, перестала быть активной в 8.26.2021 4:44:59 вечера. Это связано с тем, что строка для Рэйнбоу Дэш была удалена из главной таблицы в тот момент. Далее мы увидим, как эти данные можно восстановить.
Аналогичные запросы можно записать с помощью TemporalFromTo
, TemporalBetween
или TemporalContainedIn
. Рассмотрим пример.
var history = await context
.Employees
.TemporalBetween(timeStamp2, timeStamp3)
.Where(e => e.Name == "Rainbow Dash")
.OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
.Select(
e => new
{
Employee = e,
ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
ValidTo = EF.Property<DateTime>(e, "ValidTo")
})
.ToListAsync();
Этот запрос возвращает следующие строки:
Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
Восстановление исторических данных
Как упоминалось выше, Rainbow Dash была удалена из Employees
в таблице. Это была явно ошибка, поэтому давайте вернемся к точке времени и восстановим недостающую строку с того момента.
var employee = await context
.Employees
.TemporalAsOf(timeStamp2)
.SingleAsync(e => e.Name == "Rainbow Dash");
context.Add(employee);
await context.SaveChangesAsync();
Этот запрос возвращает одну строку для Rainbow Dash, так как она была в заданное время в формате UTC. Все запросы, использующие темпоральные операторы, по умолчанию не отслеживаются, поэтому возвращаемая сущность здесь не отслеживается. Это имеет смысл, так как он в настоящее время не существует в главной таблице. Чтобы повторно вставить сущность в основную таблицу, мы просто помечаем ее как Added
и вызываем SaveChanges
.
После повторной вставки строки «Rainbow Dash» запрос к историческим данным показывает, что строка была восстановлена в том виде, в котором она была на указанное время по UTC.
Historical data for Rainbow Dash:
Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM
Пакеты миграции
Проблема GitHub: #19693.
Миграции EF Core используются для создания обновлений схемы базы данных на основе изменений модели EF. Эти обновления схемы должны применяться во время развертывания приложения, часто в рамках системы непрерывной интеграции или непрерывного развертывания (C.I./C.D.).
EF Core теперь включает новый способ применения этих обновлений схемы: пакеты миграции. Пакет миграции — это небольшой исполняемый файл, содержащий миграции, и код, необходимый для применения этих миграций к базе данных.
Замечание
См. «Представляем удобные для DevOps пакеты миграции EF Core» в блоге .NET для более подробного обсуждения миграций, пакетов и развертывания.
Пакеты миграции создаются с помощью инструмента командной строки dotnet ef
. Прежде чем продолжить, убедитесь, что у вас установлена последняя версия программы.
Пакет должен включать в себя миграции. Они создаются с помощью dotnet ef migrations add
, как описано в документации по миграциям. После того как миграции будут готовы к развертыванию, создайте пакет с помощью dotnet ef migrations bundle
. Рассмотрим пример.
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>
Выходные данные представляют собой исполняемый файл, подходящий для целевой операционной системы. В этом случае это Windows x64, поэтому efbundle.exe
находится в локальной папке. При выполнении этого исполняемого файла применяются содержащиеся в нем миграции:
PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>
Миграции применяются к базе данных только тогда, когда они еще не были применены. Например, при повторном выполнении одного и того же пакета ничего не происходит, так как нет новых миграций для применения.
PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>
Однако если в модель были внесены изменения, и с помощью dotnet ef migrations add
созданы дополнительные миграции, они могут быть объединены в новый исполняемый файл и готовы к применению. Рассмотрим пример.
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>
Обратите внимание, что этот --force
параметр можно использовать для перезаписи существующего пакета новым.
При выполнении этого нового пакета к базе данных применяются следующие две новые миграции:
PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>
По умолчанию пакет использует строку подключения к базе данных из конфигурации приложения. Однако перенести другую базу данных можно путем передачи строки подключения в командной строке. Рассмотрим пример.
PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>
Обратите внимание, что на этот раз все три миграции были применены, так как ни одна из них еще не была применена к рабочей базе данных.
Другие параметры можно передать в командную строку. Ниже приведены некоторые распространенные параметры.
-
--output
, чтобы указать путь к создаваемому исполняемому файлу. -
--context
, чтобы указать тип DbContext, используемый, если проект содержит несколько типов контекста. -
--project
, чтобы указать используемый проект. По умолчанию используется текущий рабочий каталог. -
--startup-project
, чтобы указать используемый проект запуска. По умолчанию используется текущий рабочий каталог. -
--no-build
Чтобы предотвратить сборку проекта перед выполнением команды. Это следует использовать только в том случае, если проект, как известно, up-to-date. -
--verbose
чтобы просмотреть подробные сведения о том, что выполняет команда. Используйте этот параметр при включении сведений в отчеты об ошибках.
Используйте dotnet ef migrations bundle --help
для просмотра всех доступных параметров.
Обратите внимание, что по умолчанию каждая миграция применяется в своей собственной транзакции. См. вопрос GitHub 22616 для обсуждения возможных будущих улучшений в этой области.
Конфигурация модели предварительного соглашения
Проблема GitHub: #12229.
Предыдущие версии EF Core требуют, чтобы сопоставление для каждого свойства заданного типа было явно настроено, если это сопоставление отличается от значения по умолчанию. Сюда входят аспекты, такие как максимальная длина строк и десятичная точность, а также преобразование значений для типа свойства.
Это необходимо либо:
- Конфигурация построителя моделей для каждого свойства
- Атрибут сопоставления для каждого свойства
- Явная итерация всех свойств всех типов сущностей и использование API метаданных низкого уровня при создании модели.
Обратите внимание, что явная итерация подвержена ошибкам и сложно осуществить надёжно, так как список типов сущностей и сопоставленных свойств может не быть окончательным во время этой итерации.
EF Core 6.0 позволяет указать эту конфигурацию сопоставления один раз для заданного типа. Затем он будет применен ко всем свойствам этого типа в модели. Это называется "конфигурацией модели до применения соглашений", так как она настраивает аспекты модели, которые затем используются соглашениями построения модели. Такая конфигурация применяется путем переопределения ConfigureConventions
на вашей:DbContext
public class SomeDbContext : DbContext
{
protected override void ConfigureConventions(
ModelConfigurationBuilder configurationBuilder)
{
// Pre-convention model configuration goes here
}
}
Например, рассмотрим следующие типы сущностей:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public Money AccountValue { get; set; }
public Session CurrentSession { get; set; }
public ICollection<Order> Orders { get; } = new List<Order>();
}
public class Order
{
public int Id { get; set; }
public string SpecialInstructions { get; set; }
public DateTime OrderDate { get; set; }
public bool IsComplete { get; set; }
public Money Price { get; set; }
public Money? Discount { get; set; }
public Customer Customer { get; set; }
}
Все строковые свойства можно настроить как ANSI (вместо Юникода) и иметь максимальную длину 1024:
configurationBuilder
.Properties<string>()
.AreUnicode(false)
.HaveMaxLength(1024);
Все свойства DateTime можно преобразовать в 64-разрядные целые числа в базе данных, используя преобразование по умолчанию из DateTimes в longs:
configurationBuilder
.Properties<DateTime>()
.HaveConversion<long>();
Все логические свойства можно преобразовать в целые числа 0
или 1
с использованием одного из встроенных преобразователей значений:
configurationBuilder
.Properties<bool>()
.HaveConversion<BoolToZeroOneConverter<int>>();
Предположим Session
, что это временное свойство сущности и не должно быть сохранено, его можно игнорировать везде в модели:
configurationBuilder
.IgnoreAny<Session>();
Конфигурация модели предварительного соглашения очень полезна при работе с объектами значений. Например, тип Money
в приведенной выше модели представлен структурой только для чтения:
public readonly struct Money
{
[JsonConstructor]
public Money(decimal amount, Currency currency)
{
Amount = amount;
Currency = currency;
}
public override string ToString()
=> (Currency == Currency.UsDollars ? "$" : "£") + Amount;
public decimal Amount { get; }
public Currency Currency { get; }
}
public enum Currency
{
UsDollars,
PoundsSterling
}
Затем сериализуется в JSON и из нее с помощью пользовательского преобразователя значений:
public class MoneyConverter : ValueConverter<Money, string>
{
public MoneyConverter()
: base(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
{
}
}
Этот преобразователь значений можно настроить один раз для всех использований Money:
configurationBuilder
.Properties<Money>()
.HaveConversion<MoneyConverter>()
.HaveMaxLength(64);
Обратите внимание, что дополнительные аспекты можно указать для строкового столбца, в котором хранится сериализованный JSON. В этом случае столбец ограничен максимальной длиной 64.
Таблицы, созданные для SQL Server с помощью миграций, показывают, как конфигурация была применена ко всем сопоставленным столбцам:
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] varchar(1024) NULL,
[IsActive] int NOT NULL,
[AccountValue] nvarchar(64) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
[Id] int NOT NULL IDENTITY,
[SpecialInstructions] varchar(1024) NULL,
[OrderDate] bigint NOT NULL,
[IsComplete] int NOT NULL,
[Price] nvarchar(64) NOT NULL,
[Discount] nvarchar(64) NULL,
[CustomerId] int NULL,
CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);
Также можно указать сопоставление типов по умолчанию для заданного типа. Рассмотрим пример.
configurationBuilder
.DefaultTypeMapping<string>()
.IsUnicode(false);
Это редко требуется, но может быть полезно, если тип используется в запросе таким образом, который не связан с любым сопоставленным свойством модели.
Замечание
Дополнительные сведения и примеры по конфигурации моделей с использованием соглашений см. в блоге .NET в статье "Объявление о выпуске Entity Framework Core 6.0 Preview 6: Настройка соглашений".
Скомпилированные модели
Проблема GitHub: #1906.
Скомпилированные модели могут улучшить время запуска EF Core для приложений с большими моделями. Большая модель обычно означает 100-х до 1000-х типов сущностей и связей.
Время запуска означает время выполнения первой операции в DbContext, когда этот тип DbContext используется впервые в приложении. Обратите внимание, что только создание экземпляра DbContext не приводит к инициализации модели EF. Стандартные первые операции, которые приводят к инициализации модели, включают вызов DbContext.Add
или выполнение первого запроса.
Скомпилированные модели создаются с помощью программы командной строки dotnet ef
. Прежде чем продолжить, убедитесь, что у вас установлена последняя версия программы.
Для создания скомпилированной модели используется новая команда dbcontext optimize
. Рассмотрим пример.
dotnet ef dbcontext optimize
Параметры --output-dir
и --namespace
можно использовать для указания каталога и пространства имен, в которых будет создаваться скомпилированная модель. Рассмотрим пример.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
Выходные данные выполнения этой команды включают фрагмент кода для копирования и вставки в конфигурацию DbContext, чтобы EF Core использовала скомпилированную модель. Рассмотрим пример.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(MyCompiledModels.BlogsContextModel.Instance)
.UseSqlite(@"Data Source=test.db");
Начальная загрузка скомпилированной модели
Обычно нет необходимости проверять созданный код начальной загрузки. Однако иногда может быть полезно настроить модель или ее загрузку. Код начальной загрузки выглядит примерно так:
[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
private static BlogsContextModel _instance;
public static IModel Instance
{
get
{
if (_instance == null)
{
_instance = new BlogsContextModel();
_instance.Initialize();
_instance.Customize();
}
return _instance;
}
}
partial void Initialize();
partial void Customize();
}
Это разделяемый класс с разделяемыми методами, которые можно реализовать для настройки модели по мере необходимости.
Кроме того, для типов DbContext можно создать несколько скомпилированных моделей, которые могут использовать разные модели в зависимости от определенной конфигурации среды выполнения. Их следует поместить в разные папки и пространства имен, как показано выше. Сведения о среде выполнения, такие как строка подключения, можно проверить, а необходимая модель возвращается по мере необходимости. Рассмотрим пример.
public static class RuntimeModelCache
{
private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
= new();
public static IModel GetOrCreateModel(string connectionString)
=> _runtimeModels.GetOrAdd(
connectionString, cs =>
{
if (cs.Contains("X"))
{
return BlogsContextModel1.Instance;
}
if (cs.Contains("Y"))
{
return BlogsContextModel2.Instance;
}
throw new InvalidOperationException("No appropriate compiled model found.");
});
}
Ограничения
У скомпилированных моделей есть некоторые ограничения:
- Глобальные фильтры запросов не поддерживаются.
- Отложенная загрузка и прокси-серверы отслеживания изменений не поддерживаются.
- Пользовательские реализации IModelCacheKeyFactory не поддерживаются. Однако можно скомпилировать несколько моделей и при необходимости загрузить соответствующую.
- Модель необходимо синхронизировать вручную, повторно создавая ее при каждом изменении определения модели или конфигурации.
В связи с этими ограничениями следует использовать только скомпилированные модели, если запуск EF Core выполняется слишком медленно. Компиляция небольших моделей, как правило, не стоит того.
Если поддержка какой-либо из этих функций имеет решающее значение для вашего успеха, проголосуйте за соответствующие проблемы, указанные выше.
Контрольные показатели
Подсказка
Вы можете попробовать скомпилировать большую модель и запустить на нем тест, скачав пример кода из GitHub.
Модель в репозитории GitHub, на который ссылается выше, содержит 449 типов сущностей, 6390 свойств и 720 связей. Это умеренно большая модель. С помощью BenchmarkDotNet для измерения среднее время до первого запроса составляет 1,02 секунды на достаточно мощном ноутбуке. Использование скомпилированных моделей приводит к 117 миллисекундам на одном оборудовании. Улучшение в 8-10 раз, подобное этому, остается относительно постоянным по мере увеличения размеров модели.
Замечание
См. Announcing Entity Framework Core 6.0 Preview 5: Compiled Models в блоге .NET для более подробного обсуждения производительности запуска EF Core и скомпилированных моделей.
Улучшенная производительность в TechEmpower Fortunes
Проблема GitHub: #23611.
Мы улучшили производительность запросов для EF Core 6.0. Конкретно:
- Производительность EF Core 6.0 теперь составляет 70% быстрее на стандартном эталоне TechEmpower Fortunes по сравнению с 5,0.
- Это улучшение производительности для всего стека, включая улучшения в коде тестирования производительности, платформе .NET и т. д.
- EF Core 6.0 сам по себе выполняет неотслеживаемые запросы на 31% быстрее.
- Выделение памяти в куче уменьшилось на 43% при выполнении запросов.
После этих улучшений разрыв между популярным "микро-ORM" Dapper и EF Core в бенчмарке TechEmpower Fortunes уменьшился с 55% до чуть менее 5%.
Замечание
В блоге .NET см. публикацию "Entity Framework Core 6.0 Preview 4: Performance Edition" для подробного обсуждения улучшений производительности запросов в EF Core 6.0.
Улучшения поставщика Azure Cosmos DB
EF Core 6.0 содержит множество улучшений поставщика базы данных Azure Cosmos DB.
Подсказка
Вы можете запускать и отлаживать все специализированные примеры для Cosmos, скачав исходный код с GitHub.
По умолчанию — неявное владение
Проблема GitHub: #24803.
При создании модели для поставщика Azure Cosmos DB EF Core 6.0 помечает типы дочерних сущностей как принадлежащие родительской сущности по умолчанию. Это устраняет необходимость в значительном количестве вызовов OwnsMany
и OwnsOne
в модели Azure Cosmos DB. Это упрощает встроение дочерних типов в документ для родительского типа, который обычно является наиболее подходящим способом моделирования отношений между родителями и детьми в документной базе данных.
Например, рассмотрим следующие типы сущностей:
public class Family
{
[JsonPropertyName("id")]
public string Id { get; set; }
public string LastName { get; set; }
public bool IsRegistered { get; set; }
public Address Address { get; set; }
public IList<Parent> Parents { get; } = new List<Parent>();
public IList<Child> Children { get; } = new List<Child>();
}
public class Parent
{
public string FamilyName { get; set; }
public string FirstName { get; set; }
}
public class Child
{
public string FamilyName { get; set; }
public string FirstName { get; set; }
public int Grade { get; set; }
public string Gender { get; set; }
public IList<Pet> Pets { get; } = new List<Pet>();
}
В EF Core 5.0 эти типы моделировались бы для Azure Cosmos DB со следующей конфигурацией:
modelBuilder.Entity<Family>()
.HasPartitionKey(e => e.LastName)
.OwnsMany(f => f.Parents);
modelBuilder.Entity<Family>()
.OwnsMany(f => f.Children)
.OwnsMany(c => c.Pets);
modelBuilder.Entity<Family>()
.OwnsOne(f => f.Address);
В EF Core 6.0 владение неявно, уменьшая конфигурацию модели до следующих:
modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);
Полученные документы Azure Cosmos DB содержат родителей семьи, детей, домашних животных и адрес, включенные в семейный документ. Рассмотрим пример.
{
"Id": "Wakefield.7",
"LastName": "Wakefield",
"Discriminator": "Family",
"IsRegistered": true,
"id": "Family|Wakefield.7",
"Address": {
"City": "NY",
"County": "Manhattan",
"State": "NY"
},
"Children": [
{
"FamilyName": "Merriam",
"FirstName": "Jesse",
"Gender": "female",
"Grade": 8,
"Pets": [
{
"GivenName": "Goofy"
},
{
"GivenName": "Shadow"
}
]
},
{
"FamilyName": "Miller",
"FirstName": "Lisa",
"Gender": "female",
"Grade": 1,
"Pets": []
}
],
"Parents": [
{
"FamilyName": "Wakefield",
"FirstName": "Robin"
},
{
"FamilyName": "Miller",
"FirstName": "Ben"
}
],
"_rid": "x918AKh6p20CAAAAAAAAAA==",
"_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
"_attachments": "attachments/",
"_ts": 1632121802
}
Замечание
Важно помнить, что OwnsOne
/OwnsMany
конфигурацию необходимо использовать, если необходимо дополнительно настроить эти собственные типы.
Коллекции примитивных типов данных
Проблема GitHub: #14762.
EF Core 6.0 изначально сопоставляет коллекции примитивных типов при использовании поставщика базы данных Azure Cosmos DB. Например, рассмотрим этот тип сущности:
public class Book
{
public Guid Id { get; set; }
public string Title { get; set; }
public IList<string> Quotes { get; set; }
public IDictionary<string, string> Notes { get; set; }
}
Список и словарь можно заполнить и вставить в базу данных обычным образом:
using var context = new BooksContext();
var book = new Book
{
Title = "How It Works: Incredible History",
Quotes = new List<string>
{
"Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
"Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
"For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
},
Notes = new Dictionary<string, string>
{
{ "121", "Fridges" },
{ "144", "Peter Higgs" },
{ "48", "Saint Mark's Basilica" },
{ "36", "The Terracotta Army" }
}
};
context.Add(book);
await context.SaveChangesAsync();
Это приводит к следующему документу JSON:
{
"Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
"Discriminator": "Book",
"Notes": {
"36": "The Terracotta Army",
"48": "Saint Mark's Basilica",
"121": "Fridges",
"144": "Peter Higgs"
},
"Quotes": [
"Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
"Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
"For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
],
"Title": "How It Works: Incredible History",
"id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
"_rid": "t-E3AIxaencBAAAAAAAAAA==",
"_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
"_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
"_attachments": "attachments/",
"_ts": 1630075016
}
Затем эти коллекции можно обновить, снова в обычном режиме:
book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";
await context.SaveChangesAsync();
Ограничения:
- Поддерживаются только словари с ключами типа строка
- Запрос к содержимому примитивных коллекций в настоящее время не поддерживается. Проголосуйте за #16926, #25700 и #25701, если эти функции важны для вас.
Переводы для встроенных функций
Проблема GitHub: #16143.
Теперь поставщик Azure Cosmos DB преобразует больше методов библиотеки базовых классов (BCL) в встроенные функции Azure Cosmos DB. В следующих таблицах показаны переводы, новые в EF Core 6.0.
Строковые переводы
Метод BCL | Встроенная функция | Примечания. |
---|---|---|
String.Length |
LENGTH |
|
String.ToLower |
LOWER |
|
String.TrimStart |
LTRIM |
|
String.TrimEnd |
RTRIM |
|
String.Trim |
TRIM |
|
String.ToUpper |
UPPER |
|
String.Substring |
SUBSTRING |
|
Оператор + |
CONCAT |
|
String.IndexOf |
INDEX_OF |
|
String.Replace |
REPLACE |
|
String.Equals |
STRINGEQUALS |
Только вызовы без учета регистра |
Переводы для LOWER
, LTRIM
, RTRIM
, TRIM
, UPPER
и SUBSTRING
были внесены @Marusyk. Спасибо!
Пример:
var stringResults = await context.Triangles.Where(
e => e.Name.Length > 4
&& e.Name.Trim().ToLower() != "obtuse"
&& e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
.ToListAsync();
Что преобразуется в:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))
Математические переводы
Метод BCL | Встроенная функция |
---|---|
Math.Abs или MathF.Abs |
ABS |
Math.Acos или MathF.Acos |
ACOS |
Math.Asin или MathF.Asin |
ASIN |
Math.Atan или MathF.Atan |
ATAN |
Math.Atan2 или MathF.Atan2 |
ATN2 |
Math.Ceiling или MathF.Ceiling |
CEILING |
Math.Cos или MathF.Cos |
COS |
Math.Exp или MathF.Exp |
EXP |
Math.Floor или MathF.Floor |
FLOOR |
Math.Log или MathF.Log |
LOG |
Math.Log10 или MathF.Log10 |
LOG10 |
Math.Pow или MathF.Pow |
POWER |
Math.Round или MathF.Round |
ROUND |
Math.Sign или MathF.Sign |
SIGN |
Math.Sin или MathF.Sin |
SIN |
Math.Sqrt или MathF.Sqrt |
SQRT |
Math.Tan или MathF.Tan |
TAN |
Math.Truncate или MathF.Truncate |
TRUNC |
DbFunctions.Random |
RAND |
Эти переводы были внесли @Marusyk. Спасибо!
Пример:
var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
e => (Math.Round(e.Angle1) == 90.0
|| Math.Round(e.Angle2) == 90.0)
&& (hypotenuse * Math.Sin(e.Angle1) > 30.0
|| hypotenuse * Math.Cos(e.Angle2) > 30.0))
.ToListAsync();
Что преобразуется в:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))
Переводы даты и времени
Метод BCL | Встроенная функция |
---|---|
DateTime.UtcNow |
GetCurrentDateTime |
Эти переводы были внесли @Marusyk. Спасибо!
Пример:
var timeResults = await context.Triangles.Where(
e => e.InsertedOn <= DateTime.UtcNow)
.ToListAsync();
Что преобразуется в:
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))
Необработанные запросы SQL с помощью FromSql
Проблема GitHub: #17311.
Иногда необходимо выполнить необработанный SQL-запрос вместо использования LINQ. Теперь это поддерживается поставщиком Azure Cosmos DB с помощью FromSql
метода. Это работает так же, как всегда было сделано с реляционными поставщиками. Рассмотрим пример.
var maxAngle = 60;
var results = await context.Triangles.FromSqlRaw(
@"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}", maxAngle)
.ToListAsync();
Выполняется следующим образом:
SELECT c
FROM (
SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
) c
Отдельные запросы
Проблема GitHub: #16144.
Простой запрос с использованием Distinct
теперь переводится. Рассмотрим пример.
var distinctResults = await context.Triangles
.Select(e => e.Angle1).OrderBy(e => e).Distinct()
.ToListAsync();
Что преобразуется в:
SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]
Диагностика
Проблема GitHub: #17298.
Поставщик Azure Cosmos DB теперь записывает дополнительные диагностические сведения, включая события для вставки, запроса, обновления и удаления данных из базы данных. Единицы запроса (ЕЗ) включаются в эти события при необходимости.
Замечание
Здесь журналы используют EnableSensitiveDataLogging()
, чтобы отображались значения идентификаторов.
Вставка элемента в базу данных Azure Cosmos DB создает событие CosmosEventId.ExecutedCreateItem
. Например, этот код:
var triangle = new Triangle
{
Name = "Impossible",
PartitionKey = "TrianglesPartition",
Angle1 = 90,
Angle2 = 90,
InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
await context.SaveChangesAsync();
Регистрирует следующее диагностическое событие:
info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'
Получение элементов из базы данных Azure Cosmos DB с помощью запроса создает CosmosEventId.ExecutingSqlQuery
событие, а затем одно или несколько CosmosEventId.ExecutedReadNext
событий для чтения элементов. Например, этот код:
var equilateral = await context.Triangles.SingleAsync(e => e.Name == "Equilateral");
Регистрирует следующие диагностические события:
info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
OFFSET 0 LIMIT 2
Получение одного элемента из базы данных Azure Cosmos DB с Find
помощью ключа секции создает CosmosEventId.ExecutingReadItem
и CosmosEventId.ExecutedReadItem
события. Например, этот код:
var isosceles = await context.Triangles.FindAsync("Isosceles", "TrianglesPartition");
Регистрирует следующие диагностические события:
info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'
Сохранение обновленного элемента в базе данных Azure Cosmos DB создает CosmosEventId.ExecutedReplaceItem
событие. Например, этот код:
triangle.Angle2 = 89;
await context.SaveChangesAsync();
Регистрирует следующее диагностическое событие:
info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'
Удаление элемента из базы данных Azure Cosmos DB создает CosmosEventId.ExecutedDeleteItem
событие. Например, этот код:
context.Remove(triangle);
await context.SaveChangesAsync();
Регистрирует следующее диагностическое событие:
info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'
Настройка пропускной способности
Проблема GitHub: #17301.
Модель Azure Cosmos DB теперь можно настроить с помощью ручной или автоматической пропускной способности. Эти значения обеспечивают пропускную способность базы данных. Рассмотрим пример.
modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);
Кроме того, отдельные типы сущностей можно настроить для обеспечения пропускной способности соответствующего контейнера. Рассмотрим пример.
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasManualThroughput(5000);
entityTypeBuilder.HasAutoscaleThroughput(3000);
});
Настройка времени жизни
Проблема GitHub: #17307.
Типы сущностей в модели Azure Cosmos DB теперь можно настроить с использованием времени жизни по умолчанию и времени жизни для аналитического хранилища. Рассмотрим пример.
modelBuilder.Entity<Family>(
entityTypeBuilder =>
{
entityTypeBuilder.HasDefaultTimeToLive(100);
entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
});
Решение фабрики клиентов HTTP
Проблема GitHub: #21274. Эта функция была создана @dnperfors. Спасибо!
Используемый поставщиком Azure Cosmos DB HttpClientFactory
теперь можно задать явно. Это может быть особенно полезно во время тестирования, например для обхода проверки сертификатов при использовании эмулятора Azure Cosmos DB в Linux:
optionsBuilder
.EnableSensitiveDataLogging()
.UseCosmos(
"https://localhost:8081",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
"PrimitiveCollections",
cosmosOptionsBuilder =>
{
cosmosOptionsBuilder.HttpClientFactory(
() => new HttpClient(
new HttpClientHandler
{
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
}));
});
Замечание
См. статью в блоге .NET Taking the EF Core Azure Cosmos DB Provider for a Test Drive для подробного примера применения усовершенствований поставщика Azure Cosmos DB к существующему приложению.
Усовершенствования шаблонов из существующей базы данных
EF Core 6.0 содержит несколько улучшений при обратном проектировании модели EF из существующей базы данных.
Создание структур связей "многие ко многим"
Проблема GitHub: #22475.
EF Core 6.0 обнаруживает простые таблицы соединения и автоматически создает для них сопоставление "многие ко многим". Например, рассмотрим таблицы для Posts
и Tags
, а также таблицу PostTag
соединения, соединяющую их:
CREATE TABLE [Tags] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Description] nvarchar(max) NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Contents] nvarchar(max) NOT NULL,
[PostedOn] datetime2 NOT NULL,
[UpdatedOn] datetime2 NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));
CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] int NOT NULL,
CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);
Эти таблицы можно развернуть из командной строки. Рассмотрим пример.
dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer
В результате получается класс для Post.
public partial class Post
{
public Post()
{
Tags = new HashSet<Tag>();
}
public int Id { get; set; }
public string Title { get; set; } = null!;
public string Contents { get; set; } = null!;
public DateTime PostedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; } = null!;
public virtual ICollection<Tag> Tags { get; set; }
}
И класс для Tag:
public partial class Tag
{
public Tag()
{
Posts = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
Но для таблицы нет класса PostTag
. Вместо этого конфигурация для связи "многие ко многим" формируется следующим образом:
entity.HasMany(d => d.Tags)
.WithMany(p => p.Posts)
.UsingEntity<Dictionary<string, object>>(
"PostTag",
l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
j =>
{
j.HasKey("PostsId", "TagsId");
j.ToTable("PostTag");
j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
});
Ссылочные типы ссылок на C#, допускающие значение NULL
Проблема GitHub: #15520.
EF Core 6.0 теперь формирует шаблон модели EF и типы сущностей, использующие ссылочные типы C#, допускающие значение NULL (NRTs). Использование NRT автоматически формируется при включении поддержки NRT в проекте C#, в котором создается шаблон кода.
Например, в следующей Tags
таблице содержатся как строки с возможностью NULL, так и строки без этой возможности.
CREATE TABLE [Tags] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
[Description] nvarchar(max) NULL,
CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));
Это приводит к соответствующим свойствам строк, допускающих значение NULL и не допускающих значение NULL, в созданном классе:
public partial class Tag
{
public Tag()
{
Posts = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; } = null!;
public string? Description { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
Аналогичным образом, следующие Posts
таблицы содержат необходимую связь с таблицей Blogs
:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NOT NULL,
[Contents] nvarchar(max) NOT NULL,
[PostedOn] datetime2 NOT NULL,
[UpdatedOn] datetime2 NULL,
[BlogId] int NOT NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));
Это приводит к созданию структуры ненулевой (обязательной) связи между блогами.
public partial class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int Id { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Post> Posts { get; set; }
}
И публикации:
public partial class Post
{
public Post()
{
Tags = new HashSet<Tag>();
}
public int Id { get; set; }
public string Title { get; set; } = null!;
public string Contents { get; set; } = null!;
public DateTime PostedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; } = null!;
public virtual ICollection<Tag> Tags { get; set; }
}
Наконец, свойства DbSet в созданном DbContext создаются в понятном для NRT способе. Рассмотрим пример.
public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;
Примечания к базе данных переносятся в примечания кода
Проблема GitHub: #19113. Эта функция была представлена @ErikEJ. Спасибо!
Комментарии к таблицам и столбцам SQL теперь интегрируются в типы сущностей, создаваемые при обратного проектирования модели EF Core из существующей базы данных SQL Server.
/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
/// <summary>
/// The primary key.
/// </summary>
[Key]
public int Id { get; set; }
}
Усовершенствования запросов LINQ
EF Core 6.0 содержит несколько улучшений в переводе и выполнении запросов LINQ.
Улучшенная поддержка GroupBy
Проблемы с GitHub: #12088, #13805 и #22609.
EF Core 6.0 содержит более эффективную поддержку GroupBy
запросов. В частности, EF Core теперь:
- Перевод GroupBy, за которым следует
FirstOrDefault
(или аналогично) по группе - Поддерживает выбор лучших результатов N из группы
- Навигация расширяется после применения оператора
GroupBy
Ниже приведены примеры запросов из отчетов клиентов и их перевод на SQL Server.
Пример 1:
var people = await context.People
.Include(e => e.Shoes)
.GroupBy(e => e.FirstName)
.Select(
g => g.OrderBy(e => e.FirstName)
.ThenBy(e => e.LastName)
.FirstOrDefault())
.ToListAsync();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
SELECT [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
FROM (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]
Пример 2:
var group = await context.People
.Select(
p => new
{
p.FirstName,
FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
})
.GroupBy(p => p.FirstName)
.Select(g => g.First())
.FirstAsync();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
SELECT TOP(1) [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
FROM (
SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
Пример 3:
var people = await context.People
.Where(e => e.MiddleInitial == "Q" && e.Age == 20)
.GroupBy(e => e.LastName)
.Select(g => g.First().LastName)
.OrderBy(e => e.Length)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)
Пример 4:
var results = await (from person in context.People
join shoes in context.Shoes on person.Age equals shoes.Age
group shoes by shoes.Style
into people
select new
{
people.Key,
Style = people.Select(p => p.Style).FirstOrDefault(),
Count = people.Count()
})
.ToListAsync();
SELECT [s].[Style] AS [Key], (
SELECT TOP(1) [s0].[Style]
FROM [People] AS [p0]
INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]
Пример 5.
var results = await context.People
.GroupBy(e => e.FirstName)
.Select(g => g.First().LastName)
.OrderBy(e => e)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
SELECT TOP(1) [p1].[LastName]
FROM [People] AS [p1]
WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
Пример 6.
var results = await context.People
.Where(e => e.Age == 20)
.GroupBy(e => e.Id)
.Select(g => g.First().MiddleInitial)
.OrderBy(e => e)
.ToListAsync();
SELECT (
SELECT TOP(1) [p1].[MiddleInitial]
FROM [People] AS [p1]
WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
SELECT TOP(1) [p1].[MiddleInitial]
FROM [People] AS [p1]
WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
Пример 7.
var size = 11;
var results
= await context.People
.Where(
p => p.Feet.Size == size
&& p.MiddleInitial != null
&& p.Feet.Id != 1)
.GroupBy(
p => new
{
p.Feet.Size,
p.Feet.Person.LastName
})
.Select(
g => new
{
g.Key.LastName,
g.Key.Size,
Min = g.Min(p => p.Feet.Size),
})
.ToListAsync();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]
Пример 8.
var result = await context.People
.Include(x => x.Shoes)
.Include(x => x.Feet)
.GroupBy(
x => new
{
x.Feet.Id,
x.Feet.Size
})
.Select(
x => new
{
Key = x.Key.Id + x.Key.Size,
Count = x.Count(),
Sum = x.Sum(el => el.Id),
SumOver60 = x.Sum(el => el.Id) / (decimal)60,
TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
})
.CountAsync();
SELECT COUNT(*)
FROM (
SELECT [f].[Id], [f].[Size]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [f].[Id], [f].[Size]
) AS [t]
Пример 9.
var results = await context.People
.GroupBy(n => n.FirstName)
.Select(g => new
{
Feet = g.Key,
Total = g.Sum(n => n.Feet.Size)
})
.ToListAsync();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]
Пример 10.
var results = from Person person1
in from Person person2
in context.People
select person2
join Shoes shoes
in context.Shoes
on person1.Age equals shoes.Age
group shoes by
new
{
person1.Id,
shoes.Style,
shoes.Age
}
into temp
select
new
{
temp.Key.Id,
temp.Key.Age,
temp.Key.Style,
Values = from t
in temp
select
new
{
t.Id,
t.Style,
t.Age
}
};
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
SELECT [p].[Id], [s].[Age], [s].[Style]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
FROM [People] AS [p0]
INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]
Пример 11.
var grouping = await context.People
.GroupBy(i => i.LastName)
.Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
.OrderByDescending(e => e.LastName)
.ToListAsync();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
SELECT [p].[LastName], COUNT(*) AS [c]
FROM [People] AS [p]
GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
FROM (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
FROM [People] AS [p0]
) AS [t1]
WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
FROM (
SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
FROM [People] AS [p1]
) AS [t3]
WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]
Пример 12.
var grouping = await context.People
.Include(e => e.Shoes)
.OrderBy(e => e.FirstName)
.ThenBy(e => e.LastName)
.GroupBy(e => e.FirstName)
.Select(g => new { Name = g.Key, People = g.ToList()})
.ToListAsync();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
SELECT [p].[FirstName]
FROM [People] AS [p]
GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
FROM [People] AS [p0]
LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]
Пример 13.
var grouping = await context.People
.GroupBy(m => new {m.FirstName, m.MiddleInitial })
.Select(am => new
{
Key = am.Key,
Items = am.ToList()
})
.ToListAsync();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
SELECT [p].[FirstName], [p].[MiddleInitial]
FROM [People] AS [p]
GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]
Модель
Типы сущностей, используемые в этих примерах:
public class Person
{
public int Id { get; set; }
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string MiddleInitial { get; set; }
public Feet Feet { get; set; }
public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}
public class Shoes
{
public int Id { get; set; }
public int Age { get; set; }
public string Style { get; set; }
public Person Person { get; set; }
}
public class Feet
{
public int Id { get; set; }
public int Size { get; set; }
public Person Person { get; set; }
}
Переведите String.Concat с несколькими аргументами
Проблема GitHub: #23859. Эта функция была представлена @wmeints. Спасибо!
Начиная с EF Core 6.0 вызовы String.Concat с несколькими аргументами теперь переводятся в SQL. Например, следующий запрос:
var shards = await context.Shards
.Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToListAsync();
Будет переведен в следующий SQL при использовании SQL Server:
SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL
Более гладкая интеграция с System.Linq.Async
Проблема GitHub: #24041.
Пакет System.Linq.Async добавляет асинхронную обработку LINQ на стороне клиента. Использование этого пакета с предыдущими версиями EF Core было затруднительным из-за конфликта пространств имен для асинхронных методов LINQ. В EF Core 6.0 мы использовали сопоставление шаблонов C# таким образом, чтобы открытый интерфейс в EF Core не требовал прямой реализации.
Обратите внимание, что большинству приложений не требуется использовать System.Linq.Async, так как запросы EF Core обычно полностью преобразуются на сервере.
Более гибкий полнотекстовый поиск в SQL Server
Проблема GitHub: #23921.
В EF Core 6.0 мы расслабили требования к параметру для FreeText(DbFunctions, String, String) и Contains. Это позволяет использовать эти функции с двоичными столбцами или столбцами, сопоставленными с помощью преобразователя значений. Например, рассмотрим тип сущности со свойством Name
, определенным как объект значения:
public class Customer
{
public int Id { get; set; }
public Name Name{ get; set; }
}
public class Name
{
public string First { get; set; }
public string MiddleInitial { get; set; }
public string Last { get; set; }
}
Это сопоставляется с JSON в базе данных:
modelBuilder.Entity<Customer>()
.Property(e => e.Name)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));
Теперь запрос можно выполнить с помощью Contains
или FreeText
, даже если тип свойства не Name
, а string
. Рассмотрим пример.
var result = await context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToListAsync();
При использовании SQL Server создается следующий код SQL:
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')
Перевод ToString на SQLite
Проблема GitHub: #17223. Эта функция была разработана @ralmsdeveloper. Спасибо!
ToString() теперь преобразуются в SQL при использовании поставщика базы данных SQLite. Это может быть полезно для поиска текста, включающего нестроковые столбцы. Например, рассмотрим User
тип сущности, в который хранятся номера телефонов в виде числовых значений:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public long PhoneNumber { get; set; }
}
ToString
можно использовать для преобразования числа в строку в базе данных. Затем эту строку можно использовать с функцией, например LIKE
для поиска чисел, которые соответствуют шаблону. Например, чтобы найти все числа, содержащие 555:
var users = await context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToListAsync();
Это преобразуется в следующий SQL при использовании базы данных SQLite:
SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'
Обратите внимание, что перевод ToString() SQL Server уже поддерживается в EF Core 5.0, а также может поддерживаться другими поставщиками баз данных.
ЭФ. Functions.Random
Проблема GitHub: #16141. Эта функция была представлена @RaymondHuy. Спасибо!
EF.Functions.Random
сопоставляется с функцией базы данных, возвращающей псевдослучайное число от 0 до 1 исключительно. Переводы были реализованы в репозитории EF Core для SQL Server, SQLite и Azure Cosmos DB. Например, рассмотрим User
тип сущности со свойством Popularity
:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int Popularity { get; set; }
}
Popularity
может иметь значения от 1 до 5 включительно. С помощью EF.Functions.Random
команды можно написать запрос, чтобы вернуть всех пользователей с случайно выбранной популярностью:
var users = await context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToListAsync();
Это преобразуется в следующий SQL при использовании базы данных SQL Server:
SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)
Улучшен перевод SQL Server для IsNullOrWhitespace
Проблема GitHub: #22916. Эта функция была внесена @Marusyk. Спасибо!
Обратите внимание на следующий запрос:
var users = await context.Users.Where(
e => string.IsNullOrWhiteSpace(e.FirstName)
|| string.IsNullOrWhiteSpace(e.LastName)).ToListAsync();
Перед EF Core 6.0 это было переведено на следующий код в SQL Server:
SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))
Этот перевод был улучшен для EF Core 6.0:
SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))
Определение запроса для поставщика с хранением в памяти
Проблема GitHub: #24600.
Новый метод ToInMemoryQuery
можно использовать для записи определяющего запроса к базе данных в памяти для заданного типа сущности. Это наиболее полезно для создания эквивалента представлений в базе данных в памяти, особенно если эти представления возвращают типы сущностей без ключей. Например, рассмотрим базу данных клиента для клиентов, базирующихся в Соединенном Королевстве. У каждого клиента есть адрес:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public int Id { get; set; }
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
Теперь представьте, что мы хотим просмотреть эти данные, показывающие, сколько клиентов есть в каждой области почтового кода. Мы можем создать тип сущности без ключа, чтобы представить следующее:
public class CustomerDensity
{
public string Postcode { get; set; }
public int CustomerCount { get; set; }
}
И определите для него свойство DbSet в DbContext, а также наборы для других типов сущностей верхнего уровня:
public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }
OnModelCreating
Затем мы можем написать запрос LINQ, определяющий возвращаемые данные дляCustomerDensities
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<CustomerDensity>()
.HasNoKey()
.ToInMemoryQuery(
() => Customers
.GroupBy(c => c.Address.Postcode.Substring(0, 3))
.Select(
g =>
new CustomerDensity
{
Postcode = g.Key,
CustomerCount = g.Count()
}));
}
Затем его можно запрашивать так же, как и любое другое свойство DbSet:
var results = await context.CustomerDensities.ToListAsync();
Перевод подстроки с одним параметром
Проблема GitHub: #20173. Эта функция была представлена @stevendarby. Спасибо!
EF Core 6.0 теперь обрабатывает использование string.Substring
с одним аргументом. Рассмотрим пример.
var result = await context.Customers
.Select(a => new { Name = a.Name.Substring(3) })
.FirstOrDefaultAsync(a => a.Name == "hur");
Это преобразуется в следующий SQL при использовании SQL Server:
SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'
Разделение запросов для коллекций, не относящихся к навигации
Проблема GitHub: #21234.
EF Core поддерживает разделение одного запроса LINQ на несколько запросов SQL. В EF Core 6.0 эта поддержка была расширена, чтобы включить случаи, когда коллекции, не относящиеся к навигации, содержатся в проекции запроса.
Приведены примеры запросов, показывающих, как запросы на SQL Server могут быть преобразованы в один или несколько запросов.
Пример 1:
Запрос LINQ:
await context.Customers
.Select(
c => new
{
c,
Orders = c.Orders
.Where(o => o.Id > 1)
})
.ToListAsync();
Один SQL-запрос:
SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Order] AS [o]
WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]
Несколько запросов SQL:
SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]
SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Order] AS [o]
WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]
Пример 2:
Запрос LINQ:
await context.Customers
.Select(
c => new
{
c,
OrderDates = c.Orders
.Where(o => o.Id > 1)
.Select(o => o.OrderDate)
})
.ToListAsync();
Один SQL-запрос:
SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
FROM [Order] AS [o]
WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]
Несколько запросов SQL:
SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]
SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Order] AS [o]
WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]
Пример 3:
Запрос LINQ:
await context.Customers
.Select(
c => new
{
c,
OrderDates = c.Orders
.Where(o => o.Id > 1)
.Select(o => o.OrderDate)
.Distinct()
})
.ToListAsync();
Один SQL-запрос:
SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
OUTER APPLY (
SELECT DISTINCT [o].[OrderDate]
FROM [Order] AS [o]
WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
) AS [t]
ORDER BY [c].[Id]
Несколько запросов SQL:
SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]
SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
CROSS APPLY (
SELECT DISTINCT [o].[OrderDate]
FROM [Order] AS [o]
WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
) AS [t]
ORDER BY [c].[Id]
Удалите последнее предложение ORDER BY при соединении с коллекцией
Проблема GitHub: #19828.
При загрузке связанных сущностей по схеме «один ко многим» EF Core добавляет инструкции ORDER BY, чтобы гарантировать, что все связанные сущности для данной сущности сгруппированы вместе. Однако последнее предложение ORDER BY не требуется для EF создания необходимых группировок и может повлиять на производительность. Поэтому предложение в EF Core 6.0 было удалено.
Например, рассмотрим этот запрос:
await context.Customers
.Select(
e => new
{
e.Id,
FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
})
.ToListAsync();
При использовании EF Core 5.0 в SQL Server этот запрос преобразуется в:
SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Order] AS [o]
WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]
В EF Core 6.0 это переводится как:
SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
FROM [Order] AS [o]
WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]
Тег запросов с именем файла и номером строки
Проблема GitHub: #14176. Эта функция была представлена @michalczerwinski. Спасибо!
Теги запросов позволяют добавлять текстовый тег в запрос LINQ таким образом, чтобы он был включен в созданный SQL. В EF Core 6.0 это можно использовать для тегов запросов с именем файла и номером строки кода LINQ. Рассмотрим пример.
var results1 = await context
.Customers
.TagWithCallSite()
.Where(c => c.Name.StartsWith("A"))
.ToListAsync();
Это приводит к следующему сгенерированному SQL-коду при использовании SQL Server.
-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21
SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')
Изменения в управляемой необязательной зависимой обработке
Проблема GitHub: #24558.
Сложно определить, существует ли необязательная зависимая сущность или нет, когда она использует одну таблицу с основной сущностью. Это связано с тем, что в таблице есть строка для зависимого, так как субъект нуждается в ней независимо от того, существует ли зависимость. Однозначный способ решить эту проблему заключается в том, чтобы убедиться, что зависимость имеет по крайней мере одно обязательное свойство. Так как необходимое свойство не может иметь значение NULL, это означает, что значение в столбце для этого свойства равно NULL, то зависимый объект не существует.
Например, рассмотрим класс Customer
, в котором у каждого клиента имеется собственный Address
:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
[Required]
public string Postcode { get; set; }
}
Адрес является необязательным, что означает, что он действителен для сохранения клиента без адреса:
context.Customers1.Add(
new()
{
Name = "Foul Ole Ron"
});
Однако если у клиента есть адрес, этот адрес должен иметь по крайней мере ненулевой почтовый код:
context.Customers1.Add(
new()
{
Name = "Havelock Vetinari",
Address = new()
{
Postcode = "AN1 1PL",
}
});
Это гарантируется путем маркировки Postcode
свойства как Required
.
Теперь, когда клиенты запрашиваются, если столбец Postcode имеет значение NULL, то это означает, что у клиента нет адреса, а Customer.Address
свойство навигации остается пустым. Например, выполните итерацию по клиентам и проверьте, имеет ли адрес значение NULL:
await foreach (var customer in context.Customers1.AsAsyncEnumerable())
{
Console.Write(customer.Name);
if (customer.Address == null)
{
Console.WriteLine(" has no address.");
}
else
{
Console.WriteLine($" has postcode {customer.Address.Postcode}.");
}
}
Создает следующие результаты:
Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.
Рассмотрим вместо этого случай, когда свойство от адреса не требуется:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
Теперь можно сохранить как клиента без адреса, так и клиента с адресом, где все свойства адреса имеют значение NULL:
context.Customers2.Add(
new()
{
Name = "Foul Ole Ron"
});
context.Customers2.Add(
new()
{
Name = "Havelock Vetinari",
Address = new()
});
Однако в базе данных эти два случая неразличимы, так как мы видим, напрямую запрашивая столбцы базы данных:
Id Name House Street City Postcode
1 Foul Ole Ron NULL NULL NULL NULL
2 Havelock Vetinari NULL NULL NULL NULL
По этой причине EF Core 6.0 теперь предупреждает вас при сохранении необязательной зависимости, где все его свойства имеют значение NULL. Рассмотрим пример.
warn: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Сущность типа "Адрес" со значениями первичного ключа {CustomerId: -2147482646} является необязательной зависимой сущностью, использующей общий доступ к таблице. Сущность не имеет свойства со значением, не используемым по умолчанию, чтобы определить, существует ли сущность. Это означает, что при запросе экземпляр объекта не будет создан, и вместо этого будет создан экземпляр, в котором все свойства установлены в значения по умолчанию. Все вложенные зависимые также будут потеряны. Либо не сохраняйте экземпляр только со значениями по умолчанию, либо помечайте входящие навигации как необходимые в модели.
Это становится еще более сложным, когда необязательный зависимый выступает в роли основного для еще одного необязательного зависимого, который также сопоставлен с той же таблицей. Вместо того чтобы просто предупреждать, EF Core 6.0 запрещает случаи вложенных необязательных зависимостей. Например, рассмотрим следующую модель, где ContactInfo
принадлежит Customer
и Address
в свою очередь принадлежит ContactInfo
:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public ContactInfo ContactInfo { get; set; }
}
public class ContactInfo
{
public string Phone { get; set; }
public Address Address { get; set; }
}
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
Теперь, если ContactInfo.Phone
равно null, EF Core не создаст экземпляр Address
, если отношение является необязательным, даже если у самого адреса могут быть данные. Для такого типа модели EF Core 6.0 выдаст следующее исключение:
System.InvalidOperationException: тип сущности ContactInfo является необязательным, использующий совместное использование таблицы и содержащий другие зависимые сущности без каких-либо обязательных не разделяемых свойств, необходимых для определения существования сущности. Если все свойства, допускающие значение NULL, содержат значение NULL в базе данных, то экземпляр объекта не будет создан в запросе, что приводит к потере вложенных зависимых значений. Добавьте обязательное свойство для создания экземпляров с нулевыми значениями для других свойств или отметьте входящую навигацию как обязательную, чтобы всегда создавать экземпляр.
В данном случае важный момент заключается в том, чтобы избежать ситуации, когда необязательный зависимый элемент может содержать все свойства, допускающие значение NULL, и делиться таблицей со своим основным объектом. Существует три простых способа избежать этого:
- Сделайте зависимым обязательным. Это означает, что зависимые сущности всегда будут иметь значение после запроса, даже если все его свойства имеют значение NULL.
- Убедитесь, что зависимый содержит по крайней мере одно обязательное свойство, как описано выше.
- Сохраните необязательных зависимых в отдельную таблицу, вместо совместного использования таблицы с основным субъектом.
Зависимость может стать обязательной с помощью использования атрибута Required
в её навигации.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
[Required]
public Address Address { get; set; }
}
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
}
Или, указав его в OnModelCreating
:
modelBuilder.Entity<WithRequiredNavigation.Customer>(
b =>
{
b.OwnsOne(e => e.Address);
b.Navigation(e => e.Address).IsRequired();
});
Зависимые данные можно сохранить в другую таблицу, указав, какие таблицы использовать в OnModelCreating
:
modelBuilder
.Entity<WithDifferentTable.Customer>(
b =>
{
b.ToTable("Customers");
b.OwnsOne(
e => e.Address,
b => b.ToTable("CustomerAddresses"));
});
См. OptionalDependentsSample в репозитории GitHub для получения дополнительных примеров необязательных зависимых, включая случаи с вложенными необязательными зависимыми.
Новые атрибуты сопоставления
EF Core 6.0 содержит несколько новых атрибутов, которые можно применить к коду, чтобы изменить способ его сопоставления с базой данных.
ЮникодAttribute
Проблема GitHub: #19794. Эта функция была представлена @RaymondHuy. Спасибо!
Начиная с EF Core 6.0, строковое свойство теперь можно сопоставить со столбцом, отличным от Юникода, с помощью атрибута сопоставления без указания типа базы данных напрямую. Например, рассмотрим Book
тип сущности со свойством международного стандартного номера книги (ISBN) в форме "ISBN" 978-3-16-148410-0":
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
[Unicode(false)]
[MaxLength(22)]
public string Isbn { get; set; }
}
Так как isBNs не может содержать символы, отличные от юникода, Unicode
атрибут приведет к использованию типа строки, отличного от Юникода. Кроме того, MaxLength
используется для ограничения размера столбца базы данных. Например, при использовании SQL Server это приводит к столбцу varchar(22)
базы данных:
CREATE TABLE [Book] (
[Id] int NOT NULL IDENTITY,
[Title] nvarchar(max) NULL,
[Isbn] varchar(22) NULL,
CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));
Замечание
EF Core сопоставляет строковые свойства со столбцами Юникода по умолчанию.
UnicodeAttribute
игнорируется, если система базы данных поддерживает только типы Юникода.
АтрибутТочности
Проблема GitHub: #17914. Эта функция была представлена @RaymondHuy. Спасибо!
Теперь точность и масштаб столбца базы данных можно настроить с помощью атрибутов сопоставления без указания типа базы данных напрямую. Например, рассмотрим Product
тип сущности с десятичным Price
свойством:
public class Product
{
public int Id { get; set; }
[Precision(precision: 10, scale: 2)]
public decimal Price { get; set; }
}
EF Core сопоставляет это свойство с столбцом базы данных с точностью 10 и масштабом 2. Например, в SQL Server.
CREATE TABLE [Product] (
[Id] int NOT NULL IDENTITY,
[Price] decimal(10,2) NOT NULL,
CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));
EntityTypeConfigurationAttribute
Проблема GitHub: #23163. Эта функция была представлена @KaloyanIT. Спасибо!
IEntityTypeConfiguration<TEntity> экземпляры позволяют ModelBuilder конфигурации для каждого типа сущности содержаться в собственном классе конфигурации. Рассмотрим пример.
public class BookConfiguration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder
.Property(e => e.Isbn)
.IsUnicode(false)
.HasMaxLength(22);
}
}
Как правило, этот класс конфигурации должен быть создан и вызван из DbContext.OnModelCreating. Рассмотрим пример.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}
Начиная с EF Core 6.0, EntityTypeConfigurationAttribute
можно поместить на тип сущности, чтобы EF Core мог найти и использовать соответствующую конфигурацию. Рассмотрим пример.
[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Isbn { get; set; }
}
Этот атрибут означает, что EF Core будет использовать указанную IEntityTypeConfiguration
реализацию всякий раз, когда Book
тип сущности включен в модель. Тип сущности включен в модель с помощью одного из обычных механизмов. Например, создав DbSet<TEntity> свойство для типа сущности:
public class BooksContext : DbContext
{
public DbSet<Book> Books { get; set; }
//...
Или зарегистрируя его в OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Book>();
}
Замечание
EntityTypeConfigurationAttribute
типы не будут автоматически обнаружены в сборке. Типы сущностей необходимо добавить в модель, прежде чем атрибут будет обнаружен в этом типе сущности.
Улучшения построения модели
Помимо новых атрибутов сопоставления EF Core 6.0 содержит несколько других улучшений процесса сборки модели.
Поддержка разреженных столбцов SQL Server
Проблема GitHub: #8023.
Разреженные столбцы SQL Server — это обычные столбцы, оптимизированные для хранения значений NULL. Это может быть полезно при использовании сопоставления наследования TPH, когда свойства редко используемого подтипа приводят к появлению null значений столбцов для большинства строк в таблице. Например, рассмотрим ForumModerator
класс, который расширяется от ForumUser
:
public class ForumUser
{
public int Id { get; set; }
public string Username { get; set; }
}
public class ForumModerator : ForumUser
{
public string ForumName { get; set; }
}
Там могут быть миллионы пользователей, но лишь немногие из них являются модераторами. Это означает, что здесь может быть целесообразно сопоставление ForumName
как разреженного. Теперь это можно настроить с использованием IsSparse
в OnModelCreating. Рассмотрим пример.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<ForumModerator>()
.Property(e => e.ForumName)
.IsSparse();
}
Затем миграции EF Core помечают столбец как разреженный. Рассмотрим пример.
CREATE TABLE [ForumUser] (
[Id] int NOT NULL IDENTITY,
[Username] nvarchar(max) NULL,
[Discriminator] nvarchar(max) NOT NULL,
[ForumName] nvarchar(max) SPARSE NULL,
CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));
Замечание
Разреженные столбцы имеют ограничения. Обязательно прочтите документацию по разреженным столбцам SQL Server, чтобы убедиться, что они являются правильным выбором для вашего сценария.
Улучшения API HasConversion
Проблема GitHub: #25468.
Перед EF Core 6.0 универсальные перегрузки методов HasConversion
использовали универсальный параметр, чтобы указать тип, в который нужно преобразовать. Например, рассмотрим перечисление Currency
:
public enum Currency
{
UsDollars,
PoundsSterling,
Euros
}
EF Core можно настроить для сохранения значений этого перечисления в виде строк "ДолларыСША", "ФунтыСтерлингов" и "Евро".HasConversion<string>
Рассмотрим пример.
modelBuilder.Entity<TestEntity1>()
.Property(e => e.Currency)
.HasConversion<string>();
Начиная с EF Core 6.0 универсальный тип может вместо этого указать тип преобразователя значений. Это может быть один из встроенных преобразователей значений. Например, чтобы сохранить значения перечисления в виде 16-разрядных чисел в базе данных:
modelBuilder.Entity<TestEntity2>()
.Property(e => e.Currency)
.HasConversion<EnumToNumberConverter<Currency, short>>();
Или это может быть настраиваемый тип преобразователя значений. Например, рассмотрим преобразователь, который сохраняет значения типа enum в качестве их символов валюты.
public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
public CurrencyToSymbolConverter()
: base(
v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
{
}
}
Теперь это можно настроить с помощью универсального HasConversion
метода:
modelBuilder.Entity<TestEntity3>()
.Property(e => e.Currency)
.HasConversion<CurrencyToSymbolConverter>();
Упрощение конфигурации для связей "многие ко многим"
Проблема GitHub: #21535.
Однозначные отношения "многие ко многим" между двумя типами сущностей обнаруживаются по стандартной практике. При необходимости или по желанию можно явно указать навигацию. Рассмотрим пример.
modelBuilder.Entity<Cat>()
.HasMany(e => e.Humans)
.WithMany(e => e.Cats);
В обоих случаях EF Core создает общую сущность, типизированную на основе Dictionary<string, object>
, чтобы действовать в качестве сущности-соединения между двумя типами. Начиная с EF Core 6.0, UsingEntity
можно добавить в конфигурацию только этот тип без необходимости дополнительной настройки. Рассмотрим пример.
modelBuilder.Entity<Cat>()
.HasMany(e => e.Humans)
.WithMany(e => e.Cats)
.UsingEntity<CatHuman>();
Кроме того, тип сущности соединения можно дополнительно настроить, не требуя явного указания связей слева и справа. Рассмотрим пример.
modelBuilder.Entity<Cat>()
.HasMany(e => e.Humans)
.WithMany(e => e.Cats)
.UsingEntity<CatHuman>(
e => e.HasKey(e => new { e.CatsId, e.HumansId }));
И, наконец, можно предоставить полную конфигурацию. Рассмотрим пример.
modelBuilder.Entity<Cat>()
.HasMany(e => e.Humans)
.WithMany(e => e.Cats)
.UsingEntity<CatHuman>(
e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
e => e.HasKey(e => new { e.CatsId, e.HumansId }));
Разрешить преобразователям значений преобразовывать значения NULL
Проблема GitHub: #13850.
Это важно
Из-за проблем, описанных ниже, конструкторы для ValueConverter
, которые разрешают преобразование значений NULL, помечены [EntityFrameworkInternal]
для выпуска EF Core 6.0. Теперь использование этих конструкторов вызовет предупреждение при сборке.
Преобразователи значений обычно не допускают преобразование null в другое значение. Это связано с тем, что один и тот же преобразователь значений может использоваться как для типов, допускающих значение NULL, так и для типов, не допускающих значение NULL, что очень полезно для сочетаний PK/FK, где FK часто имеет значение NULL, и PK не является пустым.
Начиная с EF Core 6.0, можно создать преобразователь значений, который преобразует значения NULL. Однако проверка этой функции показала, что это очень проблематично на практике с множеством ошибок. Рассмотрим пример.
- Преобразование значений в значение NULL в хранилище создает неправильные запросы
- Преобразование значений из null в хранилище создает неправильные запросы
- Преобразователи значений не обрабатывают случаи, когда столбец базы данных имеет несколько разных значений, которые преобразуются в одно и то же значение.
- Разрешить преобразователям значений изменять нулевость столбцов.
Это не тривиальные проблемы, и проблемы с запросами не так просто обнаружить. Поэтому мы помечаем эту функцию как внутреннюю для EF Core 6.0. Его можно использовать, но вы получите предупреждение компилятора. Предупреждение можно отключить с помощью #pragma warning disable EF1001
.
Один из примеров, когда преобразование значений NULL может быть полезным, заключается в том, что база данных содержит значения NULL, но тип сущности хочет использовать другое значение по умолчанию для свойства. Например, рассмотрим перечисление, в котором значение по умолчанию — "Неизвестно":
public enum Breed
{
Unknown,
Burmese,
Tonkinese
}
Однако база данных может иметь значения NULL, если порода неизвестна. В EF Core 6.0 преобразователь значений можно использовать для учета этого:
public class BreedConverter : ValueConverter<Breed, string>
{
#pragma warning disable EF1001
public BreedConverter()
: base(
v => v == Breed.Unknown ? null : v.ToString(),
v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
convertsNulls: true)
{
}
#pragma warning restore EF1001
}
Кошки с породой "Неизвестно" будут иметь столбец Breed
, равный NULL в базе данных. Рассмотрим пример.
context.AddRange(
new Cat { Name = "Mac", Breed = Breed.Unknown },
new Cat { Name = "Clippy", Breed = Breed.Burmese },
new Cat { Name = "Sid", Breed = Breed.Tonkinese });
await context.SaveChangesAsync();
Что создает следующие инструкции вставки в SQL Server:
info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Cats] ([Breed], [Name])
VALUES (@p0, @p1);
SELECT [Id]
FROM [Cats]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Cats] ([Breed], [Name])
VALUES (@p0, @p1);
SELECT [Id]
FROM [Cats]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
INSERT INTO [Cats] ([Breed], [Name])
VALUES (@p0, @p1);
SELECT [Id]
FROM [Cats]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Улучшения фабрики DbContext
AddDbContextFactory также регистрирует DbContext напрямую
Проблема GitHub: #25164.
Иногда полезно зарегистрировать как тип DbContext, так и фабрику для контекстов этого типа в контейнере внедрения зависимостей приложения (D.I.). Это позволяет, например, получить экземпляр с областью действия DbContext из области запроса, а фабрика может использоваться для создания нескольких независимых экземпляров при необходимости.
Для поддержки этого AddDbContextFactory
теперь также регистрирует тип DbContext в качестве службы с областью действия. Например, рассмотрим эту регистрацию в контейнере D.I. приложения:
var container = services
.AddDbContextFactory<SomeDbContext>(
builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0"))
.BuildServiceProvider();
С помощью этой регистрации фабрика может быть извлечена из корневого контейнера D.I., так же как и в предыдущих версиях.
var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
// Contexts obtained from the factory must be explicitly disposed
}
Обратите внимание, что экземпляры контекста, созданные фабрикой, должны быть явно удалены.
Кроме того, экземпляр DbContext можно непосредственно получить из области действия контейнера.
using (var scope = container.CreateScope())
{
var context = scope.ServiceProvider.GetService<SomeDbContext>();
// Context is disposed when the scope is disposed
}
В этом случае экземпляр контекста удаляется при удалении области контейнера; контекст не должен быть явно удален.
На более высоком уровне это означает, что dbContext фабрики можно внедрить в другие типы D.I. Рассмотрим пример.
private class MyController2
{
private readonly IDbContextFactory<SomeDbContext> _contextFactory;
public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task DoSomething()
{
using var context1 = _contextFactory.CreateDbContext();
using var context2 = _contextFactory.CreateDbContext();
var results1 = await context1.Blogs.ToListAsync();
var results2 = await context2.Blogs.ToListAsync();
// Contexts obtained from the factory must be explicitly disposed
}
}
Или:
private class MyController1
{
private readonly SomeDbContext _context;
public MyController1(SomeDbContext context)
{
_context = context;
}
public async Task DoSomething()
{
var results = await _context.Blogs.ToListAsync();
// Injected context is disposed when the request scope is disposed
}
}
DbContextFactory игнорирует конструктор DbContext без параметров
Проблема GitHub: #24124.
EF Core 6.0 теперь позволяет использовать как конструктор DbContext без параметров, так и конструктор, принимающий DbContextOptions
, в рамках одного типа контекста при регистрации фабрики через AddDbContextFactory
. Например, контекст, используемый в приведенных выше примерах, содержит оба конструктора:
public class SomeDbContext : DbContext
{
public SomeDbContext()
{
}
public SomeDbContext(DbContextOptions<SomeDbContext> options)
: base(options)
{
}
public DbSet<Blog> Blogs { get; set; }
}
Пул DbContext можно использовать без внедрения зависимостей
Проблема GitHub: #24137.
Тип PooledDbContextFactory
был открыт таким образом, чтобы его можно было использовать в качестве автономного пула для экземпляров DbContext, не требуя для приложения контейнера внедрения зависимостей. Пул создается с экземпляром DbContextOptions
, который будет использоваться для создания экземпляров контекста.
var options = new DbContextOptionsBuilder<SomeDbContext>()
.EnableSensitiveDataLogging()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample;ConnectRetryCount=0")
.Options;
var factory = new PooledDbContextFactory<SomeDbContext>(options);
Затем фабрику можно использовать для создания экземпляров и их объединения в пул. Рассмотрим пример.
for (var i = 0; i < 2; i++)
{
using var context1 = factory.CreateDbContext();
Console.WriteLine($"Created DbContext with ID {context1.ContextId}");
using var context2 = factory.CreateDbContext();
Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}
Экземпляры возвращаются в пул при их удалении.
Прочие улучшения
И, наконец, EF Core содержит несколько улучшений в областях, которые не рассматриваются выше.
При создании таблиц используйте [ColumnAttribute.Order]
Проблема GitHub: #10059.
Order
Теперь ColumnAttribute
свойство можно использовать для упорядочивания столбцов при создании таблицы с миграциями. Например, рассмотрим следующую модель:
public class EntityBase
{
public int Id { get; set; }
public DateTime UpdatedOn { get; set; }
public DateTime CreatedOn { get; set; }
}
public class PersonBase : EntityBase
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Employee : PersonBase
{
public string Department { get; set; }
public decimal AnnualSalary { get; set; }
public Address Address { get; set; }
}
[Owned]
public class Address
{
public string House { get; set; }
public string Street { get; set; }
public string City { get; set; }
[Required]
public string Postcode { get; set; }
}
По умолчанию EF Core упорядочивает столбцы первичного ключа, следуя свойствам типа сущности и собственным типам, а затем свойствам базовых типов. Например, в SQL Server создается следующая таблица:
CREATE TABLE [EmployeesWithoutOrdering] (
[Id] int NOT NULL IDENTITY,
[Department] nvarchar(max) NULL,
[AnnualSalary] decimal(18,2) NOT NULL,
[Address_House] nvarchar(max) NULL,
[Address_Street] nvarchar(max) NULL,
[Address_City] nvarchar(max) NULL,
[Address_Postcode] nvarchar(max) NULL,
[UpdatedOn] datetime2 NOT NULL,
[CreatedOn] datetime2 NOT NULL,
[FirstName] nvarchar(max) NULL,
[LastName] nvarchar(max) NULL,
CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));
В EF Core 6.0 ColumnAttribute
можно использовать для указания другого порядка столбцов. Рассмотрим пример.
public class EntityBase
{
[Column(Order = 1)]
public int Id { get; set; }
[Column(Order = 98)]
public DateTime UpdatedOn { get; set; }
[Column(Order = 99)]
public DateTime CreatedOn { get; set; }
}
public class PersonBase : EntityBase
{
[Column(Order = 2)]
public string FirstName { get; set; }
[Column(Order = 3)]
public string LastName { get; set; }
}
public class Employee : PersonBase
{
[Column(Order = 20)]
public string Department { get; set; }
[Column(Order = 21)]
public decimal AnnualSalary { get; set; }
public Address Address { get; set; }
}
[Owned]
public class Address
{
[Column("House", Order = 10)]
public string House { get; set; }
[Column("Street", Order = 11)]
public string Street { get; set; }
[Column("City", Order = 12)]
public string City { get; set; }
[Required]
[Column("Postcode", Order = 13)]
public string Postcode { get; set; }
}
В SQL Server созданная таблица теперь:
CREATE TABLE [EmployeesWithOrdering] (
[Id] int NOT NULL IDENTITY,
[FirstName] nvarchar(max) NULL,
[LastName] nvarchar(max) NULL,
[House] nvarchar(max) NULL,
[Street] nvarchar(max) NULL,
[City] nvarchar(max) NULL,
[Postcode] nvarchar(max) NULL,
[Department] nvarchar(max) NULL,
[AnnualSalary] decimal(18,2) NOT NULL,
[UpdatedOn] datetime2 NOT NULL,
[CreatedOn] datetime2 NOT NULL,
CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));
При этом столбцы FistName
и LastName
перемещаются наверх, даже если они определены в базовом типе. Обратите внимание, что значения порядка столбцов могут иметь пробелы, что позволяет использовать диапазоны для постоянного размещения столбцов в конце, даже если используется несколькими производными типами.
В этом примере также показано, как можно использовать тот же ColumnAttribute
для указания как имени столбца, так и порядка.
Порядок столбцов также можно настроить с помощью ModelBuilder
API в OnModelCreating
. Рассмотрим пример.
modelBuilder.Entity<UsingModelBuilder.Employee>(
entityBuilder =>
{
entityBuilder.Property(e => e.Id).HasColumnOrder(1);
entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
entityBuilder.Property(e => e.LastName).HasColumnOrder(3);
entityBuilder.OwnsOne(
e => e.Address,
ownedBuilder =>
{
ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
});
entityBuilder.Property(e => e.Department).HasColumnOrder(8);
entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
});
Порядок в конструкторе моделей с HasColumnOrder
имеет приоритет над любым порядком, указанным с помощью ColumnAttribute
. Это значит, что HasColumnOrder
можно использовать для переопределения упорядочивания, выполненного с помощью атрибутов, включая разрешение любых конфликтов, когда атрибуты на разных свойствах указывают один и тот же порядковый номер.
Это важно
Обратите внимание, что в общем случае большинство баз данных поддерживают только упорядочение столбцов при создании таблицы. Это означает, что атрибут порядка столбцов нельзя использовать для повторного упорядочивания столбцов в существующей таблице. Одним из заметных исключений является SQLite, где в процессе миграции будет перестроена вся таблица с новым порядком столбцов.
Минимальный API на EF Core
Проблема GitHub: #25192.
.NET Core 6.0 включает обновленные шаблоны, которые содержат упрощенные "минимальные API", которые удаляют много стандартного кода, традиционно необходимого в приложениях .NET.
EF Core 6.0 содержит новый метод расширения, который регистрирует тип DbContext и предоставляет конфигурацию поставщика базы данных в одной строке. Рассмотрим пример.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCosmos<MyDbContext>(
"https://localhost:8081",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");
Это точно эквивалентно следующим:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MyDbContext>(
options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MyDbContext>(
options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MyDbContext>(
options => options.UseCosmos(
"https://localhost:8081",
"C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));
Замечание
Минимальные API EF Core поддерживают только простую регистрацию и настройку DbContext и поставщика. Используйте AddDbContext
, AddDbContextPool
AddDbContextFactory
и т. д. для доступа ко всем типам регистрации и конфигурации, доступным в EF Core.
Ознакомьтесь с этими ресурсами, чтобы узнать больше о минимальных API:
- Презентация минимальных API в .NET 6 От Марии Наггага
- Пример минимального API .NET 6 Todo Playground на блоге Скотта Ханселмана
- Краткий обзор минимальных API от Дэвида Фаулера
- Минимальный полигон API от Дамиана Эдвардса на GitHub
Сохранение контекста синхронизации в SaveChangesAsync
Проблема GitHub: #23971.
Мы изменили код EF Core в выпуске 5.0, чтобы присвоить Task.ConfigureAwait значение false
во всех местах, где мы использовали await
асинхронный код. Как правило, это лучший выбор для использования EF Core. Однако SaveChangesAsync является особым случаем, потому что EF Core устанавливает созданные значения в отслеживаемые сущности после завершения асинхронной операции с базой данных. Эти изменения могут активировать уведомления, которые, например, должны выполняться в потоке пользовательского интерфейса. Поэтому мы отменяем это изменение в EF Core 6.0 только для SaveChangesAsync метода.
База данных в оперативной памяти: убедитесь, что обязательные свойства не являются значениями NULL
Проблема GitHub: #10613. Эта функция была представлена @fagnercarvalho. Спасибо!
База данных EF Core в памяти теперь создает исключение, если предпринята попытка сохранить значение NULL для свойства, помеченного как обязательное. Например, рассмотрим User
тип с обязательным Username
свойством:
public class User
{
public int Id { get; set; }
[Required]
public string Username { get; set; }
}
Попытка сохранить сущность с значением NULL Username
приведет к следующему исключению:
Microsoft.EntityFrameworkCore.DbUpdateException: обязательные свойства "{'Username'}" отсутствуют для экземпляра типа сущности "User" со значением ключа "{Id: 1}".
Эта проверка может быть отключена при необходимости. Рассмотрим пример.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
.UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}
Сведения об источнике команд для диагностики и системы перехвата
Проблема GitHub: #23719. Эта функция была представлена @Giorgi. Спасибо!
Предоставленные CommandEventData
источникам диагностики и перехватчики теперь содержат значение перечисления, указывающее, какая часть EF отвечает за создание команды. Это можно использовать в качестве фильтра в диагностике или перехватчике. Например, может потребоваться перехватчик, который применяется только к командам, поступающим из SaveChanges
:
public class CommandSourceInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
if (eventData.CommandSource == CommandSource.SaveChanges)
{
Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
Console.WriteLine();
Console.WriteLine(command.CommandText);
}
return result;
}
}
Это фильтрует перехватчик только для SaveChanges
событий, когда он используется в приложении, которое также создает миграции и запросы. Рассмотрим пример.
Saving changes for CustomersContext:
SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Улучшена обработка временных значений
Проблема GitHub: #24245.
EF Core не предоставляет временные значения для экземпляров типов сущностей. Например, рассмотрим Blog
тип сущности с ключом, созданным в магазине:
public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; } = new List<Post>();
}
Свойство Id
ключа получит временное значение, как только Blog
будет отслежен контекстом. Например, при вызове DbContext.Add
:
var blog = new Blog();
context.Add(blog);
Временное значение можно получить из средства отслеживания изменений контекста, но оно не устанавливается в экземпляр сущности. Например, этот код:
Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");
Будут получены следующие выходные данные:
Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647
Это хорошо, так как это предотвращает утечку временного значения в код приложения, где он может случайно рассматриваться как не временный. Однако иногда бывает полезно иметь дело с временными значениями напрямую. Например, приложению может потребоваться создать собственные временные значения для графа сущностей до их отслеживания, чтобы они могли быть использованы для формирования связей с помощью внешних ключей. Это можно сделать, явно помечая значения как временные. Рассмотрим пример.
var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };
context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;
Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");
В EF Core 6.0 значение останется в экземпляре сущности, несмотря на то, что теперь оно помечено как временное. Например, приведенный выше код создает следующие выходные данные:
Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1
Аналогичным образом временные значения, созданные EF Core, можно явно задать для экземпляров сущностей и пометить как временные значения. Это можно использовать для явного задания связей между новыми сущностями с помощью временных значений ключей. Рассмотрим пример.
var post1 = new Post();
var post2 = new Post();
var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;
var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;
var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;
Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");
Результат:
Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647
Заметки EF Core для ссылочных типов, допускающих значение NULL C#
Проблема GitHub: #19007.
В кодовой базе EF Core теперь используются ссылочные типы, допускающие значение NULL (NRTs). Это означает, что вы получите корректные указания компилятора при работе с null, используя EF Core 6.0 из собственного кода.
Microsoft.Data.Sqlite 6.0
Подсказка
Вы можете запустить и отладить все примеры, приведенные ниже, скачав пример кода с GitHub.
Пул подключений
Проблема GitHub: #13837.
Как правило, соединения с базой данных держат открытыми как можно меньше времени. Это помогает предотвратить состязание по ресурсу подключения. Поэтому библиотеки, такие как EF Core, открывают подключение непосредственно перед выполнением операции базы данных и закрывают его сразу после. Например, рассмотрим этот код EF Core:
Console.WriteLine("Starting query...");
Console.WriteLine();
var users = await context.Users.ToListAsync();
Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();
foreach (var user in users)
{
if (user.Username.Contains("microsoft"))
{
user.Username = "msft:" + user.Username;
Console.WriteLine("Starting SaveChanges...");
Console.WriteLine();
await context.SaveChangesAsync();
Console.WriteLine();
Console.WriteLine("SaveChanges finished.");
}
}
Вывод этого кода при включенном логировании подключений:
Starting query...
dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
Closed connection to database 'main' on server 'test.db'.
Query finished.
Starting SaveChanges...
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
Closed connection to database 'main' on server 'test.db'.
SaveChanges finished.
Обратите внимание, что подключение открывается и закрывается быстро для каждой операции.
Однако для большинства систем баз данных открытие физического подключения к базе данных является дорогостоящей операцией. Поэтому большинство поставщиков ADO.NET создают пул физических подключений и предоставляют их DbConnection
экземплярам по мере необходимости.
SQLite немного отличается, так как доступ к базе данных обычно сводится к простому доступу к файлу. Это означает, что открытие подключения к базе данных SQLite обычно очень быстро. Однако это не всегда так. Например, открытие подключения к зашифрованной базе данных может быть очень медленным. Поэтому подключения SQLite теперь объединяются в пул при использовании Microsoft.Data.Sqlite 6.0.
Поддержка DateOnly и TimeOnly
Проблема GitHub: #24506.
Microsoft.Data.Sqlite 6.0 поддерживает новые DateOnly
и TimeOnly
типы из .NET 6. Они также можно использовать в EF Core 6.0 с поставщиком SQLite. Как и всегда с SQLite, его собственная система типов означает, что значения из этих типов должны храниться в качестве одного из четырех поддерживаемых типов. Microsoft.Data.Sqlite сохраняет их как TEXT
. Например, сущность, использующая следующие типы:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public DateOnly Birthday { get; set; }
public TimeOnly TokensRenewed { get; set; }
}
Сопоставляется со следующей таблицей в базе данных SQLite:
CREATE TABLE "Users" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
"Username" TEXT NULL,
"Birthday" TEXT NOT NULL,
"TokensRenewed" TEXT NOT NULL);
Затем значения можно сохранять, запрашивать и обновлять обычным образом. Например, этот запрос EF Core LINQ:
var users = await context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToListAsync();
Преобразуется в следующее в SQLite:
SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'
И возвращает только использованные данные с днями рождения до 1900 года н.э.
Found 'ajcvickers'
Found 'wendy'
API сохранения точек
Проблема GitHub: #20228.
Мы занимаемся стандартизацией общего API для точек сохранения в провайдерах ADO.NET. Microsoft.Data.Sqlite теперь поддерживает этот API, в том числе:
- Save(String) Создание точки сохранения в транзакции
- Rollback(String) вернуться к предыдущей точке восстановления
- Release(String) освободить точку сохранения
Использование сохранённой точки позволяет откатить часть транзакции без отката всей транзакции. Например, приведенный ниже код:
- Создает транзакцию
- Отправляет обновление в базу данных
- Создает точку сохранения
- Отправляет еще одно обновление в базу данных
- Откат к предыдущей созданной точке сохранения
- Фиксация транзакции
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
await connection.OpenAsync();
await using var transaction = await connection.BeginTransactionAsync();
using (var command = connection.CreateCommand())
{
command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
await command.ExecuteNonQueryAsync();
}
await transaction.SaveAsync("MySavepoint");
using (var command = connection.CreateCommand())
{
command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
await command.ExecuteNonQueryAsync();
}
await transaction.RollbackAsync("MySavepoint");
await transaction.CommitAsync();
Это приведет к тому, что первое обновление фиксируется в базе данных, а второе обновление не фиксируется, так как точка сохранения была откатена до фиксации транзакции.
Время ожидания команды в строке подключения
Проблема GitHub: #22505. Эта функция была внесена @nmichels. Спасибо!
ADO.NET поставщики поддерживают два разных тайм-аута.
- Время ожидания подключения, определяющее максимальное время ожидания при подключении к базе данных.
- Время ожидания команды, определяющее максимальное время ожидания выполнения команды.
Время ожидания команды можно задать из кода с помощью DbCommand.CommandTimeout. Многие поставщики теперь также включают параметр времени ожидания команды в строке подключения. Microsoft.Data.Sqlite следует этой тенденции, используя ключевое слово строки подключения Command Timeout
. Например, "Command Timeout=60;DataSource=test.db"
будет использовать 60 секунд в качестве времени ожидания по умолчанию для команд, созданных подключением.
Подсказка
Sqlite рассматривает Default Timeout
как синоним Command Timeout
и поэтому можно использовать вместо этого, если предпочтительнее.