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 配置级联行为
在OnModelCreating
的OnDelete
方法中配置:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Blog>()
.HasOne(e => e.Owner)
.WithOne(e => e.OwnedBlog)
.OnDelete(DeleteBehavior.ClientCascade);
}
不同的behavior映射到不同的数据库架构:
DeleteBehavior | 对数据库架构的影响 |
---|---|
Cascade | ON DELETE CASCADE |
Restrict | ON DELETE NO ACTION |
NoAction | database default |
SetNull | ON DELETE SET NULL |
ClientSetNull | ON DELETE NO ACTION |
ClientCascade | ON DELETE NO ACTION |
ClientNoAction | database default |
3. 并发控制
efcore使用乐观锁进行并发控制。
3.1 工作原理
某一个属性会被配置为并发令牌,每当SaveChanges
时会将数据库上这个属性的值与当前的值进行比较。如果不一样则表示冲突了,终止当前事务。各个数据库的provider负责并发令牌的比较。当有冲突之后,会抛出DbUpdateConcurrencyException
. 假如将Person
实体的LastName
属性配置为并发令牌,则对数据的UPDATE
和DELETE
操作的where条件上都会加上这个属性:
UPDATE [Person] SET [FirstName] = @p1
WHERE [PersonId] = @p0 AND [LastName] = @p2;
3.2 解决并发冲突
实体有三个值:
- CurrentValues:当前值
- GetDatabaseValues:数据库对应的值
- OriginalValues:当初从数据库查询时的原始值
常见的处理方法是:
- 在
SaveChanges
期间捕获DbUpdateConcurrencyException
异常 - 使用
DbUpdateConcurrencyException.Entries
获取冲突的实体 - 使用实体数据库值覆盖实体原始值
- 重复这个过程直到不再产生冲突
以下代码,Person.FirstName
和 Person.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
共享一个事务
此功能仅应用于关系型数据库,因为需要使用特定于关系数据库的DbTransaction
和DbConnection
。如果要共享事务,则数据库上下文必须共享DbTransaction
和DbConnection
。
首先共享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
的不足
- EF Core 依赖数据库的provider以实现对
System.Transactions
的支持。 如果provider未实现对System.Transactions
的支持,则ef可能会完全忽略对这些 API 的调用。 System.Transactions
的 .NET Core 实现当前不包括对分布式事务的支持,因此不能使用TransactionScope
或CommittableTransaction
来跨多个资源管理器协调事务。具体参见
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.Add
和context.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.Add
和context.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 数据保存原理详解