C# 更新数据库集合的最佳策略

C# 更新数据库集合的最佳策略,c#,collections,domain-driven-design,C#,Collections,Domain Driven Design,我们正在使用C#4和SQL Server 2008 R2开发一个系统,并遵循领域驱动的设计原则。没有使用ORM。有些情况下,我们有一个实体持有另一个实体的集合,例如: public class Project { public IdCollection ParticipantUsers { get { //... } } public void AddParticipantUser(Id i

我们正在使用C#4和SQL Server 2008 R2开发一个系统,并遵循领域驱动的设计原则。没有使用ORM。有些情况下,我们有一个实体持有另一个实体的集合,例如:

public class Project
{
    public IdCollection ParticipantUsers
    {
        get
        {
            //...
        }
    }

    public void AddParticipantUser(Id idUser)
    {
        //...
    }

    public void RemoveParticipantUser(Id idUser)
    {
        //...
    }
}
Project
ParticipantUsers
属性返回参与该项目的用户的ID集合。
AddParticipantUser
RemoveParticipantUser
方法,您就知道了

我们发现的问题如下。当项目实例被发送到存储库以在数据库上进行更新时,ID的
ParticipantUsers
集合可能与上次检索时发生了更改:有些ID可能已添加,有些已删除,有些可能完全相同。我们可以使用蛮力方法从数据库中删除该项目的所有参与者用户,然后重新插入当前用户,但我想探索更好的方法


有什么更好的方法可以让我只插入已添加的ID,删除已删除的ID,保留集合中仍存在的ID不变?

orgData-是包含数据库原始数据的列表/表格/其他可枚举对象(本例中更改前的IdCollection ParticipantUsers)

newData-是包含要更新的数据的列表/表格/其他可枚举对象

var orgList = orgData.Select(a => a.Id);
var newList = newData.Select(a => a.Id);

var itemsToAdd = newData.Where(a => !orgList.Contains(a.Id));
var itemsToDel = orgData.Where(a => !newList.Contains(a.Id));

实际上,我认为delete-reinsert方法比任何基于比较的解决方案都要好

首先,它很容易实现

其次,它避免了另一次数据获取以进行比较


你能进一步解释一下为什么不喜欢这种方法吗,也许我不明白。

我想如果你做了很多这类事情,事件资源将是理想的选择,但通常我们作为开发人员,不会太多地使用新技术:)

因为我不喜欢ORM,所以我之前所做的只是简单地按照工作单元所做的相同路线保留一个变更列表。因此,每次添加参与者时,我都会将其添加到
AddedParticipants
集合中,对于删除的参与者,我会将其添加到
RemovedParticipants
集合中。我想人们可以把它进一步扩展到一个列表,每个条目都有不同的变化类型。但我相信你明白了

然后,存储库将在执行相关数据操作时使用跟踪的更改。可能不是最漂亮的解决方案,但它能完成任务

我可能会补充说,对于较短的列表,我只需删除并重新插入,但正如您所提到的,在这种情况下,它可能有点沉重,一个大小不适合所有人:)

当然,正如Arie所建议的那样,重新加载列表并进行比较是可行的。

我们的方法(使用SQL Server 2008 R2)是将表值参数(TVP)与合并语句结合使用

这样,代码将项列表作为表传递给存储过程。然后,proc在执行insert/update/delete的merge语句中使用该表

您需要一个用户定义的表类型,例如:

CREATE TYPE [dbo].[Participants] AS TABLE(
    [ProjectId] [int] NOT NULL,
    [ParticipantId] [int] NOT NULL,
    PRIMARY KEY CLUSTERED 
(
    [ProjectId] ASC,
    [ParticipantId] ASC
)WITH (IGNORE_DUP_KEY = OFF)
)
GO
然后需要一个C类来保存数据。只需将您的
IdCollection
修改为类似于:

public class IdCollection : List<ParticipantUsers>, IEnumerable<SqlDataRecord> {
    #region IEnumerable<SqlDataRecord> Members

    IEnumerator<SqlDataRecord> IEnumerable<SqlDataRecord>.GetEnumerator() {
        SqlDataRecord rec = new SqlDataRecord(
            new SqlMetaData("ProjectId", System.Data.SqlDbType.Int),
            new SqlMetaData("ParticipantId", System.Data.SqlDbType.Int)
        );

        foreach (ParticipantUser ans in this) {
            rec.SetInt32(0, ans.ProjectId);
            rec.SetInt32(1, ans.ParticipantId);
            yield return rec;
        }
    }

    #endregion

}
要调用进程(使用企业库),请执行以下操作:


您是否考虑过将所有id序列化为一列?这样,您只需更新一条记录。如果您担心集合在此期间发生了变化,您可能希望实现乐观并发。

因为它没有这样标记,我猜您的存储库没有使用任何类型的ORM(EF/NHibernate)?从DB获取当前列表。与参与者用户进行比较。如果缺少则添加,如果不再在
参与者用户中则删除,否则离开untouched@SimonBelanger:正确,没有错误。我将在我的作品中添加一个澄清。@musefan:这可能会奏效。你能把它作为一个答案吗?@Cesargo:可以,但我认为Arie的回答几乎涵盖了这一点。你能添加一些上下文信息吗?什么是orgData和newData?谢谢。那么,您是建议我在每次更新之前从数据库中检索集合,正如musefan在OP的评论中所建议的那样?是,还是您逐行更新并在更新期间处理System.Data.DBConcurrencyException?它返回导致异常的行。无论如何,在某一点上,您必须将当前数据与数据库中的数据进行比较,正如您所说,数据库中的数据可以同时修改。这是ORM通常采用的方法,因为它们可以为每个值对象确定唯一的键。实际上,我们的值对象没有唯一的键(好的,除了完整的值集之外)。我探索其他方法的唯一原因是删除/重新插入看起来像蛮力,对我来说远远不是最优的。但是在阅读了这里的答案后,我开始从一个新的角度来看待它。:-)我在orm和none orm场景中都使用了删除/重新插入方法,但还没有看到任何负面影响。希望这有帮助。有趣的方法。我会考虑的。谢谢。@CESARGE:已更新以包含
MERGE
语句中正确的
delete
部分。我们在系统中整体使用乐观并发。但我们仍然需要确定哪些实体已被添加和删除。关于您建议对所有ID使用一列,这很有趣,谢谢。我会与同事讨论。
CREATE PROCEDURE [dbo].[ParticpantUpdate]
    @Particpants core.Participants READONLY
AS
BEGIN
    SET NOCOUNT ON;

    MERGE INTO dbo.ProjectParticipants pp
        USING @Particpants RG
            ON (RG.ProjectId = pp.ProjectId)
            AND (RG.ParticipantId = pp.ParticipantId)
        WHEN NOT MATCHED BY TARGET THEN
            INSERT( ProjectId, ParticipantId)
            VALUES( RG.ProjectId, RG.ParticipantId)
        WHEN NOT MATCHED BY SOURCE
                AND target.ProjectId in (SELECT ProjectId from @Participants) THEN
                DELETE;
    ;


END
SqlDatabase db = DatabaseFactory.CreateDatabase("myconnstr") as SqlDatabase;

using ( DbCommand dbCommand = db.GetStoredProcCommand("dbo.ParticipantUpdate") ) {
    db.AddInParameter(dbCommand, "Participants", SqlDbType.Structured, participantList);

    db.ExecuteNonQuery(dbCommand);

} // using dbCommand