学编程,来源栈;先学习,再交钱
当前系列: ADO&EF 修改讲义

为什么需要Logger?

没有logger,EF的内部运行就是一个“黑箱”:

  • 学习/开发:知道生成的SQL语句,SQL语句的执行情况,entity的状态变化等等
  • 运行/维护:logger记录程序运行过程,可以帮助系统维护/支持人员发现错误异常等。

LogTo()

EF core需要在OnConfiguring()中配置:

            optionsBuilder
                .UseSqlServer(connString)
                .EnableSensitiveDataLogging()  //这样log中就包含了数据库数据

为了能够获得最详细(包含在数据库中)的信息,我们需要“开启敏感数据日志”:EnableSensitiveDataLogging()。但是,项目发布以后,这样做就会有安全风险(黑客有可能拿到log文件)。怎么办呢?使用条件编译符

#if DEBUG
                //仅在调试状态使用
                .EnableSensitiveDataLogging(true)
#endif

然后继续配置

    .LogTo(
        (id, level) => level == LogLevel.Error,            //过滤条件
        log => Console.WriteLine(log)   //如何记录log
    );
类似于log4net,EF中为Log设定了不同的Level (F12演示),通常:
  • 开发中使用Debug
  • 发布到生产环境用Warning或Error

设断点演示:debug时输出显示log信息

PS:ASP.NET core中自动集成logger,详见:ASP.NET Core documentation.


增删改查

我们首先学习单个entity的数据库操作。

入口

数据的增删改查一样依赖两个EF核心对象:

  • DbContext:代表和数据库的一次会话(session),可用于:
    1. 维护(打开/关闭)数据库的连接
    2. 进行数据的同步,完成数据库的增删改查
  • DbSet:基于DbContext,是一个entity的泛型集合(实现IEnumerable等),可以想象成一个DbContext由多个DbSet组成

EF的DbContext和DbSet都提供了基于Unit Of Work and Repository patterns(F12演示DbContext注释)的增删改查方法:

我们首先实例化一个DbContext:

SqlDbContext context = new SqlDbContext();

增:Add

首先需要一个entity对象:

    Student student = new Student
    {
        //Id = 1000, Name = "李智博"
    };
然后,将其添加到DbContext或DbSet中:
context.Students.Add(student);  
最后,将改动同步到数据库:
context.SaveChanges();

关键在于:

  • Add()之后还要调用SaveChanges(),否则不会作用到数据库。实际上,除了“查”以外的所有操作,都需要调用SaveChanges()
  • 主键Id:SaveChanges()之前不能赋值,否则会和数据库中Id主键IDENTITY设定冲突;SaveChanges()之后,从数据库获得并被自动赋值

查:Find

Find()方法可以(也只能是)通过主键(可以是联合主键)查找到对应的Entity。

Student student = context.Students.Find(1);

演示:略

删:Remove

通常来说,我们会首先通过DbContext得到一个entity,

Student student = repository.Find<Student>(6);

然后再通过Remove()将其删除:

context.Students.Remove(student);  //或者:

最后进行同步。

注意:一定要是通过context.Find()出来的entity才行!

这样更符合逻辑:删除的是从DbContext中获取的那个Id=6的Student……

改:Update?

本质上,EF并没有专门的“改”的方法。惯常的办法是从DbContext中获取到entity后予以修改,

    Student student = context.Find<Student>(1003);
    student.IsFemale = true;

然后SaveChanges()予以同步。

注意:EF会检查entity有无“实质性的”更改,仅当entity属性真正的更改之后,EF才会生成UPDATE语句进行更新。

便捷方式

在EFcore里,所有DbSet引导的方法,也都可以由DbContext点出:

context.Add<Student>(student);

甚至还可以省略泛型参数:

context.Add(student); 
其他:
context.Remove<Student>(student);
context.Remove(student);
Student student = context.Find<Student>(1);//但这里的泛型参数不能省略

还可以使用AddRange()和RemoveRange()添加/删除多个entities。

SaveChange()

并不是每次增删改都需要调用,可以在所有entity操作完成之后,调用一次之后,就能一次性的将所有entity改动同步到数据库。

而且DbContext是跨entity的!演示:

Student atai = context.Students.Find(2);
atai.Name = "--阿泰--";
atai.Age = 19;

Student ljp = context.Find<Student>(3);
context.Students.Remove(ljp);

Teacher fg = new Teacher
{
    Name = "飞哥"
};
context.Add(fg);

context.SaveChanges();

常见错误

  • 增:忘记Add()
  • 删:Remove()非Find()出来的entity
  • 改:调用Update()而不是SaveChanges()
  • 忘记SaveChange() 

如果不是因为疏忽,就是因为没有理解到:

EF依靠DbContext来管理/维护/追踪entity,如果一个entity:

  • 是通过context“弄”出来(比如Find())或者被添加(比如Add())到context的,context会自动负责维护追踪
  • 和DbContext没有“关联”,EF是不会将其持久化(同步到数据库)的。


ChangeTracker

复习:会话/状态管理 

EF的DbContext中维护着一个ChangeTracker:追踪记录着所有需要被“同步”到数据库的entity的“改动”。

EntityState

断点调试演示:

用watch查看 DbContext.ChangeTracker.DebugView:

当一个new Student()被Add()到context,其LongView内容为:

Student {Id: -2147482647} Added
  Id: -2147482647 PK Temporary
  IsFemale: 'False'
  Name: '李智博'

其中 Added,表明了该entity此时的状态(EntityState)。

此外,(演示)

  • Find()出来的entity,状态为unchanged
  • Remove()删除的entity,状态为deleted

当调用SaveChanges()时,EF会根据ChangeTracker的内容,决定相应的生成相应的Insert/Delete语句。

Detect and Update

断点演示:改变entity的属性值会改变其状态。

@想一想@:为什么?

EF还没这个本事监控entity的属性值更改,^_^

PS:EF也是C#写的,C#做不到的事,EF也没办法。

EF只能在SaveChanges()的时候,将

  • 当前entity的属性值,
  • 和初次纳入context时的entity属性值

进行比对(detect),如果:

  1. 所有属性完全没有改变,啥事不做
  2. 有属性发生改变,生成这些“变动属性相关”的Update语句,比如:
    UPDATE [Students] SET [Age] = @p0
    WHERE [Id] = @p1;
    注意:仅更改了Age列,因为只有Age发生改变!


虚拟entity

EF里面暂时没有根据Id生成proxy entity( LazyLoad)的语法。这样删改就会先有一个SELECT的过程,必须先Find()或Where()。

但我们可以用“变通”的方式,直接new一个只有Id的entity,代替Find()或Where()之后的结果:

Bed bed = new Bed{ Id = 1};
然后,

直接Delete

context.Set<Bed>().Remove(bed);

这样就不会有SELECT的过程,能够在context.SaveChanges()时生成DELETE语句删除这个Bed。

直接Update

要麻烦一点,需要首先将bed对象纳入context的追踪:

context.Set<Bed>().Attach(bed);
bed.Size = 12;

Update()和Attach()

除了Add()以外,还有这两个方法,可以把context之外的entity纳入context追踪管理。

他们的区别在于:

  • Add()总是把EntityState都设置为Added
  • Update()和Attach()只有当entity主键(Id)没有赋值时,EntityState才会被设置成Added

而当entity有主键值时:

  • Update():会将EntityState设为了Modified,此后EF在SaveChanges()时不再侦测改动,直接更新所有
    UPDATE [Students]  
    SET [Age] = @p0, [Enroll] = @p1, [IsFemale] = @p2, [Name] = @p3
    WHERE [Id] = @p4;
  • Attach():会将EntityState设为了Unchanged,此后EF在SaveChanges()时需要侦测(detect)改动,只Update(Attach()之后)变动列
        Student s7 = new Student { Id = 7 };
        repository.Attach<Student>(s7);
        s7.Age = 23;//注意先后顺序!
        repository.SaveChanges();
    UPDATE [Students] SET [Age] = @p0
    WHERE [Id] = @p1;

@想一想@:直接Update时使用Update()行不行?为什么?


Unit Of Work

复习:定义和作用

SaveChanges()自带事务

默认的,调用SaveChanges()时,EF自动触发事务机制。即:
  • 只要SaveChanges()的有一个(SQL)操作失败,所有操作操作回滚;
  • 只有所有操作都成功,事务才提交。
断点演示:
  • 因调用SaveChanges(),Logger中出现Transaction……
  • 因将老师的工资设置为负数,与数据库约束冲突,所有更改回滚
    Student lzb = context.Find<Student>(1);
    lzb.Name = "李智博";

    Teacher fg = context.Find<Teacher>(1);
    fg.Salary = -100;    //违反自定义CHECK必须≥0

    context.SaveChanges();

注意

  • 对象lzb和fg都被同一个context所追踪。
  • EF的UoW依赖于数据库支持。换言之,数据库不支持事务,就没办法实现UoW了。

显式声明事务

基于模型特殊原因(封装/取Id/……),我们可能需要将多次SaveChanges()置于同一个UoW中。这就需要:
    using (IDbContextTransaction transaction = context.Database.BeginTransaction())
    {
        try
        {
            Student lzb = context.Find<Student>(1);
            lzb.Name = "lzb";
            context.SaveChanges();

            Teacher fg = context.Find<Teacher>(1);
            fg.Salary = -100;  //负数不行,必须0或正数 context.SaveChanges();

            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
            throw;
        }
    }

断点演示:事务提交/回滚

注意:transaction对象不是来源于DbContext,而是context.Database,所以可以形成“跨context”事务

@想一想@:如果在catch中注释掉transaction.Rollback();,会是什么结果?

分布式事务

即TransactionScope,在.NET core 2.1之后被支持,写法同ADO.NET。


作业

ORM概述第4题

学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 ADO&EF 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码