发布于 2026-01-06 1 阅读
0

How to Update the Database After a Drag-and-Drop Operation

如何在拖放操作后更新数据库

我研究了拖放操作发生时数据库内部的运作机制,以及拖放操作后数据的变化。
在本文中,我将以待办事项列表的实现为例,详细介绍拖放操作过程中数据库的具体操作。

这是这篇文章的演示。

你会发现,即使重新加载后,任务顺序也不会改变。

概述

问题

即使通过拖放更改了任务顺序,重新加载后拖放顺序的更改也不会保存。

解决方案

使用拖放功能时,除了前端之外,还需要在后端操作数据库。

工作原理

步骤 1. 向数据库添加一列以控制订单

在表中创建一个名为“编号”的列index_number。然后,当任务存储到数据库时,index_number除了 id 和内容之外,它还将包含该编号。

步骤 2. 将数据填充到新创建的列中

添加新数据时,
情况 1:如果表中没有行,
则插入值index_number= 1024

情况2:如果表格至少有一行,
则 Set index_number= 当前最大值index_number+ 1024

这将引导您创建如下所示的数据库表:
示例表

步骤 3. 执行拖放操作,更新index_number拖放元素

在表格中输入一些数据后,启动服务器并执行拖放操作。例如,在上图中,如果您想通过拖放操作将“学习”放在“吃饭”和“睡觉”之间,
请将 (3072(吃饭) + 4096(睡觉)) / 2 设置为“学习”的
新值。index_number

(3072 + 4096) / 2 = 3584 ← 这将是“学习”的新值index_number。表格将按如下方式更新:
更新表

步骤 4. 在检索和显示表格时使用 ORDER BY 子句

通过使用“ORDER BY index_number”查询此表,您可以按升序检索数据index_number。因此,即使通过拖放更改顺序然后重新加载,顺序也会保留。

步骤 5. 如果index_number重叠

拖放任务后,该任务的权重是通过计算上方任务和下方任务的权重index_number的平均值来得出的。index_number

所以有时候,index_number两项任务可能会重叠。

只有在这种情况下,才需要将整个表格按降序排列index_number,然后index_number按 *1024 重新赋值。

执行

使用的语言和库

前端
・JavaScript
SortableJS
后端
・Node.js
・MySQL

文件结构

文件结构

步骤1. 安装必要的npm

npm i express mysql2 path body-parser util dotenv --save
npm i nodemon --save-dev

步骤2. 为待办事项列表编写CRUD函数

编辑和删除任务以及检索单个数据的代码与具有 CRUD 功能的常规待办事项列表相同,因此我将跳过它。

我将按照以下步骤
编写代码:
列表功能(检索所有数据)、
创建功能(添加任务)和
拖放功能(SQL 操作)。

步骤3. 列表函数(检索所有数据)

基本上,它只是像往常一样提取数据,但 SQL 语句与常规的待办事项列表略有不同。

app.get("/list/apis", async (req, res) => {
  try {
    const results = await util.promisify(connection.query).bind(connection)(
      "SELECT * FROM todo ORDER BY `index_number`" // Use ORDER BY `index_number`
    );

    res.json({ results });
  } catch (e) {
    res.status(500).send({ e });
  }
});
Enter fullscreen mode Exit fullscreen mode

与通常的任务检索方式不同的是,任务是按照索引号升序排列的,index_number即 ORDER BY index_number。
这样,即使您拖放任务,数据库也能根据索引号确定所有任务的顺序index_number,从而正确检索数据。

步骤4. 创建功能(添加任务)

添加新任务时,需要获取当前任务的最大值index_number,并将该值加上 1024 后添加到index_number新任务中。
这样,新任务就会被添加到待办事项列表的底部。

app.post("/add-todos", async (req, res) => {
  // value of todo task
  const todo = req.body.todo;

  try {
  // Get and return the maximum value of `index_number`
  // if there is no data in the table, return 0
    const results = await util.promisify(connection.query).bind(connection)(
      `SELECT IFNULL((SELECT index_number FROM todo ORDER BY index_number DESC LIMIT 1) ,0) as max_index_number;`
    );
  // Add a new task
  // Put the contents of the task and the value obtained in the above query + 1024 into VALUES
    await util.promisify(connection.query).bind(connection)(
      `INSERT INTO todo(todo, index_number) VALUES('${todo}', ${results[0].max_index_number}+1024)`
    );
    res.redirect("/");
  } catch (e) {
    res.status(500).send({ e });
  }
});
Enter fullscreen mode Exit fullscreen mode

步骤5. 拖放功能(MySQL操作)

在此处输入订单,订单将被保存到数据库中,即使拖放和重新加载后,订单也将保持不变。

要点如下:

  1. 获取index_number您拖放的任务上方和下方的任务。

  2. 如果拖放的任务上方没有其他任务,index_number则无法获取该任务的值。因此,index_number拖放任务上方任务的值将为未定义。

  3. 与 (2) 相同,如果拖放的任务下方没有其他任务,index_number则无法获取该任务。因此,index_number拖放任务下方任务的值将为未定义

  4. 如果index_number重叠,则按索引号对整个表进行排序,并按index_number升序重新分配。

app.post("/order-todos/:id", async (req, res) => {
  const id = req.params.id;
  // index_number of the task above the dragged and dropped task
  let prevElIndexNumber = req.body.prevElIndexNumber;
  // index_number of the task under the dragged and dropped task
  let nextElIndexNumber = req.body.nextElIndexNumber;
  // a variable containing the index_number of the dragged and dropped task
  let currElIndexNumber;

  // prevElIndexNumber === undefined, this is happended when the drag-and-drop task is at the top of the to-do list.
  // Since there is no upper task, set the index_number of the lower task - 512 as the currElIndexNumber
  if (prevElIndexNumber === undefined) {
    currElIndexNumber = nextElIndexNumber - 512;
  // nextElIndexNumber === undefined, this is happended when the dragged-and-dropped task is at the bottom of the to-do list
  // Set the index_number of the task above + 512 as the currElIndexNumber
  } else if (nextElIndexNumber === undefined) {
    currElIndexNumber = prevElIndexNumber + 512;
  // If there are tasks both above and below the dragged-and-dropped task, then
  // currElIndexNumber = (index_number of the top task + index_number of the bottom task)/2
  } else {
    currElIndexNumber = Math.floor((prevElIndexNumber + nextElIndexNumber) / 2);
  }

    try {
    // Update currElIndexNumber as the index_number of the new task
    await util.promisify(connection.query).bind(connection)(
      `UPDATE todo SET index_number = ${currElIndexNumber} where id = ${id}`
    );

    // When index_number overlaps
    if (
      Math.abs(currElIndexNumber - prevElIndexNumber) <= 1 ||
      Math.abs(currElIndexNumber - nextElIndexNumber) <= 1
    ) {
      // Get index_number in ascending order from 1~ (= orderedData), then update the table
      const orderedData = await util
        .promisify(connection.query)
        .bind(connection)(
        `SELECT *, ROW_NUMBER() OVER (ORDER BY index_number) as orderedData FROM todo;`
      );
      await Promise.all(
        orderedData.map(async (element) => {
          await util.promisify(connection.query).bind(connection)(
            `UPDATE todo SET index_number = ${element.orderedData}*1024 where id = ${element.id}`
          );
        })
      );
    }
    res.end();
  } catch (e) {
    res.status(500).send({ e });
  }
});

Enter fullscreen mode Exit fullscreen mode

有点长,不过这里有个简图。

图表

步骤6. 前端的JavaScript

以下是对代码的简单解释,从加载时以 json 格式提取 API 以显示所有任务,到拖放完成后发送 http 请求。

提取并以 JSON 格式显示 API

// fetch api and display all stored datas
const wrapper = document.getElementById("wrapper");
window.onload = async () => {
  try {
    // fetch all data of todo
    await fetch("http://localhost:3000/list-todos")
      .then(async (allToDo) => {
        return await allToDo.json();
      })
      .then((datas) => {
        datas.results.forEach((el) => {
          const todoEl = document.createElement("div");
          todoEl.classList.add("item");
          const taskId = el.id;
          const text = el.todo;

          todoEl.setAttribute("taskId", taskId);
          todoEl.innerHTML = `<span class="txt" onClick="startEditToDo(this, ${taskId})">${text}</span><i class="trash fa fa-trash" onClick="deleteToDo(this.parentNode, ${taskId})"></i><i class="icon fa fa-bars"></i>`;
          // changePostion() after dragend
          todoEl.addEventListener("dragend", () => {
            changePosition(todoEl, taskId);
          });
          wrapper.appendChild(todoEl);
        });
      });
  } catch (e) {
    console.log(e);
  }
};
Enter fullscreen mode Exit fullscreen mode

拖放完成后的 HTTP 请求处理

在上述代码中,每次任务拖放完成后都会触发 changePosition() 函数。
在 changePosition() 函数中,index_number会获取被拖放任务上方和下方任务的位置,并将数据通过 HTTP 请求发送出去。

async function changePosition(currEl, currElId) {
  let prevElIndexNumber;
  let nextElIndexNumber;

  try {
    // Get index_number if there is a task on top of the dragged and dropped task
    // if not, undefined
    if (currEl.previousSibling !== null) {
      const prevElId = currEl.previousSibling.getAttribute("taskId");

      await fetch("http://localhost:3000/read-todos/" + prevElId)
        .then(async (data) => {
          return await data.json();
        })
        .then((json) => {
          prevElIndexNumber = json.results[0].index_number;
        });
    }

    // Get index_number if there is a task under the drag & drop task
    // if not, undefined
    if (currEl.nextSibling != null) {
      const nextElId = currEl.nextSibling.getAttribute("taskId");
      await fetch("http://localhost:3000/read-todos/" + nextElId)
        .then(async (data) => {
          return await data.json();
        })
        .then((json) => {
          nextElIndexNumber = json.results[0].index_number;
        });
    }

    // HTTP Request
    const updateUrl = "http://localhost:3000/order-todos/" + currElId;

    await fetch(updateUrl, {
      method: "POST",
      headers: {
        "Content-type": "application/json",
      },
      body: JSON.stringify({ prevElIndexNumber, nextElIndexNumber }),
    });
  } catch (e) {
    console.log(e);
  }
}
Enter fullscreen mode Exit fullscreen mode

概括

包括其余代码在内的所有内容都已上传到GitHub

拖放后保存位置的方法可能还有很多,但作为一种思考方式,我编写了一种方法,通过计算目标元素的位置来确定元素的数值,从而保存位置。

文章来源:https://dev.to/shoki/how-to-update-the-database-after-a-drag-and-drop-operation-27dc