使用C# Entity Framework实现UPSERT数据库记录 - List<Task>

10

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

消息 = "对象引用未设置为对象的实例。"

考虑以下数据传输对象(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

联系电话表:

ContactId   EmployeeId  Type    EmailAddress
______________________________________________
1           1           Private bala@gmail.com
2           1           Public  bala@ymail.com

传入的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 = "bala@gmail.com"
            },
            new DTO.ContactEmail()
            {
                Type = "Public",
                EmailAddress = "bala@ymail.com"
            }
        }
};

我收到一条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;
}

我遇到了以下异常:

消息 = “对象引用未设置为对象的实例。”

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

enter image description here

我的要求是使用Task并行执行所有数据库UPSERT操作,敬请指导如何在不出现任何异常的情况下实现


1
可能出现此错误的地方是 - employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault()。另外,DbContext 不是线程安全的,因此在线程中操作上下文时要小心。 - Developer
@开发者 - 好的。我们如何实现并行执行,还有其他方法吗? - B.Balamanigandan
1
那应该是一个完全不同的线程,我想在 SO 上已经有很多关于 DataContext 和线程的线程了。现在让我们专注于解决你当前的问题。检查我的答案并请告诉我是否有效。 - Developer
不要使用不同的任务,而是采用以下方法:使用Task.WhenAll在不同的上下文实例中加载所有数据以等待它们。释放这些上下文。创建一个新的上下文并附加您加载的数据。现在在该上下文中执行所有修改。调用SaveChangesAsync将所有更改作为一个事务提交到数据库。 - wertzui
@wertzui - 你能否在答案部分用代码讲解一下你的解决方案? - B.Balamanigandan
显示剩余4条评论
2个回答

6
第一)不要在不同的线程中使用DbContext。
DbContext不是线程安全的,这单独就可能导致许多奇怪的问题,甚至是疯狂的NullReference异常

现在,您确定并行代码比非并行实现更快吗?
我非常怀疑。

从我看到的情况来看,您甚至没有改变Employee对象,因此我不明白为什么您需要加载它(两次)

我认为你只需要这样做:
1)加载需要更新的电话并设置新号码
2)删除未使用的手机
不必加载此记录。 只需使用默认构造函数并设置ID即可。
EF可以处理其余部分(当然您需要附加新创建的对象)

3)保存您的更改
(使用相同的上下文在一个方法中完成1,2,3)

如果出于某种原因您决定使用多个任务

  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();
}

请问您能提供给我示例方法吗? - B.Balamanigandan
@B.Balamanigandan,我添加了一些伪代码,但我认为你并不真正需要它... - George Vovos
当在多个线程中使用TransactionScope时,它会尝试启动MSDTC。 - Gert Arnold
B.Balamanigandan请查看@GertArnold的评论,如果分布式事务协调器尚未启动,则会出现异常“无法列举事务”或类似情况。 - George Vovos
请告诉我,为什么我们需要在删除之前附加实体?您能简要说明一下吗? - B.Balamanigandan
这是删除实体的最佳方式,因为您不必加载记录。如果您不将其附加,EF就不知道它必须删除它。 - George Vovos

1

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

employee.ContactPhoneNumbers 可能为 null,因为您没有急切加载它,也没有将该属性标记为 virtual 以进行延迟加载。

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

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

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


默认情况下,在数据库实体类“public partial class DevDBEntities: DbContext”中,该属性被标记为“virtual”。 - B.Balamanigandan
抱歉,我混淆了DTO类。那么请问您能否验证是否在employee.ContactPhoneNumbers中获取到值? - Developer
请问您能否提供实际的明确编码吗? - B.Balamanigandan
我遇到了一个新的异常 Message = "不允许添加与已删除实体的关系。" - B.Balamanigandan
当您尝试在循环或底层中修改集合时,会引发非常常见的异常。请注释掉所有其他与任务相关的代码,只检查var phoneDetails = employee.ContactPhoneNumbers.FirstOrDefault()这一行代码。 - Developer
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接