前言
此篇文章我將深入去摸索edmx中一些不為人知的東西,有時候我們需要知道Code First模型中一些存儲以及映射的原理,個人覺得那是必要的也是有用的,因為很有可能SQL會出現一些其他問題,只有掌握了一些必備的原理,這樣當報錯時才會不知所措。
原理
我們知道實體數據模型(EDM)是應用程序和數據存儲之間的溝通橋梁,同時我們通過屬性映射的API與數據存儲之間的交互都是基於EDX,所以一切我們從EDM開始談起。
我們不用操之過急,我們先了解原理之后,再通過實際來操作你就明白為什么要先了解原理的至關重要性了(可能會有點枯燥,but please take it easy!)。
先給出數據庫表對應的類(下面會用到):
public partial class Student { public int Key { get; set; } public string Name { get; set; } public int FlowerId { get; set; } public virtual Flower Flower { get; set; } }
public partial class Flower { public Flower() { this.Students = new HashSet<Student>(); } public int Id { get; set; } public string Remark { get; set; } public virtual ICollection<Student> Students { get; set; } }
下面我們一步步來進行,我們新建一個ADO.NET實體數據模型,如下:

因為之前用的Code->Database,現在我們反其道為之,通過Database->Code來獲得生成的設計架構,於是我們需要來自數據庫的EF設計器,如下:

生成了基本的架構之后,我們看到了最重要的edmx,也就是EDM中的XML文件,但是此時看到的只是類圖,我們需要通過指定XML工具來打開如下:

此時你將清楚的看到edmx中架構:

里面最重要的就是這三部分,不用說大家也能明白:概念模型定義語言(CSDL)即概念層、存儲模型定義語言(SSDL)即存儲層、概念與存儲之間的映射語言(MSL)即映射層。
接下來我們就存儲模型具體來看看里面到底有什么東西,我們看幾個重要的部分:
EntityContainer和EntitySet
<EntityContainer Name="DBByConnectionStringModelStoreContainer"> <EntitySet Name="Flower" EntityType="Self.Flower" Schema="dbo" store:Type="Tables" /> <EntitySet Name="Student" EntityType="Self.Student" Schema="dbo" store:Type="Tables" /> <AssociationSet Name="FK_dbo_Student_dbo_Flower_FlowerId" Association="Self.FK_dbo_Student_dbo_Flower_FlowerId"> <End Role="Flower" EntitySet="Flower" /> <End Role="Student" EntitySet="Student" /> </AssociationSet> </EntityContainer>
由上我們知道:EntityContainer容器的名稱是由數據庫名稱+ModelStoreContainer,並且它是 EntitySet 和 AssociationSet 的容器,而EntityContainer的作用是查詢的重要入口,通過暴露EntitySet,來使得我們查詢到EntitySet,而EntitySet是實體的集合,所以通過它訪問到實體。
EntityType
<EntityType Name="Flower"> <Key> <PropertyRef Name="Id" /> </Key> <Property Name="Id" Type="int" StoreGeneratedPattern="Identity" Nullable="false" /> <Property Name="Remark" Type="nvarchar(max)" /> </EntityType> <EntityType Name="Student"> <Key> <PropertyRef Name="Key" /> </Key> <Property Name="Key" Type="int" StoreGeneratedPattern="Identity" Nullable="false" /> <Property Name="Name" Type="nvarchar(max)" /> <Property Name="FlowerId" Type="int" Nullable="false" /> </EntityType>
實體類型是模型中的數據類型,我們可以看到里面有我們定義的Flower和Student類,並在其節點下列出了其所有屬性。
C-S Mapping
<edmx:Mappings>
<Mapping Space="C-S" xmlns="http://schemas.microsoft.com/ado/2009/11/mapping/cs">
<EntityContainerMapping StorageEntityContainer="DBByConnectionStringModelStoreContainer" CdmEntityContainer="DBByConnectionStringEntities">
<EntitySetMapping Name="Flowers">
<EntityTypeMapping TypeName="DBByConnectionStringModel.Flower">
<MappingFragment StoreEntitySet="Flower">
<ScalarProperty Name="Id" ColumnName="Id" />
<ScalarProperty Name="Remark" ColumnName="Remark" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
<EntitySetMapping Name="Students">
<EntityTypeMapping TypeName="DBByConnectionStringModel.Student">
<MappingFragment StoreEntitySet="Student">
<ScalarProperty Name="Key" ColumnName="Key" />
<ScalarProperty Name="Name" ColumnName="Name" />
<ScalarProperty Name="FlowerId" ColumnName="FlowerId" />
</MappingFragment>
</EntityTypeMapping>
</EntitySetMapping>
</EntityContainerMapping>
</Mapping>
</edmx:Mappings>
上述EntityContainerMapping中的StorageEntityContainer(存儲實體容器)對應存儲模型中EntityContainer中的Name,而EntitySetMapping下的EntityTypeMapping中的TypeName則對應概念模型的中EntityType下的Name。也就是說通過StorageEntityContainer來獲得存儲模型中EntityContainer,接着根據下面EntityType對應的值去獲得概念模型中對應的實體,此時映射層中用TypeName來獲得概念模型中的實體,接着開始進行一一對應映射,此時MappingFragment(就字面意思暫且叫映射片段),將名稱為Name的值映射為對應的列名ColumnName的值。上述大概就是整個映射過程。
這就是我簡短的介紹,更多詳細內容請參考園友三思而后行翻譯的早期EF系列。下面進入實戰。
實戰
在進入之前,我們先得了解 DataSpace 枚舉中的幾個概念:
- CSpace:概念模型的默認名稱。
- CSSpace:概念模型和存儲模型之間的映射的默認名稱。
- OCSpace:對象模型和概念模型之間的映射的默認名稱。
- OSpace:對象模型的默認名稱。
- SSpace:存儲模型的默認名稱。
在EF中我們是無法用DbContext上下文來直接獲得表名以及各種屬性等等,但是對於這所有我們可以通過上下文ObjectContext中的MetadataWrokSpace屬性來操作,因為該屬性暴露了GetItems方法來使我們很容易獲得我們想要的數據。
那問題來了,如果我們要獲得實體的所有鍵的名稱,該如何操作呢?
我們通過給上下文添加一個擴展方法 GetKeyNames 來獲取實體所有的鍵名稱,如下:
static string[] GetKeyNames(this DbContext ctx, Type entity) { var metadata = ((IObjectContextAdapter)ctx).ObjectContext.MetadataWorkspace; /*獲得CLR type集合和medata OSpace之間的映射*/ var objItemCollection = (ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace); /*根據所給的CLR type獲得實體的元數據即metadata*/ var entitydata = metadata.GetItems<EntityType>(DataSpace.OSpace).Single(e => objItemCollection.GetClrType(e) == entity); /*返回實體的所有鍵的集合*/ return entitydata.KeyProperties.Select(p => p.Name).ToArray(); }
接下來我們調用如下就獲得該實體對應的所有鍵的集合
var keyNames = ctx.GetKeyNames(typeof(Student));
通過上述想必你也明白了為什么要講 EntityType、EntityContainer以及EntitySet 了吧。我們不禁猜想如果要獲得CLR types與存儲模型之間的映射的集合應該如何操作呢?顯然,如下:
var storeItemCollection = (StoreItemCollection)metadata.GetItemCollection(DataSpace.SSpace);
我們查看我們通過映射獲得數據庫表Flower和Student的情況如下:

那問題又來了,我們該如何獲得實體對應的表名呢?
通過上述原理的介紹,既然是表名肯定此時DataSpace枚舉必須是存儲模型即SSpace,同時我們要獲得EntitySet集合中對應的Name,同時表名我們知道默認是dbo,即上述Schema對應的值再加上Table所對應的值,基於此,我們嘗試寫出相應的代碼:
static string GetTableName(this DbContext ctx, Type entity) { var metadata = ((IObjectContextAdapter)ctx).ObjectContext.MetadataWorkspace; var storeItemCollection = metadata.GetItemCollection(DataSpace.SSpace); var entitySetBase = storeItemCollection.GetItems<EntityContainer>().Single().BaseEntitySets.Where(e => e.Name == entity.Name).Single(); string tableName = entitySetBase.MetadataProperties["Schema"].Value + "." + entitySetBase.MetadataProperties["Table"].Value; return tableName; }
上述代碼就不一一解釋了,對照edmx就很清楚了,接下來我們進行測試來獲得Student的表名:
var tableName = ctx.GetTableName(typeof(Student));
測試通過,如下:

那問題又來了,我們該如何獲得實體的所有導航屬性呢?
既然是實體的導航屬性必定是要在獲取到實體的前提下再去獲取導航屬性,主要通過此 metadata.GetItems(DataSpace.OSpace) 來篩選出實體類型最后查到需要一個內置類型種類 BuiltInTypeKind 。所以我們給出完整代碼:
static IEnumerable<PropertyInfo> GetNavigationPrpoperties(this DbContext ctx, Type entity) { var metadata = ((IObjectContextAdapter)ctx).ObjectContext.MetadataWorkspace; var entityType = metadata.GetItems(DataSpace.OSpace).Where(d => d.BuiltInTypeKind == BuiltInTypeKind.EntityType).OfType<EntityType>().Where(d => d.Name == entity.Name).Single(); return (entityType.NavigationProperties.Select(d => entity.GetProperty(d.Name)).ToList()); }
我們依然進行調用,試試能否取到:
var navigationProperties = ctx.GetNavigationPrpoperties(typeof(Student));
結果成功取到Student的導航屬性Flower,如下:

總結
上述也就簡單的摸索了下底層為edmx的EF,個人感覺了解了基本原理再去寫代碼以及當代碼出現問題時去解決會得心應手,同時了解這些基本原理對於我們在EF實體框架上構建建模工具也是非常有幫助的。
