8

我需要对数据库做出设计决定。要求是一个数据库表有一个名为id的AUTO_INCREMENT PRIMARY KEY字段。默认情况下,每一行都按id升序显示给用户(在 web 中) 。例如,如果表中有 4 条记录。UI 将按0、1、2、3的顺序显示行。

现在,要求用户可以在 UI 中拖放行以更改顺序。比如说,用户将 rom 3 拖放到 0 之前。因此,显示顺序变为3, 0, 1, 2。这个序列应该持久化到数据库中。

我想知道如何设计数据库表以使其持久化和可扩展。我的第一个想法是每一行都有一个“序列”字段指示显示顺序。默认情况下,该值应与id相同。从数据库中选择数据进行显示时,行按顺序而不是id 升序排序。

如果序列发生变化,则将其更新为新值。结果是它可能涉及到其他行的大量更改。以上面的例子,最初的表格是这样的:

|id   | sequence |
|0    | 0        |
|1    | 1        |
|2    | 2        |
|3    | 3        |

现在,将 id 为 3 的行拖到第一个。它的序列被更新为0。同时,id为0、1、2的行也应该被更新。

|id   | sequence |
|0    | 1        |
|1    | 2        |
|2    | 3        |
|3    | 0        |

恐怕这种方法会使重新排序花费大量资源并且不可扩展。所以,我想这个序列可以通过将id乘以K(比如 10)来初始化。这会在插入的序列值之间留下间隙。但是,如果将 K+1 行移至此间隙,则该间隙仍会耗尽。

|id   | sequence |
|0    | 0        |
|1    | 10       |
|2    | 20       |
|3    | 30       |

这似乎是数据库设计的一个常见问题。有人有更好的主意来实现这一目标吗?

4

8 回答 8

11

对我来说,显而易见的答案是使用您提到的最后一个解决方案,但使用小数(浮点数)。

所以你开始说:{0.1, 0.2, 0.3, 0.4, 0.5}. 如果您将最后一项移动到 between 0.20.3它将变为0.25. 如果将它移到顶部,它将变为0.05. 每次你只需取两边两个数字的中点。换句话说,上一个/下一个项目的平均值。

另一个类似的解决方案是使用字符,然后按字母顺序对字符串进行排序。从 开始{1, 2, 3, 4, 5},如果您将 5 在 2 和 3 之间移动,您将使用 25。如果您对列表进行字符串排序,您将保持正确的顺序:{1, 2, 25, 3, 4}.

这些方法我能想到的唯一问题是,最终你会达到浮点精度的极限,即试图找到一个介于0.0078125和之间的数字0.0078124。解决这个问题的几种方法:

  • 每隔一段时间运行一个脚本,该脚本会遍历每个项目并将它们重新排序为{0.1, 0.2, 0.3, ...}.
  • 当你可以使用一位时,不要使用两位小数。之间0.20.25您可以使用0.23而不是计算的0.225.
  • 本地重新排序,而不是全局重新排序。如果您有{0.2, 0.3, 0.6}并且想要在之后插入0.2,您可以将第二个设置为0.4并将新项目插入到0.3.
于 2009-10-22T16:49:02.157 回答
3

ID 和 Sequence/SortOrder 是分开的,根本不应该相互依赖。

对于上移/下移功能:您可以交换 Sequence/SortOrder 值

或者

对于拖放功能:

1) 为所选记录建立新的 Sequence/OrderNumber。

2)获取所选记录的当前顺序,然后用新编号更新所选记录。

3) a) 如果新序列号低于当前序列号,则增加序列号 >= 新序列号的记录的所有序列号(不包括选定的序列号)

b) 如果新序列号高于当前序列号,则将所有序列号递减到新选择的序列号之下和当前序列号之上。

希望这是有道理的,并且我认为它是正确的(下面是实际的实现)。

我已经在具有少量逻辑的单个 SQL 语句中实现了这一点,不是为了纯粹主义者,但它运作良好。

这是一个示例(OP:您需要将 GUID ID 更改为 INT):

CREATE PROCEDURE [proc_UpdateCountryRowOrder]
    @ID UNIQUEIDENTIFIER,
    @NewPosition INT
AS

SET NOCOUNT ON

DECLARE @CurrentPosition INT
DECLARE @MaximumPosition INT

IF (@NewPosition < 1) SET @NewPosition = 1

SELECT @CurrentPosition = [Countries].[Order]
FROM [Countries]
WHERE [Countries].[ID] = @ID

SELECT @MaximumPosition = MAX([Countries].[Order])
FROM [Countries]

IF (@NewPosition > @MaximumPosition) SET @NewPosition = @MaximumPosition

IF (@NewPosition <> @CurrentPosition)
BEGIN
    IF (@NewPosition < @CurrentPosition)
    BEGIN
        BEGIN TRAN

        UPDATE [Countries]
        SET [Countries].[Order] = [Countries].[Order] + 1
        WHERE [Countries].[Order] >= @NewPosition
        AND [Countries].[Order] < @CurrentPosition

        UPDATE [Countries]
        SET [Countries].[Order] = @NewPosition
        WHERE ID = @ID

        COMMIT TRAN
    END
    ELSE
    BEGIN
        BEGIN TRAN

        UPDATE [Countries]
        SET [Countries].[Order] = [Countries].[Order] - 1
        WHERE [Countries].[Order] <= @NewPosition
        AND [Countries].[Order] > @CurrentPosition

        UPDATE [Countries]
        SET [Countries].[Order] = @NewPosition
        WHERE ID = @ID

        COMMIT TRAN
    END
END
GO
于 2009-10-17T10:01:05.077 回答
2

链表怎么样?:-)

CREATE TABLE item(
    id INT PRIMARY KEY,
    prev INT,
    next INT
);

WITH RECURSIVE sequence AS (
    SELECT item.id, item.prev, item.next FROM item
    WHERE item.prev IS NULL
  UNION
    SELECT item.id, item.prev, item.next FROM sequence
    INNER JOIN item ON sequence.next = item.id
)
SELECT * FROM sequence;

实际上,我手头没有 PostgreSQL 来测试它是否真的有效(而且 MySQL 不支持 SQL-99's WITH RECURSIVE),我也不认真推荐它。

于 2009-10-17T05:36:50.270 回答
1

我在这里回答了一个类似的问题:Visually order large data sets

如果您移动了许多项目,那么您必须循环并移动每个项目并检查是否溢出。但总而言之,基本逻辑是有一个可以定期重新初始化的带有间隙的排序列。

于 2009-10-22T16:53:16.240 回答
0

我通过按用户选择的顺序将 ids(db 键)的 CSV 字符串返回到服务器来完成此操作。我的数据库上有一个函数,它将一个 csv 字符串转换为一个包含 2 个字段的表 - id 和一个序列(实际上是一个具有标识的 int)。此临时表中的序列字段值反映了 CSV 字符串中项目的顺序。然后我用新的序列字段值更新数据表,与 id 匹配。

编辑:赏金点看起来很好吃,我想我会提供一些关于我的答案的细节。这是代码:

declare @id_array varchar(1000)
set @id_array = '47,32,176,12,482'

declare @id_list_table table ([id] int, [sequence] int)

insert @id_list_table ([id], [sequence])
  select [id], [sequence]
  from get_id_table_from_list (@id_array)

update date_table
  set [sequence] = id_list.[sequence]
  from date_table
    inner join @id_list_table as id_list
      on (id_list.[id] = date_table.[id])

我设置@id_array为用于测试的变量 - 通常您的 UI 会按照修改后的顺序获取 id 值,并将其作为参数传递给存储的过程。该get_id_table_from_list函数将 csv 字符串解析为具有两个“int”列的表:[id] 和 [sequence]。[sequence] 列是一个标识。该函数处理我的测试数据的结果如下所示:

    序列号
    47 1  
    32 2  
    176 3  
    12 4  
    482 5  

您将需要一个解析 csv 的函数(如果您有兴趣,我可以发布我的,我已经看到其他人在这里和那里发布)。请注意,我的代码假设您使用的是 sql server - 序列取决于身份字段,并且更新查询使用 T-SQL 扩展名('from' 子句) - 如果您使用的是其他数据库,则需要做一些简单的改变。

于 2009-10-17T19:07:20.487 回答
0

此修复比您想象的要容易。一个临时表和一个更新查询,你就完成了。


CREATE TABLE #TempData
(
NewSequence bigint identity(1,1),
[Id] BigInt
)

INSERT INTO #TempData ([Id])
SELECT [Id] 
FROM TableNameGoesHere
ORDER BY Sequence

UPDATE TableNameGoesHere
SET Sequence = t2.NewSequence
FROM TableNameGoesHere t1
INNER JOIN #TempData t2
ON t1.[Id] = t2.[Id]

DROP TABLE #TempData
于 2009-10-25T02:11:30.777 回答
0

我知道这是一个 3 年前的问题,但是这些评论帮助我解决了一个类似的问题,我想提供我的代码,以防它帮助其他人寻找类似的东西。

基于@DisgruntledGoat 提供的代码示例(顺便说一句,谢谢!)我创建了以下代码,它是专门为Entity Framework 4.1 代码优先编写的。它基本上需要三个参数 - 存储库对象、实体对象的 id 和一个布尔值,用于指示实体的 DisplayOrder 是否应该在显示中向上或向下移动。实体必须从 Entity 继承,这意味着它必须具有 Id 值并实现 IOrderedEntity,这意味着它必须具有 DisplayOrder 浮动属性。

MoveDisplayOrder 方法查找相邻的两个条目(小于或大于当前显示顺序,取决于移动方向),然后平均它们的值(因此需要浮点而不是整数值)。然后它将更新该实体的存储库。然后,出于清理目的,如果生成的新显示顺序中的小数位数超过 5,它使用 EF 更新数据库中的所有值,并将它们重新设置为 1、2、3 等值。

这段代码非常适合我,如果需要清理或重构,欢迎提供任何反馈。

  public abstract class Entity : IIdentifiableEntity
  {
    public int Id { get; set; }
  }

  public interface IOrderedEntity
  {
    float DisplayOrder { get; set; }
  }

  public interface IRepository<TEntity>
  {
    TEntity FindById(int id);
    bool InsertOrUpdate(TEntity entity);
    // More repository methods here...
  }

public static class RepositoryExtenstions
{
  public static void MoveDisplayOrder<T>(IRepository<T> repository, int id, bool moveUp) where T : Entity, IOrderedEntity
  {
    var currentStatus = repository.FindById(id);
    IQueryable<IOrderedEntity> adjacentStatuses;
    if (moveUp)
      adjacentStatuses = repository.All().OrderByDescending(ms => ms.DisplayOrder).Where(ms => ms.DisplayOrder < currentStatus.DisplayOrder);
    else
      adjacentStatuses = repository.All().OrderBy(ms => ms.DisplayOrder).Where(ms => ms.DisplayOrder > currentStatus.DisplayOrder);

    var adjacentTwoDisplayOrders = adjacentStatuses.Select(ms => ms.DisplayOrder).Take(2).ToList();
    float averageOfPreviousTwoDisplayOrders;
    switch (adjacentTwoDisplayOrders.Count)
    {
      case 0:
        // It's already at the top or bottom, so don't move it
        averageOfPreviousTwoDisplayOrders = currentStatus.DisplayOrder;
        break;
      case 1:
        // It's one away, so just add or subtract 0.5 to the adjacent value
        if (moveUp)
          averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders[0] - 0.5F;
        else
          averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders[0] + 0.5F;
        break;
      default: // 2
        // Otherwise, just average the adjacent two values
        averageOfPreviousTwoDisplayOrders = adjacentTwoDisplayOrders.Average();
        break;
    }

    currentStatus.DisplayOrder = averageOfPreviousTwoDisplayOrders;
    repository.InsertOrUpdate(currentStatus);
    var floatPrecision = currentStatus.DisplayOrder.ToString().Substring(currentStatus.DisplayOrder.ToString().IndexOf('.') + 1).Length;
    if(floatPrecision > 5)
      ReorganizeDisplayOrder(repository);
  }

  public static void ReorganizeDisplayOrder<T>(IRepository<T> repository) where T : Entity, IOrderedEntity
  {
    var entities = repository.All().OrderBy(ms => ms.DisplayOrder).ToList();
    float counter = 1F;
    foreach (var entity in entities)
    {
      entity.DisplayOrder = counter;
      repository.InsertOrUpdate(entity);
      counter++;
    }
  }
}
于 2012-01-08T17:19:41.587 回答
-1

--编辑:仅对于阅读这篇文章的其他人,我因一些奇怪的个人怨恨而被否决,而不是与该主题相关的普遍不准确,阅读时考虑到这一点,并按您的意愿投票!:)

- 老的:

执行此操作的典型方法是使用您的“序列”编号(我称之为“排序顺序”)。

这真的很容易。

“可扩展”没有涉及;因为您已经获得了正在处理的所有节点的列表(如您所说,拖放)所以您真正要做的只是交换这些数字。

琐碎的。

于 2009-10-17T05:42:46.053 回答