【问题标题】:C# Linq-SQL: An UpdateByID method for the Repository PatternC# Linq-SQL:存储库模式的 UpdateByID 方法
【发布时间】:2009-05-18 22:15:14
【问题描述】:

我已经实现了一种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();
    }
}

但它不会更新传递的实体。

有没有人实现过这样的方法?


作为参考,hereGetByID 方法


[更新]

正如 Marc 正确建议的那样,我只是在更改局部变量的值。那么您认为我应该如何使用这种方法?使用反射并将属性从entity复制到item

【问题讨论】:

  • 实际上,我认为 GetByID 方法也是错误的 ;-p 研究一个修复这两者的示例(并且适用于 POCO 以及属性化)
  • 嗯,但 GetByID 始终有效(如果我没记错的话)。马克你怎么看?
  • 嗯,我看不到 GetPrimaryKey() 是如何实现的(没有显示),但我猜它正在查看属性。这是不正确。使用属性不是 LINQ-to-SQL 的要求;也可以使用外部映射文件。在这种情况下,属性将不存在,并且会失败。如果您使用元模型,它适用于任何实现(因为这是 LINQ-to-SQL 询问“主键是什么”的方式)。
  • 或者换一种说法:它对你有用,因为你使用的是属性类型。用一个未归属的 POCO 试试,看看它是如何工作的......或者没有。
  • 我相信 Dreas 要求将 T 作为已编辑实体而不是 POCO(普通旧 CLR 对象)。他说这是一个“局部变量”,但没有说 T 不是 LINQ-to-SQL 实体。他的代码将 T 和 GetByID 的结果分配给同一个 var,如果 T 不是来自 GetByID 的类型,则该 var 将无法编译。

标签: c# linq-to-sql generics repository-pattern


【解决方案1】:

你更新的只是一个局部变量;为此,您必须将 成员值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?
    }

【讨论】:

  • 啊,是的,我是这么认为的……那么我如何才能真正实现这样的存储库方法呢?您认为我应该使用反射将字段从参数(成员)传输到项目吗?
  • 是的,您可能需要使用反射。
  • Marc,我试过你的方法,效果很好,谢谢。不过,现在我需要花一些时间来真正理解您的代码;
【解决方案2】:

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

应用程序中的并发性是需要预先考虑的问题,并且实际上并没有一刀切的答案。为您的应用选择时需要考虑的事项:

  • LINQ to SQL / Entity Framework 是非常可配置的,因为没有万能的。
  • 在您的应用程序负载达到一定程度之前,您不会看到并发的效果(即,您独自一人在自己的机器上可能永远看不到它)
  • 您的应用程序多久允许 2 个(或更多)用户编辑同一个实体?
  • 您想如何处理两个编辑重叠的情况?
  • 您的应用程序是否在另一个层(例如 Ajax)之间来回序列化数据?如果是这样,那么您如何知道已编辑的实体是否在读取/更新之间被修改?时间戳?版本字段?
  • 您是否关心编辑是否重叠?特别注意 FK 关系。数据完整性是您可能会被最后一场胜利所困扰的地方。

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

  • 不要循环调用 SQL(传入实体列表时请注意这一点)
  • 当您已经通过 LINQ 进行并发检查时,不要为此使用反射
  • 尽量减少来回复制字段(跨越 N 层边界时可能需要)。
  • 不要进行单独的查询来查找旧实体,(仅当您已经拥有它时才使用它)让 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:

仅映射为 Always 或的成员 WhenChanged参与看好 并发检查。没有支票是 为标记为Never 的成员执行。 如需更多信息,请参阅UpdateCheck

要启用乐观并发,您需要指定一个字段用作并发标记(例如时间戳或版本),并且在来回序列化时必须始终存在此字段。 Mark this column with IsVersion=true.

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

【讨论】:

    【解决方案3】:

    我遇到了一些类似的问题,最终选择了PLINQO,对 LINQ-TO-SQL 生成的代码进行了许多增强。但是,如果您还没有它,它确实需要购买 CodeSmith(尽管可以免费评估 30 天)。

    【讨论】:

      【解决方案4】:

      嗯,我有这样的事情(从我的头顶):

      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.....
              }
          }
      

      它适用于简单的实体。当然,如果你有很多实体,你可以使用反射。

      【讨论】:

      • 是的,我知道我可以做到这一点(即手动将字段从参数复制到本地),但我想要做的是创建一个通用方法,它自己处理这个问题。所以我想我必须使用反射来处理这样的......
      【解决方案5】:

      嘿,dreas,我也为此苦苦挣扎,并找到了一个非常优雅的解决方案。

      您基本上必须使用 DataContext.Attach(EntityToUpdate,OriginalEntity) 方法。

      有一些陷阱......所以,read this information, it will explain everything

      读完之后,有任何问题都可以回来找我。我已经根据该信息编写了一个非常有用的 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
      

      【讨论】:

      • 酷,让我知道进展如何。我已经用一堆可能有帮助的代码更新了我的答案。
      【解决方案6】:

      如果我理解正确,您不需要为此反思。

      要对特定实体执行此操作,您需要获取实体并将其附加到数据库上下文。附加后,LINQ-to-SQL 将确定需要更新的内容。大致如下:

      // update an existing member
      dbcontext.Members.Attach(member, true);
      
      // commit to database
      dbcontext.SubmitChanges();
      

      这将用于更新成员表中的成员。真正的论点说你修改了它。或者,如果您有原始文件,您可以将其作为第二个参数传递,并让数据库上下文为您执行差异。这是 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 方法,但这可能只会导致不需要的额外查找。

      编辑:您需要set UpdateCheck to Never on your columns in the model,否则它会尝试执行并发检查。如果您将其设置为从不,您将获得最后一次更新胜利。否则,您将 tmestamp 字段添加到表中,并发检查将确定实体是否已过期。

      UpdateCheck.Never 与 Attach(entity, bool) 结合将是使用 LINQ-to-SQL 解决此问题的最简单和最高效的方法。

      【讨论】:

      • 这不会普遍适用...只有在您的实体上定义了时间戳字段时才会有效
      • 只需在您的字段上将 UpdateCheck 设置为从不,您将获得最后一次更新胜利,而无需时间戳字段。 msdn.microsoft.com/en-us/library/…
      • 这不会导致并发问题吗? Attach(entity,entity) 一开始很繁琐,但随后一直有效,无需对模型进行自定义更改。
      • 除非您正在检查版本,否则 Attach(entity, entity) 无论如何都会覆盖实体,那么为什么还要加载它呢?仅当您手头已有实体时,传递实体才会有所帮助。如果一定要查,不如让LINQ-to-SQL帮你查,因为这样效率更高。您的库在循环中加载实体,这意味着很多查找。如果您只是附加一个实体列表,它可以在一个查询中更新所有内容。为了安全起见,您最多只会抛出一个过时的数据异常,在这种情况下,您将放置时间戳/版本字段,然后再次让 LINQ 为您完成。
      • 但是有了这个,你必须手动设置所有字段的UpdateCheck?
      【解决方案7】:

      我对存储库模式不是很熟悉,但是如果您从数据库中删除旧实体,然后将新实体放入具有相同 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();
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-04
        • 2012-08-22
        • 1970-01-01
        • 2012-02-21
        相关资源
        最近更新 更多