目录
EF
EF简介
-
ORM:Object Relation Mapping ,通俗说:用操作对象的方式来操作数据库。
-
插入数据库不再是执行Insert,而是类似于Person p = new Person();p.Age=3;p.Name="英莱特";db.Save(p);这样的做法。
-
ORM工具有很多Dapper、PetaPoco、NHibernate,最首推的还是微软官方的Entity Framework,简称EF。
-
EF底层仍然是对ADO.Net的封装。EF支持SQLServer、MYSQL、Oracle、Sqlite等所有主流数据库。
-
使用EF进行数据库开发的时候有两个东西建:建数据库(T_Persons),建模型类(Person)。根据这两种创建的先后顺序有EF的三种创建方法:
DataBase First(数据库优先):先创建数据库表,然后自动生成EDM文件,EDM文件生成模型类。简单展示一下DataBase First 的使用。
Model First(模型优先):先创建Edm文件,Edm文件自动生成模型类和数据库;
Code First(代码优先):程序员自己写模型类,然后自动生成数据库。没有Edm。
DataBase First 简单、方便,但是当项目大了之后会非常痛苦;
Code First 入门门槛高,但是适合于大项目。
Model First……
无论哪种First,一旦创建好了数据库、模型类之后,后面的用法都是一样的。业界都是推荐使用Code First,新版的EF中只支持Code First,因此我们这里只讲Code First。
-
Code First的微软的推荐用法是程序员只写模型类,数据库由EF 帮我们生成,当修改模型类之后,EF 使用“DB Migration”自动帮我们更改数据库。但是这种做法太激进,不适合很多大项目的开发流程和优化,只适合于项目的初始开发阶段。Java的Hibernate 中也有类似的DDL2SQL 技术,但是也是用的较少。“DB Migration”也不利于理解EF,因此在初学阶段,我们将会禁用“DB Migration”,采用更实际的“手动建数据库和模型类”的方式。
-
如果大家用过 NHibernate 等ORM 工具的话,会发现开发过程特别麻烦,需要在配置文件中指定模型类属性和数据库字段的对应关系,哪怕名字完全也一样也要手动配置。使用过Java 中Struts、Spring 等技术的同学也有过类似“配置文件地狱”的感觉。 像ASP.Net MVC 一样,EF 也是采用“约定大于配置”这样的框架设计原则,省去了很多配置,能用约定就不要自己配置。
在.Net Framework SP1微软包含一个实体框架(Entity Framework),此框架可以理解成微软的一个ORM产品。用于支持开发人员通过对概念性应用程序模型编程(而不是直接对关系存储架构编程)来创建数据访问应用程序。目标是降低面向数据的应用程序所需的代码量并减轻维护工作。
Entity Framework应用程序有以下优点:
-
应用程序可以通过更加以应用程序为中心的概念性模型(包括具有继承性、复杂成员和关系的类型)来工作。
-
应用程序不再对特定的数据引擎或存储架构具有硬编码依赖性。
-
可以在不更改应用程序代码的情况下更改概念性模型与特定于存储的架构之间的映射。
-
开发人员可以使用可映射到各种存储架构(可能在不同的数据库管理系统中实现)的一致的应用程序对象模型。
-
多个概念性模型可以映射到同一个存储架构。
-
语言集成查询支持可为查询提供针对概念性模型的编译时语法验证。
实体框架Entity Framework是 DO.NET中的一组支持开发面向数据的软件应用程序的技术。在EF中的实体数据模型(EDM)由以下三种模型和具有相应文件扩展名的映射文件进行定义。
-
概念架构定义语言文件 (.csdl) -- 定义概念模型。
-
存储架构定义语言文件 (.ssdl) -- 定义存储模型(又称逻辑模型)。
-
映射规范语言文件 (.msl) -- 定义存储模型与概念模型之间的映射。
实体框架使用这些基于XML的模型和映射文件将对概念模型中的实体和关系的创建、读取、更新和删除操作转换为数据源中的等效操作。EDM甚至支持将概念模型中的实体映射到数据源中的存储过程。它提供以下方式用于查询 EDM 并返回对象:
-
LINQ to Entities--提供语言集成查询(LINQ)支持用于查询在概念模型中定义的实体类型。
-
Entity SQL -- 与存储无关的SQL方言,直接使用概念模型中的实体并支持诸如继承和关系等 EDM 功能。
-
查询生成器方法 --可以使用LINQ风格的查询方法构造 Entity SQL 查询。
相关知识复习
-
var类型推断:var p =new Person();
-
匿名类型。var a =new {p.Name,Age=5,Gender=p.Gender,Name1=a.Name};//{p.Name}=={Name=p.Name}
-
给新创建对象的属性赋值的简化方法:Person p = new Person{Name="tom",Age=5};等价于Person p = new Person();p.Name="tom";p.Age=5;
-
lambda表达式:
lambda表达式
函数式编程,在Entity framework编程中用的很多
Action<int> al= delegate(int i) { Console.Writeline(i); };
可以简化成(=>读作goes to) :
Action< int> a2 = (inti) = > { Console.Writeline(i); };
还可以省略参数类型(编译器会自动根据委托类型推断):
Action< int> a3 = (i) = > { Console.Writeline(i); };
如果只有一个参数还可以省略参数的小括号(多个参数不行)
Action<int> a4 = i = > { Console.Writeline(i); };
如果委托有返回值,并且方法体只有一行代码,这一行代码还是返回值,那么就可以连方法的大括号和return都省略:
Func<int,int,string> fl= delegate(int i, int j) { return "结果是" + (i + j); };
Func<int,int,string> f2= (i,j)=>"结果是"+ (i+ j);
集合常用扩展方法
where (支持委托)、Select (支持委托)、Max 、Min 、OrderBy
First (获取第一个,如果一个都没有则异常)
FirstOrDefault (获取第一个,如果—个都没有则返回默认值)
Single (获取唯一一个,如果没有或者有多个则异常)
SingleOrDefoult (获取唯一一个, 如果没有则返回默认值,如果有多个则异常)
注意lambda中照样要避免变量重名的问题:var p =persons.Where(p => p.Name =="yltedu.com").First();
高级集合扩展方法
//学生
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public bool Gender { get; set; }
public int Salary { get; set; }
public override string ToString()
{
return string.Format("Name={0},Age={1},Gender={2},Salary={3}",Name, Age, Gender, Salary);
}
}
//老师
public class Teacher
{
public Teacher()
{
this.Students=new List<Person>();
}
public string Name { get; set; }
public List<Person> Students { get; set; }
}
var s0 =new Person { Name="tom",Age=3,Gender=true,Salary=6000};
var s1 = new Person { Name = "jerry", Age = 8, Gender = true, Salary = 5000 };
var s2 = new Person { Name = "jim", Age = 3, Gender = true, Salary = 3000 };
var s3 = new Person { Name = "lily", Age = 5, Gender = false, Salary = 9000 };
var s4 = new Person { Name = "lucy", Age = 6, Gender = false, Salary = 2000 };
var s5 = new Person { Name = "kimi", Age = 5, Gender = true, Salary = 1000 };
List<Person> list = new List<Person>();
list.Add(s0);
list.Add(s1);
list.Add(s2);
list.Add(s3);
list.Add(s4);
list.Add(s5);
Teacher t1 = new Teacher { Name="英莱特.net"};
t1.Students.Add(s1);
t1.Students.Add(s2);
Teacher t2 = new Teacher { Name = "英莱特Python" };
t2.Students.Add(s2);
t2.Students.Add(s3);
t2.Students.Add(s5);
Teacher[] teachers = { t1,t2};
-
Any(),判断集合是否包含元素,返回值是bool,一般比Cout()>0 效率高。Any还可以指定条件表达式。
bool b = list.Any(p => p.Age > 50); 等价于bool b =list.Where(p=>p.Age>50).Any();
-
Distinct(),剔除完全重复数据。(*)注意自定义对象的Equals 问题:需要重写Equals 和GetHashCode 方法来进行内容比较。
-
排序:升序list.OrderBy(p=>p.Age);降序list.OrderByDescending(p=>p.Age)。指定多个排序规 则,而不是多个OrderBy,而是:list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary),也支 持ThenByDescending()。注意这些操作不会影响原始的集合数据。
-
Skip(n)跳过前n条数据;
-
Take(n)获取最多n条数据,如果不足n条也不会报错。常用来分页获取数据。
-
list.Skip(3).Take(2)跳过前3条数据获取2条数据。
-
Except(items1)排除当前集合中在items1中存在的元素。用int数组举例。
-
Union(items1)把当前集合和items1中组合。用int 数组举例。
-
Intersect(items1) 把当前集合和items1 中取交集。用int 数组举例。
-
分组:
foreach(var g in list.GroupBy(p => p.Age)) { Console.WriteLine(g.Key+":"+g.Average(p=>p.Salary)); }
-
SelectMany:把集合中每个对象的另外集合属性的值重新拼接为一个新的集合
foreach(var s in teachers.SelectMany(t => t.Students)) { Console.WriteLine(s);//每个元素都是Person } }
注意不会去重,如果需要去重要自己再次调用Distinct()
-
Join
class Master { public long Id { get; set; } public string Name { get; set; } } class Dog { public long Id { get; set; } public long MasterId { get; set; } public string Name { get; set; } } Master m1 = new Master { Id = 1, Name = "英莱特" }; Master m2 = new Master { Id = 2, Name = "比尔盖茨" }; Master m3 = new Master { Id = 3, Name = "周星驰" }; Master[] masters = { m1,m2,m3}; Dog d1 = new Dog { Id = 1, MasterId = 3, Name = "旺财" }; Dog d2 = new Dog { Id = 2, MasterId = 3, Name = "汪汪" }; Dog d3 = new Dog { Id = 3, MasterId = 1, Name = "京巴" }; Dog d4 = new Dog { Id = 4, MasterId = 2, Name = "泰迪" }; Dog d5 = new Dog { Id = 5, MasterId = 1, Name = "中华田园" }; Dog[] dogs = { d1, d2, d3, d4, d5 };
Join 可以实现和数据库一样的Join 效果,对有关联关系的数据进行联合查询 下面的语句查询所有Id=1 的狗,并且查询狗的主人的姓名。
var result = dogs.Where(d => d.Id > 1).Join(masters, d => d.MasterId, m => m.Id,(d,m)=>new {DogName=d.Name,MasterName=m.Name}); foreach(var item in result) { Console.WriteLine(item.DogName+","+item.MasterName); }
EF 的安装
-
基础阶段用控制台项目。使用NuGet 安装EntityFramework。会自动在App.config中增加两个entityFramework 相关配置段;
-
在 web.config 中配置连接字符串
<add name="conn1" connectionString="Data Source=.;Initial Catalog=test1;UserID=sa;Password=123" providerName="System.Data.SqlClient" />
易错点:不能忘了写providerName="System.Data.SqlClient"增加两个entityFramework 相关配置段;
EF 简单DataAnnotations 实体配置
-
数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime字段。
-
创建Person类[Table("T_Persons")]因为类名和表名不一样,所以要使用Table标注
[Table("T_Persons")] public class Person { public long ID { get; set; } public string Name { get; set; } public DateTime CreateTime { get; set; } }
因为EF约定主键字段名是Id,所以不用再特殊指定Id是主键,如果非要指定就指定[Key]。因为字段名字和属性名字一致,所以不用再特殊指定属性和字段名的对应关系,如果需要特殊指定,则要用[Column("Name")]
(*)必填字段标注[Required]、字段长度[MaxLength(5)]、可空字段用int?、如果字段在数据库有默认值,则要在属性上标注[DatabaseGenerated]注意实体类都要写成public,否则后面可能会有麻烦。
-
创建DbContext类(模型类、实体类)
public class MyDBContext: DbContext { //表示使用连接字符串中名字为conn1 的去连接数据库 public MyDBContext() : base("name=strcon") { } //通过对Persons 集合的操作就可以完成对T_Persons的操作 public DbSet<Person> Persons { get; set; } }
-
测试
protected void Button1_Click(object sender, EventArgs e) { MyDBContext context = new MyDBContext(); Person p=new Person(); p.Name =TextBox1.Text; p.CreateTime = DateTime.Now; context.Persons.Add(p); context.SaveChanges(); }
注意:MyDbContext 对象是否需要using有争议,不using也没事。每次用的时候new MyDbContext就行,不用共享同一个实例,共享反而会有问题。SaveChanges()才会把修改更新到数据库中。
EF的开发团队都说要using DbContext,很多人不using,只是想利用LazyLoad 而已,但是那样做是违反分层原则的。我的习惯还是using。
异常的处理:如果数据有错误可能在SaveChanges()的时候出现异常,一般仔细查看异常信息或者一直深入一层层的钻InnerException 就能发现错误信息。
举例:创建一个Person对象,不给Name、CreateDateTime赋值就保存。
EF 模型的两种配置方式
EF 中的模型类的配置有DataAnnotations、FluentAPI 两种。
上面这种在模型类上[Table("T_Persons")]、[Column("Name")]这种方式就叫DataAnnotations这种方式比较方便,但是耦合度太高,一般的类最好是POCO(Plain Old C# Object,没有继承什么特殊的父类,没有标注什么特殊的Attribute,没有定义什么特殊的方法,就是一堆普通的属性);不符合大项目开发的要求。微软推荐使用FluentAPI 的使用方式,因此后面主要用FluentAPI 的使用方式。
FluentAPI 配置T_Persons 的方式
-
数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime 字段。
-
创建 Person 类。模型类就是普通C#类
public class Person { public long ID { get; set; } public string Name { get; set; } public DateTime CreateTime { get; set; } }
-
创建一个 PersonConfig 类,放到ModelConfig 文件夹下(PersonConfig、EntityConfig这样的名字都不是必须的)
public class PersonConfig : EntityTypeConfiguration<Person> { public PersonConfig() { this.ToTable("T_Person"); } }
-
创建 DbContext 类
public class MyDBContext:DbContext { public MyDBContext() : base("name=strcon") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly()); } public DbSet<Person> Persons { get; set; } }
下面这句话:
modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly());
代表从这句话所在的程序集加载所有的继承自EntityTypeConfiguration 为模型配置类。还有很多加载配置文件的做法(把配置写到OnModelCreating中或者把加载的代码写死到OnModelCreating 中),但是这种做法是最符合大项目规范的做法。
和以前唯一的不同就是:模型不需要标注Attribute;编写一个XXXConfig类配置映射关系;DbContext 中override OnModelCreating;
-
测试
protected void Button1_Click(object sender, EventArgs e) { MyDBContext context = new MyDBContext(); Person p = new Person(); p.Name = TextBox1.Text; p.CreateTime = DateTime.Now; context.Persons.Add(p); context.SaveChanges(); }
EF 的基本增删改查
获取DbSet除了可以ctx.Persons之外,还可以ctx.Set()。
-
增加,一个点:如果Id是自动增长的,创建的对象显然不用指定Id的值,并且在SaveChanges ()后会自动给对象的Id属性赋值为新增行的Id字段的值。
-
删除。先查询出来要删除的数据,然后Remove。这种方式问题最少,虽然性能略低,但是删除操作一般不频繁,不用考虑性能。后续在“状态管理”中会讲其他实现方法。
MyDBContext context = new MyDBContext(); if (e.CommandName=="BtnDelete") { int id = Convert.ToInt32(e.CommandArgument); var p = context.Persons.Where(per => per.ID == id).SingleOrDefault(); if (p!=null) { context.Persons.Remove(p); } int i= context.SaveChanges(); if (i>0) { Repeater1.DataSource = context.Persons.ToList(); Repeater1.DataBind(); } }
怎么批量删除,比如删除Id>3 的?查询出来一个个Remove。性能坑爹。如果操作不频繁或者数据量不大不用考虑性能,如果需要考虑性能就直接执行sql 语句
-
修改:先查询出来要修改的数据,然后修改,然后SaveChanges()
MyDbContext ctx = new MyDbContext(); var ps = ctx.Persons.Where(p => p.Id > 3); foreach(var p in ps) { p.CreateDateTime = p.CreateDateTime.AddDays(3); p.Name = "haha"; } ctx.SaveChanges();
性能问题?同上。
-
查。因为DbSet 实现了IQueryable 接口,而IQueryable 接口继承了IEnumerable 接口,所以可以使用所有的linq、lambda 操作。给表增加一个Age 字段,然后举例orderby、groupby、where 操作、分页等。一样一样的。
-
查询 order by 的一个细节
EF调用Skip之前必须调用OrderBy:如下调用var items = ctx.Persons.Skip(3).Take(5); 会报错“The method 'OrderBy' must be called before the method 'Skip'.)”,要改成:var items = ctx.Persons.OrderBy(p=>p.CreateDateTime).Skip(3).Take(5);
这也是一个好习惯,因为以前就发生过(写原始sql):分页查询的时候没有指定排序规则,以为默认是按照Id 排序,其实有的时候不是,就造成数据混乱。写原始SQL 的时候也要注意一定要指定排序规则。
EF 原理及SQL 监控
EF 会自动把Where()、OrderBy()、Select()等这些编译成“表达式树(Expression Tree)”,然后会把表达式树翻译成SQL 语句去执行。(编译原理,AST)因此不是“把数据都取到内存中,然后使用集合的方法进行数据过滤”,因此性能不会低。但是如果这个操作不能被翻译成SQL语句,则或者报错,或者被放到内存中操作,性能就会非常低。
-
怎么查看真正执行的SQL是什么样呢?
DbContext有一个Database属性,其中的Log属性,是Action委托类型,也就是可以指向一个void A(string s)方法,其中的参数就是执行的SQL语句,每次EF执行SQL语句的时候都会执行Log。因此就可以知道执行了什么SQL。
EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select 查询,ToList()内部也是遍历结果集形成List。
查看Update操作,会发现只更新了修改的字段。
-
观察一下前面学学习时候执行的SQL是什么样的。Skip().Take()被翻译成了?Count()被翻译成了?
var result = ctx.Persons.Where(p => p.Name.StartsWith("inlett"));//看看翻译成了什么? var result = ctx.Persons.Where(p => p.Name.Contains("com")); var result = ctx.Persons.Where(p => p.Name.Length>5); var result = ctx.Persons.Where(p => p.CreateDateTime>DateTime.Now); long[] ids = { 2,5,6};//不要写成int[] var result = ctx.Persons.Where(p => ids.Contains(p.Id));
-
EF中还可以多次指定where来实现动态的复合检索:
查看一下生成的SQL语句。
-
EF是跨数据库的,如果迁移到MYSQL上,就会翻译成MYSQL的语法。要配置对应数据库的Entity Framework Provider。
-
细节:
每次开始执行的__MigrationHistory等这些SQL语句是什么?是DBMigration用的,也就是由EF帮我们建数据库,现在我们用不到,用下面的代码禁用:
Database.SetInitializer(null);
XXXDbContext就是项目DbContext的类名。一般建议放到XXXDbContext构造函数中。注意这里的Database 是System.Data.Entity下的类,不是DbContext的Database属性。如果写到DbContext中,最好用上全名,防止出错。
执行原始SQL
不要“手里有锤子,到处都是钉子”在一些特殊场合,需要执行原生SQL。
执行非查询语句,调用DbContext的Database属性的ExecuteSqlCommand方法,可以通过占位符的方式传递参数:
ctx.Database.ExecuteSqlCommand("update T_Persons set Name={0},CreateDateTime=GetDate()","YLT.com");
占位符的方式不是字符串拼接,经过观察生成的SQL语句,发现仍然是参数化查询,因此不会有SQL注入漏洞。
执行查询:
var q1 = ctx.Database.SqlQuery<Item1>("select Name,Count(*) Count from T_Persons where Id>{0} and CreateDateTime<={1} group by Name",2, DateTime.Now); //返回值是DbRawSqlQuery<T>类型,也是实现IEnumerable 接口
foreach(var item in q1)
{
Console.WriteLine(item.Name+":"+item.Count);
}
class Item1
{
public string Name { get; set; }
public int Count { get; set; }
}
类似于ExecuteScalar的操作比较麻烦:
int c = ctx.Database.SqlQuery<int>("select count(*) from T_Persons").SingleOrDefault();
不是所有lambda 写法都能被支持
下面想把Id转换为字符串比较一下是否为"3"(别管为什么):
var result = ctx.Persons.Where(p => Convert.ToString(p.Id)=="3");
运行会报错(也许高版本支持了就不报错了),这是一个语法、逻辑上合法的写法,但是EF目前无法把他解析为一个SQL语句。
出现“System.NotSupportedException”异常一般就说明你的写法无法翻译成SQL语句
想获取创建日期早于当前时间一小时以上的数据:
var result = ctx.Persons.Where(p => (DateTime.Now - p.CreateDateTime).TotalHours>1);
同样也可能会报错。
怎么解决?
尝试其他替代方案(没有依据,只能乱试):
var result = ctx.Persons.Where(p => p.Id==3);
EF中提供了一个SQLServer专用的类SqlFunctions,对于EF不支持的函数提供了支持,比如:
var result = ctx.Persons.Where(p =>SqlFunctions.DateDiff("hour",p.CreateDateTime,DateTime.Now)>1);
EF对象的状态
简介
为什么查询出来的对象Remove()、再SaveChanges()就会把数据删除。而自己new一个Person()对象,然后Remove()不行?为什么查询出来的对象修改属性值后、再SaveChanges()就会把数据库中的数据修改。
因为EF会跟踪对象状态的改变。
EF中中对象有五个状态:Detached(游离态,脱离态)、Unchanged(未改变)、Added(新增)、Deleted(删除)、Modified(被修改)。
Add()、Remove()修改对象的状态。所有状态之间几乎都可以通过:Entry(p).State=xxx的方式进行强制状态转换。
通过代码来演示一下。这个状态转换图没必要记住,了解即可。
状态改变都是依赖于Id的(Added除外)
应用(*)
当SavaChanged()方法执行期间,会查看当前对象的EntityState的值,决定是去新增(Added)、修改Modified)、删除(Deleted)或者什么也不做(UnChanged)。下面的做法不推荐,在旧版本中一些写法不被支持,到新版EF中可能也会不支持。
-
不先查询再修改再保存,而是直接更新部分字段的方法
var p = new Person(); p.Id = 2; ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged; p.Name = "adfad"; ctx.SaveChanges();
也可以:
var p = new Person(); p.Id = 5; p.Name = "yltedu"; ctx.Persons.Attach(p);//等价于ctx.Entry(p).State = System.Data.Entity.EntityState.Unchanged; ctx.Entry(p).Property(a => a.Name).IsModified = true; ctx.SaveChanges();
-
不先查询再Remove再保存,而是直接根据Id删除的方法:
var p = new Person(); p.Id = 2; ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted; ctx.SaveChanges();
注意下面的做法并不会删除所有Name="ylt.com" 的,因为更新、删除等都是根据Id进行的:
var p = new Person(); p.Name = "yltedu.com"; ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted; ctx.SaveChanges(); 上面其实是在: delete * from t_persons where Id=0
EF优化的一个技巧
如果查询出来的对象只是供显示使用,不会修改、删除后保存,那么可以使用AsNoTracking()来使得查询出来的对象是Detached状态,这样对对象的修改也还是Detached状态,EF不再跟踪这个对象状态的改变,能够提升性能。
var p1 = ctx.Persons.Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);
改成:
var p1 = ctx.Persons.AsNoTracking().Where(p => p.Name == "rupeng.com").FirstOrDefault();
Console.WriteLine(ctx.Entry(p1).State);
因为AsNoTracking()是DbQuery类(DbSet的父类)的方法,所以要先在DbSet后调用AsNoTracking()。
Fluent API更多配置
基本EF配置只要配置实体类和表、字段的对应关系、表间关联关系即可。如果利用EF的高级配置,可以达到更多效果:如果数据错误(比如字段不能为空、字符串超长等),会在EF层就会报错,而不会被提交给数据库服务器再报错;如果使用自动生成数据库,也能帮助EF生成更完美的数据库表。
这些配置方法无论是DataAnnotations、FluentAPI都支持,下面讲FluentAPI的用法,DataAnnotations感兴趣的自己查(http://blog.csdn.net/beglorious/article/details/39637475)。
尽量用约定,EF配置越少越好。Simple is best 参考资料:http://www.cnblogs.com/nianming/archive/2012/11/07/2757997.html
HasMaxLength设定字段的最大长度
public PersonConfig()
{
this.ToTable("T_Persons");
this.Property(p => p.Name).HasMaxLength(50);//长度为50
}
依赖于数据库的“字段长度、是否为空”等的约束是在数据提交到数据库服务器的时候才会检查;EF的配置,则是由EF来检查的,如果检查出错,根本不会被提交给服务器。
如果插入一个Person对象,Name属性的值非常长,保存的时候就会报DbEntityValidationException异常,这个异常的Message中看不到详细的报错消息,要看EntityValidationErrors属性的值。
var p = new Person();
p.Name = "非常长的字符串";
ctx.Persons.Add(p);
try
{
ctx.SaveChanges();
}
catch(DbEntityValidationException ex)
{
StringBuilder sb = new StringBuilder();
foreach(var ve in ex.EntityValidationErrors.SelectMany(eve=>eve.ValidationErrors))
{
sb.AppendLine(ve.PropertyName+":"+ve.ErrorMessage);
}
Console.WriteLine(sb);
}
(有用)字段是否可空
this.Property(p => p.Name).IsRequired() 属性不能为空;
this.Property(p => p.Name).IsOptional() 属性可以为空;(没用的鸡肋!)
EF默认规则是“主键属性不允许为空,引用类型允许为空,可空的值类型long?等允许为空,值类型不允许为空。”基于“尽量少配置”的原则:如果属性是值类型并且允许为null,就声明成long?等,否则声明成long等;如果属性属性值是引用类型,只有不允许为空的时候设置IsRequired()。
其他一般不用设置的(了解即可)
-
主键:this.HasKey(p => p.pId);
-
某个字段不参与映射数据库:this.Ignore(p => p.Name1);
-
this.Property(p => p.Name).IsFixedLength(); 是否对应固定长度
-
this.Property(p => p.Name).IsUnicode(false) 对应的数据库类型是varchar类型,而不是nvarchar
-
this.Property(p => p.Id).HasColumnName("Id1"); Id列对应数据库中名字为Id的字段
-
this.Property(p=>p.Id).HasDatabaseGeneratedOption(System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedOption.Identity) 指定字段是自动增长类型。
流动起来
因为ToTable()、Property()、IsRequired()等方法的还是配置对象本身,因此可以实现类似于StringBuilder的链式编程,这就是“Fluent”一词的含义; 因此下面的写法:
public PersonConfig ()
{
this. ToTabl e ("T—Persons");
this.HasKey(p => p. Id);
this. Ignore(p => p. Name2);
this.Property(p => p.Name) . HasMaxLength (50);
this. Property (p => p. Name) . I sRequired ();
this.Property(p => p.CreateDateTime) . HasCol umnName ("CreateDateTi me");
this. Property (p => p. Name) . I sRequired () ;
}
可以简化成:
public PersonConfig()
{
this. ToTable ("T_Persons") . HasKey (p => p. Id). Ignore (p => p. Name2) ;
this. Property (p => p. Name) . HasMaxLength (50). IsRequired O ;
this. Property (p => p. CreateDateTime) . HasColumnName ("CreateDateTime") . IsRequiredO;
}
后面用的时候都Database.SetInitializer(null);
一对多关系映射
EF最有魅力的地方在于对于多表间关系的映射,可以简化工作。 复习一下表间关系:
-
一对多(多对一):一个班级对应着多个学生,一个学生对着一个班级。一方是另外一方的唯一。在多端有一个指向一端的外键。举例:班级表:T_Classes(Id,Name) 学生表
T_Students(Id,Name,Age,ClassId)
-
多对多:一个老师对应多个学生,一个学生对于多个老师。任何一方都不是对方的唯一。 需要一个中间关系表。具体: 学生表T_Students(Id,Name,Age,ClassId) , 老师表 T_Teachers(Id,Name,PhoneNum),关系表T_StudentsTeachers(Id,StudentId,TeacherId)
和关系映射相关的方法:
-
基本套路this.Has(p=>p.A).With***() 当前这个表和A 属性的表的关系是Has 定义, With 定义的是A 对应的表和这个表的关系。Optional/Required/Many
-
HasOptional() 有一个可选的(可以为空的)
-
HasRequired() 有一个必须的(不能为空的)
-
HasMany() 有很多的
-
WithOptional() 可选的
-
WithRequired() 必须的
-
WithMany() 很多的
举例:
在AAA 实体中配置this.HasRequired(p=>p.BBB).WithMany();是什么意思? 在AAA 实体中配置this.HasRequired(p=>p.BBB).WithRequired ();是什么意思?
配置一对多关系
-
先按照正常的单表配置把Student、Class 配置起来,T_Students 的ClassId 字段就对应Student类的ClassId 属性。WithOptional()
using (MyDbContext ctx = new MyDbContext ()) { Class c l = new Class { Name= " 三年二班,, } ; ctx. Cl asses. Add (cl) ; ctx. SaveChanges () ; Student s l = new Student { Age = 11, Nam e = " 张三" , Cl assl d = cl. Id } ; Student s2 = new Student { Name = " 李四" , Classld = cl. Id } ; ctx.Students.Add(s1); ctx. Students. Add(s2); ctx. SaveChanges O ; }
-
给Student类增加一个Class类型、名字为Class(不一定非叫这个,但是习惯是:外键名去掉Id)的属性,要声明成virtual(后面讲原因)。
-
然后就可以实现各种对象间操作了:
-
Console.WriteLine(ctx.Students.First().Class.Name)
-
然后数据插入也变得简单了,不用再考虑“先保存Class,生成Id,再保存Student”了。这样就是纯正的“面向对象模型”,ClassId 属性可以删掉。
Class c1 = new Class { Name = "五年三班" }; ctx.Classes.Add(c1); Student s1 = new Student { Age = 11, Name = "皮皮虾"}; Student s2 = new Student { Name = "巴斯"}; s1.Class = c1; s2.Class = c1; ctx.Students.Add(s1); ctx.Students.Add(s2); ctx.Classes.Add(c1); ctx.SaveChanges();
-
-
如果ClassId 字段可空怎么办?直接把ClassId 属性设置为long?
-
还可以在Class中配置一个public virtual ICollection Students { get; set; } = new List(); 属性。最好给这个属性初始化一个对象。注意是virtual。这样就可以获得所有指向了当前对象的Stuent 集合,也就是这个班级的所有学生。我个人不喜欢这个属性,业界的大佬也是建议“尽量不要设计双向关系”,因为可以通过Class clz = ctx.Classes.First(); var students =ctx.Students.Where(s => s.ClassId == clz.Id);来查询获取到,思路更清晰。
不过有了这样的集合属性之后一个方便的地方:
Class c1 = new Class { Name = "五年三班" }; ctx.Classes.Add(c1); Student s1 = new Student { Age = 11, Name = "皮皮虾" }; Student s2 = new Student { Name = "巴斯" }; c1.Students.Add(s1);//注意要在Students属性声明的时候= new List<Student>();或者在之前赋值 c1.Students.Add(s2); ctx.Classes.Add(c1); ctx.SaveChanges();
EF会自动追踪对象的关联关系,给那些有关联的对象也自动进行处理。
在进行数据遍历的时候可能会报错“已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭。”
foreach(var s in ctx.Students) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
一对多深入:
-
默认约定配置即可,如果非要配置,可以在StudentConfig 中如下配置:
this.HasRequired(s=> s.Class).WithMany().HasForeignKey(s => s.ClassId);
表示“我需要(Require)一个Class,Class有很多(Many)的Student;ClassId是这样一个外键”。
如果ClassId 可空,那么就要写成:
this.HasOptional (s => s.Class).WithMany().HasForeignKey(s => s.ClassId);
-
如果这样Class clz = ctx.Classes.First();foreach (Student s in clz.Students)访问,也就是从一端发起对多端的方法,那么就会报错“找不到Class_Id 字段”需要在ClassConfig中再反向配置一遍 HasMany(e =>e.Students).WithRequired().HasForeignKey(e=>e.ClassId); 因为如果在Class 中引入Students 属性,还要再在ClassConfig 再配置一遍反向关系,很麻烦。因此再次验证“不要设计双向关系”。
-
如果一张表中有两个指向另外一个表的外键怎么办?比如学生有“正常班级Class”(不能空)和“小灶班级XZClass”(可以空)两个班。在StudentConfig 中:
this.HasRequired(s => s.Class).WithMany().HasForeignKey(s => s.ClassId); this. HasOptional (s => s.XZClass).WithMany().HasForeignKey(s => s.XZClassId);
多对多关系配置
老师和学生:
class Student
{
public long Id { set; get; }
public string Name { get; set; }
public virtual ICollection<Teacher> Teachers { get; set; }=new List<Teacher>();
}
class Teacher
{
public long Id { set; get; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }=new List< Student >();
}
class StudentConfig : EntityTypeConfiguration<Student>
{
public StudentConfig()
{
ToTable("T_Students");
}
}
class TeacherConfig : EntityTypeConfiguration<Teacher>
{
public TeacherConfig()
{
ToTable("T_Teachers");
this.HasMany(e => e.Students).WithMany(e => e.Teachers)//易错,容易丢了WithMany 的参数
.Map(m =>
m.ToTable("T_TeacherStudentRelations").MapLeftKey("TeacherId").MapRightKey("StudentId"));
}
}
关系配置到任何一方都可以
这样不用中间表建实体(也可以为中间表建立一个实体,其实思路更清晰),就可以完成多对多映射。当然如果中间关系表还想有其他字段,则要必须为中间表建立实体类。 测试:
Teacher t1 = new Teacher();
t1.Name = "张老师";
t1.Students = new List<Student>();
Teacher t2 = new Teacher();
t2.Name = "王老师";
t2.Students = new List<Student>();
Student s1 = new Student();
s1.Name = "tom";
s1.Teachers = new List<Teacher>();
Student s2 = new Student();
s2.Name = "jerry";
s2.Teachers = new List<Teacher>();
t1.Students.Add(s1);
附录:
-
关于WithMany()的参数
-
在一对多关系中,如果只配置多端关系并且没有给WithMany()指定参数的话,在进行反向关系操作的时候就会报错。要么在一端也配置一次,最好的方法就是还是只配置多端,只不过给WithMany()指定参数:
class StudentConfig:EntityTypeConfiguration<Student> { public StudentConfig() { ToTable("T_Students"); this.HasRequired(e => e.Class).WithMany(e=>e.Students) .HasForeignKey(e=>e.ClassId); } }
当然还是不建议用反向的集合属性,如果Class没有Students这个集合属性的话,就不用(也不能)WithMany的参数了。
-
关于多对多关系配置的WithMany()问题
上次讲配置多对多的关系没有给WithMany设定参数,这样反向操作的时候就会出错,应该改成:this.HasMany(e => e.Students).WithMany(e=>e.Teachers)
总结:一对多的中不建议配置一端的集合属性,因此配置的时候不用给WithMany()参数,如果配置了集合属性,则必须给WithMany 参数;多对多关系必须要给WithMany()参数。
总结一对多、多对多的“最佳实践”
-
-
一对多最佳方法(不配置一端的集合属性):
-
多端
public class Student { public long Id { get; set; } public string Name { get; set; } public long ClassId { get; set; } public virtual Class Class { get; set; } }
-
一端
public class Class { public long Id { get; set; } public string Name { get; set; } }
-
在多端的模型配置(StudentConfig)中:
this.HasRequired(e => e.Class).WithMany() .HasForeignKey(e=>e.ClassId);
-
-
一对多的配置(在一端配置一个集合属性,极端不推荐)
-
多端
public class Student { public long Id { get; set; } public string Name { get; set; } public long ClassId { get; set; } public virtual Class Class { get; set; } }
-
一端
public class Class { public long Id { get; set; } public string Name { get; set; } public virtual ICollection<Student> Students { get; set; } = new List<Student>(); }
-
多端的配置(StudentConfig)中
this.HasRequired(e => e.Class).WithMany(e=>e.Students)//WithMany()的参数不能丢 .HasForeignKey(e=>e.ClassId);
-
-
多对多最佳配置
-
两端模型
public class Student { public long Id { get; set; } public string Name { get; set; } public virtual ICollection<Teacher> Teachers { get; set; } = new List<Teacher>(); } public class Teacher { public long Id { get; set; } public string Name { get; set; } public virtual ICollection<Student> Students { get; set; } = new List<Student>(); }
-
在其中一端配置(StudentConfig)
this.HasMany(e => e.Teachers).WithMany(e=>e.Students).Map(m =>//不要忘了WithMany的参数 m.ToTable("T_StudentTeachers").MapLeftKey("StudentId").MapRightKey("TeacherId"));
-
多对多中 移除关系:t.Students.Remove(t.Students.First()); 添加关系
-
(*)多对多中还可以为中间表建立一个实体方式映射。当然如果中间关系表还想有其他字段,则要必须为中间表建立实体类(中间表和两个表之间就是两个一对多的关系了)。
-
数据库创建策略(*): 如果数据库创建好了再修改模型或者配置,运行就会报错,那么就要手动删除数据库或者:Database.SetInitializer(new DropCreateDatabaseIfModelChanges());如果报错“数据库正在使用”,可能是因为开着Mangement Studio,先关掉就行了。知道就行了,只适合学习时候使用。
CodeFirst Migration 参考(*): http://www.cnblogs.com/libingql/p/3330880.html 太复杂, 不符合Simple is Best 的原则,这是为什么有一些开发者不用EF,而使用Dapper 的原因。
做项目的时候建议初期先把主要的类使用EF 自动生成表,然后干掉Migration 表,然后就 Database.SetInitializer(null);以后对数据库表的修改都手动完成,也就是手动改实体类、 手动改数据库表。
-