EF - 使用子项更新记录
本文关键字:更新 新记录 EF | 更新日期: 2023-09-27 18:30:45
我已经研究这个问题一个星期了,我对EF感到非常沮丧。 首先,我有一个超级表 - 数据库中>子表模式。 它是使用代码优先方法设计的。 超类型称为工作流任务,定义如下:
<!-- language: c# -->
public abstract class WorkflowTask
{
public int WorkflowTaskId { get; set; }
public int Order { get; set; }
public WorkflowTaskType WorkflowTaskType { get; set; }
public WorkFlowTaskState State { get; set; }
public ParentTask ParentTask { get; set; }
public WorkflowDefinition WorkflowDefinition { get; set; }
}
示例子任务将从此任务继承并提供其他属性:
<!-- language: c# -->
public class DelayTask : WorkflowTask
{
public int Duration { get; set; }
}
这将映射到数据库,如下所示:
<!-- language: c# -->
public class WorkflowTaskEntityConfiguration : EntityTypeConfiguration<WorkflowTask>
{
public WorkflowTaskEntityConfiguration()
{
HasKey(w => w.WorkflowTaskId);
Property(w => w.WorkflowTaskId).HasColumnName("Id");
Property(w => w.Order).HasColumnName("Order");
Property(w => w.WorkflowTaskType).HasColumnName("TaskTypeId");
Property(w => w.State).HasColumnName("TaskStateId");
HasOptional(c => c.ParentTask).WithMany()
.Map(c => c.MapKey("ParentTaskId"));
}
}
延迟任务映射如下:
<!-- language: c# -->
public class DelayTaskEntityConfiguration : EntityTypeConfiguration<DelayTask>
{
public DelayTaskEntityConfiguration()
{
Property(d => d.WorkflowTaskId).HasColumnName("DelayTaskId");
Property(d => d.Duration).HasColumnName("Duration");
}
}
希望你明白了。 现在我有另一个称为容器任务的子类型。 此任务将保留其他任务,并可能保留其他容器任务。 这是它的外观以及映射:
<!-- language: c# -->
public class ContainerTask : ParentTask
{
public ContainerTask()
{
base.WorkflowTaskType = WorkflowTaskType.Container;
base.ParentTaskType = ParentTaskType.ContainerTask;
}
public List<WorkflowTask> ChildTasks { get; set; }
}
public class ContainerTaskEntityConfiguration : EntityTypeConfiguration<ContainerTask>
{
public ContainerTaskEntityConfiguration()
{
Property(x => x.WorkflowTaskId).HasColumnName("ContainerTaskId");
HasMany(c => c.ChildTasks).WithMany()
.Map(c => c.ToTable("ContainerTaskChildren", WorkflowContext.SCHEMA_NAME)
.MapLeftKey("ContainerTaskId")
.MapRightKey("ChildTaskId"));
}
}
为了确保我包含所有内容;这是ParentTask对象以及它的映射:
<!-- language: c# -->
public abstract class ParentTask : WorkflowTask
{
public ParentTaskType ParentTaskType {get; set;}
}
public class ParentTaskEntityConfiguration : EntityTypeConfiguration<ParentTask>
{
public ParentTaskEntityConfiguration()
{
Property(w => w.WorkflowTaskId).HasColumnName("ParentTaskId");
Property(w => w.ParentTaskType).HasColumnName("ParentTaskTypeId");
}
}
现在,我尝试保存的项目是工作流定义对象。 它将按顺序执行一堆任务。 它的定义如下:
<!-- language: c# -->
public class WorkflowDefinition
{
public int WorkflowDefinitionId { get; set; }
public string WorkflowName { get; set; }
public bool Enabled { get; set; }
public List<WorkflowTask> WorkflowTasks { get; set; }
}
public class WorkflowDefinitionEntityConfiguration :
EntityTypeConfiguration<WorkflowDefinition>
{
public WorkflowDefinitionEntityConfiguration()
{
Property(w => w.WorkflowDefinitionId).HasColumnName("Id");
HasMany(w => w.WorkflowTasks)
.WithRequired(t=> t.WorkflowDefinition)
.Map(c => c.MapKey("WorkflowDefinitionId"));
Property(w => w.Enabled).HasColumnName("Enabled");
Property(w => w.WorkflowName).HasColumnName("WorkflowName");
}
}
因此,通过定义所有这些内容,我将工作流定义对象传递到数据存储库层,并希望使用 EF 保存它。 由于对象在 UI 中处理时丢失了上下文;我必须重新关联它,以便它知道要保存什么。 在UI中,我可以向工作流添加新任务,编辑现有任务以及删除任务。 如果只有一个级别的任务(定义=>任务),这将是小菜一碟。 我的问题在于存在无限级别的可能性(定义 => 任务 =>子任务 =>子任务等......
目前,我从数据库中检索现有工作流并分配值(工作流是正在传入的值,类型为工作流定义):
<!-- language: c# -->
// retrieve the workflow definition from the database so that it's within our context
var dbWorkflow = context.WorkflowDefinitions
.Where(w => w.WorkflowDefinitionId ==workflow.WorkflowDefinitionId)
.Include(c => c.WorkflowTasks).Single();
// transfer the values of the definition to the one we retrieved.
context.Entry(dbWorkflow).CurrentValues.SetValues(workflow);
然后,我循环浏览任务列表,并将它们添加到定义中,或者找到它们并设置它们的值。 我向 WorkflowTask 对象添加了一个名为 SetDefinition 的函数,该函数将 WorkflowDefinition 设置为上下文中的工作流(以前我会收到一个关键错误,因为它认为父工作流是不同的工作流,即使 Id 匹配)。 如果它是一个容器,我会运行一个递归函数来尝试将所有子项添加到上下文中。
<!-- language: c# -->
foreach (var task in workflow.WorkflowTasks)
{
task.SetDefinition(dbWorkflow);
if (task.WorkflowTaskId == 0)
{
dbWorkflow.WorkflowTasks.Add(task);
}
else
{
WorkflowTask original = null;
if (task is ContainerTask)
{
original = context.ContainerTasks.Include("ChildTasks")
.Where(w => w.WorkflowTaskId == task.WorkflowTaskId)
.FirstOrDefault();
var container = task as ContainerTask;
var originalContainer = original as ContainerTask;
AddChildTasks(container, dbWorkflow, context, originalContainer);
}
else
{
original = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId ==
task.WorkflowTaskId);
}
context.Entry(original).CurrentValues.SetValues(task);
}
}
AddChildTasks 函数如下所示:
<!-- language: c# -->
private void AddChildTasks(ContainerTask container, WorkflowDefinition workflow,
WorkflowContext context, ContainerTask original)
{
if (container.ChildTasks == null) return;
foreach (var task in container.ChildTasks)
{
if (task is ContainerTask)
{
var subContainer = task as ContainerTask;
AddChildTasks(subContainer, workflow, context, container);
}
if (task.WorkflowTaskId == 0)
{
if (container.ChildTasks == null)
container.ChildTasks = new List<WorkflowTask>();
original.ChildTasks.Add(task);
}
else
{
var originalChild = original.ChildTasks
.Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
context.Entry(originalChild).CurrentValues.SetValues(task);
}
}
}
要删除任务,我ve found I
必须执行两步过程。 步骤 1 涉及遍历原始定义并标记不再在传入定义中的任务以进行删除。 步骤 2 只是将这些任务的状态设置为已删除。
<!-- language: c# -->
var deletedTasks = new List<WorkflowTask>();
foreach (var task in dbWorkflow.WorkflowTasks)
{
if (workflow.WorkflowTasks.Where(t => t.WorkflowTaskId ==
task.WorkflowTaskId).FirstOrDefault() == null)
deletedTasks.Add(task);
}
foreach (var task in deletedTasks)
context.Entry(task).State = EntityState.Deleted;
这就是我遇到问题的地方。 如果我删除容器,则会收到约束错误,因为该容器包含子项。 UI 会将所有更改保存在内存中,直到我点击保存,因此即使我先删除了子项,它仍然会引发约束错误。 我想我需要以不同的方式映射孩子,也许是级联删除或其他东西。 此外,当我循环访问删除循环中的任务时,当我只希望标记容器并因此删除子项时,容器和子项都会被标记为删除。
最后,上面的保存部分花了我一个星期的时间才弄清楚,它看起来很复杂。 有没有更简单的方法可以做到这一点? 我对 EF 很陌生,我开始认为让代码生成 SQL 语句并按我想要的顺序运行它们会更容易。
这是我在这里的第一个问题,所以我为长度和格式道歉......希望它清晰易读:-)
一个建议,我没有在存在这种无穷无尽的可能递归的情况下使用它,但是如果您希望删除上的级联在本地工作,并且看起来所有任务都直接属于某个父任务,您可以通过定义识别关系来处理它:
modelBuilder.Entity<WorkFlowTask>().HasKey(c => new {c.WorkflowTaskID,
c.ParentTask.WofkflowTaskId});
此问题是相关的:EF 是否可以自动删除未删除父级的孤立数据?
编辑:和这个链接:https://stackoverflow.com/a/4925040/1803682
好吧,我花了比我希望的时间更长的时间,但我想我终于想通了。 至少我所有的测试都通过了,到目前为止,我一直保存的所有定义都有效。 可能有一些我没有考虑过的组合,但现在我至少可以转向其他事情。
首先,我将对象切换为树,并决定将其扁平化。 这只是意味着所有任务都可以从根目录看到,但我仍然需要设置父属性和子属性。
foreach (var task in workflow.WorkflowTasks)
{
taskIds.Add(task.WorkflowTaskId); //adding the ids of all tasks to use later
task.SetDefinition(dbWorkflow); //sets the definition to record in context
SetParent(context, task); //Attempt to set the parent for any task
if (task.WorkflowTaskId == 0)
{
// I found if I added a task as a child it would duplicate if I added it
// here as well so I only add tasks with no parents
if (task.ParentTask == null)
dbWorkflow.WorkflowTasks.Add(task);
}
else
{
var dbTask = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
context.Entry(dbTask).CurrentValues.SetValues(task);
}
}
SetParent 函数必须检查任务是否有父任务,并确保父任务不是新任务 (id == 0)。 然后,它尝试在定义的上下文版本中找到父级,这样我就不会以重复项结束(即 - 如果未引用父级,即使它存在于数据库中,它也会尝试添加新的父级)。 一旦确定了父任务,我就会检查它是子任务,看看该任务是否已经存在,如果没有,我会添加它。
private void SetParent(WorkflowContext context, WorkflowTask task)
{
if (task.ParentTask != null && task.ParentTask.WorkflowTaskId != 0)
{
var parentTask = context.WorkflowTasks.Where(t => t.WorkflowTaskId == task.ParentTask.WorkflowTaskId).FirstOrDefault();
var parent = parentTask as ParentTask;
task.ParentTask = parent;
if (parentTask is ContainerTask)
{
var container = context.ContainerTasks.Where(c => c.WorkflowTaskId == parentTask.WorkflowTaskId).Include(c => c.ChildTasks).FirstOrDefault() as ContainerTask;
if (container.ChildTasks == null)
container.ChildTasks = new List<WorkflowTask>();
var childTask = container.ChildTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId
&& t.Order == task.Order);
if(childTask == null)
container.ChildTasks.Add(task);
}
}
}
您会在 SetParent 代码中注意到的一件事是,我正在按 ID 和 Order 搜索任务。 我必须这样做,因为如果我将两个新子级添加到容器中,两个 Id 都将为零,并且第二个 ID 不会添加,因为它找到了第一个。 每个任务都有唯一的顺序,所以我用它来进一步区分它们。
我对这段代码感觉不是很好,但是我已经在这个问题上工作了很长时间,而且这很有效,所以我现在要离开它。 我希望我涵盖了所有信息,我不太确定有多少人会真正需要这个,但你永远不知道。