EF Core实体配置:注解、Fluent API与关系映射详解
在使用EF Core进行数据库操作时,实体配置是一个关键环节。本文将详细介绍EF Core中的注解和导航属性,以及强大的Fluent API配置方法,还会涉及各种实体关系的配置和一些特殊场景的处理。
注解与导航属性
-
ForeignKey注解
:它能让EF Core知晓哪个属性是导航属性的后备字段。按照约定,
<TypeName>Id会自动被设为外键属性。不过,为了提高代码的可读性,也可以显式设置。这样不仅支持不同的命名风格,还能为同一个表设置多个外键。需要注意的是,在一对一关系中,只有依赖实体才有外键。 - InverseProperty属性 :它能告知EF Core实体之间的关联方式,通过指定另一个实体上导航回当前实体的导航属性来实现。当一个实体与另一个实体有多次关联时,InverseProperty是必需的,而且在我看来,它能让代码更易读。
Fluent API配置
Fluent API通过C#代码来配置应用程序实体,其方法由
DbContext
的
OnModelCreating()
方法中的
ModelBuilder
实例公开。它是最强大的配置方法,会覆盖任何与之冲突的约定或数据注解。部分配置选项只能通过Fluent API实现,例如设置导航属性的默认值和级联行为。
类和属性方法
Fluent API是数据注解的超集,在塑造单个实体时,它不仅支持数据注解中的所有功能,还具备额外的能力,比如指定复合键和索引,以及定义计算列。
类和属性映射
以下代码展示了如何使用Fluent API实现与数据注解等效的
Car
示例(省略了导航属性,后续会详细介绍):
modelBuilder.Entity<Car>(entity =>
{
entity.ToTable("Inventory","dbo");
});
下面的代码将
Radio
类的
CarId
属性映射到
Makes
表的
InventoryId
列:
modelBuilder.Entity<Radio>(entity =>
{
entity.Property(e => e.CarId).HasColumnName("InventoryId");
});
键和索引
-
设置主键
:使用
HasKey()方法来设置实体的主键,示例如下:
modelBuilder.Entity<Car>(entity =>
{
entity.ToTable("Inventory","dbo");
entity.HasKey(e=>e.Id);
});
-
设置复合键
:在
HasKey()方法的表达式中选择构成键的属性。例如,如果Car实体的主键由Id列和OrganizationId属性组成,可以这样设置:
modelBuilder.Entity<Car>(entity =>
{
entity.ToTable("Inventory","dbo");
entity.HasKey(e=> new { e.Id, e.OrganizationId});
});
-
创建索引
:创建索引的过程与设置键类似,只是使用
HasIndex()Fluent API方法。例如,创建一个名为IX_Inventory_MakeId的索引:
modelBuilder.Entity<Car>(entity =>
{
entity.ToTable("Inventory","dbo");
entity.HasKey(e=>e.Id);
entity.HasIndex(e => e.MakeId, "IX_Inventory_MakeId");
});
要使索引唯一,可以使用
IsUnique()
方法,该方法接受一个可选的布尔值,默认值为
true
:
entity.HasIndex(e => e.MakeId, "IX_Inventory_MakeId").IsUnique();
字段大小和可空性
通过
Property()
方法选择属性,然后使用其他方法对属性进行配置。例如,将
Car
类的
Color
和
PetName
属性设置为必需,且最大长度为50个字符:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(e => e.Color)
.IsRequired()
.HasMaxLength(50);
entity.Property(e => e.PetName)
.IsRequired()
.HasMaxLength(50);
});
默认值
Fluent API提供了设置列默认值的方法,默认值可以是值类型或SQL字符串。
-
设置普通默认值
:将新
Car
的默认
Color
设置为
Black
:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(e => e.Color)
.IsRequired()
.HasMaxLength(50)
.HasDefaultValue("Black");
});
-
设置数据库函数默认值
:如果要将属性的默认值设置为数据库函数(如
getdate()),可以使用HasDefaultValueSql()方法。假设Car类中添加了一个名为DateBuilt的DateTime属性:
public class Car : BaseEntity
{
public DateTime? DateBuilt { get; set; }
}
使用以下Fluent API代码将其默认值设置为当前日期:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(e => e.DateBuilt)
.HasDefaultValueSql("getdate()");
});
然而,当布尔或数值属性的数据库默认值与CLR默认值冲突时,会出现问题。例如,将
Car
类中的
IsDrivable
布尔属性的数据库默认值设置为
true
,但CLR默认值为
false
。当保存
IsDrivable = false
的新记录时,该值会被忽略,数据库将使用默认值,导致
IsDrivable
的值始终为
true
。
EF Core在创建迁移时会提醒这个问题,例如会输出如下警告:
The 'bool' property 'IsDrivable' on entity type 'Car' is configured with a database-
generated default. This default will always be used for inserts when the property has
the value 'false', since this is the CLR default for the 'bool' type. Consider using the
nullable 'bool?' type instead, so that the default will only be used for inserts when the
property value is 'null'.
解决这个问题的方法有两种:
-
将属性设为可空
:由于可空值类型的默认值是
null
,将布尔属性设置为
false
就能按预期工作。但改变属性的可空性可能不符合业务需求。
-
使用后备字段
:EF Core支持后备字段,如果存在后备字段(并通过约定、数据注解或Fluent API将其标识为属性的后备字段),EF Core会使用后备字段进行读写操作,而不是公共属性。
以下是使用可空后备字段解决该问题的示例:
public class Car
{
private bool? _isDrivable;
public bool IsDrivable
{
get => _isDrivable ?? true;
set => _isDrivable = value;
}
}
使用Fluent API告知EF Core后备字段的存在:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.IsDrivable)
.HasField("_isDrivable")
.HasDefaultValue(true);
});
行版本/并发令牌
要将属性设置为
rowversion
数据类型,可以使用
IsRowVersion()
方法;若要将其设置为并发令牌,则使用
IsConcurrencyToken()
方法。这两个方法的组合与
[Timestamp]
数据注解的效果相同:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(e => e.TimeStamp)
.IsRowVersion()
.IsConcurrencyToken();
});
SQL Server稀疏列
SQL Server稀疏列经过优化,可用于存储空值。EF Core 6通过Fluent API的
IsSparse()
方法支持稀疏列。以下代码展示了如何将虚构的
IsRaceCar
属性设置为使用SQL Server稀疏列:
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.IsRaceCare).IsSparse();
});
计算列
根据数据存储的能力,列也可以设置为计算列。对于SQL Server,有两种选择:基于同一记录中其他字段的值计算,或使用标量函数。例如,在
Inventory
表上创建一个计算列
Display
,它结合了
PetName
和
Color
的值:
public class Car : BaseEntity
{
public string Display { get; set; }
}
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.Display)
.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'");
});
在EF Core 5中,计算值可以持久化,即仅在创建或更新行时计算该值。不过,并非所有数据存储都支持这一功能,所以需要查看数据库提供程序的文档。
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.Display)
.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'", stored:true);
});
为了提高代码的可读性,
DatabaseGenerated
数据注解常与Fluent API结合使用:
public class Car : BaseEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public string Display { get; set; }
}
检查约束
检查约束是SQL Server的一个特性,用于定义行必须满足的条件。例如,在电子商务系统中,可以添加检查约束以确保数量大于零或价格大于折扣价。这里创建一个检查约束,防止在
Makes
表中使用“Lemon”作为名称:
modelBuilder.Entity<Make>()
.HasCheckConstraint(name:"CH_Name", sql:"[Name]<>'Lemon'",
buildAction:c => c.HasName("CK_Check_Name"));
此检查约束在SQL中的定义如下:
ALTER TABLE [dbo].[Makes] WITH CHECK ADD CONSTRAINT [CK_Check_Name]
CHECK (([Name]<>'Lemon'))
当向表中添加名称为“Lemon”的记录时,会抛出SQL异常。
实体关系配置
一对多关系
使用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");
});
一对一关系
一对一关系的配置方式类似,只是使用
WithOne()
Fluent API方法代替
WithMany()
。同时,依赖实体上需要有唯一索引,如果未定义,会自动创建。以下是使用依赖实体(
Radio
)配置
Car
和
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);
});
如果在主体实体上定义关系,依赖实体上仍会添加唯一索引。
多对多关系
Fluent API能让多对多关系的配置更加灵活,外键字段名、索引名和级联行为都可以在定义关系的语句中设置,还能直接指定中间表,方便添加额外字段和简化查询。
首先添加
CarDriver
实体:
//CarDriver.cs
namespace AutoLot.Samples.Models;
[Table("InventoryToDrivers", Schema = "dbo")]
public class CarDriver : BaseEntity
{
public int DriverId {get;set;}
[ForeignKey(nameof(DriverId))]
public Driver DriverNavigation {get;set;}
[Column("InventoryId")]
public int CarId {get;set;}
[ForeignKey(nameof(CarId))]
public Car CarNavigation {get;set;}
}
在
ApplicationDbContext
中添加新实体的
DbSet<T>
:
public DbSet<CarDriver> CarsToDrivers {get;set;}
更新
Car
实体,添加对新
CarDriver
实体的导航属性:
public class Car : BaseEntity
{
[InverseProperty(nameof(CarDriver.CarNavigation))]
public IEnumerable<CarDriver> CarDrivers { get; set; } = new List<CarDriver>();
}
更新
Driver
实体,添加对
CarDriver
实体的导航属性:
public class Driver : BaseEntity
{
[InverseProperty(nameof(CarDriver.DriverNavigation))]
public IEnumerable<CarDriver> CarDrivers { get; set; } = new List<CarDriver>();
}
最后,添加多对多关系的Fluent API代码:
modelBuilder.Entity<Car>()
.HasMany(p => p.Drivers)
.WithMany(p => p.Cars)
.UsingEntity<CarDriver>(
j => j
.HasOne(cd => cd.DriverNavigation)
.WithMany(d => d.CarDrivers)
.HasForeignKey(nameof(CarDriver.DriverId))
.HasConstraintName("FK_InventoryDriver_Drivers_DriverId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne(cd => cd.CarNavigation)
.WithMany(c => c.CarDrivers)
.HasForeignKey(nameof(CarDriver.CarId))
.HasConstraintName("FK_InventoryDriver_Inventory_InventoryId")
.OnDelete(DeleteBehavior.ClientCascade),
j =>
{
j.HasKey(cd => new { cd.CarId, cd.DriverId });
});
排除实体迁移
如果一个实体在多个
DbContext
之间共享,每个
DbContext
都会在迁移文件中为该实体的创建或更改生成代码。这会导致问题,因为如果数据库中已经存在这些更改,第二个迁移脚本就会失败。在EF Core 5之前,唯一的解决办法是手动编辑其中一个迁移文件以移除这些更改。
在EF Core 5中,
DbContext
可以将一个实体标记为排除在迁移之外,让另一个
DbContext
成为该实体的记录系统。以下代码展示了如何将一个实体排除在迁移之外:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<LogEntry>().ToTable("Logs", t => t.ExcludeFromMigrations());
}
使用IEntityTypeConfiguration类
随着模型变得越来越复杂,
OnModelCreating()
方法可能会变得冗长且难以维护。EF Core 6引入了
IEntityTypeConfiguration
接口和
EntityTypeConfiguration
属性,允许将实体的Fluent API配置移到单独的类中,这样可以使
ApplicationDbContext
更简洁,同时支持关注点分离的设计原则。
以下是具体的操作步骤:
1. 在
Models
目录下创建一个名为
Configuration
的新目录。
2. 在新目录中添加一个名为
CarConfiguration.cs
的新文件,使其为公共类,并实现
IEntityTypeConfiguration<Car>
接口:
namespace AutoLot.Samples.Models.Configuration;
public class CarConfiguration : IEntityTypeConfiguration<Car>
{
public void Configure(EntityTypeBuilder<Car> builder)
{
}
}
-
将
ApplicationDbContext中OnModelCreating()方法里Car实体的配置内容移到CarConfiguration类的Configure()方法中,将entity变量替换为builder变量:
public void Configure(EntityTypeBuilder<Car> builder)
{
builder.ToTable("Inventory", "dbo");
builder.HasKey(e => e.Id);
builder.HasIndex(e => e.MakeId, "IX_Inventory_MakeId");
builder.Property(e => e.Color)
.IsRequired()
.HasMaxLength(50)
.HasDefaultValue("Black");
builder.Property(e => e.PetName)
.IsRequired()
.HasMaxLength(50);
builder.Property(e => e.DateBuilt).HasDefaultValueSql("getdate()");
builder.Property(e => e.IsDrivable)
.HasField("_isDrivable")
.HasDefaultValue(true);
builder.Property(e => e.TimeStamp)
.IsRowVersion()
.IsConcurrencyToken();
builder.Property(e => e.Display).HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'",
stored: true);
builder.HasOne(d => d.MakeNavigation)
.WithMany(p => p.Cars)
.HasForeignKey(d => d.MakeId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Inventory_Makes_MakeId");
}
-
对于
Car和Driver之间的多对多关系配置,也可以选择添加到CarConfiguration类的Configure()方法末尾:
public void Configure(EntityTypeBuilder<Car> builder)
{
builder
.HasMany(p => p.Drivers)
.WithMany(p => p.Cars)
.UsingEntity<CarDriver>(
j => j
.HasOne(cd => cd.DriverNavigation)
.WithMany(d => d.CarDrivers)
.HasForeignKey(nameof(CarDriver.DriverId))
.HasConstraintName("FK_InventoryDriver_Drivers_DriverId")
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne(cd => cd.CarNavigation)
.WithMany(c => c.CarDrivers)
.HasForeignKey(nameof(CarDriver.CarId))
.HasConstraintName("FK_InventoryDriver_Inventory_InventoryId")
.OnDelete(DeleteBehavior.ClientCascade),
j =>
{
j.HasKey(cd => new { cd.CarId, cd.DriverId });
});
}
-
更新
GlobalUsings.cs文件,包含配置类的新命名空间:
global using AutoLot.Samples.Models.Configuration;
-
将
ApplicationDbContext.cs类中OnModelBuilding()方法里配置Car类和Car与Driver多对多关系的所有代码替换为以下单行代码:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
new CarConfiguration().Configure(modelBuilder.Entity<Car>());
}
-
为
Car类添加EntityTypeConfiguration属性:
[Table("Inventory", Schema = "dbo")]
[Index(nameof(MakeId), Name = "IX_Inventory_MakeId")]
[EntityTypeConfiguration(typeof(CarConfiguration))]
public class Car : BaseEntity
{
//...
}
对于
Radio
实体的Fluent API代码,也可以重复上述步骤。创建一个名为
RadioConfiguration
的新类,实现
IEntityTypeConfiguration<Radio>
接口,并将
ApplicationDbContext
中
OnModelBuilding()
方法的代码添加到其中:
namespace AutoLot.Samples.Models.Configuration;
public class RadioConfiguration : IEntityTypeConfiguration<Radio>
{
public void Configure(EntityTypeBuilder<Radio> builder)
{
builder.Property(e => e.CarId).HasColumnName("InventoryId");
builder.HasIndex(e => e.CarId, "IX_Radios_CarId").IsUnique();
builder.HasOne(d => d.CarNavigation)
.WithOne(p => p.RadioNavigation)
.HasForeignKey<Radio>(d => d.CarId);
}
}
通过以上这些方法和技巧,我们可以更灵活、高效地使用EF Core进行实体配置和数据库操作。希望本文能帮助你更好地理解和应用EF Core的相关知识。
EF Core实体配置:注解、Fluent API与关系映射详解
总结与对比
为了更清晰地展示各种配置方式的特点和适用场景,下面通过表格进行总结对比:
| 配置类型 | 优点 | 缺点 | 适用场景 |
| — | — | — | — |
| 注解 | 代码简洁,使用方便,直接在实体类上标注 | 配置灵活性相对较低,难以处理复杂场景 | 简单实体配置,快速搭建基础模型 |
| Fluent API | 功能强大,可定制性高,能覆盖所有配置需求 | 代码量可能较大,配置复杂时不易维护 | 复杂实体关系、特殊配置需求场景 |
| IEntityTypeConfiguration类 | 提高代码可维护性,实现关注点分离 | 需要额外创建类和方法 | 大型项目,实体配置复杂且需要模块化管理 |
常见问题及解决方案
在使用EF Core进行实体配置时,可能会遇到一些常见问题,下面为大家列举并给出解决方案:
1.
数据库默认值与CLR默认值冲突
:如布尔属性
IsDrivable
,数据库默认值为
true
,CLR默认值为
false
,保存
IsDrivable = false
时会被忽略。
- 解决方案:可以将属性设为可空,或者使用后备字段。具体代码示例如下:
// 使用可空属性
public class Car
{
public bool? IsDrivable { get; set; }
}
// 使用后备字段
public class Car
{
private bool? _isDrivable;
public bool IsDrivable
{
get => _isDrivable ?? true;
set => _isDrivable = value;
}
}
modelBuilder.Entity<Car>(entity =>
{
entity.Property(p => p.IsDrivable)
.HasField("_isDrivable")
.HasDefaultValue(true);
});
-
迁移文件冲突
:当一个实体在多个
DbContext之间共享时,每个DbContext都会在迁移文件中为该实体的创建或更改生成代码,导致迁移脚本冲突。-
解决方案:在EF Core 5中,可以使用
ExcludeFromMigrations()方法将实体标记为排除在迁移之外,让另一个DbContext成为该实体的记录系统。
-
解决方案:在EF Core 5中,可以使用
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<LogEntry>().ToTable("Logs", t => t.ExcludeFromMigrations());
}
-
OnModelCreating()方法冗长 :随着模型复杂度增加,OnModelCreating()方法可能会变得冗长且难以维护。-
解决方案:使用
IEntityTypeConfiguration类将实体的Fluent API配置移到单独的类中,实现关注点分离。具体步骤如下:
-
解决方案:使用
graph LR
A[在Models目录下创建Configuration目录] --> B[在Configuration目录中创建CarConfiguration.cs文件]
B --> C[实现IEntityTypeConfiguration<Car>接口]
C --> D[将OnModelCreating()中Car实体配置移到Configure()方法]
D --> E[更新GlobalUsings.cs文件包含新命名空间]
E --> F[替换OnModelBuilding()中Car配置代码]
F --> G[为Car类添加EntityTypeConfiguration属性]
最佳实践建议
在使用EF Core进行实体配置时,遵循以下最佳实践建议可以提高开发效率和代码质量:
1.
合理选择配置方式
:对于简单实体配置,优先使用注解;对于复杂实体关系和特殊配置需求,使用Fluent API;对于大型项目,使用
IEntityTypeConfiguration
类进行模块化管理。
2.
注意默认值冲突
:在设置数据库默认值时,要考虑与CLR默认值的冲突问题,提前采取解决方案。
3.
及时处理迁移问题
:遇到迁移文件冲突时,及时使用
ExcludeFromMigrations()
方法进行处理,避免后续问题。
4.
遵循命名规范
:在设置键、索引、约束等名称时,遵循统一的命名规范,提高代码可读性和可维护性。
未来趋势展望
随着技术的不断发展,EF Core也在持续更新和完善。未来可能会有以下发展趋势:
1.
性能优化
:进一步优化查询性能和内存使用,提高数据库操作的效率。
2.
更多数据库支持
:支持更多类型的数据库,满足不同用户的需求。
3.
简化配置
:提供更简洁、易用的配置方式,降低开发门槛。
4.
集成更多工具
:与更多开发工具和框架集成,提高开发体验。
通过深入学习和掌握EF Core的实体配置方法,我们可以更好地应对各种数据库操作需求,提高开发效率和代码质量。希望大家在实际项目中能够灵活运用这些知识,取得更好的开发效果。
超级会员免费看
1117

被折叠的 条评论
为什么被折叠?



