简体   繁体   中英

Best way to support reordering html table rows using jquery as well as make SQL backend fast?

I have an asp.net-mvc website and I have an html table of data and one of the columns is ranking which represents the priority of that row (each row represents a request). I allow people to edit the data in a row with a new number but it doesn't actually affect any of the other rows (for example, nothing is stopping folks from entering a value that alraedy exists in another row)

I am looking for a solution to have a more effective front end and a fast backend. I will break down each:

  1. Front End:

I want some functionality where I can either drag and drop rows up and down or click up/down arrow or when i enter a ranking value in a row it updates the ranking of all other rows. I basically don't want the user to have to update every row if there is a new entry that is ranked #1 (basically having to update previous 1 => 2, and previous 2=>3, and previous 3=>4 etc . .

Are there any jquery libraries or useful patterns that help support this functionality (to avoid having to wire it all up from scratch) as this seems like generic functionality that i can imagine many other people using.

For reference, if anyone is familiar with the JIRA Greenhopper Planning board , this would be ideal as an example to mimic.

  1. Back End

My related question is that when i go update the backend, i am trying to figure out how to avoid this being really slow operation. If there is a rank field in my database table and someone updates an item to be number one, i essentially have to run an update query on EVERY other row to change the value to the existing value + 1. Is there some pattern to do this more effectively?

For the frontend look at jQueryUI's Sortable feature. It's normally applied to <ul/> or <ol/> elements, but a quick Google Search turns up guides to adapting it for <table/> elements too, if needed.

The backend issue of one insert cascading into many updates can be solved by treating it as a linked-list.

I'm assuming you're storing your data as:

Rank | Key
1    | Zebra
2    | Pig
3    | Pony

So inserting a new row could require updating potentially every existing row:

Rank | Key
1    | Penguin * Inserted
2    | Zebra   * Updated
3    | Pig     * Updated
4    | Pony    * Updated

Instead, store their order by relating them to another row in the table.

Key | ComesAfter
Zebra | null
Pig   | Zebra
Pony  | Pig

So inserting a new row will require at most one update.

Key     | ComesAfter
Penguin | null    * Inserted
Zebra   | Penguin * Updated
Pig     | Zebra   * Unchanged
Pony    | Pig     * Unchanged

Of course you can use IDs for this


However

You might be prematurely optimizing; it could be that some simple SQL work could prove to be quite efficient even while issuing many updates. Consider this:

Key   | Rank
Zebra | 3
Pig   | 1
Pony  | 2

Notice that the Value is acting as a natural key here, it could also have an ID (which is different from the Rank ).

To add a new row as Rank 1 you'd need to do something like:

begin tran
  insert values ('Penguin', 1)
  update table set Rank = Rank + 1 where Rank >= 1
commit

Adding indexes and possibly partitions (depending on the size of the table) will offer great performance improvements--the only tradeoff being a bit more occasional DB administration.

The main question here is which aspect you reuqire mose: fast reads, fast writes, or simple design? You can have two of the three :)

For your back end : is your question pure speculation ? or have you actually tried something ?

If your table has an index on its "rank" column, there should be no problem running queries like :

UDPATE table SET rank=rank+1 WHERE rank >= {begin value} AND rank <= {end value};

even if your table reaches the 1M rows range and your query affects several thousands lines.

You will need to adapt the index to your needs - eg if your query is actually :

UDPATE table SET rank=rank+1 WHERE listID = {given list ID} AND rank >= {begin value} AND rank <= {end value};

your should index listID, rank (in this order).

For the front end : as said in other answers, read the doc for jqueryUI Sortable widget

For moving items Up and Down you may use jQuery UI sortable . The simplest solution is to add to all items a floating point number called score, possibly a double. This is known to be the more efficient technique to keep a set of elements ordered when ordering may change. Thid score must be kept in all layers from the DB, to the presentation layer. When you need to put an element in between two other elements you just set its float to the avrerage of the two elements, so you are sure it is in between them.

You retrieve all items from the DB sorted on their score.

You start the algorithm with two fake items (not rendered, and not inserted in the DB, but purely virtual) whose scores are 0. and 1. All "real" items will be inserted in between them. Thus your first element will have a 0.5 score, and so on.

You must store in the DB the minimum distance between two scores you reached. When this number is becoming too small and is being close to the minimum precison you may get with a double, you must reorganize your table by assiging scores, such that all scores distances between two consecutive elements are the same and are equal to 1/(number of items).

If you need to show to the user a "classic rank" 1, 2, 3.... you may create it in the web page just before rendering the page, and immediately after each user modification. Updating all ranks has a cost equal to the items shown in the screen. This is not a big This wiil cause no performance decrease since all elements on the screen must be re-rendered by the browser at a cost that is higher than the cost of re-writing all ranks on the screen. ...IMPORTANT... if you have virtual scrolling or paging ...the rank re-writing involves only the active elements on the screen since the rank has the only purpose of being shown to the user since all computations are done with the score.

Here are couple ideas for the backend.

Make values in the backend be like: 1 – rank #1 100 – rank #2 200 – rank #3 Etc…

This will allow you to update minimal number of rows when you add some value between other two.

For example if you want to insert a value at rank #3 you can just enter value of 150.

When showing data to users you don't display the actual values in the database but use a ROW_NUMBER function to display it in a user friendly way.

When you want to add something at place #1 you can just enter a value that's smaller than existing first place by some factor. It doesn't matter if actual value ends up looking like -250 as long as order is correct an ROW_NUMBER function will take care of the presentation.

All you would have to do once in a while is to reorder values so there is enough room between the two.

Extending @Dwoolk's answer, you could use floats for the ordering. Then you should always (at least for a long time) be able to find new values between two existing values.

Angularjs is awesome at taking care of the "wiring" of these kinds of things. Here's an example, of the algorithm suggested by Francesco implemented with an Angular controller handling all of the binding between the items/scores and the table and a directive handling the sorting/re-scoring:

angular.module("listApp", []).
  controller("listController", function($scope) {
    var sortByKey = function(arr, key) {
      arr.sort(function(a, b) {
        if (a[key] > b[key]) {
          return -1;
        } else if (b[key] > a[key]) {
          return 1;
        } else {
          return 0;
        }
      });
    };
    $scope.items = [{id: 1111, name: "Item 1", score:0.2},
                    {id: 3333, name: "Item 2", score:0.5},
                    {id: 5555, name: "Item 3", score:0.7}];
    sortByKey($scope.items, "score");
  }).
  directive("sortable", function() {
    return function(scope, element) {
      var startIndex = null;
      var stopIndex = null;
      var averageOfNeighbors = function(items, index) {
        var beforeScore = index === 0 ? 1 : items[index -1].score;
        var afterScore = index === items.length - 1 ? 0 : items[index + 1].score;
        return (beforeScore + afterScore) / 2;
      }
      element.sortable( {
        start: function(e, ui) {
          startIndex = ui.item[0].sectionRowIndex;
        },
        stop: function(e, ui) {
          stopIndex = ui.item[0].sectionRowIndex;
          if (stopIndex !== startIndex) {
            var toMove = scope.items.splice(startIndex, 1)[0];
            scope.items.splice(stopIndex, 0, toMove);
            toMove.score = averageOfNeighbors(scope.items, stopIndex);
            console.log("Set item(" + toMove.id + ").score to " + toMove.score);
            scope.$apply();
          }
        }
      });
    }
  })

This can then be trivially bound to a table like this:

    <tbody sortable>
      <tr ng-repeat="item in items">
        <td>{{item.name}}</td>
        <td>{{$index + 1}}</td>
        <td>{{item.score}}</td>
      </tr>
    </tbody>

Here's a Plunker.

Also, note that after each re-sort, an AJAX call could be made to update a single score record in the database.

I have done a similar functionality in the project that I am working on.

  • For front end: I have used jQuery before() and after() APIs to move the rows, for example:

    $(prevRow).before(currentRow)

  • For back end: I have a int column in the database that stores the orders of every record.

Now once the rows are arranged at the front end, the new sequence of all the rows is sent to database via Ajax and all the records are updated (In my case only the filtered records on the table needs to be updated).

This is probably not the best solution but I could not find any other way to make this work.

是完整实现代码的一个很好的例子。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM