在我們使用EntityFrameworkCore作為數據庫ORM框架的時候,不可避免的要重載DbContext中的一個虛方法OnModelCreating,那么這個方法到底是做什么的?到底有哪些作用呢?帶着這些問題我們來看看在EntityFrameworkCore到底該如何使用OnModelCreating這個方法,首先我們來看看Microsoft.EntityFrameworkCore命名空間下面的DbContext中關於OnModelCreating的定義及注釋。
/// <summary> /// Override this method to further configure the model that was discovered by convention from the entity types /// exposed in <see cref="T:Microsoft.EntityFrameworkCore.DbSet`1" /> properties on your derived context. The resulting model may be cached /// and re-used for subsequent instances of your derived context. /// </summary> /// <remarks> /// If a model is explicitly set on the options for this context (via <see cref="M:Microsoft.EntityFrameworkCore.DbContextOptionsBuilder.UseModel(Microsoft.EntityFrameworkCore.Metadata.IModel)" />) /// then this method will not be run. /// </remarks> /// <param name="modelBuilder"> /// The builder being used to construct the model for this context. Databases (and other extensions) typically /// define extension methods on this object that allow you to configure aspects of the model that are specific /// to a given database. /// </param> protected internal virtual void OnModelCreating(ModelBuilder modelBuilder) { }
按照官方的解釋:在完成對派生上下文的模型的初始化后,並在該模型已鎖定並用於初始化上下文之前,將調用此方法。 雖然此方法的默認實現不執行任何操作,但可在派生類中重寫此方法,這樣便能在鎖定模型之前對其進行進一步的配置。通常,在創建派生上下文的第一個實例時僅調用此方法一次, 然后將緩存該上下文的模型,並且該模型適用於應用程序域中的上下文的所有后續實例另外在做數據庫遷移生成遷移文件的時候也會調用OnModelCreating方法。通過在給定的 ModelBuidler 上設置 ModelCaching 屬性可禁用此緩存,但注意這樣做會大大降低性能。 通過直接使用 DbModelBuilder 和 DbContextFactory 類來提供對緩存的更多控制。那么這些深度的自定義配置有哪些方面的內容呢?
一 通過Fluent API配置實體
這個部分是整個配置的重點,我們能夠通過Fluent API來進行各種各樣的配置,下面通過一系列的例子來加以說明,這里推薦一篇非常好的文章,這里便不再贅述,這里只講述之前不同的部分,從而使整篇文章更加完整,如果你對數據庫中的關系映射不太清楚,請閱讀這篇文章。
A 設置主外鍵關系
#region Sales.Basis 主鍵及關系設定 modelBuilder.Entity<DealerMarketDptRelation>() .HasOne(d => d.MarketingDepartment) .WithMany(e => e.DealerMarketDptRelations) .HasForeignKey(d => d.MarketId); modelBuilder.Entity<Company>() .HasOne(c => c.Branch) .WithOne(d => d.Company) .HasForeignKey<Branch>(d => d.Id); modelBuilder.Entity<Company>() .HasOne(c => c.Dealer) .WithOne(d => d.Company) .HasForeignKey<Dealer>(d => d.Id); #endregion
B 設置索引
modelBuilder.Entity<RepairContract>(r => { r.HasIndex(p => p.Code); r.HasIndex(p => p.Vin); r.HasIndex(p => p.DealerCode); r.HasIndex(p => p.DealerName); r.HasIndex(p => p.CreateTime); r.HasIndex(p => p.ModifyTime); });
在查詢數據庫的時候我們經常需添加索引,在Fluent API中我們可以通過上面的方式來為某一個表添加索引。
C 創建Sequence
modelBuilder.HasSequence("S_PdiCheck"); modelBuilder.HasSequence("S_PdiCheckItem"); modelBuilder.HasSequence("S_PdiCheckItemDetail"); modelBuilder.HasSequence("S_PdiCheckAth"); modelBuilder.HasSequence("S_CancellationHandlingTime"); modelBuilder.HasSequence("S_DealerBlacklist"); modelBuilder.HasSequence("S_PropagateMethod"); modelBuilder.HasSequence("S_VehicleOwnerChange"); modelBuilder.HasSequence("S_CustomerVehicleAth"); modelBuilder.HasSequence("S_DealerVehicleStockSnapshots");
D 設置表級聯刪除
/// <summary> /// 調整生成器的默認行為 /// <para/>生成的表名稱為單數,外鍵關閉級聯刪除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction(this ModelBuilder modelBuilder) { foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null) continue; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach (var foreignKey in mutableEntityType.GetForeignKeys()) {
//設置表級聯刪除方式 foreignKey.DeleteBehavior = DeleteBehavior.Restrict;
} } }
但是有的時候我們由於業務的需要又需要開啟級聯刪除怎么辦,當然我們可以通過OnDelete方法進行刪除,就像下面的例子
modelBuilder.Entity<RepairSettlement>(b => { b.HasMany(rs => rs.RepairSettlementWorkItems) .WithOne(rsi => rsi.RepairSettlement) .OnDelete(DeleteBehavior.Cascade); });
這里表示的是一組具有主清單關系的例子,其中一個RepairSettlement實體里面可以包含多個RepairSettlementWorkItem實體,這個我們可以在RepairSettlement中定義一個List<RepairSettlementWorkItem>來表示兩者之間的關系,然后通過OnDelete方法就會實現兩者之間的級聯刪除關系。
但是如果一個系統中存在大量這種主清單關系需要進行級聯刪除的話,那么我們的OnModelCreating中就會存在大量這種重復代碼,那么有不有一種更加合適的方法用於指定兩者之間的級聯刪除關系呢?
答案是利用自定義屬性,直接在實體的外鍵上面定義好級聯刪除關系,這樣就能最大程度上簡化代碼。
1 定義ForeignKeyReferenceAttribute
/// <summary> /// 當前自定義屬性用於當我們需要開啟實體之間的級聯刪除行為時候使用 /// </summary> public class ForeignKeyReferenceAttribute : Attribute { /// <summary> /// 設置默認的級聯刪除方式 /// </summary> public ForeignKeyReferenceAttribute() { DeleteBehavior = DeleteBehavior.Restrict; } /// <summary> /// 設置當前外鍵的級聯刪除方式 /// </summary> public DeleteBehavior DeleteBehavior { get; set; } }
2 修改設置級聯刪除的方式
/// <summary> /// 調整生成器的默認行為 /// <para/>生成的表名稱為單數,外鍵關閉級聯刪除 /// </summary> /// <param name="modelBuilder"></param> public static void AdjustDbDefautAction(this ModelBuilder modelBuilder) { foreach (var mutableEntityType in modelBuilder.Model.GetEntityTypes()) { if (mutableEntityType.ClrType == null) continue; if (mutableEntityType.ClrType.GetCustomAttribute<TableAttribute>() == null) mutableEntityType.Relational().TableName = mutableEntityType.ClrType.Name; foreach (var foreignKey in mutableEntityType.GetForeignKeys()) { var canCascadeDelete = foreignKey.Properties[0]?.PropertyInfo?.GetCustomAttributes<ForeignKeyReferenceAttribute>()?.SingleOrDefault(); foreignKey.DeleteBehavior = canCascadeDelete?.DeleteBehavior ?? DeleteBehavior.Restrict; } } }
這里不再統一設置foreignKey.DeleteBehavior = DeleteBehavior.Restrict;而是根據當前外鍵定義的ForeignKeyReferenceAttribute值來決定當前外鍵的級聯刪除關系。
3 設置外鍵級聯刪除關系
通過上面的例子我們就可以在表RepairSettlementWorkItem中的外鍵RepairSettlementId上面設置外鍵級聯刪除關系。
[ForeignKeyReference(DeleteBehavior = DeleteBehavior.Cascade)] public Guid RepairSettlementId { get; set; }
E 設置不同數據庫專有特性
由於數據庫有多種類型,所以不可避免我們需要針對特定數據庫來做一些配置,比如Oracle數據庫會限定列名最大為30個字符,有比如有些實體中定義的屬性,比如Level可能在Oracle數據庫中作為一種關鍵字,而在另外的數據庫系統中則沒有這個限制,所以我們通過modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName("FLevel"))這種方式來將實體屬性名稱設置為FLevel,針對這個方面我們也可以在OnModelCreating中統一進行處理。
if (Database.IsOracle()) { // Oracle 要求列名不超過30個字符 modelBuilder.Entity<NotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName("EntityTypeAssyQualifiedName"); }); modelBuilder.Entity<NotificationSubscriptionInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName("EntityTypeAssyQualifiedName"); }); modelBuilder.Entity<TenantNotificationInfo>(entity => { entity.Property(e => e.EntityTypeAssemblyQualifiedName) .HasColumnName("EntityTypeAssyQualifiedName"); }); // 日志的時間需要精確 modelBuilder.Entity<AuditLog>(entity => { entity.Property(e => e.ExecutionTime).HasColumnType("TimeStamp"); }); modelBuilder.Entity<EntityChange>(entity => { entity.Property(e => e.ChangeTime).HasColumnType("TimeStamp"); }); modelBuilder.Entity<EntityChangeSet>(entity => { entity.Property(e => e.CreationTime).HasColumnType("TimeStamp"); entity.Property(e => e.ExtensionData).HasMaxLength(int.MaxValue); }); foreach (var entity in modelBuilder.Model.GetEntityTypes()) { if (entity.ClrType == null) continue; if (!entity.ClrType.GetAttributes<TableAttribute>().Any()) { entity.Relational().TableName = entity.ClrType.Name; } foreach (var property in entity.GetProperties().Where(p => p.PropertyInfo != null)){ if (property.PropertyInfo.Name.ToUpper().EndsWith("ID") && property.ClrType == typeof(string)) { property.Relational().ColumnType = "CHAR(36)"; } else if (property.Name == "RowVersion" && (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))) { property.Oracle().ColumnType = "TIMESTAMP"; } else if (property.ClrType == typeof(decimal) || property.ClrType == typeof(decimal?)) { property.Oracle().ColumnType = "NUMBER(16,4)"; } } } modelBuilder.Entity<ClaimApplyAth>(d => d.Property(p => p.FileId).HasColumnType("VARCHAR2(200)")); modelBuilder.Entity<MarketQualityAth>(d => d.Property(p => p.FileId).HasColumnType("VARCHAR2(200)")); modelBuilder.Entity<FaultCategoryType>(d => d.Property(p => p.Level).HasColumnName("FLevel")); }
再比如表中常用的RowVersion字段在Oracle中實體可定義為DateTime類型,而在SQL Server中就只能夠定義為byte[ ]這種方式了,所以在使用的時候都可以通過上面的方式來統一進行處理。
/// <summary> /// dotConnect的默認DateTime類型是 TimeStamp,無長度限制的String 是 Clob /// </summary> /// <param name="modelBuilder"></param> public static void AdjustOracleDefaultAction(this ModelBuilder modelBuilder) { foreach (var item in modelBuilder.Model .GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof(string) || NoneAnnotationDateTime(p)) .Select(p => new { p.ClrType, p.Name, Pb = modelBuilder.Entity(p.DeclaringEntityType.ClrType).Property(p.Name), MaxLength = p.GetMaxLength() })) { if (item.ClrType == typeof(DateTime?)) { item.Pb.HasColumnType("date"); } else if (item.Name == "Discriminator") item.Pb.HasMaxLength(100); // ReSharper disable once PossibleInvalidOperationException else if (item.ClrType == typeof(string) && item.MaxLength == null) item.Pb.HasMaxLength(2000); } }
這里通過一些實際項目中經驗來講述EntityFrameworkCore的一些特性,后續有進一步的新的內容也會不斷加入,從而使文章內容更加豐富。