【问题标题】:Best way to implement "ReOrderable Collection" and Persist it to database实现“ReOrderable Collection”并将其持久化到数据库的最佳方法
【发布时间】:2012-01-09 01:23:12
【问题描述】:

我的域对象:

public class MyDomainObject
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public int DisplayOrder { get; set; }
}

假设样本数据:

var list = new List<MyDomainObject>()
               {
                   new MyDomainObject {Name = "Element1", DisplayOrder = 0},
                   new MyDomainObject {Name = "Element2", DisplayOrder = 1},
                   new MyDomainObject {Name = "Element3", DisplayOrder = 2},
                   new MyDomainObject {Name = "Element4", DisplayOrder = 3},
               };

现在我将“Element3”的 DisplayOrder 从 2 更改为 1。我的列表应该如下所示:

  • 元素 1 (DisplayOrder = 0)
  • Element3 (DisplayOrder = 1)
  • Element2 (DisplayOrder = 2)
  • Element4 (DisplayOrder = 3)

现在我删除“Element3”

  • 元素 1 (DisplayOrder = 0)
  • 元素 2 (DisplayOrder = 1)
  • Element4 (DisplayOrder = 2)

那么,将这种机制持久化到数据库的最佳方式是什么?

基本上我需要一个“ReOrderableCollection”,它将从数据库中填充一个 OrderBy“DisplayOrder”,其中 Collection Index 匹配“DisplayOrder”,并通过从 Collection Index 分配 DisplayOrder 来保留项目。

【问题讨论】:

  • 为什么 DisplayOrder != List 索引?你为什么不根据基于数据库的 DisplayOrder 填充列表,然后再不考虑这个“其他顺序”呢?就用 List 支持的吧?
  • DisplayOrder 不是 != 列表索引。问题是我需要在添加/删除/移动项目时使 DisplayOrder 与 List 索引保持同步并从 List 索引中保留新的 DisplayOrder

标签: c# collections


【解决方案1】:

我在这里回答了关于重新订购的先前/类似问题: How to design table that can be re-sequenced?

这可以很好地毫无间隙地重新保存订单。根据列表的大小,重新保存订单可能是一个完全可行的选择,对于长列表,Mark Byers 的想法看起来不错。

【讨论】:

    【解决方案2】:

    从您的示例看来,您似乎总是希望序列没有间隙,从零开始。但这意味着删除第一个元素将需要为列表中的每个项目更新数据库中的行。它很简单,并且会起作用(这些都是好事),但并不总是理想的。由于您要求“最好的方法”而没有真正说明您的意思,请允许我建议一种替代方法:

    DisplayOrder 真正重要的不是实际值,而是它们的相对顺序。如果你想提高数据库的性能,你可以考虑放宽不应该有间隙的要求,然后尝试找到对 DisplayOrders 的最小更改数量,以确保存储正确的顺序,即使间隙存在结果序列。如果您这样做,那么添加、删除或移动单个项目通常只需要更新数据库中的一行,但偶尔需要移动其他项目以创建一个必须插入到其他两个项目之间的间隙有连续的 DisplayOrders。

    您还可以通过从 DisplayOrder 100、200、300 开始,然后允许在其间插入 DisplayOrder 150(或者可能使用实数/浮点类型而不是整数)。

    此方法的另一个优点是,如果您使用数据库数据比较工具来观察数据库当前版本与旧版本之间的变化,则可以更轻松地查看对显示顺序进行了哪些修改。您只会看到用户实际移动的项目的显示顺序的变化,而不是每次删除项目时列表的一半发生变化。如果您使用增量备份策略,它也会减少备份的大小。

    我想说的是,在大多数情况下,这些优势与幼稚方法相比并没有显着优势。这取决于您的系统是否值得实施此系统或保持简单。如果有疑问,请保持简单。对于列表较小、修改很少且您不关心更改历史记录的系统,每次使用新的 DisplayOrder 覆盖整个列表可能就可以了。

    【讨论】:

    • 使用这种方法,您是否建议定期“重新播种”您的 DisplayOrder 字段,以确保您在每个字段之间始终有一个大缓冲区?
    • 一个简单的解决方案是在每次发生碰撞时重新播种整个列表。一旦你遇到第一次碰撞,我认为其他碰撞很快就会发生的可能性很高。
    【解决方案3】:

    就我所见,DisplayOrder 似乎与集合的 index 属性具有相同的值。所以我会尝试使用它而不是 DisplayOrder 属性。在数据库上,我将使用 DisplayOrder 列来读取和保存项目,但不在域对象上。 高温高压 电子地图

    【讨论】:

      【解决方案4】:

      现在我假设您确实希望始终重新组织您的列表,以便 DisplayOrder 从 0 开始并无间隙地增加,并且您希望这自动发生。您可以实现自己的集合类型和接口 IDisplayOrderable,并让更改列表的类型的成员也自动更新集合中项目的 DisplayOrder。与我的另一个答案相反,该答案是关于将数据存储在数据集中的另一种方法,此答案显示了如何编写一个客户端类,该类可以更轻松地自动将对象中的 DisplayOrder 与列表索引同步,以便当您已准备好将更改提交到数据库,DisplayOrder 字段已为您正确设置。

      我认为最好给出一些源代码的答案:

      using System;
      using System.Collections.Generic;
      using System.Linq;
      
      interface IDisplayOrderable
      {
          int DisplayOrder { get; set; }
      }
      
      class ReorderableList<T> : IList<T> where T : IDisplayOrderable
      {
          List<T> list = new List<T>();
      
          private void updateDisplayOrders()
          {
              int displayOrder = 0;
              foreach (T t in list)
              {
                  t.DisplayOrder = displayOrder++;
              }
          }
      
          public ReorderableList() { }
      
          public ReorderableList(IEnumerable<T> items)
          {
              list = new List<T>(items.OrderBy(item => item.DisplayOrder));
          }
      
          public void Insert(int index, T item)
          {
              list.Insert(index, item);
              updateDisplayOrders();
          }
      
          public void Add(T item)
          {
              list.Add(item);
              updateDisplayOrders();
          }
      
          public bool Remove(T item)
          {
              bool result = list.Remove(item);
              if (result)
                  updateDisplayOrders();
              return result;
          }
      
          public IEnumerator<T> GetEnumerator()
          {
              return list.GetEnumerator();
          }
      
          // TODO: Other members and methods required to implement IList<T>...    
      }
      
      class Item : IDisplayOrderable
      {
          public string Name { get; set; }
          public int DisplayOrder { get; set; }
      }
      
      class Program
      {
          static void Main()
          {
              Item foo = new Item { Name = "foo", DisplayOrder = 0 };
              Item bar = new Item { Name = "bar", DisplayOrder = 1 };
              Item baz = new Item { Name = "baz", DisplayOrder = 2 };
      
              // Pretend this came from the database.
              IEnumerable<Item> query = new Item[] { bar, foo };
      
              // The constructor automatically reorder the elements.
              ReorderableList<Item> items = new ReorderableList<Item>(query);
              items.Add(baz);
              items.Remove(foo);
              items.Insert(1, foo);
      
              foreach (Item item in items)
                  Console.WriteLine("{0} : {1}", item.Name, item.DisplayOrder);
          }
      }
      

      输出:

      bar : 0
      foo : 1
      baz : 2
      

      也许这就是您要寻找的答案?

      【讨论】:

      • @Mark:我想到了同样的解决方案。唯一的限制是对象的DisplayOrder 属性可以从外部修改,我认为这可能是一个问题。
      • @shahkalpesh:是的。如果您担心这一点,您可以公开 updateDisplayOrders 方法,并在您将更改提交到数据库之前调用它。虽然我不清楚如果显示顺序或列表顺序不同步,是否应该将它们视为“正确”顺序。
      • 我想知道是否有可能在根本没有对象的 DisplayOrder 属性的情况下解决这个问题,或者实现使 DisplayOrder 始终与列表索引保持同步的属性的方法。当然这里还有改进的余地,但我没有看到一种特别干净的方法。
      • 完全不同的解决方案:列表索引不应与 DisplayOrder 保持顺序。 DisplayOrder 应该是唯一重要的事情。如果要按显示顺序显示列表,请执行 foreach(Ite​​m item in items.OrderBy(item => item.DisplayOrder)) ...。这要简单得多,因为您不需要保持任何同步。在我看来,不需要 ReorderableCollection 类型。
      • @shahkalpesh & Mark :为了避免 DisplayOrder 属性在外部被修改,一种解决方案应该是实现这样的表达式选择器来选择“DisplayOrder like”属性。所以 ReOrderableCollection 将能够根据选择器表达式设置属性。像 Linq 一样:Collection.Select(p => p.SomeProperty)
      【解决方案5】:

      我可能通过创建一个自定义列表来创建一个解决方案,该列表在构造函数参数中采用 Lamba 表达式,以便列表能够自我更新项目属性“DisplayOrder”。

      示例类

      public class MyItem
      {
          public string Name { get; set; }
          public int DisplayOrder { get; set; }
      }
      

      示例程序

      public class Program
      {
          static void Main(string[] args)
          {
              var list = new DisplayOrderableList<MyItem>(p => p.DisplayOrder)
                             {
                                 new MyItem{ Name = "Item 1"},
                                 new MyItem{ Name = "Item 2"},
                                 new MyItem{ Name = "Item 3"},
                             };
      
              var item = list.Where(p => p.Name == "Item 2").FirstOrDefault();
      
              list.MoveUp(item);
      
              list.ForEach(p => Console.WriteLine("{0}-{1}", p.Name, p.DisplayOrder));
              Console.WriteLine();
      
              list.MoveDown(item);
      
              list.ForEach(p => Console.WriteLine("{0}-{1}", p.Name, p.DisplayOrder));
              Console.WriteLine();
      
              Console.ReadLine();
          }
      }
      

      DisplayOrderableList 的自定义实现

      public class DisplayOrderableList<T> : List<T>
      {
          #region Private Fields
      
          private PropertyInfo _property;
      
          #endregion
      
          #region Constructors
      
          public DisplayOrderableList(Expression<Func<T, int>> expression)
          {
              ValidateExpression(expression);
          }
      
          #endregion
      
          #region Public Methods
      
          public void MoveUp(T item)
          {
              if (!Contains(item))
                  throw new ArgumentNullException("item", "item doesn't exists in collection");
      
              var idx = IndexOf(item);
      
              RemoveAt(idx);
              if (idx > 0)
                  Insert(idx - 1, item);
              else
                  Insert(0, item);
      
              UpdateDisplayOrder();
          }
      
          public void MoveDown(T item)
          {
              if (!Contains(item))
                  throw new ArgumentNullException("item", "item doesn't exists in collection");
      
              var idx = IndexOf(item);
      
              RemoveAt(idx);
              if (idx + 1 > Count)
                  Add(item);
              else
                  Insert(idx + 1, item);
      
              UpdateDisplayOrder();
          }
      
          #endregion
      
      
          #region Private Methods
      
          private void UpdateDisplayOrder()
          {
              foreach (var item in this)
              {
                  _property.SetValue(item, IndexOf(item), null);
              }
          }
      
          #endregion
      
          #region Expression Methods
      
          private void ValidateExpression(Expression<Func<T, int>> expression)
          {
              var lamba = ToLambaExpression(expression);
      
              var propInfo = ToPropertyInfo(lamba);
      
              if (!propInfo.CanWrite)
              {
                  throw new ArgumentException(String.Format("Property {0} as no setters", propInfo.Name));
              }
      
              _property = propInfo;
          }
      
          private static LambdaExpression ToLambaExpression(Expression expression)
          {
              var lambda = expression as LambdaExpression;
              if (lambda == null)
              {
                  throw new ArgumentException("Invalid Expression");
              }
              var convert = lambda.Body as UnaryExpression;
              if (convert != null && convert.NodeType == ExpressionType.Convert)
              {
                  lambda = Expression.Lambda(convert.Operand, lambda.Parameters.ToArray());
              }
              return lambda;
          }
      
          private static PropertyInfo ToPropertyInfo(LambdaExpression expression)
          {
              if (expression == null)
              {
                  throw new ArgumentNullException("expression", "Expression cannot be null.");
              }
      
              var prop = expression.Body as MemberExpression;
              if (prop == null)
              {
                  throw new ArgumentException("Invalid expression");
              }
      
              var propInfo = prop.Member as PropertyInfo;
              if (propInfo == null)
              {
                  throw new ArgumentException("Invalid property");
              }
      
              return propInfo;
          }
      
          #endregion
      }
      

      现在得到以下输出:

      Item 2-0
      Item 1-1
      Item 3-2
      
      Item 1-0
      Item 2-1
      Item 3-2
      

      这是一个概念证明,应该加强,但它只是一个开始。

      您对此有何看法?

      【讨论】:

      • 我使用了基本的 List 但是从 ICollection 或 IList 实现这个类以支持添加/删除方法应该很棒。
      【解决方案6】:

      我知道这是一个老问题,但是这里和另一个问题中的 cmets 帮助我解决了类似的问题,我想提供我的代码,以防它帮助其他人寻找类似的东西。您可以在以下链接中找到我的代码:

      How to design table that can be re-sequenced?

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-06
        • 1970-01-01
        • 2021-09-19
        • 1970-01-01
        • 2020-04-13
        • 1970-01-01
        相关资源
        最近更新 更多