.NET EF(Entity Framework)详解

一丶Entity Framework

(一)EF简介

(1)ORM:Object Relation Mapping ,通俗说:用操作对象的方式来操作数据库。
(2)插入数据库不再是执行Insert,而是类似于Person p = new Person();p.Age=3;p.Name=“英莱特”;db.Save§;这样的做法。
(3)ORM工具有很多Dapper、PetaPoco、NHibernate,最首推的还是微软官方的Entity Framework,简称EF。
(4)EF底层仍然是对ADO.Net的封装。EF支持SQLServer、MYSQL、Oracle、Sqlite等所有主流数据库。
(5)使用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。

(6)Code First的微软的推荐用法是程序员只写模型类,数据库由EF 帮我们生成,当修改模型类之后,EF 使用“DB Migration”自动帮我们更改数据库。但是这种做法太激进,不适合很多大项目的开发流程和优化,只适合于项目的初始开发阶段。Java的Hibernate 中也有类似的DDL2SQL 技术,但是也是用的较少。“DB Migration”也不利于理解EF,因此在初学阶段,我们将会禁用“DB Migration”,采用更实际的“手动建数据库和模型类”的方式。

(7)如果大家用过 NHibernate 等ORM 工具的话,会发现开发过程特别麻烦,需要在配置文件中指定模型类属性和数据库字段的对应关系,哪怕名字完全也一样也要手动配置。使用过Java 中Struts、Spring 等技术的同学也有过类似“配置文件地狱”的感觉。 像ASP.Net MVC 一样,EF 也是采用“约定大于配置”这样的框架设计原则,省去了很多配置,能用约定就不要自己配置。

在.Net Framework SP1微软包含一个实体框架(Entity Framework),此框架可以理解成微软的一个ORM产品。用于支持开发人员通过对概念性应用程序模型编程(而不是直接对关系存储架构编程)来创建数据访问应用程序。目标是降低面向数据的应用程序所需的代码量并减轻维护工作。

(二)Entity Framework应用程序有以下优点:

(1)应用程序可以通过更加以应用程序为中心的概念性模型(包括具有继承性、复杂成员和关系的类型)来工作。
(2)应用程序不再对特定的数据引擎或存储架构具有硬编码依赖性。
(3)可以在不更改应用程序代码的情况下更改概念性模型与特定于存储的架构之间的映射。
(4)开发人员可以使用可映射到各种存储架构(可能在不同的数据库管理系统中实现)的一致的应用程序对象模型。
(5)多个概念性模型可以映射到同一个存储架构。
(6)语言集成查询支持可为查询提供针对概念性模型的编译时语法验证。

实体框架Entity Framework是 DO.NET中的一组支持开发面向数据的软件应用程序的技术。在EF中的实体数据模型(EDM)由以下三种模型和具有相应文件扩展名的映射文件进行定义。

  • 概念架构定义语言文件 (.csdl) – 定义概念模型。

  • 存储架构定义语言文件 (.ssdl) – 定义存储模型(又称逻辑模型)。

  • 映射规范语言文件 (.msl) – 定义存储模型与概念模型之间的映射。
    实体框架使用这些基于XML的模型和映射文件将对概念模型中的实体和关系的创建、读取、更新和删除操作转换为数据源中的等效操作。EDM甚至支持将概念模型中的实体映射到数据源中的存储过程。它提供以下方式用于查询 EDM 并返回对象:

  • LINQ to Entities–提供语言集成查询(LINQ)支持用于查询在概念模型中定义的实体类型。

  • Entity SQL – 与存储无关的SQL方言,直接使用概念模型中的实体并支持诸如继承和关系等 EDM 功能。

  • 查询生成器方法 --可以使用LINQ风格的查询方法构造 Entity SQL 查询。

(三)相关知识复习

(1)var类型推断:var p =new Person();
(2)匿名类型。var a =new {p.Name,Age=5,Gender=p.Gender,Name1=a.Name};//{p.Name}=={Name=p.Name}
(3)给新创建对象的属性赋值的简化方法:Person p = new Person{Name=“tom”,Age=5};等价于Person p = new Person();p.Name=“tom”;p.Age=5;

(四)lambda表达式:

函数式编程,在Entity framework编程中用的很多

Action al= delegate(int i) { Console.Writeline(i); };

可以简化成(=>读作goes to) :

Action< int> a2 = (inti) = > { Console.Writeline(i); };

还可以省略参数类型(编译器会自动根据委托类型推断):

Action< int> a3 = (i) = > { Console.Writeline(i); };

如果只有一个参数还可以省略参数的小括号(多个参数不行)

Action a4 = i = > { Console.Writeline(i); };

如果委托有返回值,并且方法体只有一行代码,这一行代码还是返回值,那么就可以连方法的大括号和return都省略:

Func fl= delegate(int i, int j) { return "结果是" + (i + j); };
Func 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();
}
public string Name { get; set; }
public List 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 list = new List();
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};

(1)Any(),判断集合是否包含元素,返回值是bool,一般比Cout()>0 效率高。Any还可以指定条件表达式。

bool b = list.Any(p => p.Age > 50); 等价于bool b =list.Where(p=>p.Age>50).Any();

(2)Distinct(),剔除完全重复数据。(*)注意自定义对象的Equals 问题:需要重写Equals 和GetHashCode 方法来进行内容比较。
(3)排序:升序list.OrderBy(p=>p.Age);降序list.OrderByDescending(p=>p.Age)。指定多个排序规 则,而不是多个OrderBy,而是:list.OrderByDescending(p=>p.Age).ThenBy(p=>p.Salary),也支 持ThenByDescending()。注意这些操作不会影响原始的集合数据。
(4)Skip(n)跳过前n条数据;
(5)Take(n)获取最多n条数据,如果不足n条也不会报错。常用来分页获取数据。
(6)list.Skip(3).Take(2)跳过前3条数据获取2条数据。
(7)Except(items1)排除当前集合中在items1中存在的元素。用int数组举例。
(8)Union(items1)把当前集合和items1中组合。用int 数组举例。
(9)Intersect(items1) 把当前集合和items1 中取交集。用int 数组举例。
(10)分组

foreach(var g in list.GroupBy(p => p.Age))
{
    Console.WriteLine(g.Key+":"+g.Average(p=>p.Salary));
}

(11)SelectMany:把集合中每个对象的另外集合属性的值重新拼接为一个新的集合

foreach(var s in teachers.SelectMany(t => t.Students))
{
    Console.WriteLine(s);//每个元素都是Person
}
}

注意不会去重,如果需要去重要自己再次调用Distinct()
(12)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 的安装

(1)基础阶段用控制台项目。使用NuGet 安装EntityFramework。会自动在App.config中增加两个entityFramework 相关配置段;
(2)在 web.config 中配置连接字符串


易错点:不能忘了写providerName="System.Data.SqlClient"增加两个entityFramework 相关配置段;

(八)EF 简单DataAnnotations 实体配置

(1)数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime字段。
(2)创建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,否则后面可能会有麻烦。

(3)创建DbContext类(模型类、实体类)

  public class MyDBContext: DbContext
    {
        //表示使用连接字符串中名字为conn1 的去连接数据库
        public MyDBContext() : base("name=strcon")
        {

        }
        //通过对Persons 集合的操作就可以完成对T_Persons的操作
        public DbSet Persons { get; set; }
    }

(4)测试

   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 的方式

(1)数据库中建表T_Perons,有Id(主键,自动增长)、Name、CreateDateTime 字段。
(2)创建 Person 类。模型类就是普通C#类

   public class Person
    {
        public long ID { get; set; }
        public string Name { get; set; }
        public DateTime CreateTime { get; set; }
    }

(3)创建一个 PersonConfig 类,放到ModelConfig 文件夹下(PersonConfig、EntityConfig这样的名字都不是必须的)

public class PersonConfig : EntityTypeConfiguration
    {
        public PersonConfig()
        {
            this.ToTable("T_Person");
        }
    }

(4)创建 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 Persons { get; set; }
    }

下面这句话:

modelBuilder.Configurations.AddFromAssembly(Assembly.GetExecutingAssembly());

代表从这句话所在的程序集加载所有的继承自EntityTypeConfiguration 为模型配置类。还有很多加载配置文件的做法(把配置写到OnModelCreating中或者把加载的代码写死到OnModelCreating 中),但是这种做法是最符合大项目规范的做法。

和以前唯一的不同就是:模型不需要标注Attribute;编写一个XXXConfig类配置映射关系;DbContext 中override OnModelCreating;
(5)测试

    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()。

(1)增加,一个点:如果Id是自动增长的,创建的对象显然不用指定Id的值,并且在SaveChanges ()后会自动给对象的Id属性赋值为新增行的Id字段的值。

(2)删除。先查询出来要删除的数据,然后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 语句
(3)修改:先查询出来要修改的数据,然后修改,然后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();

性能问题?同上。
(4)查。因为DbSet 实现了IQueryable 接口,而IQueryable 接口继承了IEnumerable 接口,所以可以使用所有的linq、lambda 操作。给表增加一个Age 字段,然后举例orderby、groupby、where 操作、分页等。一样一样的。
(5)查询 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语句,则或者报错,或者被放到内存中操作,性能就会非常低。

(1)怎么查看真正执行的SQL是什么样呢?

DbContext有一个Database属性,其中的Log属性,是Action委托类型,也就是可以指向一个void A(string s)方法,其中的参数就是执行的SQL语句,每次EF执行SQL语句的时候都会执行Log。因此就可以知道执行了什么SQL。

EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select 查询,ToList()内部也是遍历结果集形成List。

查看Update操作,会发现只更新了修改的字段。

(2)观察一下前面学学习时候执行的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));

(3)EF中还可以多次指定where来实现动态的复合检索:

//必须写成IQueryable,如果写成IEnumerable 就会在内存中取后续数据
IQueryable items = ctx.Persons;//为什么把IQueryable换成var 会编译出错
items = items.Where(p=>p.Name=="inlett");
items = items.Where(p=>p.Id>5);

查看一下生成的SQL语句。
(4)EF是跨数据库的,如果迁移到MYSQL上,就会翻译成MYSQL的语法。要配置对应数据库的Entity Framework Provider。
(5)细节:
每次开始执行的__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("select Name,Count(*) Count from T_Persons where Id>{0} and CreateDateTime<={1} group by Name",2, DateTime.Now); //返回值是DbRawSqlQuery类型,也是实现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("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(被修改)。
.NET EF(Entity Framework)详解_第1张图片
Add()、Remove()修改对象的状态。所有状态之间几乎都可以通过:Entry§.State=xxx的方式进行强制状态转换。
通过代码来演示一下。这个状态转换图没必要记住,了解即可。
状态改变都是依赖于Id的(Added除外)

应用(*)

当SavaChanged()方法执行期间,会查看当前对象的EntityState的值,决定是去新增(Added)、修改Modified)、删除(Deleted)或者什么也不做(UnChanged)。下面的做法不推荐,在旧版本中一些写法不被支持,到新版EF中可能也会不支持。
(1)不先查询再修改再保存,而是直接更新部分字段的方法

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();

(2)不先查询再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()。

其他一般不用设置的(了解即可)

(1)主键:this.HasKey(p => p.pId);
(2)某个字段不参与映射数据库:this.Ignore(p => p.Name1);
(3)this.Property(p => p.Name).IsFixedLength(); 是否对应固定长度
(4)this.Property(p => p.Name).IsUnicode(false) 对应的数据库类型是varchar类型,而不是nvarchar
(5)this.Property(p => p.Id).HasColumnName(“Id1”); Id列对应数据库中名字为Id的字段
(6)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最有魅力的地方在于对于多表间关系的映射,可以简化工作。 复习一下表间关系:
(1)一对多(多对一):一个班级对应着多个学生,一个学生对着一个班级。一方是另外一方的唯一。在多端有一个指向一端的外键。举例:班级表:T_Classes(Id,Name) 学生表

T_Students(Id,Name,Age,ClassId)

(2)多对多:一个老师对应多个学生,一个学生对于多个老师。任何一方都不是对方的唯一。 需要一个中间关系表。具体: 学生表T_Students(Id,Name,Age,ClassId) , 老师表 T_Teachers(Id,Name,PhoneNum),关系表T_StudentsTeachers(Id,StudentId,TeacherId)

和关系映射相关的方法:

(1)基本套路this.Has(p=>p.A).With***() 当前这个表和A 属性的表的关系是Has 定义, With 定义的是A 对应的表和这个表的关系。Optional/Required/Many
(2)HasOptional() 有一个可选的(可以为空的)
(3)HasRequired() 有一个必须的(不能为空的)
(4)HasMany() 有很多的
(5)WithOptional() 可选的
(6)WithRequired() 必须的
(7)WithMany() 很多的
举例:
在AAA 实体中配置this.HasRequired(p=>p.BBB).WithMany();是什么意思? 在AAA 实体中配置this.HasRequired(p=>p.BBB).WithRequired ();是什么意思?

配置一对多关系

(1)先按照正常的单表配置把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 ;
}

(2)给Student类增加一个Class类型、名字为Class(不一定非叫这个,但是习惯是:外键名去掉Id)的属性,要声明成virtual(后面讲原因)。
(3)然后就可以实现各种对象间操作了:

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();

(4)如果ClassId 字段可空怎么办?直接把ClassId 属性设置为long?
(5)还可以在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();或者在之前赋值
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 Teachers { get; set; }=new List();
}
class Teacher
{
    public long Id { set; get; }
    public string Name { get; set; }
    public virtual ICollection Students { get; set; }=new List< Student >();
}
class StudentConfig : EntityTypeConfiguration
{
    public StudentConfig()
    {
        ToTable("T_Students");
    }
}
class TeacherConfig : EntityTypeConfiguration
{
    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();
Teacher t2 = new Teacher();
t2.Name = "王老师";
t2.Students = new List();
Student s1 = new Student();
s1.Name = "tom";
s1.Teachers = new List();
Student s2 = new Student();
s2.Name = "jerry";
s2.Teachers = new List();
t1.Students.Add(s1);

附录:

(1)关于WithMany()的参数

在一对多关系中,如果只配置多端关系并且没有给WithMany()指定参数的话,在进行反向关系操作的时候就会报错。要么在一端也配置一次,最好的方法就是还是只配置多端,只不过给WithMany()指定参数:

class StudentConfig:EntityTypeConfiguration
{
    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()参数。
总结一对多、多对多的“最佳实践”

(2)一对多最佳方法(不配置一端的集合属性):

多端

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);

(3)一对多的配置(在一端配置一个集合属性,极端不推荐)

多端

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 Students { get; set; } = new List();
}

多端的配置(StudentConfig)中

this.HasRequired(e => e.Class).WithMany(e=>e.Students)//WithMany()的参数不能丢 .HasForeignKey(e=>e.ClassId);

(4)多对多最佳配置

两端模型


public class Student
{
    public long Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection Teachers { get; set; } = new List();
}
public class Teacher
{
    public long Id { get; set; }
    public string Name { get; set; }
    public virtual ICollection Students { get; set; } = new List();
}

在其中一端配置(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);以后对数据库表的修改都手动完成,也就是手动改实体类、 手动改数据库表。

三丶 延迟加载(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 等中,解决方法?

(一) 不延迟加载,怎么样一次性加

用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();

(二) 延迟加载的一些坑

DbContext销毁后就不能再延迟加载了,因为数据库连接已经断开
下面的代码最后一行会报错:


Student s;
using (MyDbContext ctx = new MyDbContext())
{
   s = ctx.Students.First();
}
Console.WriteLine(s.Class.Name);

两种解决方法:
用Include,不延迟加载(推荐)

Student s;
using (MyDbContext ctx = new MyDbContext())
{
    s = ctx.Students.Include(t=>t.Class).First();
}
Console.WriteLine(s.Class.Name);

关闭前把要用到的数据取出来


Class c;
using (MyDbContext ctx = new MyDbContext())
{
    Student s = ctx.Students.Include(t=>t.Class).First();\
    c = s.Class;
}
Console.WriteLine(c.Name);

(2)两个取数据一起使用

下面的程序会报错:已有打开的与此 Command 相关联的 DataReader,必须首先将它关闭。

foreach(var s in ctx.Students)
{
    Console.WriteLine(s.Name);
    Console.WriteLine(s.Class.Name);
}

(3)因为EF的查询是“延迟执行”的,只有遍历结果集的时候才执行select 查询,而由于延迟加载的存在到s.Class.Name也会再次执行查询。ADO.Net中默认是不能同时遍历两个DataReader。因此就报错。

解决方法有如下

允许多个DataReader 一起执行:在连接字符串上加上MultipleActiveResultSets=true,但只适用于SQL 2005以后的版本。其他数据库不支持。

执行一下ToList(),因为ToList()就遍历然后生成List:

foreach(var s in ctx.Students.ToList())
{
    Console.WriteLine(s.Name);
    Console.WriteLine(s.Class.Name);
}

推荐做法:用Include预先加载:


foreach(var s in ctx.Students.Include(e=>e.Class))
{
    Console.WriteLine(s.Name);
    Console.WriteLine(s.Class.Name);
}

四丶 实体类的继承

所有实体类都会有一些公共属性,可以把这些属性定义到一个父类中。比如:

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 操作类:

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 where T:BaseEntity
{
    private MyDbContext ctx;//不自己维护MyDbContext 而是由调用者传递,因为调用者可以要执行很多操作,由调用者决定什么时候销毁。
    public BaseDAO (MyDbContext ctx)
    {
        this.ctx = ctx;
    }
    public IQueryable GetAll()//获得所有数据(不要软删除的)
    {
        return ctx.Set().Where(t=>t.IsDeleted==false);//这样自动处理软删除,避免了忘了过滤软删除的数据
    }
    public IQueryable 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 dao = new BaseDAO(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 dao = new BaseDAO(ctx);
    foreach(var s in dao.GetAll().Include(t=>t.Class))
    {
        Console.WriteLine(s.Name);
        Console.WriteLine(s.Class.Name);
    }
}

有两个版本的Include、AsNoTracking:

(1)DbQuery 中的:DbQuery AsNoTracking()、DbQuery Include(string path)
(2)QueryableExtensions 中的扩展方法: AsNoTracking(this IQueryable source) 、 Include(this IQueryable source, string path)、Include(this IQueryablesource, Expression> path)
DbSet继承自DbQuery;

Where()、Order、Skip()等这些方法返回的是IQueryable接口。

因此如果在IQueryable接口类型的对象上调用Include、AsNoTracking就要using System.Data.Entity

五丶 linq

(一) 简介

查询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写法”更清晰,更紧凑。反编译得知,这两种写法最终编译成同样的东西,所以本质上一样的。

(二) 辟谣

“Linq被淘汰了”是错误的说法,应该是“Linq2SQL被淘汰了”。linq就是微软发明的这个语法,可以用这种语法操作很多数据,操作SQL数据就是Linq2SQL,linq操作后面学的EntityFramework就是Linq2Entity,linq操作普通.Net 对象就是Linq2Object、Linq操作XML文档就是Linq2XML。

(三) Linq 基本语法

以from item in items 开始,items为待处理的集合,item为每一项的变量名;最后要加上select,表示结果的数据;记得select一定要最后。这是刚用比较别扭的地方。

看各种用法,不用解析:

var r= from d in dogs select d.Id;
var r= from d in dogs select new{d.Id,d.Name,Desc="一条狗"};

排序

var items = from d in dogs
//orderby d.Age
//orderby d.Age descending
orderby d.Age,d.MasterId descending
select d;

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”漂亮

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() };

(四) 混用

只有Where,Select,OrderBy,GroupBy,Join 等这些能用linq写法,如果要用下面的 “Max,Min,Count,Average,Sum,Any,First,FirstOrDefault,Single,SingleOrDefault,Distinct,Skip,Take等”则还要用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说:论漂亮我不行,论强大你不行!

六丶C#6.0 语法

(1)属性的初始化“public int Age{get;set;}=6”。低版本.Net中怎么办?

(2)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))]
(3)??语法:int j= i??3; 如果i为null则表达式的值为3,否则表达式的值就是i的值。如果在低版本中怎么办?int j = (i== null)?3:(int)i;
应用案例:string name = null;Console.WriteLine(name??“未知”);
(4)?.语法:string s8 = null;string s9 = s8?.Trim(); 如果s8为null,则不执行Trim(),让表达式的结果为null。在低版本中怎么办?

string s9=null;if(s8!=null){s9=s8.Trim();};

七丶ORM

(一)ORM简介

ORM(Object Relational Mapping)对象关系映射,一般指持久化数据和实体对象的映射
.NET EF(Entity Framework)详解_第2张图片
数据存储是绝大多数软件系统都要接触到的技术,具有一定规模的软件产品,为了方便存储和管理数据,便引入了数据库这一工具,但是数据如何从程序写入数据库的呢?

为方便程序员通过代码将数据写入数据库,一般的语言开发的厂商都会为各种数据库适配数据库连接的驱动程序,比如ADO.Net,JDBC等。

但是数据库连接的驱动程序的职责在于管理连接数据库,设置连接参数等信息,通常会返回各自封装好的数据集类型,驱动程序封装的类型往往是以数据为核心进行描述的,现代化的软件设计为了简便描述事物的特征都而以面向对象思想为核心,两者之间的转换还有很多的路要走。

除却转换部分,Sql语句的编写也是一大学问,一般的编程语言都没有为sql语句定义类型,这是因为每种数据库的sql语句风格都是不一样的,难以给出一个统一的方案。退而求其次,一般的编程语言都采用字符串形式传递sql语句到数据库驱动程序。抛弃各种各样的sql语句的学习之外,这种方式有一个很大的弊端,那就是sql语句的拼写极容易由于手误而犯错。

在这种场景下,ORM框架诞生了!

(二)ORM的工作原理

没有ORM的情况下,主要有两个槽点:
驱动返回类型和对象不能良好映射
SQL语句的学习成本及易错率(多种数据库语句难以全部掌握)
那么,且看我们的ORM如何改善这两个槽点:.NET EF(Entity Framework)详解_第3张图片
(1)数据驱动返回的数据通常都是以数据为核心的数据集合,我们需要通过手动将类对象和数据库返回的列数据进行一一匹配获取,然后赋值到对象上。在这里要感谢泛型和反射两大语法,通过泛型和反射,我们可以获取到任何实体类的属性而不是具体到某一种类型,通过遍历实体类的属性去数据集合中一一获取并复制返回。这一操作便将数据集合的数据完美包装成了以面向对象为核心的和类相关的对象数据集合。

(2)sql语句的拼写,我们可以提供一套公共sql语句模板,然后在具体实体对象操作的时候将实体对象的属性名称和属性值当作参数拼接进去,组装成完整的sql语句(例如java体系中的Mybatis框架)或者依旧采用封装一套浅显易懂的Api,Api内部通过对应方法和实体对象的组装成sql语句(例如.Net体系中EntityFramework框架)

(3)最重要的两个问题解决完之后,我们可以在框架中做一些对我们有帮助的其他事情。ORM框架做的最多的便是“缓存”。
.NET EF(Entity Framework)详解_第4张图片
作为程序员应该掌握的基础知识,数据库操作是要和硬盘打交道的,而程序是在内存中运行的,操作内存的速度要比操作硬盘快数十倍以上,可见一个访问量较高的大型系统很容易由于数据库操作过于频繁而拖慢整体速度,从而影响系统的使用。因此,ORM框架要帮助我们减少数据库的访问,加快系统速度。

ORM框架的缓存系统一般是较为复杂的,而且每种ORM框架对缓存的实现机制都是不同的。整体的思路却是一致的,对访问频率较高的数据进行缓存,并在对数据编辑的时候要对缓存进行更新,以免出现数据不一致的问题。详细的缓存实现策略这里不一一赘述,感兴趣可以针对某个ORM框架进行剖析。

(三)ORM的优缺点

优点:

(1)ORM框架降低了学习门槛,一个对sql语句并不熟悉的开发人员也可以很容易通过简易的ORM框架Api进行数据库的操作。
(2)提高了开发效率,ORM使我们减少很多繁琐重复的工作量,让我们的注意力集中在实现业务上。
(3)一定程度上提高了程序的响应速度。

弊端:

(1)框架会自动生成Sql语句,所有场景的sql语句都是同一套模板,难以自动针对场景对sql语句进行良好的优化,某种场景下很容易生成执行很慢的sql语句。如果让DBA看到这样的执行sql,必定引来抓狂崩溃。
(2)ORM框架只是为了满足绝大多数的场景而生的,特殊需要优化sql的场景下,我们完全可以直接使用驱动手动执行sql或使用ORM框架内提供的sql语句api进行自定义sql语句。

你可能感兴趣的:(.NET EF(Entity Framework)详解)