美文网首页
2020-05-24 - EFCore保存数据提纲

2020-05-24 - EFCore保存数据提纲

作者: daiwei_9b9c | 来源:发表于2020-05-24 22:20 被阅读0次

基本保存

  • 添加数据
  • 更新数据
  • 删除数据
  • 单个 SaveChanges中的多个操作(事务性)

相关数据

  • 添加新实体和相关实体
  • 往导航属性中添加相关实体
  • 新建主实体并修改已存在子实体的导航属性
  • 删除关系
    通过将引用导航设置为 null 或从集合导航中删除相关实体来删除关系
    • 默认情况 ( 不配置级联删除的情况下 )
      对于必选关系,将配置级联删除行为,并将从数据库中删除子实体/依赖实体
      对于可选关系,默认情况下不会配置级联删除,但会将外键属性设置为 null

级联删除

描述一种允许在删除某行时自动触发删除相关行的特性

  • 删除行为 DeleteBehavior 枚举类型, 传递到 OnDelete FluentAPI 来控制是主体/父实体的删除还是依赖实体/子实体关系的断开会对依赖实体/子实体产生副作用
    删除主体/父实体或断开与子实体的关系时有三个 EF 可执行的操作:
    a. 可以删除子项/依赖项
    b. 子项的外键值可以设置为 null (外键不可以为null时, savechanges将抛出异常)
    c. 子项保持不变

#删除主实体的代码

var blog = context.Blogs.Include(b => b.Posts).First();
context.Remove(blog);
context.SaveChanges();

1. (可选关系) 外键可为null时的级联删除

a. Cascade -- 删除实体和子实体
b. ClientSetNull(默认)

内存中相关实体外键属性变为null, SaveChanges 时会变更数据库;
但如果存在未加载的关联子实体,可能会抛出异常,因为引用的主实体不存在

c. SetNull, 外键属性设置为 null

内存中相关实体外键属性变为null, SaveChanges 时内存中关联实体将设置外键为 null;
如果存在未加载的关联子实体,数据库支持时会设置关联子实体的外键为null,数据库不支持时引发异常

d. Restrict, 不进行任何改变

EFCore 抛出异常, 子实体的外键不发生改变, 引用了删除的实体

2. 必选关系 ( 外键不可为null时) 的级联删除

DeleteBehavior 在 OnDelete 中的值
a. Cascade(默认) 删除相关实体
b. ClientSetNull,
SQL抛出异常, 不可以设置外键字段为 null
c. SetNull, ,
SQL抛出异常, 不可以设置外键字段为 null
d. Restrict, 不进行任何改变
EFCore 抛出异常, 子实体的外键不发生改变, 引用了删除的实体

删除孤立项代码

var blog = context.Blogs.Include(b => b.Posts).First();
blog.Posts.Clear();
context.SaveChanges();

a. DeleteBehavior.Cascade ( 无论是可选还是必选)
子实体从数据库删除
b. 具有必选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNull
SQL执行异常,无法设置 BlogId字段为 null
c. 具有可选关系的 DeleteBehavior.ClientSetNull 或 DeleteBehavior.SetNull
Post 的 BlogId 字段设置为 null
d. 具有必选或可选关系的 DeleteBehavior.Restrict
EFCore 抛出异常, Post有外键,但是未指向内存中的 Blog 对象
Blog '1' is in state Unchanged with 2 posts referenced.
Post '1' is in state Modified with FK '1' and no reference to a blog.

并发冲突

  • 有三组值可用于帮助解决并发冲突:

“当前值” 是应用程序尝试写入数据库的值。
“原始值” 是在进行任何编辑之前最初从数据库中检索的值。
“数据库值” 是当前存储在数据库中的值。

  • 处理并发冲突的常规方法是:

在 SaveChanges 期间捕获 DbUpdateConcurrencyException。
使用 DbUpdateConcurrencyException.Entries 为受影响的实体准备一组新更改。
刷新并发令牌的原始值以反映数据库中的当前值。
重试该过程,直到不发生任何冲突。

事务

  • 默认事务行为
  • 控制事务
  • 共享事务 --
    要共享事务,上下文必须共享 DbConnection 和 DbTransaction
var connectionString = @"Server=(localdb)\mssqllocaldb;Database=EFSaving.Transactions;Trusted_Connection=True;ConnectRetryCount=0";
var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseSqlServer(new SqlConnection(connectionString))
                .Options;   
using (var context1 = new BloggingContext(options))
{
        using (var transaction = context1.Database.BeginTransaction())
        {
           using (var context2 = new BloggingContext(options))
           {
                context2.Database.UseTransaction(transaction.GetDbTransaction());
          }  
      }
}
public class BloggingContext : DbContext
{
            public BloggingContext(DbContextOptions<BloggingContext> options)
                : base(options)
            { }
            ....
}
  • 使用外部 DbTransactions(仅限关系数据库)
using (var connection = new SqlConnection(connectionString))
{
    connection.Open();
    using (var transaction = connection.BeginTransaction())
    {
           var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseSqlServer(connection)
                .Options;
          using (var context = new BloggingContext(options))
            {
                context.Database.UseTransaction(transaction);  //使用外部的事务
            }
            transaction.Commit();
    }
}
  • 使用 System.Transactions
    a. 环境事务
    TransactionScope
using (var scope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
     using (var connection = new SqlConnection(connectionString))
     {
           connection.Open();
           var options = new DbContextOptionsBuilder<BloggingContext>()
                .UseSqlServer(connection)
                .Options;
          using (var context = new BloggingContext(options))
            {
                context.Database.UseTransaction(transaction);  //使用外部的事务
            }
            scope.Complete();
     }
}

b. 在显式事务中登记
context.Database.EnlistTransaction(transaction)

using (var transaction = new CommittableTransaction(
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
         var connection = new SqlConnection(connectionString);
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;
        using (var context = new BloggingContext(options))
        {
            context.Database.OpenConnection();
            context.Database.EnlistTransaction(transaction);
        }
        transaction.Commit();
}

异步保存

必须调用 await 否则, DbContext可能会被dispose

using (var context = new BloggingContext())
    {
        var blog = new Blog { Url = url };
        context.Blogs.Add(blog);
        await context.SaveChangesAsync();
    }

断开连接的实体

实例不是由 DbContext 从数据库查询而来,但是需要保存到数据库
DbContext实例需要知道实体是新实体(应插入)还是现有实体(应更新)

  • 标识新实体
    a. 客户端标识新实体
    b. 使用自动生成的键来判断是否是新实体
    b1. public static bool IsItNew(Blog blog) => blog.BlogId == 0;
    b2. public static bool IsItNew(DbContext context, object entity) => !context.Entry(entity).IsKeySet;
    c. 从数据库查询
    public static bool IsItNew(BloggingContext context, Blog blog) => context.Blogs.Find(blog.BlogId) == null;
  • 保存单个实体
    知道是需要插入还是需要更新,则可以相应地使用 Add 或 Update
    Update 方法通常将实体标记为更新,而不是插入。 但是,如果实体具有自动生成的键且未设置任何键值,则实体会自动标记为插入。
public static void InsertOrUpdate(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs.Find(blog.BlogId);
    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
    }
    context.SaveChanges();
}

SetValues 仅将与跟踪实体中的属性具有不同值的属性标记为“已修改”。这意味着当发送更新时,只会更新实际发生更改的列。 (如果未发生更改,则根本不会发送任何更新。)

设置已生成属性的显式值

  • 设置属性的数据库默认值
    modelBuilder.Entity<Employee>() .Property(b => b.EmploymentStarted) .HasDefaultValueSql("CONVERT(date, GETDATE())");
  • 显式值插入 SQL Server IDENTITY 列
    只能插入,不能更新
    context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Employees ON");
    context.SaveChanges();
    context.Database.ExecuteSqlRaw("SET IDENTITY_INSERT dbo.Employees OFF");
  • 在更新期间设置显式值
    modelBuilder.Entity<Employee>() .Property(b => b.LastPayRaise) .ValueGeneratedOnAddOrUpdate();
    默认情况下,如果尝试保存配置为在更新期间生成的属性的显式值,EF Core 将引发异常。 若要避免此问题,必须下拉到较低级别的元数据 API 并设置 AfterSaveBehavior(如上所示)。
    即默认不能在客户端设置LastPayRaise属性的值,除非像下面这样设置 AfterSaveBehavior
    modelBuilder.Entity<Employee>() .Property(b => b.LastPayRaise) .Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Ignore);
CREATE TRIGGER [dbo].[Employees_UPDATE] ON [dbo].[Employees]
    AFTER UPDATE
AS
BEGIN
    SET NOCOUNT ON;
    -- 避免被下面的 update 循环执行此触发器;              
    IF ((SELECT TRIGGER_NESTLEVEL()) > 1) RETURN;
                  
    IF UPDATE(Salary) AND NOT Update(LastPayRaise) -- 是否更新了 Salary 字段
    BEGIN
        DECLARE @Id INT
        DECLARE @OldSalary INT
        DECLARE @NewSalary INT
          
        SELECT @Id = INSERTED.EmployeeId, @NewSalary = Salary        
        FROM INSERTED
          
        SELECT @OldSalary = Salary        
        FROM deleted
          
        IF @NewSalary > @OldSalary
        BEGIN
            UPDATE dbo.Employees
            SET LastPayRaise = CONVERT(date, GETDATE())
            WHERE EmployeeId = @Id
        END
    END
END

TRIGGER_NESTLEVEL --
When no parameters are specified, TRIGGER_NESTLEVEL returns the total number of triggers on the call stack. This includes itself.
无参数时,返回此语句执行时在调用堆栈中的触发器的数量;
SQLSERVER最大支持 32层触发器嵌套;

相关文章

网友评论

      本文标题:2020-05-24 - EFCore保存数据提纲

      本文链接:https://www.haomeiwen.com/subject/lcajahtx.html