• APS.NET MVC + EF (02)---深入理解ADO.NET Entity Framework


    2.7 深入理解Entity Framework

    性能问题几乎是一切ORM框架的通病,对于EF来说,引起性能低的原因主要在以下几个方面。

    • 复杂的对象管理机制
      为了在
      .NET中更好地管理模型对象,EF提供了一套内部机制管理和跟踪对象的状态,保持对象的一致性,带来方便的同时,降低了性能。
    • 高度封装的执行机制

      在EF应用中,编写的任何一个查询表达式都需要经过分析,解析成SQL语句,然后调用底层的ADO.NET Providers去执行。直接执行SQL语句相比,性能上有一定的降低。

    • 低效的SQL语句

      EF采用映射机制将对象操作转换为SQL语句,SQL语句一般是基于标准模板生成的,不会进行特殊的优化,这与直接针对业务编写的SQL语句去操作数据相比,效率一般会打折扣,特别是复杂的数据库操作。

         

    Linq查询最终生成的SQL语句是什么样的?我们可以使用ToString()方法直接输出T-SQL代码。如示例10所示。

    示例10

    using (MySchoolContext db = new MySchoolContext())

    {

    var result = db.Student.Where(n => n.StudentName.Contains("张"));

    Console.WriteLine(result);

    }

    运行结果如图2-10所示。

    图2-10 Linq查询生成的SQL语句

    当然,EF本身对性能有一系列的优化措施,会使用这写手段的前提是对EF的执行机制有足够的了解。

       

    2.7.1 EF的状态管理

    在程序中实现数据的增、删、改操作,EF会监控这些状态的变化,在执行SaveChange()方法时,会根据对象状态的变化执行相应的操作。如示例11所示。

    示例11

    using (MySchoolContext db = new MySchoolContext())

    {

    Grade grade = new Grade() { GradeName = "Y3" };

    //输出当前对象状态

    Console.WriteLine(db.Entry(grade).State);

       

    db.Grade.Add(grade);

    Console.WriteLine(db.Entry(grade).State);

       

    db.SaveChanges();

    Console.WriteLine(db.Entry(grade).State);

    }

       

    示例11中,通过Entry()方法获取模型状态,该方法是DbContext类的成员方法,定义如下:

    public DbEntityEntry<TEntity> Entry<TEntity>(TEntity entity)

    返回类型 DbEntityEntry<T> 封装了对象状态相关的信息。常用成员如表3-2所示。

    表3-2 DbEntityEntry 的主要成员

    方法或属性

    说明

    CurrentValues

    获取由此对象表示的跟踪实体的当前属性值

    OriginalValues

    获取由此对象表示的跟踪实体的原始属性值

    State

    获取或设置实体的状态

    Reload()

    从数据库重新加载对象的值

    其中,State属性是一个EntityState枚举类型,其取值如下:

    Detached:表示对象存在,但没有被跟踪

    Unchanged:表示对象尚未经过修改

    Added:表示对象为新对象,并且已添加到对象上下文

    Deleted:对象已从对象上下文中删除

    Modified:表示对象的一个标量属性已更改

       

    通过设置实体的State属性也可以实现对数据的操作,如:

    using (MySchoolContext db = new MySchoolContext())

    {

    Grade grade = new Grade() { GradeName = "Y3" };

    db.Entry(grade).State = EntityState.Added;

    db.SaveChanges( );

    }

       

    除了需要增删改的对象会受状态管理机制管理外,通过EF查询的数据也默认会进行状态管理。可以通过两种方式指定查询不进行状态管理。

    方式一:使用AsNoTracking()方法,如示例12所示。

    示例12

    using (MySchoolContext db = new MySchoolContext())

    {

    var result = db.Student.AsNoTracking().FirstOrDefault();

    Console.WriteLine(db.Entry(result).State);

    }

       

    方式二:设置Configuration.AutoDetectChangesEnabled 属性为false,如示例13。

    示例13

    using (MySchoolContext db = new MySchoolContext())

    {

    //禁用自动跟踪变化

    db.Configuration.AutoDetectChangesEnabled = false;

    for (int i = 0; i < 5000; i++)

    {

    var stu = new Student() { StudentName = "alex",

    GradeId = 1, Age = 20 };

    db.Student.Add(stu);

    }

    db.SaveChanges();

    }

       

    在使用EF修改或删除数据时,必须先查询对象,然后再对其进行修改或删除。然而现实开发中很多情况都是通过主键删除一条数据。我们可以通过实体的状态特性来进行优化。

    示例14

    using (MySchool1Entities entities = new MySchool1Entities())

    {

    //创建替身对象

    var stu = new Student { StudentNo = "10001" };

    //给实体附加到上下文中

    entities.Student.Attach(stu);

    //删除

    entities.Student.Remove(stu);

    entities.SaveChanges();

    }

    代码中的Attach()方法可以将EntityState.Unchangee状态的对象附加到上下文中。

       

    2.7.2 延迟加载和贪婪加载

    1. 延迟加载

      又称为懒加载,只有每次调用子实体(外键所在的实体)的时候,才去查询数据库, 主表数据加载的时候,不去查询外键所在的从表。

      实现延迟加载需要满足两个条件:

    • poco类是public且不能为sealed。
    • 导航属性需要标记为Virtual。

    也可以关闭延迟加载,方法是:

    db.Configuration.LazyLoadingEnabled = false;

    关闭延迟加载后,查询主表数据时候,主表的中从表实体为null。

    示例15

    using (dbContext1 db = new dbContext1())

    {

    Console.WriteLine("---------------- 01-延迟加载 ---------------");

    //EF默认就是延迟加载,默认下面的语句就是true,所以下面语句注释没有任何影响

    db.Configuration.LazyLoadingEnabled = true;

       

    var list = db.Student.ToList(); //此处加载的数据,没有对从表进行任何查询操作

    foreach (var stu in list)

    {

    Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId, stu.studentName);

    //下面调用导航属性(一对一的关系) 每次调用时,都要去查询数据库

    var stuAddress = stu.StudentAddress;

    Console.WriteLine("地址编号:{0},地址名称:{1}",

    stuAddress.studentAddressId, stu.studentName);

    }

    }

       

    1. 贪婪加载

      又名:立即加载、贪婪加载、预加载。查询主表的时候通过Include()方法一次性将数据查询了出来,在调用从表数据的时候,从缓存中读取,无须查询数据库。

      实现方式:

    • 先关闭延迟加载:db.Configuration.LazyLoadingEnabled = false;
    • 查询主表的同时通过Include把从表数据也查询出来。

    示例16

    using (dbContext1 db = new dbContext1())

    {

    Console.WriteLine("------------------- 03-立即加载 ------------------");

       

    //1.关闭延迟加载

    db.Configuration.LazyLoadingEnabled = false;

       

    //2. 获取主表数据的同时,通过Include将从表中的数据也全部加载出来

    var list = db.Student.Include("StudentAddress").ToList();

    foreach (var stu in list)

    {

    Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId,

    stu.studentName);

    //这里获取从表中的数据,均是从缓存中获取,无需查询数据库

    var stuAddress = stu.StudentAddress;

    Console.WriteLine("地址编号:{0},地址名称:{1}",

    stuAddress.studentAddressId, stu.studentName);

    }

    }

       

    1. 显示加载

      关闭了延迟加载,单纯查询了主表数据,这个时候需要重新查询从表数据,就要用到显式加载了。

      使用步骤:

      ①:单个实体用:Reference

      ②:集合用:Collection

      ③:最后需要Load一下

      示例17

    using (dbContext1 db = new dbContext1())

    {

    Console.WriteLine("----------------- 04-显式加载 ------------------");

    //1.关闭延迟加载

    db.Configuration.LazyLoadingEnabled = false;

       

    //2.此处加载的数据,不含从表中的数据

    var list = db.Student.ToList();

    foreach (var stu in list)

    {

    Console.WriteLine("学生编号:{0},学生姓名:{1}", stu.studentId,

    stu.studentName);

    //3.下面的这句话,可以开启重新查询一次数据库

    //3.1 单个属性的情况用Refercence

    db.Entry<Student>(stu).Reference(c => c.StudentAddress).Load();

    //3.2 集合的情况用Collection

    //db.Entry<Student>(stu).Collection(c => c.StudentAddress).Load();

       

    //下面调用导航属性(一对一的关系) 每次调用时,都要去查询数据库

    var stuAddress = stu.StudentAddress;

    Console.WriteLine("地址编号:{0},地址名称:{1}",

    stuAddress.studentAddressId, stu.studentName);

    }

    }

       

    1. 小结

      什么时候使用延迟加载,什么时候又使用贪婪加载呢?

      延迟加载只有在需要使用数据时加载,如果不需要使用实体的关联数据,可以使用延迟加载。使用贪婪加载适用于预先了解要使用什么数据的情况,利用这种方式一次性加载数据,可以减少数据库访问次数。

      从实际情况来看,使用默认的延迟加载就可以了,2次或3次的数据库查询是可以接受的。而循环中多次读取数据库,可以考虑使用贪婪加载。

    2.7.3 本地缓存

    在使用EF时,有时会多次使用一个查询结果,如示例18所示。

    示例18

    using (MySchoolEntities db = new MySchoolEntities())

    {

    //查询全部学生

    IQueryable<Student> stus = db.Student;

    Console.WriteLine("全部学生姓名:");

    foreach (var stu in stus)

    {

    Console.WriteLine("学生姓名:{0}",stu.StudentName);

    }

    //查询并输出学生人数

    Console.WriteLine("学生人数为:{0}",db.Student.Count());

    }

       

    示例18中会产生两次查询,但从需求来看,完全没有必要,因为第二次完全可以利用第一次查询的结果。事实上,完全可以使用EF的缓存功能,直接利用缓存的结果,DbSet<T>的Local属性正是用于提供缓存的数据。

    示例18中,将"db.Student.Count()"替换为"db.Student.Local.Count()",这样就不会产生新的查询了。

    另外,DbSet<T>提供了 Find()方法,用于通过主键查找实体,其查询速度比First()和FirstOrDefault()方法快的多,并且如果相应的实体已经被DbContext缓存,EF会在缓存中直接返回对应的实体,而不会执行数据库访问。

       

    2.7.4 EF中的事务

    EF中的事务主要分为三类,分别是SaveChanges、DbContextTransaction 和

    TransactionScope。

    1. SaveChanges事务

      在前面内容中,SaveChanges一次性将本地缓存中所有的状态变化一次性提交到数据库,这就是一个事务,要么统一成功,要么统一回滚。

      示例19

    using (DbContext db = new CodeFirstModel())

    {

    //增加

    TestInfor t1 = new TestInfor()

    {

    id = Guid.NewGuid().ToString("N"),

    txt1 = "txt1",

    txt2 = "txt2"

    };

    db.Set<TestInfor>().Add(t1);

    //删除

    TestInfor t2 = db.Set<TestInfor>().Where(u => u.id == "1").FirstOrDefault();

    if (t2 != null)

    {

    db.Set<TestInfor>().Remove(t2);

    }

    //修改

    TestInfor t3 = db.Set<TestInfor>().Where(u => u.id == "3").FirstOrDefault();

    t3.txt2 = "我是李马茹23";

       

    //SaveChanges事务提交

    int n = db.SaveChanges();

    Console.WriteLine("数据作用条数:" + n);

    }

       

    示例19中,如果三个操作中有任意一个出现错误,就会回滚,结果n为0。

       

    1. DbContextTransaction 事务

      使用场景:EF调用SQL语句的时候使用该事务、 多个SaveChanges的情况。

      示例20

    using (DbContext db = new CodeFirstModel())

    {

    DbContextTransaction trans = null;

    try

    {

    //开启事务

    trans = db.Database.BeginTransaction();

    //增加

    string sql1 = @"insert into TestInfor values(@id,@txt1,@txt2)";

    SqlParameter[] pars1 ={

    new SqlParameter("@id",Guid.NewGuid().ToString("N")),

    new SqlParameter("@txt1","txt11"),

    new SqlParameter("@txt2","txt22")

    };

    db.Database.ExecuteSqlCommand(sql1, pars1);

    //删除

    string sql2 = @"delete from TestInfor where id=@id";

    SqlParameter[] pars2 ={

    new SqlParameter("@id","22")

    };

    db.Database.ExecuteSqlCommand(sql2, pars2);

    //修改

    string sql3 = @"update TestInfor set txt1=@txt1 where id=@id";

    SqlParameter[] pars3 ={

    new SqlParameter("@id","3"),

    new SqlParameter("@txt1","二狗子")

    };

    db.Database.ExecuteSqlCommand(sql3, pars3);

       

    //提交事务

    trans.Commit();

    Console.WriteLine("事务成功了");

    }

    catch (Exception ex)

    {

    Console.WriteLine(ex.Message);

    trans.Rollback(); //回滚

       

    }

    finally

    {

    //也可以把该事务写到using块中,让其自己托管,就不需要手动释放了

    trans.Dispose();

    }

    }

       

    DbContextTransaction事务也适用于多个SaveChanges的情况。

    示例21

    using (DbContext db = new CodeFirstModel())

    {

    //自动脱管,不需要手动释放

    using (DbContextTransaction trans = db.Database.BeginTransaction())

    {

    try

    {

    TestInfor t1 = new TestInfor()

    {

    id = Guid.NewGuid().ToString("N"),

    txt1 = "111111111",

    txt2 = "222222222222"

    };

    db.Entry(t1).State = EntityState.Added;

    db.SaveChanges();

       

    TestInfor t2 = new TestInfor()

    {

    id = Guid.NewGuid().ToString("N") + "123",

    txt1 = "111111111",

    txt2 = "222222222222"

    };

    db.Entry(t2).State = EntityState.Added;

    db.SaveChanges();

       

    trans.Commit();

    }

    catch (Exception)

    {

    trans.Rollback();

    }

    }

    }

       

    1. TransactionScope事务

      该种事务适用于多数据库连接的情况,在此不做介绍,请自行查阅相关资料。

       

    2.7.5 从实体框架回归SQL

    EF虽然本身有很多优化机制,但和直接使用ADO.NET相比,还是有一定的性能差距,因此EF在DbContext类的Database属性里提供了ExecuteSqlCommand()和SqlQuery()两个方法,用来直接访问数据库。

    1. ExecuteSqlCommand()

      方法的定义如下:

      public int ExecuteSqlCommand(string sql, params object[] parameters)

      用来执行增、删、改操作,返回结果为受影响行数。

    2. SqlQuery()

      方法的定义如下:

      public DbRawSqlQuery<T> SqlQuery<T>(string sql, params object[] parameters);

      用来执行查询操作,返回结果是一个集合。

    示例22

    using (MySchool1Entities db = new MySchool1Entities())

    {

    //执行update语句

    string sql = "update grade set gradeName=@gradeName where

    gradeId=@gradeId";

    SqlParameter[] ps =

    {

    new SqlParameter("@gradeName","第二学年"),

    new SqlParameter("@gradeId",3)

    };

    int result=db.Database.ExecuteSqlCommand(sql, ps);

    if (result>0)

    {

    Console.WriteLine("数据更新完成!");

    }

    //执行查询语句

    sql = "select * from from student where studentNo=@stuNo";

    ps = new SqlParameter[] { new SqlParameter("@stuNo", "S1001234") };

    var stu = db.Database.SqlQuery<Student>(sql, ps);

    Console.WriteLine(stu.ToList()[0]);

    }

       

    2.8 封装EFDAL

       

      1     public class BaseDAL<T> where T:class
      2     {
      3         private DbContext db
      4         {
      5             get
      6             {
      7                 DbContext dbContext = CallContext.GetData("dbContext") as DbContext;
      8                 if (dbContext == null)
      9                 {
     10                     dbContext = new MySchoolContext();
     11                     CallContext.SetData("dbContext", dbContext);
     12                 }
     13                 return dbContext;
     14             }
     15         }
     16 
     17         /// <summary>
     18         /// 执行增加,删除,修改操作(或调用存储过程)
     19         /// </summary>
     20         /// <param name="sql"></param>
     21         /// <param name="pars"></param>
     22         /// <returns></returns>
     23         public int ExecuteSql(string sql, params SqlParameter[] pars)
     24         {
     25             return db.Database.ExecuteSqlCommand(sql, pars);
     26         }
     27 
     28         /// <summary>
     29         /// 执行查询操作
     30         /// </summary>
     31         /// <typeparam name="T"></typeparam>
     32         /// <param name="sql"></param>
     33         /// <param name="pars"></param>
     34         /// <returns></returns>
     35         public List<T> ExecuteQuery(string sql, params SqlParameter[] pars)
     36         {
     37             return db.Database.SqlQuery<T>(sql, pars).ToList();
     38         }
     39 
     40         /// <summary>
     41         /// 添加
     42         /// </summary>
     43         /// <param name="model"></param>
     44         /// <returns></returns>
     45         public int Add(T model)
     46         {
     47             db.Set<T>().Add(model);
     48             return db.SaveChanges();
     49         }
     50 
     51         /// <summary>
     52         /// 删除(适用于先查询后删除的单个实体)
     53         /// </summary>
     54         /// <param name="model">需要删除的实体</param>
     55         /// <returns></returns>
     56         public int Del(T model)
     57         {
     58             db.Set<T>().Attach(model);
     59             db.Set<T>().Remove(model);
     60             return db.SaveChanges();
     61         }
     62 
     63         /// <summary>
     64         /// 根据条件删除(支持批量删除)
     65         /// </summary>
     66         /// <param name="delWhere">传入Lambda表达式(生成表达式目录树)</param>
     67         /// <returns></returns>
     68         public int DelBy(Expression<Func<T, bool>> delWhere)
     69         {
     70             var listDels = db.Set<T>().Where(delWhere);
     71             foreach(var model in listDels)
     72             {
     73                 db.Set<T>().Attach(model);
     74                 db.Set<T>().Remove(model);
     75             }
     76             return db.SaveChanges();
     77         }
     78 
     79         /// <summary>
     80         /// 修改
     81         /// </summary>
     82         /// <param name="model">修改后的实体</param>
     83         /// <returns></returns>
     84         public int Modify(T model)
     85         {
     86             db.Entry(model).State = EntityState.Modified;
     87             return db.SaveChanges();
     88         }
     89 
     90         /// <summary>
     91         /// 批量修改
     92         /// </summary>
     93         /// <param name="model">要修改实体中 修改后的属性 </param>
     94         /// <param name="whereLambda">查询实体的条件</param>
     95         /// <param name="proNames">lambda的形式表示要修改的实体属性名</param>
     96         /// <returns></returns>
     97         public int ModifyBy(T model, Expression<Func<T, bool>> whereLambda, params string[] proNames)
     98         {
     99             List<T> listModifes = db.Set<T>().Where(whereLambda).ToList();
    100             Type t = typeof(T);
    101             List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
    102             Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
    103             proInfos.ForEach(p =>
    104             {
    105                 if (proNames.Contains(p.Name))
    106                 {
    107                     dicPros.Add(p.Name, p);
    108                 }
    109             });
    110             foreach (string proName in proNames)
    111             {
    112                 if (dicPros.ContainsKey(proName))
    113                 {
    114                     PropertyInfo proInfo = dicPros[proName];
    115                     object newValue = proInfo.GetValue(model, null);
    116                     foreach (T m in listModifes)
    117                     {
    118                         proInfo.SetValue(m, newValue, null);
    119                     }
    120                 }
    121             }
    122             return db.SaveChanges();
    123         }
    124 
    125         /// <summary>
    126         /// 根据条件查询
    127         /// </summary>
    128         /// <param name="whereLambda">查询条件(lambda表达式的形式生成表达式目录树)</param>
    129         /// <returns></returns>
    130         public IQueryable<T> GetListBy(Expression<Func<T, bool>> whereLambda)
    131         {
    132             return db.Set<T>().Where(whereLambda);
    133         }
    134         /// <summary>
    135         /// 根据条件排序和查询
    136         /// </summary>
    137         /// <typeparam name="Tkey">排序字段类型</typeparam>
    138         /// <param name="whereLambda">查询条件</param>
    139         /// <param name="orderLambda">排序条件</param>
    140         /// <param name="isAsc">升序or降序</param>
    141         /// <returns></returns>
    142         public IQueryable<T> GetListBy<Tkey>(Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
    143         {
    144             if (isAsc)
    145             {
    146                 return db.Set<T>().Where(whereLambda).OrderBy(orderLambda);
    147             }
    148             else
    149             {
    150                 return db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda);
    151             }
    152         }
    153         /// <summary>
    154         /// 分页查询
    155         /// </summary>
    156         /// <typeparam name="Tkey">排序字段类型</typeparam>
    157         /// <param name="pageIndex">页码</param>
    158         /// <param name="pageSize">页容量</param>
    159         /// <param name="whereLambda">查询条件</param>
    160         /// <param name="orderLambda">排序条件</param>
    161         /// <param name="isAsc">升序or降序</param>
    162         /// <returns></returns>
    163         public IQueryable<T> GetPageList<Tkey>(int pageIndex, int pageSize, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
    164         {
    165 
    166             IQueryable<T> list = null;
    167             if (isAsc)
    168             {
    169                 list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
    170                .Skip((pageIndex - 1) * pageSize).Take(pageSize);
    171             }
    172             else
    173             {
    174                 list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
    175               .Skip((pageIndex - 1) * pageSize).Take(pageSize);
    176             }
    177             return list;
    178         }
    179         /// <summary>
    180         /// 分页查询输出总行数
    181         /// </summary>
    182         /// <typeparam name="Tkey">排序字段类型</typeparam>
    183         /// <param name="pageIndex">页码</param>
    184         /// <param name="pageSize">页容量</param>
    185         /// <param name="whereLambda">查询条件</param>
    186         /// <param name="orderLambda">排序条件</param>
    187         /// <param name="isAsc">升序or降序</param>
    188         /// <returns></returns>
    189         public IQueryable<T> GetPageList<Tkey>(int pageIndex, int pageSize, out int rowCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true)
    190         {
    191             IQueryable<T> list = null;
    192             rowCount = db.Set<T>().Where(whereLambda).Count();
    193             if (isAsc)
    194             {
    195                 list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
    196                    .Skip((pageIndex - 1) * pageSize).Take(pageSize);
    197             }
    198             else
    199             {
    200                 list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
    201                  .Skip((pageIndex - 1) * pageSize).Take(pageSize);
    202             }
    203             return list;
    204         }
    205 
    206 
    207     }
    View Code

    <<BaseDAL.cs>>

       

  • 相关阅读:
    openstack项目【day23】:glance基础
    openstack项目【day23】:openstack组件介绍
    openstack项目【day23】:云计算介绍(一)
    学习有五个层次和境界
    gdb 初步学习记录
    Linux samba 服务的配置
    QT 5.7.0 移植之 tslib 编译配置
    tiny4412 u-boot 启动参数的设置
    Windows 只能安装32位虚拟机问题
    Tiny4412 虚拟机交叉编译环境的设置以及编译u-boot 和 kernel
  • 原文地址:https://www.cnblogs.com/mrfang/p/10743557.html
Copyright © 2020-2023  润新知