List<Task> - 使用 C# 实体框架的 UPSERT 数据库记录

List<Task> - UPSERT database record using C# Entity Framework

提问人:B.Balamanigandan 提问时间:1/6/2017 最后编辑:shA.tB.Balamanigandan 更新时间:1/14/2017 访问量:2018

问:

我有一个对象,我正在尝试使用单个数据库实体上下文使用多个任务(并行执行)更新记录(即更新/删除)。但是我得到以下异常Employee

Message =“对象引用未设置为对象的实例。

请考虑以下 DTO

public class Employee
{
    public int EmployeeId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<ContactPhone> ContactPhoneNumbers { get; set; }
    public List<ContactEmail> ContactEmailAddress { get; set; }
}

public class ContactPhone
{
    public int ContactId { get; set; }
    public string Type { get; set; }
    public string Number { get; set; }
}

public class ContactEmail
{
    public int ContactId { get; set; }
    public string Type { get; set; }
    public string Number { get; set; }
}

员工表

EmployeeId  FirstName   LastName
_________________________________
1           Bala        Manigandan

ContactPhone 表

ContactId   EmployeeId  Type    Number
__________________________________________
1           1           Fax     9123456789
2           1           Mobile  9123456789

ContactPhone 表

ContactId   EmployeeId  Type    EmailAddress
______________________________________________
1           1           Private [email protected]
2           1           Public  [email protected]

传入 API 对象是

DTO.Employee emp = new DTO.Employee()
{
    EmployeeId = 1,
    FirstName = "Bala",
    LastName = "Manigandan",
    ContactPhoneNumbers = new List<DTO.ContactPhone>
        {
            new DTO.ContactPhone()
            {
                Type = "Mobile",
                Number = "9000012345"
            }
        },
    ContactEmailAddress = new List<DTO.ContactEmail>()
        {
            new DTO.ContactEmail()
            {
                Type = "Private",
                EmailAddress = "[email protected]"
            },
            new DTO.ContactEmail()
            {
                Type = "Public",
                EmailAddress = "[email protected]"
            }
        }
};

我收到一个 API 请求,要求更新手机号码并删除指定员工的传真号码

考虑以下任务方法:

public void ProcessEmployee(DTO.Employee employee)
{
    if(employee != null)
    {
        DevDBEntities dbContext = new DevDBEntities();

        DbContextTransaction dbTransaction = dbContext.Database.BeginTransaction();

        List<Task> taskList = new List<Task>();
        List<bool> transactionStatus = new List<bool>();

        try
        {
            Employee emp = dbContext.Employees.FirstOrDefault(m => m.EmployeeId == employee.EmployeeId);

            if (emp != null)
            {
                Task task1 = Task.Factory.StartNew(() =>
                {
                    bool flag = UpdateContactPhone(emp.EmployeeId, employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault().Number, dbContext).Result;
                    transactionStatus.Add(flag);
                });

                taskList.Add(task1);

                Task task2 = Task.Factory.StartNew(() =>
                {
                    bool flag = RemoveContactPhone(emp.EmployeeId, "Fax", dbContext).Result;
                    transactionStatus.Add(flag);
                });

                taskList.Add(task2);
            }

            if(taskList.Any())
            {
                Task.WaitAll(taskList.ToArray());
            }
        }
        catch
        {
            dbTransaction.Rollback();
        }
        finally
        {
            if(transactionStatus.Any(m => !m))
            {
                dbTransaction.Rollback();
            }
            else
            {
                dbTransaction.Commit();
            }

            dbTransaction.Dispose();
            dbContext.Dispose();
        }
    }
}

public async Task<bool> UpdateContactPhone(int empId, string type, string newPhone, DevDBEntities dbContext)
{
    bool flag = false;

    try
    {
        var empPhone = dbContext.ContactPhones.FirstOrDefault(m => (m.EmployeeId == empId) && (m.Type == type));
        if (empPhone != null)
        {
            empPhone.Number = newPhone;
            await dbContext.SaveChangesAsync();
            flag = true;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return flag;
}

public async Task<bool> RemoveContactPhone(int empId, string type, DevDBEntities dbContext)
{
    bool flag = false;

    try
    {
        var empPhone = dbContext.ContactPhones.FirstOrDefault(m => (m.EmployeeId == empId) && (m.Type == type));
        if (empPhone != null)
        {
            dbContext.ContactPhones.Remove(empPhone);
            await dbContext.SaveChangesAsync();
            flag = true;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return flag;
}

我收到以下异常:

Message =“对象引用未设置为对象的实例。

在这里,我附上了屏幕截图供您参考

enter image description here

我的要求是并行执行所有数据库进程,请帮助我如何使用 Task 毫无例外地实现这一目标UPSERT

C# 实体框架 并行库任务 对象引用

评论

1赞 Developer 1/6/2017
可能发生此错误的地方是 - 。顺便说一句,DbContext 不是线程安全的,所以在线程中使用上下文时要小心employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault()
0赞 B.Balamanigandan 1/6/2017
@Developer - 好的。我们如何在并行执行中实现,还有其他方法吗?
1赞 Developer 1/6/2017
这应该是一个不同的线程,我认为 SO re: datacontext 和 threads 中已经有很多线程可用。现在让我们专注于解决您当前的问题。检查我的答案,请让我知道这是否有效
0赞 wertzui 1/6/2017
不要使用不同的任务,而是执行以下操作: 使用 Task.WhenAll 在不同的上下文实例中加载所有数据以等待它们。处理这些上下文。创建一个新上下文并附加加载的数据。现在对该上下文进行所有修改。调用 SaveChangesAsync 以将一个事务中的所有更改提交到数据库。
0赞 B.Balamanigandan 1/6/2017
@wertzui - 您能否在回答部分用代码描述您的解决方案。

答:

1赞 Developer 1/6/2017 #1

可能发生此错误的地方是 -employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault()

可能是空的,因为您没有急于加载它,也没有将属性标记为它会延迟加载。employee.ContactPhoneNumbersvirtual

因此,要解决此问题: 1.将导航属性标记为延迟加载virtual

public virtual List<ContactPhone> ContactPhoneNumbers { get; set; }
public virtual List<ContactEmail> ContactEmailAddress { get; set; }
  1. 或者 Eager 加载实体.Include
  2. 或显式加载实体

dbContext.Entry(emp).Collection(s => s.ContactPhoneNumbers).Load(); dbContext.Entry(emp).Collection(s => s.ContactEmailAddress ).Load();

评论

0赞 B.Balamanigandan 1/6/2017
默认情况下,所述属性在数据库实体类中标记为virtualpublic partial class DevDBEntities : DbContext
0赞 Developer 1/6/2017
我的意思是说,在实体中将其作为虚拟,以便延迟加载这些关系Employee
0赞 B.Balamanigandan 1/6/2017
有其标记为 => invirtualpublic virtual ICollection<ContactPhone> ContactPhones { get; set; }public partial class Employee
0赞 Developer 1/6/2017
哎呀,对不起,我把它和 DTO 类混淆了。那么你能验证一下你是否在 ?employee.ContactPhoneNumbers
0赞 B.Balamanigandan 1/6/2017
你能给我实际的显式编码吗?
6赞 George Vovos 1/12/2017 #2

1st)停止在不同线程中使用上下文。
DbContext 不是线程安全的,仅此一项就会导致许多奇怪的问题,甚至是疯狂的 NullReference 异常

现在,你确定你的并行代码比非并行实现更快吗?
我非常怀疑这一点。

据我所知,您甚至没有更改您的 Employee 对象,所以我不明白为什么要加载它(两次)

我认为您所需要的只是
1) 加载您需要更新并设置新号码
的手机 2)删除未使用的手机
不必加载此记录。只需使用默认构造函数并设置 Id。
EF 可以处理其余的(当然需要附加新创建的对象)

3)保存更改
(使用相同的上下文执行 1,2,3 合 1 方法)

如果出于某种原因,您决定执行多项任务

  1. 在每个任务中创建一个新上下文
  2. 将代码包装在 TransactionScope

更新
我刚刚注意到这一点:

catch (Exception ex) { throw ex;    }

这很糟糕(你丢失了堆栈跟踪)
删除 try/catch 或使用

catch (Exception ex) { throw ; }

更新 2
一些示例代码(我假设您的输入包含要更新/删除的实体的 ID)

 var toUpdate= ctx.ContactPhones.Find(YourIdToUpdate);
 toUpdate.Number = newPhone;

 var toDelete= new ContactPhone{ Id = 1 };
 ctx.ContactPhones.Attach(toDelete);
 ctx.ContactPhones.Remove(toDelete);
 ctx.SaveChanges();

如果您采用并行方法

using(TransactionScope tran = new TransactionScope()) {
    //Create and Wait both Tasks(Each task should create it own context)
    tran.Complete();
}

评论

0赞 B.Balamanigandan 1/13/2017
你能给我样品方法吗?
0赞 George Vovos 1/14/2017
@B.Balamanigandan 我添加了一些伪代码,但我认为你真的不需要它......
0赞 Gert Arnold 1/14/2017
TransactionScope 将在多个线程中使用时尝试启动 MSDTC。
0赞 George Vovos 1/14/2017
B.Balamanigandan 检查@GertArnold的评论,如果分布式事务处理协调器尚未启动,您将收到异常“无法登记事务”或类似的东西
0赞 B.Balamanigandan 1/16/2017
让我知道,为什么我们需要在删除之前附加实体?你能告诉我吗?