基本保存
- 添加数据
- 更新数据
- 删除数据
- 单个 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层触发器嵌套;
网友评论