02-EF Core笔记之保存数据
EF Core通过ChangeTracker跟踪需要写入数据库的更改,当需要保存数据时,调用DbContext的SaveChanges方法完成保存。
基本的添加、更新、删除操作示例如下:
using (var context = new BloggingContext())
{
// seeding database
context.Blogs.Add(new Blog { Url = "http://sample.com/blog" });
context.Blogs.Add(new Blog { Url = "http://sample.com/another_blog" });
context.SaveChanges();
}
using (var context = new BloggingContext())
{
// add
context.Blogs.Add(new Blog { Url = "http://sample.com/blog_one" });
context.Blogs.Add(new Blog { Url = "http://sample.com/blog_two" });
// update
var firstBlog = context.Blogs.First();
firstBlog.Url = "";
// remove
var lastBlog = context.Blogs.Last();
context.Blogs.Remove(lastBlog);
context.SaveChanges();
}
关联数据
在EF Core中,除了独立的模型外,还有与模型关联的数据,这部分数据通过独立模型添加到模型中,在SaveChanges时将会持久化到数据库中。例如:
using (var context = new BloggingContext())
{
var blog = new Blog
{
Url = "http://blogs.msdn.com/dotnet",
Posts = new List<Post>
{
new Post { Title = "Intro to C#" },
new Post { Title = "Intro to VB.NET" },
new Post { Title = "Intro to F#" }
}
};
context.Blogs.Add(blog);
context.SaveChanges();
}
在这段代码中,Blog对象和三个Post对象将会被持久化。
如果要更改关系的引用,可将Post对象中的Blog引用设置为其它Blog对象即可:
using (var context = new BloggingContext())
{
var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" };
var post = context.Posts.First();
post.Blog = blog;
context.SaveChanges();
}
如果要删除关系,只需将Post对象中的Blog引用设置为null即可,此时EF Core将判断是否为必须关系,如果为必须关系,则从数据库中删除Post对象,如果为非必须关系,则将数据库中对应的外键设置为null。
级联删除
级联删除是数据库的概念,意思是当主体被删除时,所有依赖该主体的项(通过外键关联)也会被自动删除。
EF Core对于提供了更细粒度的管理,它允许我们定义删除行为,来控制依赖关系被移除时,如何处理关系的子实体。需要注意的是,EF Core的删除行为仅对已加载的数据生效,如果关系未加载到内存中,则超出了EF Core的管控范围。
事务
事务允许以原子方式处理多个数据库操作。 如果已提交事务,则所有操作都会成功应用到数据库。 如果已回滚事务,则所有操作都不会应用到数据库。
默认情况下,每次SaveChanges方法的所保存的所有更改都将在一个事务中,要么全部保存成功,要么全部保存失败。此种情况已能满足大多数应用的需要。
共享事务(通过共享连接实现)
共享事务仅对关系型数据库有效,因为此机制用到了DbConnection和DbTransaction。要实现该机制,首先要在多个DbContext之间共享数据库连接。
以下代码演示了如何共享数据库连接:
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);
}
}
对于上面代码中的BloggingContext,可以先创建DbConnection来进行实例化,也可以通过DbTransaction获取DbConnection来实例化。随后即可在同一个DbConnection上共享事务了。
使用 System.Transactions(环境事物)
如果需要跨较大作用域进行协调,则可以使用环境事务。例如:
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 (System.Exception)
{
// TODO: Handle failure
}
}
}
显示登记到环境事务中:
context.Database.EnlistTransaction(transaction);
使用环境事务前,需验证使用的提供程序是否支持环境事务。
并发控制
数据库并发指多个进程或用户同时访问或更改数据库中的相同数据的情况。 并发控制指的是用于在发生并发更改时确保数据一致性的特定机制。
EF Core采用乐观并发控制来解决并发冲突问题。工作原理:每当在 SaveChanges 期间执行更新或删除操作时,会将数据库上的并发令牌值与通过 EF Core 读取的原始值进行比较。如果一致则可以完成操作,如果不一致,则终止事务。
在关系数据库上,EF Core 会对任何 UPDATE 或 DELETE 语句的 WHERE 子句中的并发令牌值进行检查。 执行这些语句后,EF Core 会读取受影响的行数。如果未影响任何行,将检测到并发冲突,并且 EF Core 会引发 DbUpdateConcurrencyException。
在检测到并发冲突后,EF Core会引发DbUpdateConcurrencyException异常,该异常中提供了一些有用的参数来帮助我们解决冲突:
- “当前值”是应用程序尝试写入数据库的值。
- “原始值”是在进行任何编辑之前最初从数据库中检索的值。
- “数据库值”是当前存储在数据库中的值。
此处可进行数据合并或用户选择等方式决策如何解决冲突。
状态断开对象的处理
EF Core判断更新或添加数据是通过ChangeTrancker来进行的,这个操作需要在同一个DbContext中进行,而web应用通常先查询到数据,然后将数据发送到客户端进行相应的操作,随后再由客户端提交到服务器端,此时实体所在的DbContext已发生变化,如何判断对实体进行更新或添加就成了一个问题。
解决这个问题最简单的方法是,更新和添加使用不同的web路径,服务器端通过提供Add方法和Update方法来区分操作。
除此之外,如果实体使用自动生成的主键,EF Core则可以通过判断主键是否为默认值(null、0)来判断是新增或更新。并且,对于这种情况,可直接使用DbContext的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方法将比较两个实体的值,并对发生改变的属性进行重新赋值,未发生改变的值保持不变,生成更新数据库语句时也仅更新改变的字段。
对于依赖关系的操作,同样遵循以上几种方式。
删除操作
对于删除操作,如果是删除一个对象,则可以明确该对象的主键,并从数据库中移除,此种情况不进行探讨。
这里需要探讨的是,当对依赖关系中的列表进行部分删除,如何进行更新的问题。例如Blog对象中有多个Post对象,如果从Blog中删除部分Post,则意味着直接移除了Post对象,此时如果是断开连接的情况,则EF Core无法跟踪到Post实体列表的变更,从而导致无法正确的处理删除。
一种可用的方案是采用软删除,将数据标记为已删除,此时的操作与更新相同。然后在查询数据时,使用查询筛选器,将标记为已删除的数据过滤掉,从而达到删除的效果。
对于物理删除,一种可用的方案是对Post列表进行对比,相应的代码如下:
public static void InsertUpdateOrDeleteGraph(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);
}
}
foreach (var post in existingBlog.Posts)
{
if (!blog.Posts.Any(p => p.PostId == post.PostId))
{
context.Remove(post);
}
}
}
context.SaveChanges();
}