实体框架
实体框架的使用分为Model First,Code First(代码生成模型),Database First。Model First和Database First会使用实体设计器(edmx文件)来创建实体数据模型。
DbContext所使用的连接字符串如果是常规连接字符串则使用Code First,如果使用的是特殊的实体框架连接字符串,则使用Database First或Model First。
Database First:
从现有数据库构建edmx文件,然后edmx会生成DbContext和poco模型(model类),并将实体框架专用的连接字符串保存到所在项目的app.config中,但配置文件中的连接字符串并不能直接使用,连接字符串中包含有指向csdl,ssdl,msl文件的路径,自动生成的连接字符串中这3条路径包含一个“*”号,需将edmx所在类库的程序集名称替换“*”号。
Model First:
建立空的edmx文件,手动构建数据库模型,再由根据模型创建数据库和生成DbContext以及poco模型(model类)
Code First:
手写POCO,再由DbContext生成对应的数据库。
POCO中外键和导航属性应用virtual修饰(详情见导航属性的加载),以代理类便于重写。集合类型的导航属性应在构造方法中初始化为HashSet集合。
IDatabaseInitializer<TContext>(TContext是DbContext类型的实例),用于指定DbContext初次使用时用于初始化数据库的策略,使用策略须调用DbContext的DataBase属性(System.Data.Entity.Database实例)的SetInitializer方法,此初始化数据库策略在Global.asax的Application_Start方法中直接调用Database.SetInitializer静态方法,或者在DbContext的构造方法中执行Database.SetInitializer静态方法,或者在配置文件的entityFramwork/contexts的子节点配置,示例如下:
<entityFramework>
<contexts>
<context type="[DbContext类],[程序集名称]">
<databaseInitializer type="[IDatabaseInitializer实现类],[程序集名称]" />
</context>
</contexts>
</entityFramework>
可使用的初始化策略有DropCreateDatabaseIfModelChanged<TContext>,表示如果模型改变,则删除并重建数据库,Database的CompatibleWithModel方法可用于检查模型是否发生改变;DropCreateDatabaseAlways<TContext>,表示每次使用DbContext都重建数据库;CreateDatabaseIfNotExists<TContext>,表示如果数据库不存在则创建数据库。用户选择使用策略时最好继承者三个类中的一个并重写Seed方法,该方法会在数据库创建后执行,此方法称为种子方法。
System.Data.Entity.Database类,可通过DbContext的Database属性获取Database类的实例,该实例可用于检查数据库的状态或执行一些数据库操作,如数据库是否存在,模型是否改变,创建或删除数据库操作,执行sql脚本等操作。
对模型的定义可通过System.Component.DataAnnotations命名空间的特性,也可以通过Fluent API,Fluent API 的使用工具是DbModelBuilder类,此类可在DbContext的OnModelCreating方法的参数获得,通过Fluent API对模型映射所做的修改如同使用Data Annotation对模型所作的修改一样都会在数据迁移DbMigration中做出相应的反应,即数据迁移支持Fluent API和Data Annotation对模型的映射设置和修改。
Code First约定:
注:
主表(主体):具有主键的表。
从表(依赖实体):使用主表的主键作为本表的外键。
主键约定:类的“ID”名称的属性或“<类名>ID”的属性默认为模型的主键。
关系约定:模型的外键关系通过导航属性来推断。建议显示指定外键属性,外键的属性名称为“<导航属性名称>_<主体主键属性>”(如果外键属性是自动生成的,则导航属性名称与主体主键属性之间有下划线,如果是手写的可有可无)、“<主体类名><主体主键名称>”、“<主体模型名称>”。外键属性如果可为null,则外键关系可选的,且外键关系为非级联删除,如果不可为null则为必选的,并设置为级联删除。
对于无法推断出主键的引用类型的属性将作为复杂数据类型,在数据库中的体现是<复杂数据类型的属性名>_<复杂数据类型的类属性>。
当模型的导航属性无法在导航属性对应的另一个模型中找到合适的或应该的导航属性时,可使用InversePropertyAttribute手动指定导航属性对应的另一个模型中的导航属性。
当需要手动指定导航属性在模型中对应的外键属性时,使用ForeignKeyAttribute。
注意:
通过DbContext的访问类集合,返回的结果包括指定的类以及派生类,此结果在TPT策略下,通过执行外联接实现,在TPC下通过union操作实现,而正因此,导致在同时使用数据库的标识列和TPC策略下,派生类(可能多个)对应的表和基类对应的表中的记录不能具有相同的主键值,否则会导致DbContext内部调用的ObjectContext会创建重复的实体键,详情见TPC说明。
Fluent API的表映射策略:
每个继承层次结构一张表(TPH)(Fluent API的默认策略),每个类型一张表(TPT),每个具体类一张表(TPC);
单个实体对应多个表(实体拆分),将多个实体映射到一个表(表拆分)
TPH Table per Hierarachy:每个层次结构一张表,表示继承此结构中的所有类型使用同一张表,这张表使用鉴别器列区分每行对应的模型,这个鉴别器列是对应鉴别器表“Discrimination”的外键,鉴别器表中具有鉴别器值,此值即为模型的类型名称。
modelBuilder.Entity<FatherClass>()
.Map<SonClass1>(m=>m.Requires(“Type”).HasValue(“SonClass1”))
.Map<SonClass2>(m=>m.Requires(“Type”).HasValue(“SonClass2”))
如果要求多态关系或查询,并且子类声明了很少的属性甚至子类的不同在于行为的话,建议使用TPH。目标是尽可能的减少列,并且在长期需求中这种非标准的结构不会引发问题。
TPH策略中,基础类型的字段会被用设为可为空,这会导致不可为空的外键属性因成为可为空而引发问题。
TPT Table per Type:每个类型一张表,表示继承结构中的基类和派生类分别对应一张表,表中只映射对应类中定义的属性,不映射继承的属性,派生类对应的表和基类对应的表通过外键进行关联,以表示模型的继承的关系。
modelBuilder.Entity<SonClass1>().ToTable(“SonClass1”);
modelBuilder.Entity<SonClass2>().ToTable(“SonClass2”);
如果要求多态关系或查询,并且子类定义了较多的属性的话,建议使用TPT,或者如果在继承结构较复杂的情况下,join查询比union查询较节省资源的话,使用TPT,反之使用TPC。
TPC Table per Concrete Type:每个具体类一张表,表示继承层级中的基类和派生类各自对应一张表,基类对应的表与派生类对应的表没有外键关系,派生类的属性包括继承的属性均映射到数据表中。使用MapInheritedProperties方法指示将继承的属性映射到数据表。此策略与数据库的标识列存在矛盾的情况,这个表映射策略会因为多个派生类以及基类具有同名称的标识列类型的主键属性,导致DbContext内部使用的ObjectContext的跟踪机制创建了重复的实体键(EntityKey)从而导致了数据插入失败,可以关闭标识列或设置为其他类型主键来手动设置主键属性来规避这个异常。
modelBuilder.Entity<FatherClass>()
.Property(c => c.ID)
.HasDatabaeGeneratedOption(DatabaseGeneratedOption.None)//在同时保存父类和子类时虽数据表没有外键关联,但因为继承关系会引发主键相关的异常,修改为手动设置主键则不会有此问题。
modelBuilder.Entity<SonClass1>().Map(m => {
m.MapInheritedProperties();
m.ToTable(“SonClass1”);
})
modelBuilder.Entity<SonClass2>().Map(m=>{
m.MapInheritedProperties();
m.ToTable(“SonClass2”);
})
如果不要求多态关系或查询,建议使用TPC,当很少查询基类或者很少有其他类类与基类关联,并且基类很少改动的情况下建议对继承树的高层使用TPC。
注意:使用TPC的模型所继承的属性不能来自这样的基类,这个基类进行了实体拆分,或所处的表进行了表拆分。
实体拆分:将单个实体映射到多个表,通过多次调用Map方法,将不同的属性分配到不同的表中。
modelBulder.Entity<Department>()
.Map(m=>{
m.Properties(t=>new{t.DepartmentID,t.Name});
m.ToTable(“Department”);’
})
.Map(m=>{
m.Properties(t=>new{t.DepartmentID,t.Administrator,t.StartDate,t.Budget});
m.ToTable(“DepartmentDetails”);
});
表拆分:将多个实体映射到一个表,通过指定相同的主键将两个实体映射到同一个表
modelBuilder.Entity<OfficeAssignment>().HasKey(t => t.InstructorID);
modelBuilder.Entity<Instructor>().HasRequired(t=>t.OfficeAssignment).WithRequriedPrincipal(t=>t.Instructor);
modelBuilder.Entity<OfficeAssignment>().ToTable(“Instructor”);
modelBuilder.Entity<Instructor>().ToTable(“Instructor”);
表关系映射与导航属性:
使用HasMany、HasRequired、HasOptional方法设置设置模型的导航属性的类型为多、单个必须、单个可选,这些方法返回的OptionalNavigationPropertyConfiguration类中With开头的方法用于设置关系类中导航属性和主体与依赖对象的关系。
略:普通的一对一与一对多关系示例代码
多对多关系:
modelBuilder.Entity<Model1>().HasMany(t1=>t1.NaviPropertyCollection).WithMany(t2=>t2.NaviPropertiyCollection)
多对多关系的表之间会自动增加一个关系表。
modelBuilder.Entity<Model1>()
.HasMany(m1=>m1.Model2Collection)
.WithMany(m2=>m2.Model1Collection)
.Map(m=>{
m.ToTable(“Many1ToMany2”)//指定关系表的表名
m.MapLeftKey(“Many1ID”);//指定关系表中对应设置表的外键列名
m.MapRightKey(“Many2ID”);//指定关系表中对应设置表相关联的关系表的外键列名
});
级联删除:
如果外键不可为空,则默认级联删除,否则,不是级联删除,主体删除后,外键置为null
modelBuilder.Convertions.Remove<OneToManyCascadeDeleteConvertion>();//移除当一对多必选关系时默认的级联删除选项
modelBuilder.Convertions.Remove<ManyToManyCascadeDeleteConvertion>();//移除当多对多必选关系时默认的级联删除
使用WillCascadeOnDelete方法手动设置是否级联删除
modelBuilder.Entity<TModel>()
.HasRquired(t=>t.NaviProperty)
.WithMany(anotherT=>anotherT.NaviPropertyCollection)
.HasForeighKey(t=>t.ForeighId)
.WillCascadeOnDelete(false)//设置为不级联删除
DbContext编写:
添加公共的DbSet类型或IDbSet类型的属性会由DbContext默认调用设置DbSet实例,如:
public IDbSet<TModel> TModels{get;set;}
public DbSet<TModel> TModels{get;set;}
访问实例也可通过Set方法,下面两行代码等效:
DbSet<TModel> result = dbContext.TModels;
DbSet<TModel> result = dbContext.Set<TModel>();
在访问DbSet属性但又仅仅进行只读操作时,为了提高性能可禁用跟踪,通过DbSet实例的AsNoTracking方法禁用追踪。
实体关系的修改:
实体关系可通过外键属性或导航属性进行修改,但只有执行DbContext的SaveChange后,外键属性以及导航属性才会反应真是的情况,如,将实体的导航属性进行了设置,这是对应的外键属性(如果有的话)可能为0(或其他表示未指定的值),只有执行SaveChange方法后,导航属性对应的外键值才为关联的模型的主键值。外键属性与导航属性的同步会在DetectChanged方法来完成,以下方法会自动调用DetectChanges方法:
DbSet.Add,DbSet.Find,DbSet.Remove,DbSet.Local,DbContext.SaveChange,DbContext.Attach,DbContext.GetValidationErrors,DbContext.Entry,DbChagne.Tracher.Entries,DbSet执行的linq查询。
删除关系可通过将导航属性设置为null的方式进行,亦或执行如下代码:
dbContext.Entry(tModel).Reference(c=>c.NaviProperty).CurrentValue=null;
通过ObjectContext的ObjectStateManager.ChangeRelationshipState方法也可修改关系:
((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.ChangeRelationshipState(model,anotherModel,m=>m.AnotherModel,EntityState.Added);//EntityState.Deleted表示删除,如果是更新关系的话需要Entity.Added新关系并Deleted旧关系。
DbSet.Local属性说明:该属性包含所有上下文中的对象,包括那些Add的但未保存到数据库的对象;但不包括Remove的但仍在数据库中的对象。
导航属性数据的加载:
导航属性对应的数据默认是惰性加载(在访问时才加载),如果希望查询数据时一同加载关联属性,须调用DbSet的Include方法并指定要求预加载的导航属性数据。
Var result = dbContext.Models.Include(b=>b.NaviProperty);//单级加载
Var result = dbContext.Models.Include(“NaviProperty.SecondNaviProperty”);//多级加载,加载Model的导航属性NaviProperty以及导航属性的导航属性SecondNaviProperty。
导航属性的惰性加载在首次访问时自动进行加载,加载通过继承自模型类的代理类进行,该代理类会重写导航属性,因此,模型类的导航属性必须用virtual修饰,不适用virtual修饰意味着惰性加载的关闭,在DbContext的构造方法中调用Configuration属性(DbContextConfiguration实例)的LazyLoadingEnabled=false也可设置惰性加载为关闭。
显示加载通过DbReferenceEntry的Load方法执行:
dbContext.Entry(model).Reference(m=>m.NavProperty).Load();
在更改外键属性后,可通过Load的方法显示加载同步的导航属性数据。通过dbContext的Entry访问代表集合类型的导航属性的DbCollectionEntry的Query方法获取可操作的IQueryable,来实现对部分实体的导航数据进行加载的要求。
代理类:
在为POCO实体类型实例化时,实体框架会动态生成派生自POCO实体的类,成为代理类,这些代理类通过重写POCO实体的virtual成员来实现一些自动操作,如之前提到的惰性加载。DbContext的Configuration属性(DbContextConfiguration实例)的ProxyCreationEnabled=false可将关闭代理类的生成。
实体的管理:
添加实体可通过DbSet的Add方法,也可通过dbContext的Attach方法并更改返回的Entry的状态为Added的方式。
如果仅仅希望保存实体的某个字段的修改到数据库的话,可通过Entry方法获取的DbEntityEntry实例并通过Property方法获取DbPropertyEntry对象实例并修改IsModified属性为true,这样就可以在保存数据库时,仅仅将IsModified=true的属性保存到数据库。
在调用SaveChange时,可能会引发DbUpdateConcurrentException,这表示更新时的并发异常,可通过dbContext.Entry(model)获取修改实体的DbEntityEntry实例,修改OriginValue和CurrentValue的方式来尝试消除引发并发的数据,DbEntityEntry的GetDatabaseValue方法可用来获取当前数据库中的数据,在重写原始值(OriginValue)或当前值(CurrentValue)时可能会使用到。
>>编辑时间2015-10-01