Entity Framework Core 数据保存原理详解

Posted JimCarter

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Entity Framework Core 数据保存原理详解相关的知识,希望对你有一定的参考价值。

https://docs.microsoft.com/zh-cn/ef/core/saving/


每个上下文实例都有一个 ChangeTracker,它负责跟踪需要写入数据库的更改。 更改实体类的实例时,这些更改会记录在 ChangeTracker中,然后在调用 SaveChanges 时被写入数据库。EF将这些数据变更转换为特定的数据库操作(例如,关系数据库的 INSERT、UPDATE 和 DELETE 命令)。

1. 增删改的基本用法

using (var context = new BloggingContext())
{
    // add
    context.Blogs.Add(new Blog { Url = "http://example.com/blog_one" });
    context.Blogs.Add(new Blog { Url = "http://example.com/blog_two" });

    // update 
    var firstBlog = context.Blogs.First();
    firstBlog.Url = "";

    // remove
    var lastBlog = context.Blogs.OrderBy(e => e.BlogId).Last();
    context.Blogs.Remove(lastBlog);

    context.SaveChanges();
}

对于大多数数据库提供程序,SaveChanges是事务性的。 意味着只会全部成功或全部失败。

2. 级联删除

ef使用外键来表示两个实体间的关系。如果一个父实体里有一条数据删除了,那么子实体的外键有可能就没得对应了,在大多数数据库中都会引起外键约束错误。有两种方式可以解决这个问题:

  • 将子实体的外键设置为null:只能应用于外键可空的关系上(影子外键可空)
  • 删除对应的子实体:可应用到任何关系上,又称为“级联删除”(cascade delete)

2.1 配置级联行为

OnModelCreatingOnDelete方法中配置:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Blog>()
        .HasOne(e => e.Owner)
        .WithOne(e => e.OwnedBlog)
        .OnDelete(DeleteBehavior.ClientCascade);
}

不同的behavior映射到不同的数据库架构:

DeleteBehavior对数据库架构的影响
CascadeON DELETE CASCADE
RestrictON DELETE NO ACTION
NoActiondatabase default
SetNullON DELETE SET NULL
ClientSetNullON DELETE NO ACTION
ClientCascadeON DELETE NO ACTION
ClientNoActiondatabase default

详见

3. 并发控制

efcore使用乐观锁进行并发控制。

3.1 工作原理

某一个属性会被配置为并发令牌,每当SaveChanges时会将数据库上这个属性的值与当前的值进行比较。如果不一样则表示冲突了,终止当前事务。各个数据库的provider负责并发令牌的比较。当有冲突之后,会抛出DbUpdateConcurrencyException. 假如将Person实体的LastName属性配置为并发令牌,则对数据的UPDATEDELETE操作的where条件上都会加上这个属性:

UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;

3.2 解决并发冲突

实体有三个值:

  1. CurrentValues:当前值
  2. GetDatabaseValues:数据库对应的值
  3. OriginalValues:当初从数据库查询时的原始值

常见的处理方法是:

  1. SaveChanges期间捕获DbUpdateConcurrencyException异常
  2. 使用DbUpdateConcurrencyException.Entries获取冲突的实体
  3. 使用实体数据库值覆盖实体原始值
  4. 重复这个过程直到不再产生冲突

以下代码,Person.FirstNamePerson.LastName 为并发令牌:

using var context = new PersonContext();
var person = context.People.Single(p => p.PersonId == 1);
person.PhoneNumber = "555-555-5555";

context.Database.ExecuteSqlRaw(
    "UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 1");

var saved = false;
while (!saved)
{
    try
    {
        context.SaveChanges();
        saved = true;
    }
    catch (DbUpdateConcurrencyException ex)
    {
        foreach (var entry in ex.Entries)
        {
            if (entry.Entity is Person)
            {
                var proposedValues = entry.CurrentValues;
                var databaseValues = entry.GetDatabaseValues();

                foreach (var property in proposedValues.Properties)
                {
                    var proposedValue = proposedValues[property];
                    var databaseValue = databaseValues[property];

                    // 自行决定应该用哪个值
                    proposedValues[property] = <value to be saved>;
                }
                entry.OriginalValues.SetValues(databaseValues);
            }
            else
            {
                throw new NotSupportedException("Don't know how to handle concurrency conflicts for "+ entry.Metadata.Name);
            }
        }
    }
}

4. 事务

4.1 默认事务行为

默认情况下如果数据库的provider支持事务,则会在SaveChanges方法中应用事务。所以SaveChanges方法时原子性的,要么全部成功要么全部失败。大多数情况下这个功能已经足够了,如果有必要就自己手动控制事务。

4.2 手动控制事务

事务会自动回滚,不需要自己在catch里手动回滚

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();
try
{
    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context.SaveChanges();

    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
    context.SaveChanges();

    var blogs = context.Blogs
        .OrderBy(b => b.Url)
        .ToList();

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

4.3 事务保存点

可以控制事务回滚到哪个状态

using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
    context.SaveChanges();

    transaction.CreateSavepoint("BeforeMoreBlogs");//创建保存点

    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
    context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
    context.SaveChanges();

    transaction.Commit();
}
catch (Exception)
{
    // If a failure occurred, we rollback to the savepoint and can continue the transaction
    transaction.RollbackToSavepoint("BeforeMoreBlogs");

    // TODO: Handle failure, possibly retry inserting blogs
}

4.4 多个DbContext共享一个事务

此功能仅应用于关系型数据库,因为需要使用特定于关系数据库的DbTransactionDbConnection。如果要共享事务,则数据库上下文必须共享DbTransactionDbConnection

首先共享DbConnection,最简单的方法就是不要在OnConfiguring方法里配置上下文只从外部传过来:

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

如果非要在OnConfiguring里进行配置也可以这么写:

public class BloggingContext : DbContext
{
    private DbConnection _connection;

    public BloggingContext(DbConnection connection)
    {
      _connection = connection;
    }

    public DbSet<Blog> Blogs { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connection);
    }
}

然后就可以共享DbTransaction了:

using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
    .UseSqlServer(connection)
    .Options;

using var context1 = new BloggingContext(options);
using var transaction = context1.Database.BeginTransaction();
try
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
    context1.SaveChanges();

    using (var context2 = new BloggingContext(options))
    {
        context2.Database.UseTransaction(transaction.GetDbTransaction());

        var blogs = context2.Blogs
            .OrderBy(b => b.Url)
            .ToList();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

4.5 ADO.NET与EFCORE共享一个事务

using var connection = new SqlConnection(connectionString);
connection.Open();

using var transaction = connection.BeginTransaction();
try
{
    // Run raw ADO.NET command in the transaction
    var command = connection.CreateCommand();
    command.Transaction = transaction;
    command.CommandText = "DELETE FROM dbo.Blogs";
    command.ExecuteNonQuery();

    // Run an EF Core command in the transaction
    var options = new DbContextOptionsBuilder<BloggingContext>()
        .UseSqlServer(connection)
        .Options;

    using (var context = new BloggingContext(options))
    {
        context.Database.UseTransaction(transaction);
        context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
        context.SaveChanges();
    }

    // Commit transaction if all commands succeed, transaction will auto-rollback
    // when disposed if either commands fails
    transaction.Commit();
}
catch (Exception)
{
    // TODO: Handle failure
}

4.6 使用System.Transactions控制更大scope的事务

如果需要跨较大作用域进行协调,则可以使用环境事务。

using (var scope = new TransactionScope(
    TransactionScopeOption.Required,
    new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
    using var connection = new SqlConnection(connectionString);
    connection.Open();

    try
    {
        // Run raw ADO.NET command in the transaction
        var command = connection.CreateCommand();
        command.CommandText = "DELETE FROM dbo.Blogs";
        command.ExecuteNonQuery();

        // Run an EF Core command in the transaction
        var options = new DbContextOptionsBuilder<BloggingContext>()
            .UseSqlServer(connection)
            .Options;

        using (var context = new BloggingContext(options))
        {
            context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
            context.SaveChanges();
        }

        // Commit transaction if all commands succeed, transaction will auto-rollback
        // when disposed if either commands fails
        scope.Complete();
    }
    catch (Exception)
    {
        // TODO: Handle failure
    }
}

System.Transactions的不足

  1. EF Core 依赖数据库的provider以实现对 System.Transactions 的支持。 如果provider未实现对 System.Transactions的支持,则ef可能会完全忽略对这些 API 的调用。
  2. System.Transactions 的 .NET Core 实现当前不包括对分布式事务的支持,因此不能使用 TransactionScopeCommittableTransaction来跨多个资源管理器协调事务。具体参见

5. 断开连接的实体

由客户端传过来的实体一般都是属于断开连接的实体,处于Detached状态. 这种实体要么需要add要么需要update。一般是通过不同的路由来区分。但是你也可以通过其它方法进行区分

5.1 主键自动生成

以下方法通过判断主键有没有值进行区分:

public static bool IsItNew(DbContext context, object entity)
    => !context.Entry(entity).IsKeySet;

5.2 主键未自动生成

有两种方法进行判断:

  • 查询实体:从数据库根据主键先查一遍
  • 从客户端传递标识:客户端请求参数里告诉应该是新增还是更新

5.3 保存实体

5.3.1 保存单个实体

如果你明确的知道需要add还是update,则直接调用对应的context.Addcontext.Update方法即可。

//插入
public static void Insert(DbContext context, object entity)
{
    context.Add(entity);
    context.SaveChanges();
}
//更新
public static void Update(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

如果没法确定,且主键是自动生成的,那么直接调用context.Update即可,它自己会判断应该插入还是更新:

//插入或更新
public static void InsertOrUpdate(DbContext context, object entity)
{
    context.Update(entity);
    context.SaveChanges();
}

如果没法确定,且主键不是自动生成的,那需要自己从数据库先查一遍才行:

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

提示:SetValue只会将值不相同的属性标记为“已修改”。提交数据库时也只会提交这些字段。

5.3.2 同时保存多个实体

如果这多个实体都是新实体或者都是已有实体,则和之前一样直接调用context.Addcontext.Update方法操作他们的父实体或root实体就行了。

如果多个实体状态不一致,有新实体有旧实体,且主键都是自动生成的,则同上直接调用context.Update方法即可:

public static void InsertOrUpdateGraph(DbContext context, object rootEntity)
{
    context.Update(rootEntity);
    context.SaveChanges();
}

如果多个实体状态不一致,且主键不是自动生成,则需要先进行查询再处理:

public static void InsertOrUpdateGraph(BloggingContext context, Blog blog)
{
    var existingBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }
    }

    context.SaveChanges();
}

以上是关于Entity Framework Core 数据保存原理详解的主要内容,如果未能解决你的问题,请参考以下文章

Entity Framework Core 快速开始

Entity Framework Core 在保存时不验证数据?

Entity Framework Core 迁移命令

Entity Framework Core 数据保存原理详解

Entity Framework Core 数据保存原理详解

Entity Framework Core 数据查询原理详解