EF里的默认映射以及如何使用Data Annotations和Fluent API配置数据库的映射
I.EF里的默认映射
上篇文章演示的通过定义实体类就可以自动生成数据库,并且EF自动设置了数据库的主键、外键以及表名和字段的类型等,这就是EF里的默认映射。具体分为:
- 数据库映射:Code First 默认会在本地的SQL Expression数据库中建立一个和DbContext的子类的全名相同的数据库,全名指的是命名空间加上类名;
- 表映射:Code First 默认会按照类型名复数建立数据表,比如说Destination类对应的表名就叫Destinations;
- 列映射:Code First 默认会按照类中的属性名建立column,它还有默认的数据类型映射习惯,int会映射为interger,string会映射为 nvarchar(max),decimal会映射为decimal(18,2);
- 主键映射:Code First 默认会在类的属性中需找名字为Id或类型名称+Id的int类型的属性作为主键,并且是自增字段。
摘自这里
默认的映射一般只是简单映射,方便使用罢了。当然这些都是可以进行修改的,请往下看。
II.使用Data Annotations和Fluent API配置数据库的映射
Data Annotations翻译过来就是数据注解,是通过直接在实体类的属性上加注类似标签的东西达到对数据库的映射;
Fluent API翻译过来就是流利的API,Fluent API是在DbContext中定义数据库配置的一种方式。要使用Fluent API 就必须在你自定义的继承自DbContext的类中重载OnModelCreating这个方法。注意:
- Data Annotations和Fluent API任选其一就可以了,不需要同时配置;
- 使用Data Annotations需添加引用:using System.ComponentModel.DataAnnotations; 更具体的引用请参考本章结尾提供的源码。
实战:
1.Data Annotations:
设置Destination表的Name不为null:
[Required] public string Name { get; set; }
很简单,直接在属性上加[Required]标注即可;
2.Fluent API:
用Fluent api必须重写OnModelCreating方法,我们在上下文类里重写下OnModelCreating方法并添加不为空的配置:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<CodeFirst.Model.Destination>().Property(d => d.Name).IsRequired(); }
方法分析:先找到需要配置的实体类,然后点Property就是点出属性,=>是lambda表达式的写法,找到Name属性,然后调用IsRequired方法设置不为null。初见这东西肯定不好理解,多写写就熟悉了。
两种配置方式个人更喜欢Fluent API的方式,故放出的demo中Data Annotations也是有的,不过都被注释了。随着开发的深入,有些东西还是必须用Fluent API的方式才能配置出来的。
注意:我的类库都修改了默认命名空间(右键类库 - 属性),都加了个CodeFirst. 方便区分,其他类库中调用,我也是习惯用完整的命名空间.类库再.实体类来调用。
思考:使用Fluent API方式配置,每次都需要在OnModelCreating方法里写上一行配置,这样一个实体类如果有3个属性需要配置,10个实体类就需要配置30个,那么就得在OnModelCreating方法里写30行,很麻烦且不易维护。
解决办法:注意返回值可以看出modelBuilder的Entity<>泛型方法的返回值是EntityTypeConfiguration<>泛型类。我们可以定义一个继承自EntityTypeConfiguration<>泛型类的类来定义domain中每个类的数据库配置。
ok,我们在DataAccess类库下新建一个继承自EntityTypeConfiguration<>泛型类的DestinationMap类,在构造函数里写上配置:
public class DestinationMap : EntityTypeConfiguration<CodeFirst.Model.Destination> { public DestinationMap() { Property(d => d.Name).IsRequired(); } }
需添加引用:using System.Data.Entity.ModelConfiguration;
这样,以后Destination需要配置的都来这里配置,然后添加到上下文中的OnModelCreating方法即可:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new DestinationMap()); }
其他类的配置也是这样,先添加一个类名+Map的方法(命名随意),然后添加到OnModelCreating方法。
随意配置一些属性,然后跑下程序看生成的数据库:
可见:Name列已经不可空,Description也设置了长度不超过500等等。
注意:如果重新跑程序生成数据库报这个错:
The model backing the 'BreakAwayContext' context has changed since the database was created. Either manually delete/update the database, or call Database.SetInitializer with an IDatabaseInitializer instance. For example, the DropCreateDatabaseIfModelChanges strategy will automatically delete and recreate the database, and optionally seed it with new data.
意思就是,当前数据库正在使用中,无法删除再重新生成。这个时候断开数据库连接,再跑下程序就可以了。以后每次配置实体类再重新跑程序都需要断开数据库连接。
不知道大家还记不记得第一篇文章讲的EdmMetadata这个表,EF会自动生成这个表。这个表是监控实体类变化的,其实是个诟病,每次操作数据库都会发访问这张表的sql到数据库,用sql Profiler跟踪下即可发现如下sql:
2013.08.07补充:感谢园长dudu在回复中(4楼)的纠正,不是每次操作数据库都会发送sql到数据库,只是在EF初始化的时候会发送访问EdmMetadata表的sql到数据库。另推荐dudu的两篇文章供大家阅读:让Entity Framework不再私闯sys.databases 揭开Entity Framework LINQ查询的一点面纱
SELECT TOP (1) [Extent1].[Id] AS [Id], [Extent1].[ModelHash] AS [ModelHash] FROM [dbo].[EdmMetadata] AS [Extent1] ORDER BY [Extent1].[Id] DESC
注:sql Profiler是一个监控发送到数据库sql语句的工具。sql server 2008自带,操作简单,如果不会请自行搜索相关资料。如果没有这个工具,那么是数据库版本不对或者数据库装出了问题。sql Profiler这工具后面讲一对一、一对多、多对多的各种操作时会经常使用到,也是我们调试EF语句性能的好帮手:
扩展:可以在OnModelCreating方法里添加:
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();//移除复数表名的契约 modelBuilder.Conventions.Remove<IncludeMetadataConvention>();//防止黑幕交易 要不然每次都要访问
需引入命名空间:
using System.Data.Entity.ModelConfiguration.Conventions; using System.Data.Entity.Infrastructure;
第一句是移除复数表名的契约,就是EF默认生成的表名都是实体类的复数形式,有这句,表名就是实体类的名字了,不会再加个s了,当然你也可以通过强大的Fluent API配置这个。
第二句就是移除对EdmMetadata表的访问的,下次再操作数据库,就不会有访问EdmMetadata表的sql了。
注意:如果之前已经有数据库了,那么再加上移除对EdmMetadata表访问的配置再跑程序会报一个NotSupportedException的错:
Model compatibility cannot be checked because the EdmMetadata type was not included in the model. Ensure that IncludeMetadataConvention has been added to the DbModelBuilder conventions.
这个时候,先把BreakAwayConfigFile数据库分离,然后在本地删除掉数据库文件,再跑程序就可以了。如果不知道sql server的mdf和ldf文件的默认路径,请自行搜索。
结果跑了下程序,的确没有产生对EdmMetadata表访问的sql了,但是sql Profiler监控到了更多的sql语句被发送到了数据库。当初为了少一个访问EdmMetadata表的sql,结果多了更多的sql,并且如果再次生成数据库还必须要手动分离现有数据库并删除硬盘上的数据库文件。这里使不使用还是有待商量的,暂时注释掉:
//modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
思考:EF小组设计EdmMetadata表肯定有它的道理,移除对其的访问也不一定是科学的。这里仅做学习演示,真正的项目中可能也很少用Code First的方式生成数据库的。至少4.1版本下的EF使用Code First是有缺陷的,每次都要干掉所有数据,这个肯定不合适。后续版本的EF有数据迁徙的功能能解决这个,目前只是学习4.1这个经典的EF版本。
补充:实际开发中还是database First的方式比较主流。
上面只演示了简单的Data Annotations和Fluent API的一些简单配置,其实还有很多,下面列出部分常用,初学者应该都试着敲敲,然后对照着生成的数据库好好学习下如何配置:
//【主键】
//Data Annotations:
[Key]
public int DestinationId { get; set; }
//Fluent API:
public class BreakAwayContext : DbContext
{
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Destination>().HasKey(d => d.DestinationId);
}
}
//【外键】
//Data Annotations:
public int DestinationId { get; set; }
[ForeignKey("DestinationId")]
public Destination Destination { get; set; }
//Fluent API:
modelBuilder.Entity<Lodging>().HasRequired(p => p.Destination).WithMany(p=>p.Lodgings).HasForeignKey(p => p.DestinationId);
//【长度】
//Data Annotations:通过StringLength(长度),MinLength(最小长度),MaxLength(最大长度)来设置数据库中字段的长度
[MinLength(10),MaxLength(30)]
public string Name { get; set; }
[StringLength(30)]
public string Country { get; set; }
//Fluent API:没有设置最小长度这个方法
modelBuilder.Entity<Destination>().Property(p => p.Name).HasMaxLength(30);
modelBuilder.Entity<Destination>().Property(p => p.Country).HasMaxLength(30);
//【非空】
//Data Annotations:
[Required(ErrorMessage="请输入描述")]
public string Description { get; set; }
//Fluent API:
modelBuilder.Entity<Destination>().Property(p => p.Country).IsRequired();
//【数据类型】
Data Annotations:
将string映射成ntext,默认为nvarchar(max)
[Column(TypeName = "ntext")]
public string Owner { get; set; }
//Fluent API:
modelBuilder.Entity<Lodging>().Property(p => p.Owner).HasColumnType("ntext");
//【表名】
//Data Annotations:
[Table("MyLodging")]
public class Lodging
{
}
//Fluent API
modelBuilder.Entity<Lodging>().ToTable("MyLodging");
//【列名】
//Data Annotations:
[Column("MyName")]
public string Name { get; set; }
//Fluent API:
modelBuilder.Entity<Lodging>().Property(p => p.Name).HasColumnName("MyName");
//【自增长】
//Data Annotations
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] Guid类型的主键、自增长
public Guid SocialId { get; set; }
//Fluent API:
modelBuilder.Entity<Person>().Property(p => p.SocialId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
//【忽略列映射】
//Data Annotations:
[NotMapped]
public string Name
{
get
{
return FirstName + " " + LastName;
}
}
//Fluent API:
modelBuilder.Entity<Person>().Ignore(p => p.Name);
//【忽略表映射】
//Data Annotations:
[NotMapped]
public class Person
{ }
//Fluent API:
modelBuilder.Ignore<Person>();
//【时间戳】
//Data Annotations:Timestamp
[Timestamp]
public Byte[] TimeStamp { get; set; } 只能是byte类型
//Fluent API:
modelBuilder.Entity<Lodging>().Property(p => p.TimeStamp).IsRowVersion();
//【复杂类型】
//Data Annotations:
[ComplexType]
public class Address
{
public string Country { get; set; }
public string City { get; set; }
}
//Fluent API:
modelBuilder.ComplexType<Address>();
部分摘自这里
目前阶段看这个的确稍显复杂,仅做了解和方便后期查阅。后续讲一对一、一对多和多对多的关系时反复的手写Fluent API的时候就会很好理解了。