Linq to SQL与Rob Conery仓储模式下的并发性问题

3
我已经使用Rob Conery对存储库模式的改进(来自MVC Storefront项目)实现了一个DAL,其中我使用Linq将数据库对象映射到域对象,并使用Linq to SQL实际获取数据。
这一切都非常出色,让我完全掌控了我想要的域对象的形状,但是我遇到了并发问题,我觉得我应该在这里提问。我已经解决了并发性问题,但解决方案感觉可能是错误的(只是那种让人不舒服的感觉)。
基本模式如下:
private MyDataContext _datacontext
private Table _tasks;

public Repository(MyDataContext datacontext)
{
    _dataContext = datacontext;
}

public void GetTasks()
{
    _tasks = from t in _dataContext.Tasks;

    return from t in _tasks
        select new Domain.Task
        {
            Name = t.Name,
            Id = t.TaskId,
            Description = t.Description                              
        };
}

public void SaveTask(Domain.Task task)
{
    Task dbTask = null;

    // Logic for new tasks omitted...

    dbTask = (from t in _tasks
        where t.TaskId == task.Id
        select t).SingleOrDefault();

    dbTask.Description = task.Description,
        dbTask.Name = task.Name,

    _dataContext.SubmitChanges();
} 

因为将任务映射到域任务,所以我失去了并发跟踪。通过存储私有Table(即我的数据上下文任务列表),我可以重新获得它。然后,我从这个存储的Table中更新任务并保存我已经更新的内容。这个方法行得通 - 当存在并发冲突时,我会像想要的那样收到更改冲突异常。但是,我感觉我错过了什么好方法。是否有更好的方法呢?我看过数据上下文的.Attach方法,但似乎需要以与我已经在做的方式类似的方式存储原始版本。我也知道,通过放弃域对象并让Linq to SQL生成的对象一路到达我的堆栈,我可以避免所有这些问题 - 但我同样不喜欢处理并发的方式。

我也在使用Rob Con的存储库模式,非常喜欢它。但是我并不担心并发检查。我加载最新的对象,然后更新字段并提交更改。我会考虑看看是否可以使用Attach和/或时间戳。我已经有一段时间没研究过linq +并发了。 - Pure.Krome
我也在使用Rob的设计模式,并且目前像Pure.Krome一样进行更新。但是我认为并发性太重要了,不能忽视,并且应该成为任何企业应用程序开发人员的习惯。我现在暂时忽略它,但它将在我的后续迭代中加入。我真的很想看看你的方法最终会变成什么样子,David。 - Matt Kocaj
我刚刚偶然发现了这个CodePlex项目http://linq2sqleb.codeplex.com/,它处理了断开连接的情况下的Linq,并且显然处理并发。如果我有时间,我会试一试。在那之前,希望其他人能够找到这个链接有用。 - David Hall
2个回答

1

我通过这个问题并找到了以下解决方案。它在我(更重要的是,我的测试人员!)所能想到的所有测试用例中都有效。

我正在使用数据上下文上的.Attach()方法和一个时间戳列。这对于第一次将特定主键保存回数据库很好用,但我发现数据上下文会抛出一个System.Data.Linq.DuplicateKeyException“无法添加已经在使用中的键的实体”。

我创建的解决方法是添加一个存储我第一次附加的项目的字典,然后每次保存时都重复使用该项目。

示例代码如下,我不知道是否错过了任何技巧 - 并发性非常基本,所以我跳过的障碍似乎有点过多。

希望下面的内容对您有用,或者有人可以指向我更好的实现方式!

private Dictionary<int, Payment> _attachedPayments;

public void SavePayments(IList<Domain.Payment> payments)
    {
        Dictionary<Payment, Domain.Payment> savedPayments =
            new Dictionary<Payment, Domain.Payment>();

        // Items with a zero id are new
        foreach (Domain.Payment p in payments.Where(p => p.PaymentId != 0))
        {
            // The list of attached payments that works around the linq datacontext  
            // duplicatekey exception
            if (_attachedPayments.ContainsKey(p.PaymentId)) // Already attached
            {
                Payment dbPayment = _attachedPayments[p.PaymentId];                    
                // Just a method that maps domain to datacontext types
                MapDomainPaymentToDBPayment(p, dbPayment, false);
                savedPayments.Add(dbPayment, p);
            }
            else // Attach this payment to the datacontext
            {
                Payment dbPayment = new Payment();
                MapDomainPaymentToDBPayment(p, dbPayment, true);
                _dataContext.Payments.Attach(dbPayment, true);
                savedPayments.Add(dbPayment, p);
            }
        }

        // There is some code snipped but this is just brand new payments
        foreach (var payment in newPayments)
        {
            Domain.Payment payment1 = payment;
            Payment newPayment = new Payment();
            MapDomainPaymentToDBPayment(payment1, newPayment, false);
            _dataContext.Payments.InsertOnSubmit(newPayment);
            savedPayments.Add(newPayment, payment);
        }

        try
        {
            _dataContext.SubmitChanges();
            // Grab the Timestamp into the domain object
            foreach (Payment p in savedPayments.Keys)
            {
                savedPayments[p].PaymentId = p.PaymentId;
                savedPayments[p].Timestamp = p.Timestamp;
                _attachedPayments[savedPayments[p].PaymentId] = p;
            }
        }
        catch (ChangeConflictException ex)
        {
            foreach (ObjectChangeConflict occ in _dataContext.ChangeConflicts)
            {
                Payment entityInConflict = (Payment) occ.Object;

                // Use the datacontext refresh so that I can display the new values
                _dataContext.Refresh(RefreshMode.OverwriteCurrentValues, entityInConflict);
                _attachedPayments[entityInConflict.PaymentId] = entityInConflict;

            }
            throw;
        }

    }

0

我建议尝试使用.Attach方法,通过传递“原始”和“更新”的对象来实现LINQ2SQL的真正乐观并发检查。在我的看法中,这比在DBML对象或您的域对象中使用版本或日期时间戳要好。但是,我不确定MVC如何允许持久化“原始”数据的想法...我一直在尝试调查验证脚手架,希望它存储“原始”数据...但我怀疑它只能像最近的帖子(和/或失败的验证)一样有效。所以那个想法可能行不通。

另一个我想到的疯狂想法是:覆盖所有领域对象的GetHashCode(),其中哈希表示该对象的唯一数据集(当然不包括ID)。然后,手动或使用助手将该哈希值嵌入HTML POST表单中的隐藏字段中,并将其与更新后的领域对象一起发送回您的服务层 - 在服务层或数据层中执行并发检查(通过比较原始哈希和新提取的领域对象的哈希),但请注意您需要自己检查并引发并发异常。使用DMBL函数很好,但抽象出数据层的想法是为了不依赖于特定实现的功能等。因此,在服务层(例如)中完全控制领域对象的乐观并发检查似乎对我来说是一个不错的方法。

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