Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
// Car.cs
public class Car : BaseEntity
{
...
public bool IsDrivable { get; set; }
}
// ApplicationDbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Car>(entity =>
{
...
entity.Property(e => e.IsDrivable).HasDefaultValue(true);
});
В случае если вы сохраните новую запись с IsDrivable = false, то значение false игнорируется (т.к. оно является стандартным значением для булевского типа) и будет применяться стандартное значение. Таким образом, значение для IsDrivable всегда будет равно true! Одно из решений предусматривает превращение вашего открытого свойства (и, следовательно, столбца) в допускающее null, но это может не соответствовать бизнес-потребностям.
Другое решение предлагается инфраструктурой EF Core, в частности, ее работой с поддерживающими полями. Вспомните, что если поддерживающее поле существует (и идентифицируется как таковое для свойства через соглашение, аннотацию данных или Fluent API), тогда для действий по чтению и записи EF Core будет использовать поддерживающее поле, а не открытое свойство.
Если вы модифицируете IsDrivable с целью применения поддерживающего поля, допускающего null (но оставите свойство не допускающим null), то EF Core будет выполнять чтение и запись, используя поддерживающее поле, а не свойство. Стандартным значением для булевского типа, допускающего null, является null — не false. Описанное изменение обеспечит ожидаемое поведение свойства:
public class Car
{
...
private bool? _isDrivable;
public bool IsDrivable
{
get => _isDrivable ?? true;
set => _isDrivable = value;
}
Для информирования EF Core о поддерживающем поле используется Fluent API:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.IsDrivable)
.HasField("_isDrivable")
.HasDefaultValue(true);
});
На заметку! В приведенном примере вызов метода HasField() не обязателен, потому что имя поддерживающего поля следует соглашениям об именовании. Он включен в целях демонстрации применения Fluent API для указания поддерживающего поля.
Исполняющая среда EF Core транслирует поле в показанное ниже определение SQL:
CREATE TABLE [dbo].[Inventory](
...
[IsDrivable] [BIT] NOT NULL,
...
GO
ALTER TABLE [dbo].[Inventory] ADD DEFAULT (CONVERT([BIT],(1)))
FOR [IsDrivable]
GO
Вычисляемые столбцы
Столбцы также могут вычисляться на основе возможностей хранилища данных. Для SQL Server есть два варианта: вычислять значение, основываясь на других полях в той же самой записи, либо использовать скалярную функцию. Скажем, чтобы создать в таблице Inventory вычисляемый столбец, который объединяет значения PetName и Color для создания DisplayName, применяйте функцию HasComputedColumnSql():
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.FullName)
.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'");
});
В версии EF Core 5 появилась возможность сохранения вычисляемых значений, так что значение вычисляется только при создании или обновлении строки. Хотя в SQL Server упомянутая возможность поддерживается, она присутствует не во всех хранилищах данных, поэтому проверяйте документацию по своему поставщику баз данных:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.FullName)
.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'", stored:true);
});
Отношения "один ко многим"
Чтобы определить отношение "один ко многим" с помощью Fluent API, выберите одну из сущностей, подлежащих обновлению. Обе стороны навигационной цепочки устанавливаются в одном блоке кода:
modelBuilder.Entity<Car>(entity =>
{
...
entity.HasOne(d => d.MakeNavigation)
.WithMany(p => p.Cars)
.HasForeignKey(d => d.MakeId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Inventory_Makes_MakeId");
});
Если вы выберете в качестве основы для конфигурации навигационной сущности главную сущность, тогда код будет выглядеть примерно так:
modelBuilder.Entity<Make>(entity =>
{
...
entity.HasMany(e=>e.Cars)
.WithOne(c=>c.MakeNavigation)
.HasForeignKey(c=>c.MakeId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Inventory_Makes_MakeId");
});
Отношения "один к одному"
Отношения "один к одному" конфигурируются аналогично, но только вместо метода WithMany() интерфейса Fluent API используется метод WithOne(). К зависимой сущности добавляется уникальный индекс. Вот код для отношения между сущностями Car и Radio, где применяется зависимая сущность (Radio):
modelBuilder.Entity<Radio>(entity =>
{
entity.HasIndex(e => e.CarId, "IX_Radios_CarId")
.IsUnique();
entity.HasOne(d => d.CarNavigation)
.WithOne(p => p.RadioNavigation)
.HasForeignKey<Radio>(d => d.CarId);
});
Даже если отношение определено в главной сущности, то к зависимой сущности все равно добавляется уникальный индекс. Далее приведен код установки отношения между сущностями Car и Radio, где для отношения используется главная сущность:
modelBuilder.Entity<Radio>(entity =>
{
entity.HasIndex(e => e.CarId, "IX_Radios_CarId")
.IsUnique();
});
modelBuilder.Entity<Car>(entity =>
{
entity.HasOne(d => d.RadioNavigation)
.WithOne(p => p.CarNavigation)
.HasForeignKey<Radio>(d => d.CarId);
});
Отношения "многие ко многим"
Отношения "многие ко многим" гораздо легче настраивать посредством Fluent API. Имена полей внешних ключей, имена индексов и каскадное поведение могут быть установлены в операторах, определяющих отношение. Ниже показан пример отношения "многие ко многим", переделанный с применением Fluent API (имена ключей и столбцов были изменены, чтобы улучшить читабельность):
modelBuilder.Entity<Car>()
.HasMany(p => p.Drivers)
.WithMany(p => p.Cars)
.UsingEntity<Dictionary<string, object>>(
"CarDriver",
j => j
.HasOne<Driver>()
.WithMany()
.HasForeignKey("DriverId")
.HasConstraintName("FK_CarDriver_Drivers_DriverId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne<Car>()
.WithMany()
.HasForeignKey("CarId")
.HasConstraintName("FK_CarDriver_Cars_CarId")
.OnDelete(DeleteBehavior.ClientCascade));
Соглашения, аннотации данных и Fluent API — что выбрать?
В настоящий момент вас может интересовать, какой из вариантов следует выбирать для формирования ваших сущностей, а также их связей друг с другом и с хранилищем данных? Ответ: все три. Соглашения активны всегда (если только вы не переопределите их посредством аннотаций данных или Fluent API). С помощью аннотаций данных можно делать почти все то, на что способны методы Fluent API, и хранить информацию в самом сущностном классе, повышая в ряде случаев читабельность кода и удобство его сопровождения. Из трех вариантов