74

I was wondering if anyone has a good solution to a problem I've encountered numerous times during the last years.

I have a shopping cart and my customer explicitly requests that it's order is significant. So I need to persist the order to the DB.

The obvious way would be to simply insert some OrderField where I would assign the number 0 to N and sort it that way.

But doing so would make reordering harder and I somehow feel that this solution is kinda fragile and will come back at me some day.

(I use C# 3,5 with NHibernate and SQL Server 2005)

Thank you

Machavity
  • 30,841
  • 27
  • 92
  • 100
Tigraine
  • 23,358
  • 11
  • 65
  • 110

12 Answers12

51

Ok here is my solution to make programming this easier for anyone that happens along to this thread. the trick is being able to update all the order indexes above or below an insert / deletion in one update.

Using a numeric (integer) column in your table, supported by the SQL queries

CREATE TABLE myitems (Myitem TEXT, id INTEGER PRIMARY KEY, orderindex NUMERIC);

To delete the item at orderindex 6:

DELETE FROM myitems WHERE orderindex=6;    
UPDATE myitems SET orderindex = (orderindex - 1) WHERE orderindex > 6;

To swap two items (4 and 7):

UPDATE myitems SET orderindex = 0 WHERE orderindex = 4;
UPDATE myitems SET orderindex = 4 WHERE orderindex = 7;
UPDATE myitems SET orderindex = 7 WHERE orderindex = 0;

i.e. 0 is not used, so use a it as a dummy to avoid having an ambiguous item.

To insert at 3:

 UPDATE myitems SET orderindex = (orderindex + 1) WHERE orderindex > 2;
 INSERT INTO myitems (Myitem,orderindex) values ("MytxtitemHere",3)
Community
  • 1
  • 1
Kickaha
  • 3,680
  • 6
  • 38
  • 57
32

Best solution is a Doubly Linked list. O(1) for all operations except indexing. Nothing can index SQL quickly though except a where clause on the item you want.

0,10,20 types fail. Sequence column ones fail. Float sequence column fails at group moves.

Doubly Linked list is same operations for addition, removal, group deletion, group addition, group move. Single linked list works ok too. Double linked is better with SQL in my opinion though. Single linked list requires you to have the entire list.

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
sean wagner
  • 423
  • 4
  • 5
  • 3
    This is the best solution IMHO. For e.g., I have more than 300 movies in my Netflix queue. If I move an item from position 297 to the top, netflix would not have to do 300 updates. Just 3 updates at best – user35559 Mar 08 '12 at 03:16
  • 6
    What is the best way to get the first 50 items? Do I need to read them one-by-one, or is there any magic sql solution? – akn Apr 05 '15 at 11:33
  • @akn This would be possible using a recursive query and then limiting the number of rows by 50 to get the first 50 rows. – Decade Moon Jun 18 '15 at 01:24
  • Does the absence of locality grouping cause problems? i.e., does reading the list in sequence cause a prohibitive number of disk seeks as the cursor jumps around the database? – David Given Mar 10 '16 at 14:11
  • 2
    I do not understand how does doubly linked list help with "ordering" and "saving order sequences as float or int" in database? Isn't doubly linked list operation only happen on the application side? How do you update the database with the list? you'll still have to write an algorithm to save the ordered column in database. Unless I'm missing something. – ANewGuyInTown May 16 '16 at 02:42
  • 1
    @ANewGuyInTown He is saying implement your row data structure as if it were a doubly linked list. An entry would have references to before and after siblings in the list hierarchy. – States Nov 05 '16 at 23:03
  • I also thought about this but what about the performance. Wouldn't recursively querying dozens of items take much more time than the rank ID method? Guess it might be more suited to scenarios where reordering is frequent and the number of items isn't very large then, just as what we learnt from the algorithms and data structures basics. – xji Jul 09 '18 at 20:28
25

FWIW, I think the way you suggest (i.e. committing the order to the database) is not a bad solution to your problem. I also think it's probably the safest/most reliable way.

Galwegian
  • 41,475
  • 16
  • 112
  • 158
  • 1
    I somehow feel that keeping that order may cause headaches in the future. So I asked and maybe someone finds a better solution. – Tigraine Dec 01 '08 at 10:48
  • 13
    Don't worry - there is no better way than using the ordering column. As for the future headaches... do not try to optimize for the use cases that you are not aware of. – Roland Tepp Dec 01 '08 at 12:11
  • 1
    Since you usually fetch the whole cart for display purposes, renumbering the whole cart doesn't seem to actually be a problem – S.Lott Dec 01 '08 at 15:21
  • 1
    I agree, it just felt too fragile. But after I realized I can put the whole ordering logic in the Repository and the whole thing is transparent to my Business Code it works perfectly. Thanks for your advice. – Tigraine Dec 01 '08 at 15:44
10

How about using a linked list implementation? Having one column the will hold the value (order number) of the next item. I think it's by far the easiest to use when doing insertion of orders in between. No need to renumber.

Js.
  • 101
  • 1
  • 2
5

Unfortunately there is no magic bullet for this. You cannot guarentee the order of any SELECT statement WITHOUT an order by clause. You need to add the column and program around it.

I don't know that I'd recommend adding gaps in the order sequence, depending on the size of your lists and the hits on the site, you might gain very little for the over head of handling the logic (you'd still need to cater for the occasion where all the gaps have been used up). I'd take a close look to see what benifits this would give you in your situation.

Sorry I can't offer anything better, Hope this helped.

Binary Worrier
  • 50,774
  • 20
  • 136
  • 184
  • 2
    Since you usually fetch the whole cart for display purposes, renumbering the whole cart doesn't seem to actually be a problem. – S.Lott Dec 01 '08 at 11:58
  • 1
    If the list happened to be large enough that you would not want to update every row when you reorder something, you might be able to use a floating point DisplayOrder column. I haven't tried it but it's just an idea... – Kevin Coulombe Dec 20 '13 at 22:02
3

I wouldn't recommend the A, AA, B, BA, BB approach at all. There's a lot of extra processing involved to determine hierarchy and inserting entries in between is not fun at all.

Just add an OrderField, integer. Don't use gaps, because then you have to either work with a non-standard 'step' on your next middle insert, or you will have to resynchronize your list first, then add a new entry.

Having 0...N is easy to reorder, and if you can use Array methods or List methods outside of SQL to re-order the collection as a whole, then update each entry, or you can figure out where you are inserting into, and +1 or -1 each entry after or before it accordingly.

Once you have a little library written for it, it'll be a piece of cake.

Adam
  • 7,800
  • 2
  • 25
  • 24
1

On a level of abstraction above the cart Items let's say CartOrder (that has 1-n with CartItem) you can maintain a field called itemOrder which could be just a comma - separated list of id(PK) of cartItem records relevant . It will be at application layer that you require to parse that and arrange your item models accordingly . The big plus for this approach will be in case of order reshufflings , there might not be changes on individual objects but since order is persisted as an index field inside the order item table rows you will have to issue an update command for each one of the rows updating their index field. Please let me know your criticisms on this approach, i am curious to know in which ways this might fail.

redzedi
  • 1,957
  • 21
  • 31
1

I solved it pragmatically like this:

  1. The order is defined in the UI.

  2. The backend gets a POST request that contains the IDs and the corresponding Position of every item in the list.

  3. I start a transaction and update the position for every ID.

Done.

So ordering is expensive but reading the ordered list is super cheap.

Bijan
  • 25,559
  • 8
  • 79
  • 71
1

I would just insert an order field. Its the simplest way. If the customer can reorder the fields or you need to insert in the middle then just rewrite the order fields for all items in that batch.

If down the line you find this limiting due to poor performance on inserts and updates then it is possible to use a varchar field rather than an integer. This allows for quite a high level of precision when inserting. eg to insert between items 'A' and 'B' you can insert an item ordered as 'AA'. This is almost certainly overkill for a shopping cart though.

Jack Ryan
  • 8,396
  • 4
  • 37
  • 76
0

I would recommend keeping gaps in the order number, so instead of 1,2,3 etc, use 10,20,30... If you need to just insert one more item, you could put it at 15, rather than reordering everything at that point.

harriyott
  • 10,505
  • 10
  • 64
  • 103
  • 22
    That would only be OK if you wrote the system in BASIC ;-) – belugabob Aug 03 '09 at 07:24
  • 10
    Seriously, though, this approach would only postpone the need to actually do a sort, and then you have two different bits of functionality in your code. If you're probably going to have to handle a sort at some point, why not just do it at the start and keep the level of complexity down? – belugabob Aug 03 '09 at 07:26
0

Well, I would say the short answer is:

Create a primary key of autoidentity in the cartcontents table, then insert rows in the correct top-down order. Then by selecting from the table with order by the primary key autoidentity column would give you the same list. By doing this you have to delete all items and reinsert then in case of alterations to the cart contents. (But that is still quite a clean way of doing it) If that's not feasible, then go with the order column like suggested by others.

sindre j
  • 4,404
  • 5
  • 31
  • 32
-1

When I use Hibernate, and need to save the order of a @OneToMany, I use a Map and not a List.

@OneToMany(fetch = FetchType.EAGER, mappedBy = "rule", cascade = CascadeType.ALL)
@MapKey(name = "position")
@OrderBy("position")
private Map<Integer, RuleAction>    actions             = LazyMap.decorate(new LinkedHashMap<>(), FactoryUtils.instantiateFactory(RuleAction.class, new Class[] { Rule.class }, new Object[] { this }));

In this Java example, position is an Integer property of RuleAction so the order is persisted that way. I guess in C# this would look rather similar.

yglodt
  • 13,807
  • 14
  • 91
  • 127