C# Linq-SQL: Repository模式的UpdateByID方法

3
我已经实现了一种类似于“仓库”(Repository)的类,该类具有GetByIDDeleteByID等方法,但我在实现UpdateByID方法时遇到了问题。
我做了如下操作:
public virtual void UpdateByID(int id, T entity)
{
        var dbcontext = DB;
        var item = GetByID(dbcontext, id);
        item = entity; 
        dbcontext.SubmitChanges();
}

protected MusicRepo_DBDataContext DB
{
    get
    {
        return new MusicRepo_DBDataContext();
    }
}

但它没有更新传递的实体。
有人实现过这样的方法吗?
供参考,这里GetByID方法。
[更新] 正如Marc所建议的那样,我只是改变了本地变量的值。那么你认为我该如何处理这个方法?使用反射并将属性从entity复制到item吗?

其实,我认为GetByID方法也是错的 ;-p 正在努力编写一个示例,可以修复这两个问题(并且适用于POCO和属性)。 - Marc Gravell
嗯,但 GetByID 总是有效的(如果我没有记错的话)。你认为呢,马克? - Andreas Grech
我看不到GetPrimaryKey()的实现方式(它没有显示),但我猜测它是在查看属性。这是不正确的。在LINQ-to-SQL中使用属性并不是必需的;也可以使用外部映射文件。在这种情况下,属性将不存在,并且它将失败。如果您使用元模型,它适用于任何实现(因为这是LINQ-to-SQL询问“主键是什么”的方式)。 - Marc Gravell
或者换句话说:它在你使用属性类型时工作正常。尝试使用未标注的POCO,看看它是如何工作的......或者不工作。 - Marc Gravell
我认为Dreas的问题是关于T是否作为编辑实体而不是POCO(Plain Old CLR Object)。他说它是一个“本地变量”,但没有说明T不是LINQ-to-SQL实体。他的代码将T和GetByID的结果分配给同一个变量,如果T不是从GetByID中返回的类型,则无法编译。 - mckamey
7个回答

8
你所更新的只是一个本地变量;要使其起作用,你需要将成员值从“entity”复制到“item” - 这并不简单。
像下面这样; 我使用TKey的唯一原因是我在Northwind.Customer上进行了测试,它具有字符串键 ;-p 使用元模型的优点是,即使您使用POCO类(和基于xml的映射),它也可以工作,并且不会尝试更新与模型无关的任何内容。
为了示例的目的,我已经传递了数据上下文,并且您需要在某个时候添加SubmitChanges,但其余部分应该直接可比。
顺便说一句 - 如果您愿意从传入的对象中获取ID,那么也很容易 - 然后您可以支持复合标识表。
    static void Update<TEntity>(DataContext dataContext, int id, TEntity obj)
        where TEntity : class
    {
        Update<TEntity, int>(dataContext, id, obj);
    }
    static void Update<TEntity, TKey>(DataContext dataContext, TKey id, TEntity obj)
        where TEntity : class
    {
        // get the row from the database using the meta-model
        MetaType meta = dataContext.Mapping.GetTable(typeof(TEntity)).RowType;
        if(meta.IdentityMembers.Count != 1) throw new InvalidOperationException("Composite identity not supported");
        string idName = meta.IdentityMembers[0].Member.Name;

        var param = Expression.Parameter(typeof(TEntity), "row");
        var lambda = Expression.Lambda<Func<TEntity,bool>>(
            Expression.Equal(
                Expression.PropertyOrField(param, idName),
                Expression.Constant(id, typeof(TKey))), param);

        object dbRow = dataContext.GetTable<TEntity>().Single(lambda);

        foreach (MetaDataMember member in meta.DataMembers)
        {
            // don't copy ID
            if (member.IsPrimaryKey) continue; // removed: || member.IsVersion
            // (perhaps exclude associations and timestamp/rowversion? too)

            // if you get problems, try using StorageAccessor instead -
            // this will typically skip validation, etc
            member.MemberAccessor.SetBoxedValue(
                ref dbRow, member.MemberAccessor.GetBoxedValue(obj));
        }
        // submit changes here?
    }

啊,我就知道……那么我该如何实现这样的存储库方法呢?你觉得我应该使用反射将参数(成员)中的字段传输到项目中吗? - Andreas Grech
是的,可能你需要使用反射。 - Marc Gravell
Marc,我试了你的方法,它有效,谢谢。现在我需要花些时间来真正理解你的代码。 - Andreas Grech

3

重新审视这里,之前的回答对应用程序做出了各种假设。

在应用程序中并发是需要事先考虑的事情,而且并没有一个适用于所有情况的标准答案。在选择应用程序时需要考虑以下因素:

  • LINQ to SQL / Entity Framework非常可配置,因为没有一种适用于所有情况的方法。
  • 只有在应用程序负载达到一定程度时才会看到并发的影响(例如,仅您自己在自己的计算机上可能永远不会看到它)
  • 您的应用程序允许多个用户同时编辑相同的实体的频率有多高?
  • 当两个编辑重叠时,您希望如何处理情况?
  • 您的应用程序是否在另一层中来回序列化数据(例如Ajax)?如果是,则如何知道编辑的实体是否在读取/更新之间被修改?时间戳?版本字段?
  • 您是否关心编辑是否重叠?特别注意FK关系。数据完整性是最后一个赢得胜利的地方。

不同的解决方案具有非常不同的性能影响!您在开发过程中可能不会注意到,但是您的应用程序可能在25人同时使用时崩溃。注意大量来回复制和许多SQL读取:

  • 不要在循环中调用SQL(当您传递实体列表时请注意此问题)
  • 如果已经通过LINQ进行并发检查,则不要使用反射进行此操作
  • 最小化字段来回复制的次数(可能在跨N-Tier边界时必需)。
  • 不要创建单独的查询来查找旧实体(仅在已经拥有它的情况下使用它),让LINQ执行此操作,因为它更适用于在SQL中执行此操作。

以下是一些深入阅读以确定您特定需求的好链接:

我推荐的解决方案:

public virtual void Update(T entity)
{
    var DB = ...;
    DB.GetTable<T>().Attach(entity, true);
    try
    {
        // commit to database
        DB.SubmitChanges(ConflictMode.ContinueOnConflict);
    }
    catch (ChangeConflictException e)
    {
        Console.WriteLine(e.Message);
        foreach (ObjectChangeConflict occ in DB.ChangeConflicts)
        {
            occ.Resolve(REFRESH_MODE);
        }
    }
}

REFRESH_MODE指定以下之一:

  • RefreshMode.KeepChanges (保留更改)
  • RefreshMode.KeepCurrentValues (保留当前值)
  • RefreshMode.OverwriteCurrentValues (覆盖当前值)

您还需要考虑模型:

可能不用说,但您需要让 LINQ 知道哪个字段是您要更新实体的主键。您不必将此作为另一个参数传递(如原始方法中所示),因为 LINQ 已经知道这是 PK。

您可以决定检查哪些字段。例如,外键字段非常重要,需要并发检查,而描述字段可能只需要最后一个获胜。您可以通过 UpdateCheck 属性控制。默认值为 UpdateCheck.Always。从 MSDN 中获取以下信息:

仅将映射为 AlwaysWhenChanged 的成员参与乐观并发性检查。对于标记为 Never 的成员,不执行检查。有关详细信息,请参见 UpdateCheck

要启用乐观并发性,您需要指定要用作并发令牌(例如时间戳或版本)的字段,并且此字段在序列化来回传递时始终存在。使用 IsVersion=true 标记此列。

如果不想进行并发检查,则必须将所有标记为 UpdateCheck.Never。


1

我之前也遇到了一些类似的问题,最终选择了 PLINQO,它可以对 LINQ-TO-SQL 生成的代码进行大量增强。不过如果你没有 CodeSmith 的话,需要购买(可以免费评估 30 天)。


0

我脑海中有这样的东西:

public Question UpdateQuestion(Question newQuestion)
    {
        using (var context = new KodeNinjaEntitiesDataContext())
        {
            var question = (from q in context.Questions where q.QuestionId == newQuestion.QuestionId select q).SingleOrDefault();
            UpdateFields(newQuestion, question);
            context.SubmitChanges();                
            return question;
        }
    }

    private static void UpdateFields(Question newQuestion, Question oldQuestion)
    {
        if (newQuestion != null && oldQuestion != null)
        {
            oldQuestion.ReadCount = newQuestion.ReadCount;
            oldQuestion.VotesCount = newQuestion.VotesCount;
            //.....and so on and so on.....
        }
    }

对于简单的实体来说,它运行得很好。当然,如果你有很多实体,你可以使用反射。


是的,我知道我可以这样做(即手动从参数复制字段到本地变量),但我试图做的是创建一个能够自行处理此问题的通用方法。所以我猜我将不得不使用反射来实现这一点... - Andreas Grech

0

如果我理解正确的话,你不需要使用反射来完成这个。

要针对特定实体执行此操作,您需要将实体附加到DB上下文中。一旦它被附加,LINQ-to-SQL将确定需要更新什么。大致如下:

// update an existing member
dbcontext.Members.Attach(member, true);

// commit to database
dbcontext.SubmitChanges();

这是用于更新Members表中的成员。true参数表示您已修改它。或者,如果您有原始数据,可以将其作为第二个参数传递,并让DB上下文为您执行差异。这是DB上下文实现(实现“工作单元”模式)的重要部分。

要通用化此操作,您应将Member类型替换为T,并将.Members替换为.GetTable:

public virtual void Update(T entity)
{
        var dbcontext = DB;
        dbcontext.GetTable<T>().Attach(entity, true);
        dbcontext.SubmitChanges();
}

假设实体的ID已经在模型中正确设置为主键,您甚至不需要先查找它。如果您感觉有必要,可以通过ID查找并将其传递到Attach方法中,但这可能会导致不必要的额外查找。
编辑:您需要在模型中将UpdateCheck设置为Never在列上,否则它会尝试执行并发检查。如果将其设置为Never,则会得到最后更新的结果。否则,您可以在表中添加时间戳字段,以便并发检查确定实体是否过时。
UpdateCheck.Never与Attach(entity, bool)结合使用将是使用LINQ-to-SQL解决此问题的最简单和最高效的方法。

只需将字段的UpdateCheck设置为Never,您就可以获得最后更新的胜利,而无需时间戳字段。 - mckamey
“couldn't that lead to concurrency issues?” 可能会导致并发问题吗? “Attach(entity,entity)” 一开始可能有些棘手,但是之后就可以一直使用,而不需要对您的模型进行自定义更改。 - andy
除非您正在检查版本,否则Attach(entity, entity)只会覆盖实体,所以为什么要加载它呢?只有在手头已经拥有实体时,传递实体才有帮助。如果您必须查找它,那您可能不如让LINQ-to-SQL为您执行,因为它的效率更高。您的库在循环中加载实体,这意味着需要进行大量查找。如果您只是附加一个实体列表,它可以通过一个查询更新所有实体。为了安全起见,最多只需引发过时数据异常,在这种情况下,您应该放置时间戳/版本字段,并再次让LINQ为您执行。 - mckamey
但是这样,您必须手动设置所有字段的UpdateCheck吗? - Andreas Grech
对于这个特定的答案,你要么需要设置所有字段,要么需要告诉 LINQ to SQL 使用哪个字段作为并发标记。 - mckamey
显示剩余2条评论

0

嘿Dreas,我也曾经为此苦恼过,但找到了一个非常优雅的解决方案。

你实际上需要使用DataContext.Attach(EntityToUpdate,OriginalEntity)方法。

有一些需要注意的地方...所以,阅读这些信息,它将会解释一切

一旦你阅读完毕,随时可以向我提出任何问题。我基于那些信息编写了一个非常有用的EntitySaver类,如果需要,我们可以在你掌握要点后深入讨论你的类。

干杯!

编辑: 以下是我的完整类,如果你想尝试一下,这个类实际上可以自动处理更新和插入操作。如果你有任何问题,请告诉我。

实体保存器:

    using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QDAL.CoreContext;
using QDAL.CoreEntities;
using LinqExtension.CustomExtensions;

namespace QDAL
{
    internal class DisconnectedEntitySaver
    {
        private QDataDataContext ContextForUpdate;

        public DisconnectedEntitySaver() {
            ContextForUpdate = Base.CreateDataContext();
        }

        public List<TEntityType> SaveEntities<TEntityType, TKeyType>(List<TEntityType> EntitiesToSave) {

            string PKName;

            PKName = Base.GetPrimaryKeyName(typeof(TEntityType), ContextForUpdate);

            return SaveEntities<TEntityType, TKeyType>(EntitiesToSave, PKName);
        }

        public List<TEntityType> SaveEntities<TEntityType, TKeyType>(List<TEntityType> EntitiesToSave, string KeyFieldName)
        {
            List<TEntityType> EntitiesToPossiblyUpdate;
            List<TEntityType> EntitiesToInsert;
            List<TEntityType> HandledEntities = new List<TEntityType>();

            bool TimeStampEntity;
            Type ActualFieldType;

            if (EntitiesToSave.Count > 0) {
                TimeStampEntity = Base.EntityContainsTimeStamp(typeof(TEntityType), ContextForUpdate);

                ActualFieldType = EntitiesToSave.FirstOrDefault().GetPropertyType(KeyFieldName);

                if (ActualFieldType != typeof(TKeyType)) {
                    throw new Exception("The UniqueFieldType[" + typeof(TKeyType).Name + "] specified does not match the actual field Type[" + ActualFieldType.Name + "]");
                }

                if (ActualFieldType == typeof(string)) {
                    EntitiesToPossiblyUpdate = EntitiesToSave.Where(ent => string.IsNullOrEmpty(ent.GetPropertyValue<string>(KeyFieldName)) == false).ToList();
                    EntitiesToInsert = EntitiesToSave.Where(ent => string.IsNullOrEmpty(ent.GetPropertyValue<string>(KeyFieldName)) == true).ToList();
                } else {
                    EntitiesToPossiblyUpdate = EntitiesToSave.Where(ent => EqualityComparer<TKeyType>.Default.Equals(ent.GetPropertyValue<TKeyType>(KeyFieldName), default(TKeyType)) == false).ToList();
                    EntitiesToInsert = EntitiesToSave.Where(ent => EqualityComparer<TKeyType>.Default.Equals(ent.GetPropertyValue<TKeyType>(KeyFieldName), default(TKeyType)) == true).ToList();
                }

                if (EntitiesToPossiblyUpdate.Count > 0) {
                    EntitiesToInsert.AddRange(ResolveUpdatesReturnInserts<TEntityType, TKeyType>(EntitiesToPossiblyUpdate, KeyFieldName));

                    HandledEntities.AddRange(EntitiesToPossiblyUpdate.Where(ent => EntitiesToInsert.Select(eti => eti.GetPropertyValue<TKeyType>(KeyFieldName)).Contains(ent.GetPropertyValue<TKeyType>(KeyFieldName)) == false));
                }

                if (EntitiesToInsert.Count > 0) {
                    ContextForUpdate.GetTable(typeof(TEntityType)).InsertAllOnSubmit(EntitiesToInsert);

                    HandledEntities.AddRange(EntitiesToInsert);
                }

                ContextForUpdate.SubmitChanges();
                ContextForUpdate = null;

                return HandledEntities;
            } else {
                return EntitiesToSave;
            }
        }

        private List<TEntityType> ResolveUpdatesReturnInserts<TEntityType, TKeyType>(List<TEntityType> PossibleUpdates, string KeyFieldName)
        {
            QDataDataContext ContextForOrginalEntities;

            List<TKeyType> EntityToSavePrimaryKeys;
            List<TEntityType> EntitiesToInsert = new List<TEntityType>();
            List<TEntityType> OriginalEntities;

            TEntityType NewEntityToUpdate;
            TEntityType OriginalEntity;

            string TableName;

            ContextForOrginalEntities = Base.CreateDataContext();

            TableName = ContextForOrginalEntities.Mapping.GetTable(typeof(TEntityType)).TableName;
            EntityToSavePrimaryKeys = (from ent in PossibleUpdates select ent.GetPropertyValue<TKeyType>(KeyFieldName)).ToList();

            OriginalEntities = ContextForOrginalEntities.ExecuteQuery<TEntityType>("SELECT * FROM " + TableName + " WHERE " + KeyFieldName + " IN('" + string.Join("','", EntityToSavePrimaryKeys.Select(varobj => varobj.ToString().Trim()).ToArray()) + "')").ToList();

            //kill original entity getter
            ContextForOrginalEntities = null;

            foreach (TEntityType NewEntity in PossibleUpdates)
            {
                NewEntityToUpdate = NewEntity;
                OriginalEntity = OriginalEntities.Where(ent => EqualityComparer<TKeyType>.Default.Equals(ent.GetPropertyValue<TKeyType>(KeyFieldName),NewEntityToUpdate.GetPropertyValue<TKeyType>(KeyFieldName)) == true).FirstOrDefault();

                if (OriginalEntity == null)
                {
                    EntitiesToInsert.Add(NewEntityToUpdate);
                }
                else
                {
                    ContextForUpdate.GetTable(typeof(TEntityType)).Attach(CloneEntity<TEntityType>(NewEntityToUpdate), OriginalEntity);
                }
            }

            return EntitiesToInsert;
        }

        protected  TEntityType CloneEntity<TEntityType>(TEntityType EntityToClone)
        {
            var dcs = new System.Runtime.Serialization.DataContractSerializer(typeof(TEntityType));
            using (var ms = new System.IO.MemoryStream())
            {
                dcs.WriteObject(ms, EntityToClone);
                ms.Seek(0, System.IO.SeekOrigin.Begin);
                return (TEntityType)dcs.ReadObject(ms);
            }
        }
    }
}

你也需要这些辅助工具:

    using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QDAL.CoreContext;
using QDAL.CoreEntities;
using System.Configuration;

namespace QDAL
{
    internal class Base
    {
        public Base() {
        }

        internal static QDataDataContext CreateDataContext() {
            QDataDataContext newContext;
            string ConnStr;

            ConnStr = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;

            newContext = new QDataDataContext(ConnStr);

            return newContext;
        }

        internal static string GetTableName(Type EntityType, QDataDataContext CurrentContext) {
            return CurrentContext.Mapping.GetTable(EntityType).TableName;
        }

        internal static string GetPrimaryKeyName(Type EntityType, QDataDataContext CurrentContext) {
            return (from m in CurrentContext.Mapping.MappingSource.GetModel(CurrentContext.GetType()).GetMetaType(EntityType).DataMembers where m.IsPrimaryKey == true select m.Name).FirstOrDefault();
        }

        internal static bool EntityContainsTimeStamp(Type EntityType, QDataDataContext CurrentContext) {
            return (CurrentContext.Mapping.MappingSource.GetModel(CurrentContext.GetType()).GetMetaType(EntityType).DataMembers.Where(dm => dm.IsVersion == true).FirstOrDefault() != null);
        }
    }
}

这些扩展使反射更容易:

<System.Runtime.CompilerServices.Extension()> _
    Public Function GetPropertyValue(Of ValueType)(ByVal Source As Object, ByVal PropertyName As String) As ValueType
        Dim pInfo As System.Reflection.PropertyInfo

        pInfo = Source.GetType.GetProperty(PropertyName)

        If pInfo Is Nothing Then
            Throw New Exception("Property " & PropertyName & " does not exists for object of type " & Source.GetType.Name)
        Else
            Return pInfo.GetValue(Source, Nothing)
        End If
    End Function

    <System.Runtime.CompilerServices.Extension()> _
    Public Function GetPropertyType(ByVal Source As Object, ByVal PropertyName As String) As Type
        Dim pInfo As System.Reflection.PropertyInfo

        pInfo = Source.GetType.GetProperty(PropertyName)

        If pInfo Is Nothing Then
            Throw New Exception("Property " & PropertyName & " does not exists for object of type " & Source.GetType.Name)
        Else
            Return pInfo.PropertyType
        End If
    End Function

很酷,让我知道进展如何。我已经更新了我的答案,并添加了一堆代码,这可能会有所帮助。 - andy

0

我对存储库模式不是很熟悉,但如果您从数据库中删除旧实体,并使用相同的ID将新实体放入数据库中,会怎样呢? 类似这样:

public virtual void UpdateByID(int id, T entity)
{
    DeleteByID(id);
    var dbcontext = DB;
    //insert item (would have added this myself but you don't say how)
    dbcontext.SubmitChanges();
}

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