1.Entity Framework
1.1相关知识复习
- 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表达式:
1.2lambda表达式
函数式编程,在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都省略:
Fune< int, int, string> fl = delegate(int i, int j) { return "结果是" + (i + j); };
Func<int,int,string> f2= (i,j)=>"结果是"+ (i+ j);
1.3集合常用扩展方法
where (支持委托)、Select (支持委托)、Max 、Min 、OrderBy
First (获取第一个,如果一个都没有则异常)
FirstOrDefault (获取第一个,如果—个都没有则返回默认值)
Single (获取唯一一个,如果没有或者有多个则异常)
SingleOrDefoult (获取唯一一个, 如果没有则返回默认值,如果有多个则异常)
注意lambda中照样要避免变量重名的问题:var p =persons.Where(p => p.Name ==“yltedu.com”).First();
1.4高级集合扩展方法
//学生
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,一般比Coun()>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:把集合中每个对象的另外集合属性的值重新拼接为一个新的集合
注意不会去重,如果需要去重要自己再次调用Distinct()foreach(var s in teachers.SelectMany(t => t.Students)) { Console.WriteLine(s);//每个元素都是Person }
- Join
Join 可以实现和数据库一样的Join 效果,对有关联关系的数据进行联合查询 下面的语句查询所有Id=1 的狗,并且查询狗的主人的姓名。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 };
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); }
2.linq
2.1简介
查询Id>1 的狗有如下两种写法:
var r1 = dogs.Where(d => d.Id > 1);
var r2 = from d in dogs where d.Id>1 select d;
第一种写法是使用lambda 的方式写的,官方没有正式的叫法,我们就叫“lambda 写法”;
第二种是使用一种叫Linq(读作:link)的写法,是微软发明的一种类似SQL 的语法,给我们一个新选择。两种方法是可以互相替代的,没有哪个好、哪个坏,看个人习惯。我的经验:需要join 等复杂用法的时候Linq 更易懂,一般的时候“lambda 写法”更清晰,更紧凑。反编译得知,这两种写法最终编译成同样的东西,所以本质上一样的。
2.2辟谣
“Linq 被淘汰了”是错误的说法,应该是“Linq2SQL 被淘汰了”。linq 就是微软发明的这个语法,可以用这种语法操作很多数据,操作SQL 数据就是Linq2SQL,linq 操作后面学的EntityFramework 就是Linq2Entity,linq 操作普通.Net 对象就是Linq2Object、Linq 操作XML文档就是Linq2XML。
2.3linq 基本语法
以from item in items 开始,items 为待处理的集合,item 为每一项的变量名;最后要加上select,表示结果的数据;记得select 一定要最后。这是刚用比较别扭的地方。
看各种用法,不用解析:
1.
var r= from d in dogs select d.Id;
var r = from d in dogs select new{d.Id,d.Name,Desc="一条狗"};
3.排序
var items = from d in dogs
//orderby d.Age
//orderby d.Age descending
orderby d.Age,d.MasterId descending
select d;
4.join
var r9 = from d in dogs
join m in masters on d.MasterId equals m1.Id
select new { DogName=d.Name,MasterName=m.Name};
注意join 中相等不要用==,要用equals。写join 的时候linq 比“lambda” 漂亮
5.group by
var r1 = from p in list
group p by p.Age into g
select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() };
2.4混用
只有Where,Select,OrderBy,GroupBy,Join 等这些能用linq 写法,如果要用下面的 “Max,Min,Count,Average,Sum,Any,First,FirstOrDefault,Single,SingleOrDefault,Distinct,Skip,Ta ke等”则还要用lambda 的写法(因为编译后是同一个东西,所以当然可以混用)。
var r1 = from p in list
group p by p.Age into g
select new { Age = g.Key, MaxSalary = g.Max(p=>p.Salary), Count = g.Count() };
int c = r1.Count();
var item = r1.SingleOrDefault();
var c = (
from p in list
where p.Age>3
select p
).Count();
lambda 对linq 说:论漂亮我不行,论强大你不行!
3. C#6.0中的语法
- 属性的初始化“public int Age{get;set;}=6”。低版本.Net 中怎么办?
- nameof:可以直接获得变量、属性、方法等的名字的字符串表现形式。获取的是最后一段的名称。如果在低版本中怎么办?
class Program { static void Main(string[] args) { Person p1 = new Person(); string s1 = nameof(p1); string s2 = nameof(Person); string s3 = nameof(p1.Age); string s4 = nameof(Person.Age); string s5 = nameof(p1.F1); Console.ReadKey(); } } public class Person { public int Age { get; set; } public string Name { get; set; } public void Hello() { } public static void F1() { } }
- 好处:避免写错了,可以利用编译时检查。
应用案例:ASP.Net MVC 中的[Compare(“BirthDay”)]改成[Compare(nameof(BirthDay))] - ??语法:int j = i ?? 3; 如果i 为null 则表达式的值为3,否则表达式的值就是i 的值。如果在低版本中怎么办?int j = (i == null)?3:(int)i;
应用案例:string name = null;Console.WriteLine(name??“未知”); - ?.语法:string s8 = null;string s9 = s8?.Trim(); 如果s8 为null,则不执行Trim(),让表达式的结果为null。在低版本中怎么办?string s9=null;if(s8!=null){s9=s8.Trim();};
4.Entity Framework 简介
- ORM:Object Relation Mapping ,通俗说:用操作对象的方式来操作数据库。
- 插入数据库不再是执行Insert , 而是类似于Person p = new Person();p.Age=3;p.Name=“英莱特”;db.Save§;这样的做法。
- 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 也是采用“约定大于配置”这样的框架设计原则,省去了很多配置,能用约定就不要自己配置。
5.EF 的安装
- 基础阶段用控制台项目。使用NuGet 安装EntityFramework。会自动在App.config中增加两个entityFramework 相关配置段;
在 web.config 中配置连接字符串
<add name="conn1" connectionString="Data Source=.;Initial Catalog=test1;UserID=sa;Password=msn@qq888" providerName="System.Data.SqlClient" />
易错点:不能忘了写providerName="System.Data.SqlClient"增加两个entityFramework 相关配置段;
6.EF 简单DataAnnotations 实体配置
-
数据库中建表 T_Perons,有Id(主键,自动增长)、Name、CreateDateTime 字段。
-
创建 Person 类 [Table(“T_Persons”)]//因为类名和表名不一样,所以要使用Table 标注
public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
因为EF 约定主键字段名是Id,所以不用再特殊指定Id 是主键,如果非要指定就指定[Key]。因为字段名字和属性名字一致,所以不用再特殊指定属性和字段名的对应关系,如果需要特殊指定,则要用[Column(“Name”)]
(*)必填字段标注[Required]、字段长度[MaxLength(5)]、可空字段用int?、如果字段 在数据库有默认值,则要在属性上标注[DatabaseGenerated] 注意实体类都要写成public,否则后面可能会有麻烦。
-
创建 DbContext 类(模型类、实体类)
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1") //name=conn1 表示使用连接字符串中名字为conn1 的去连接数据库 { } public DbSet<Person> Persons { get; set; }//通过对Persons 集合的操作就可以完成 对T_Persons 表的操作 }
-
测试
MyDbContext ctx = new MyDbContext(); Person p = new Person(); p.CreateDateTime = DateTime.Now; p.Name = "YLT"; ctx.Persons.Add(p); ctx.SaveChanges();
注意:MyDbContext 对象是否需要using 有争议,不using 也没事。每次用的时候newMyDbContext 就行,不用共享同一个实例,共享反而会有问题。SaveChanges()才会把修改更新到数据库中。
EF 的开发团队都说要using DbContext,很多人不using,只是想利用LazyLoad 而已,但是那样做是违反分层原则的。我的习惯还是using。
异常的处理:如果数据有错误可能在SaveChanges()的时候出现异常,一般仔细查看异常信息或者一直深入一层层的钻InnerException 就能发现错误信息。举例:创建一个Person对象,不给Name、CreateDateTime 赋值就保存。
7.EF 模型的两种配置方式
EF 中的模型类的配置有DataAnnotations、FluentAPI 两种。上面这种在模型类上[Table(“T_Persons”)]、[Column(“Name”)]这种方式就叫DataAnnotations这种方式比较方便,但是耦合度太高,一般的类最好是POCO(Plain Old C# Object,没有继承什么特殊的父类,没有标注什么特殊的Attribute,没有定义什么特殊的方法,就是一堆普通的属性);不符合大项目开发的要求。微软推荐使用FluentAPI 的使用方式,因此后面主要用FluentAPI 的使用方式。
8. FluentAPI 配置T_Persons 的方式
-
数据库中建表 T_Perons,有Id(主键,自动增长)、Name、CreateDateTime 字段。
-
创建 Person 类。模型类就是普通C#类
public class Person { public long Id { set; get; } public string Name { get; set; } public DateTime CreateDateTime { get; set; } }
-
创建一个 PersonConfig 类,放到ModelConfig 文件夹下(PersonConfig、EntityConfig这样的名字都不是必须的)
class PersonConfig: EntityTypeConfiguration<Person> { public PersonConfig() { this.ToTable("T_Persons");//等价于[Table("T_Persons")] } }
-
创建 DbContext 类
public class MyDbContext:DbContext { public MyDbContext():base("name=conn1") { } 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;
-
以后的用法和以前一样。
-
多个表怎么办?创建多个表的实体类、Config 类,并且在DbContext 中增加多个DbSet 类型的属性即可。
9.EF 的基本增删改查
获取DbSet 除了可以ctx.Persons 之外,还可以ctx.Set()。
-
增加,讲过了。一个点:如果Id 是自动增长的,创建的对象显然不用指定Id 的值,并且在SaveChanges ()后会自动给对象的Id 属性赋值为新增行的Id 字段的值。
-
删除。先查询出来要删除的数据,然后Remove。这种方式问题最少,虽然性能略低,但是删除操作一般不频繁,不用考虑性能。后续在“状态管理”中会讲其他实现方法。
MyDbContext ctx = new MyDbContext(); var p1= ctx.Persons.Where(p => p.Id == 3).SingleOrDefault(); if(p1==null) { Console.WriteLine("没有id=3 的人"); } else { ctx.Persons.Remove(p1); } ctx.SaveChanges();
怎么批量删除,比如删除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 的时候也要注意一定要指定排序规则。
10.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(“rupeng”));看看翻译成了什么? 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 来实现动态的复合检索:
//必须写成IQueryable<Person>,如果写成IEnumerable 就会在内存中取后续数据
IQueryable<Person> items = ctx.Persons;//为什么把IQueryable<Person>换成var 会编译出错
items = items.Where(p=>p.Name=="YLT");
items = items.Where(p=>p.Id>5);
查看一下生成的SQL 语句。
(*)EF是跨数据库的,如果迁移到MYSQL上,就会翻译成MYSQL的语法。要配置对应数据库的Entity Framework Provider。
细节:
每次开始执行的__MigrationHistory 等这些SQL 语句是什么?是DBMigration 用的,也就是由EF 帮我们建数据库,现在我们用不到,用下面的代码禁用:
Database.SetInitializer(null);
XXXDbContext 就是项目DbContext 的类名。一般建议放到XXXDbContext 构造函数中。注意这里的Database 是System.Data.Entity 下的类,不是DbContext 的Database 属性。如果写到DbContext中,最好用上全名,防止出错。
11.执行原始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();
12.不是所有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);
13.EF对象的状态
13.1简介
为什么查询出来的对象Remove()、再SaveChanges()就会把数据删除。而自己new一个Person()对象,然后Remove()不行?为什么查询出来的对象修改属性值后、再SaveChanges()就会把数据库中的数据修改。
因为EF会跟踪对象状态的改变。
EF中中对象有五个状态:Detached(游离态,脱离态)、Unchanged(未改变)、Added(新增)、Deleted(删除)、Modified(被修改)。
13.2状态转换
Add()、Remove()修改对象的状态。所有状态之间几乎都可以通过:Entry§.State=xxx的方式 进行强制状态转换。
通过代码来演示一下。这个状态转换图没必要记住,了解即可。
状态改变都是依赖于Id的(Added除外)
13.3应用(*)
当SavaChanged()方法执行期间,会查看当前对象的EntityState的值,决定是去新增(Added)、修改(Modified)、删除(Deleted)或者什么也不做(UnChanged)。下面的做法不推荐,在旧版本中一些写法不被支持,到新版EF中可能也会不支持。
ObjectStateManager
- 不先查询再修改再保存,而是直接更新部分字段的方法
也可以: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删除的方法:
注意下面的做法并不会删除所有Name=“ylt.com” 的,因为更新、删除等都是根据Id进行的:var p = new Person(); p.Id = 2; ctx.Entry(p).State = System.Data.Entity.EntityState.Deleted; ctx.SaveChanges();
上面其实是在: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
13.4EF优化的一个技巧
如果查询出来的对象只是供显示使用,不会修改、删除后保存,那么可以使用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()。
14.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
14.1HasMaxLength设定字段的最大长度
```
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);
}
14.2(有用)字段是否可空:
this.Property(p => p.Name).IsRequired() 属性不能为空; this.Property(p => p.Name).IsOptional() 属性可以为空;(没用的鸡肋!)
EF默认规则是“主键属性不允许为空,引用类型允许为空,可空的值类型long?等允许为空,值类型不允许为空。”基于“尽量少配置”的原则:如果属性是值类型并且允许为null,就声明成long?等,否则声明成long等;如果属性属性值是引用类型,只有不允许为空的时候设置IsRequired()。
14.3其他一般不用设置的(了解即可)
- 主键: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) 指定字段是自动增长类型。
14.4流动起来
因为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);
15.一对多关系映射
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)
15.1和关系映射相关的方法:
- 基本套路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 ();是什么意思?
16.配置一对多关系、
- 先按照正常的单表配置把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);
}
16.1一对多深入
- 默认约定配置即可,如果非要配置,可以在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);
17.多对多关系配置
老师和学生:
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()的参数
A.在一对多关系中,如果只配置多端关系并且没有给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 的参数了。
B.关于多对多关系配置的WithMany()问题上次讲配置多对多的关系没有给WithMany 设定参数,这样反向操作的时候就会出错,应该改成:this.HasMany(e => e.Students).WithMany(e=>e.Teachers)
总结:一对多的中不建议配置一端的集合属性,因此配置的时候不用给WithMany()参数,如果配置了集合属性,则必须给WithMany 参数;多对多关系必须要给WithMany()参数。
总结一对多、多对多的“最佳实践”
-
一对多最佳方法(不配置一端的集合属性):
A.多端public class Student { public long Id { get; set; } public string Name { get; set; } public long ClassId { get; set; } public virtual Class Class { get; set; } }
B.一端
public class Class { public long Id { get; set; } public string Name { get; set; } }
C.在多端的模型配置(StudentConfig)中: this.HasRequired(e => e.Class).WithMany() .HasForeignKey(e=>e.ClassId);
-
一对多的配置(在一端配置一个集合属性,极端不推荐)
A.多端public class Student { public long Id { get; set; } public string Name { get; set; } public long ClassId { get; set; } public virtual Class Class { get; set; } }
B.一端
public class Class { public long Id { get; set; } public string Name { get; set; } public virtual ICollection<Student> Students { get; set; } = new List<Student>(); }
C.多端的配置(StudentConfig)中
this.HasRequired(e => e.Class).WithMany(e=>e.Students)//WithMany()的参数不能丢 .HasForeignKey(e=>e.ClassId); -
多对多最佳配置
A.两端模型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>(); }
B.在其中一端配置(StudentConfig)
this.HasMany(e => e.Teachers).WithMany(e=>e.Students).Map(m =>//不要忘了WithMany的参数 m.ToTable(“T_StudentTeachers”).MapLeftKey(“StudentId”).MapRightKey(“TeacherId”));C.多对多中 移除关系:t.Students.Remove(t.Students.First()); 添加关系
D.(*)多对多中还可以为中间表建立一个实体方式映射。当然如果中间关系表还想有其他字段,则要必须为中间表建立实体类(中间表和两个表之间就是两个一对多的关系了)。
E.数据库创建策略(*): 如果数据库创建好了再修改模型或者配置,运行就会报错,那么就要手动删除数据库或者: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);以后对数据库表的修改都手动完成,也就是手动改实体类、 手动改数据库表。
18.延迟加载(LazyLoad)
如果public virtual Class Class { get; set; }(实体之间的关联属性又叫做“导航属性(Navigation Property)”)把virtual 去掉,那么下面的代码就会报空引用异常
var s = ctx.Students.First();
Console.WriteLine(s.Class.Name);
联想为什么?凭什么!!! 改成virtual 观察SQL 的执行。执行了两个SQL,先查询T_Students,再到T_Classes 中查到对应的行。 这叫“延迟加载”(LazyLoad),只有用到关联的对象的数据,才会再去执行select 查询。注意延迟加载只在关联对象属性上,普通属性没这个东西。 注意:启用延迟加载需要配置如下两个属性(默认就是true,因此不需要去配置,只要别手贱设置为false 即可)
context.Configuration.ProxyCreationEnabled = true;
context.Configuration.LazyLoadingEnabled = true;
分析延迟加载的原理:打印一下拿到的对象的GetType(),再打印一下GetType().BaseType;我们 发现拿到的对象其实是Student 子类的对象。(如果和我这里结果不一致的话,说明:类不是public, 没有关联的virtual 属性) 因此EF 其实是动态生成了实体类对象的子类,然后override 了这些virtual 属性,类似于这样的 实现:
public class StudentProxy:Student
{
private Class clz;
public override Class Class
{
get
{
if(this.clz==null)
{
this.clz= ....//这里是从数据库中加载Class 对象的代码
}
return this.clz;
}
}
}
再次强调:如果要使用延迟加载,类必须是public,关联属性必须是virtual。 延迟加载(LazyLoad)的优点:用到的时候才加载,没用到的时候才加载,因此避免了一次性加载所有数据,提高了加载的速度。缺点:如果不用延迟加载,就可以一次数据库查询就可以把所有数据都取出来(使用join 实现),用了延迟加载就要多次执行数据库操作,提高了数据库服务器的压力。 因此:如果关联的属性几乎都要读取到,那么就不要用延迟加载;如果关联的属性只有较小的概率(比如年龄大于7 岁的学生显示班级名字,否则就不显示)则可以启用延迟加载。这个概率到底是多少是没有一个固定的值,和数据、业务、技术架构的特点都有关系,这是需要经验和直觉,也需要测试和平衡的。 注意:启用延迟加载的时候拿到的对象是动态生成类的对象,是不可序列化的,因此不能直接放到进程外Session、Redis 等中,解决方法?
19.不延迟加载,怎么样一次性加载
用EF 永远都要把导航属性设置为virtual。又想方便(必须是virtual)又想效率高! 使用Include()方法:var s = ctx.Students.Include(“Class”).First();//
观察生成的SQL 语句,会发现只执行一个使用join 的SQL 就把所有用到的数据取出来了。当然拿到的对象还是Student 的子类对象,但是不会延迟加载。(不用研究“怎么让他返回Student 对象”) Include(“Class”)的意思是直接加载Student 的Class 属性的数据。注意只有关联的对象属性才可以用Include,普通字段不可以
直接写"Class"可能拼写错误,如果用C#6.0,可以使用nameof 语法解决问这个问题:
var s = ctx.Students.Include(nameof(Student.Class)).First();
也可以using System.Data.Entity;然后var s = ctx.Students.Include(e=>e.Class).First(); 推荐这种做法。 如果有多个属性需要一次性加载,也可以写多个Include:
var s = ctx.Students.Include(e=>e.Class) .Include(e=>e.Teacher).First();
如果Class 对象还有一个School 属性,也想把School 对象的属性也加载,就要:
var s = ctx.Students.Include("Class").Include("Class. School").First(); 或者更好的
var s = ctx.Students.Include(nameof(Student.Class))
.Include(nameof(Student.Class)+"."+nameof(Class.School)).First();
20.延迟加载的一些坑
-
DbContext 销毁后就不能再延迟加载了,因为数据库连接已经断开
下面的代码最后一行会报错:
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.First(); } Console.WriteLine(s.Class.Name);
两种解决方法:
A. 用Include,不延迟加载(推荐)
Student s; using (MyDbContext ctx = new MyDbContext()) { s = ctx.Students.Include(t=>t.Class).First(); } Console.WriteLine(s.Class.Name);
B.关闭前把要用到的数据取出来
Class c; using (MyDbContext ctx = new MyDbContext()) { Student s = ctx.Students.Include(t=>t.Class).First(); c = s.Class; } Console.WriteLine(c.Name);
-
两个取数据一起使用
下面的程序会报错: 已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭。foreach(var s in ctx.Students) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
因为EF 的查询是“延迟执行”的,只有遍历结果集的时候才执行select 查询,而由于延迟加载的存在到s.Class.Name 也会再次执行查询。ADO.Net 中默认是不能同时遍历两个DataReader。因此就报错。
解决方法有如下:
A.允许多个DataReader 一起执行:在连接字符串上加上MultipleActiveResultSets=true,但只适用于SQL 2005 以后的版本。其他数据库不支持。
B.执行一下ToList(),因为ToList()就遍历然后生成List:foreach(var s in ctx.Students.ToList()) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
C.推荐做法:用Include 预先加载:
foreach(var s in ctx.Students.Include(e=>e.Class)) { Console.WriteLine(s.Name); Console.WriteLine(s.Class.Name); }
21.实体类的继承
所有实体类都会有一些公共属性,可以把这些属性定义到一个父类中。比如:
public abstract class BaseEntity
{
public long Id { get; set; } //主键
public bool IsDeleted { get; set; } = false; //软删除
public DateTime CreateDateTime { get; set; } = DateTime.Now;//创建时间
public DateTime DeleteDateTime { get; set; } //删除时间
}
使用公共父类的好处不仅是写实体类简单了,而且可以提供一个公共的Entity 操作类:
class BaseDAO<T> where T:BaseEntity
{
private MyDbContext ctx;//不自己维护MyDbContext 而是由调用者传递,因为调用者可以要执行很多操作,由调用者决定什么时候销毁。
public BaseDAO (MyDbContext ctx)
{
this.ctx = ctx;
}
public IQueryable<T> GetAll()//获得所有数据(不要软删除的)
{
return ctx.Set<T>().Where(t=>t.IsDeleted==false);//这样自动处理软删除,避免了忘了过滤软删除的数据
}
public IQueryable<T> GetAll(int start,int count) //分页获得所有数据(不要软删除的)
{
return GetAll().Skip(start).Take(count);
}
public long GetTotalCount()//获取所有数据的条数
{
return GetAll().LongCount();
}
public T GetById(long id)//根据id 获取
{
return GetAll().Where(t=>t.Id==id).SingleOrDefault();
}
public void MarkDeleted(long id)//软删除
{
T en = GetById(id);
if(en!=null)
{
en.IsDeleted = true;
en.DeleteDateTime = DateTime.Now;
ctx.SaveChanges();
}
}
}
DAL 同层内返回IQueryable 比IEnumerable 更好
下面的代码会报错:
using (MyDbContext ctx = new MyDbContext())
{
BaseDAO<Student> dao = new BaseDAO<Student>(ctx);
foreach(var s in dao.GetAll())
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
}
原因是什么? 怎么Include?需要using System.Data.Entity;
using (MyDbContext ctx = new MyDbContext())
{
BaseDAO<Student> dao = new BaseDAO<Student>(ctx);
foreach(var s in dao.GetAll().Include(t=>t.Class))
{
Console.WriteLine(s.Name);
Console.WriteLine(s.Class.Name);
}
}
有两个版本的Include、AsNoTracking:
- DbQuery 中的:DbQuery AsNoTracking()、DbQuery Include(string path)
- QueryableExtensions 中的扩展方法: AsNoTracking(this IQueryable source) 、 Include(this IQueryable source, string path)、Include<T, TProperty>(this IQueryablesource, Expression<Func<T, TProperty>> path)
DbSet 继承自DbQuery;Where()、Order、Skip()等这些方法返回的是IQueryable 接口。因此如果在IQueryable 接口类型的对象上调用Include 、AsNoTracking 就要using System.Data.Entity
22.其他
还有其他优秀的ORM 框架:NHibernate、Dapper、PetaPoco、IBatis.Net;
23.ASP.Net MVC+Entity Framework 的架构
23.1了解一些不推荐的做法
有的项目里是直接把EF 代码写到ASP.Net MVC 的Controller 中,这样做其实不符合分层的原则。ASP.Net MVC 是UI 层的框架,EF 是数据访问的逻辑。 如果就要这么做怎么做的呢? 如果在Controller 中using DbContext,把查询的结果的对象放到cshtml 中显示,那么一旦在cshtml 中访问关联属性,那么就会报错。因为关联属性可以一直关联下去,很诱惑人,include 也来不及。如果不using 也没问题,因为会自动回收。但是这是打开了“潘多拉魔盒”,甚至可以在UI 层更新数据。相当于把数据逻辑写到了UI 层。 有的三层架构中用实体类做Model,这样也是不好的,因为实体类属于DAL 层的逻辑。 没有最好的架构,只有最合适的架构! 架构不是设计出来的,而是演化出来的!
23.2EO、DTO、ViewModel
EO(Entity Object,实体对象)就是EF 中的实体类,对EO 的操作会对数据库产生影响。 EO 不应该传递到其他层。 DTO(Data Transfer Object,数据传输对象),用于在各个层之间传递数据的普通类。DTO有哪些属性取决于其他层要什么数据。DTO 一般是“扁平类”,也就是没有关联属性,都是普通类型属性。一些复杂项目中,数据访问层(DAL)和业务逻辑层(BLL)直接传递用一个DTO 类,UI 层和BLL 层之间用一个新的DTO 类。简单的项目共用同一个DTO。DTO 类似于三层架构中的Model。 EO相当于DataTable,不能传输到DAL 之外; DTO就是三层Model,在各个层中间传输数据用的 ViewModel(视图模型),用来组合来自其他层的数据显示到UI 层。简单的数据可能可以直接把DTO 交给界面显示,一些复杂的数据可以要从新转换为ViewModel 对象。
23.3多层架构
搭建一个ASP.Net MVC 三层架构项目:DAL、BLL、DTO、UI(asp.net mvc)。UI、DAL、BLL都引用DTO;BLL 引用DAL;EF 中的所有代码都定义到DAL 中,BLL 中只访问DAL、BLL 中不要引用DAL 中的EF 相关的类、不要在BLL 中执行Include 等操作、所有数据的准备工作都在DAL 中完成。
23.4架构退化
因为很多项目中的逻辑都写到DAL 中,因为主要就是对数据库的操作,BLL 就变成了对DAL 的单纯的转发,没有必要的麻烦。因此对于这种情况,可以把UI+BLL+DAL 的架构退化成UI+Service 的架构。可以理解把BLL+DAL 都写到一个Service 层中。没有“正确的架构”、“错误的架构”,只有最合适的架构。 CRUD 例子,带关联关系。班级管理、学生管理、民族。 注意:.Net 中配置文件都是加载UI 项目(ASP.net MVC)的,而不是加载DAL 中的配置文件,因此EF 的配置、连接字符串应该挪到UI 项目中。
“合适的架构”:能够满足当前项目的要求,并且适度的考虑以后项目的发展,不要想 得“太远”,不要“过度架构”;让新手能够非常快的上手(金蝶、赞同)。
UI 项目虽然不直接访问EF 中的类,但是仍然需要在UI 项目的App.config(Web.config)中对EF 做配置,也要在项目中通过Nuget 安装EF,然后并且要把连接字符串也配置到UI项目的App.config(Web.config)中。