0

I have couple insert queries which are merged in transaction. First of that insert is to create new product articel number incrementing the most higher in table by one. Unfortunetly i just noticed that mostly during tests if for instance two users from two diffrent applications click button which trigger my transaction's method they could get same new product number. How can avoid that situation? Is there something like lock on first insertion so that if first user accessing table to insert restrict other's user/s about their insertion so they have to wait in queue after first user insert is finished? Is there something like that? Besides i thought if someone inserts other users are not able to insert. I made comments in code you to understand.

Part of my transaction query below:

Public Sub ProcessArticle(ByRef artikel As ArticlesVariations)
        Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(System.String)).ToString()
        Using connection As New SqlConnection(strcon)
            connection.Open()
            Using transaction = connection.BeginTransaction()
                Try

                    For Each kvp As KeyValuePair(Of Integer, Artikel) In artikel.collection
                        articleIndex = kvp.Key
                        Dim art As Artikel = kvp.Value

                            Using cmd As New SqlCommand("INSERT INTO tbArtikel (Nummer) VALUES (@Nummer);Select Scope_Identity()", transaction.Connection)
                                cmd.CommandType = CommandType.Text
                                cmd.Connection = connection
                                cmd.Transaction = transaction

                                 'Get next product number from table tbArtikel  (this will be new product number)'
                                 Dim NewArtNummer as String =  New DALArtikel().GetNewArtikelNumber(transaction)
                                 art.Nummer = NewArtNummer

                                cmd.Parameters.AddWithValue("@Nummer", art.Nummer)
'Get inserted product id for other diffrent inserts below'
 newArticleRowId = CInt(cmd.ExecuteScalar()) 

'....
other INSERTs queries to other tables ...
...'

       transaction.Commit()
                Catch ex As Exception
                    transaction.Rollback()
                    Throw 'Rethrow exception.'
                End Try
            End Using
        End Using
    End Sub
Juan Carlos Oropeza
  • 47,252
  • 12
  • 78
  • 118
  • 1
    Assigning some sort of unique value for these things is not a lot different than an AutoIncrement PK. You need to work out a way to assign the value at the moment the row is inserted. Since it is apparently a string with 3 values, maybe a stored procedure. Especially in a loop which is also a Transaction you will have plenty of dupes – Ňɏssa Pøngjǣrdenlarp Sep 21 '16 at 18:16
  • @Plutonix hi Plutonix, is there any way to do it from the method i shown? –  Sep 21 '16 at 18:38
  • 1
    I have no idea what `DALArtikel().GetNewArtikelNumber(transaction)` does but presumably it looks at the rows in the DB doing some sort of Max()+1. IF another user is doing the same thing, he/she will get the same values. – Ňɏssa Pøngjǣrdenlarp Sep 21 '16 at 18:48
  • @Plutonix There is more behind that but i can say - yes - this is the method which is doing something like take max from article table and increment it so i will get new number. P.S As i thought its not about insert statment itself but because NewArtNummer process right? –  Sep 21 '16 at 18:51
  • 1
    Since it is in a transaction all those calculations are tentative until you commit - gives it more time for others users to do the same thing and get the same numbers. – Ňɏssa Pøngjǣrdenlarp Sep 21 '16 at 18:54

3 Answers3

2

Just about the only way to assure that users are not assigned the same values is to issue them from the server when the row is inserted. It is the entire premise behind the server issuing AI values for PKs.

BUT since your thing is a multi-segment, "numeric string" that presents a problem. Rather than tearing the string apart to find the Max()+1 for one segment with a WHERE clause on parts of the string. Consider something like this:

Start with a table used to increment and issue the values:

 {DocId Int, SegmentB int, SegmentC Int}

This will simply track the values to use in the other table. Then a stored procedure to create/increment a new code (MySQL - this is a conceptual answer):

CREATE DEFINER=`root`@`localhost` PROCEDURE `GetNextProductCode`(in docId int,
        in Minr int, 
        in Rev int 
        )
BEGIN
    SET @maxR = 0;
    SET @retCode ='';

    if Minr =-1 then
        Start transaction;
        SET @maxR = (SELECT Max(SegmentB) FROM articlecode WHERE MainId = docId) + 1;
        UPDATE articlecode SET SegmentB = @maxR WHERE MainId = docId;
        Commit;
        Select concat(Cast(docId As char) , '.', 
                      Cast(@maxR AS char) , '.',
                      Cast(Rev As char)
                      );
    end if;
END

This is a rough idea of the process. As such, it only works on the second segment (I dunno what happens when you create a NEW SegmentB - does SegmentC reset to 1???). The idea is:

  • pass numbers so there is no need to tear up a string
  • pass -1 for the segment you need the next value for
  • the sp gets the Max()+1 and updates the counter table so the next user will get a new value
  • If for some reason you end up not saving the row, there will be gaps
  • the sp uses a transaction (probably only needs to protect the update) so that only 1 update can happen at a time
  • returns the new code. it could just return 2 values, but your going to glue them together anyway

There is much To Do:

  • It only does SegmentB
  • For a NEW DocId (-1), insert a new row with 1000 and 1(?) defaults
  • Same for a NEW segmentB (whatever it is): insert a new row for that DocId with default values

To get a new code before you insert a row:

cmd.CommandType = CommandType.StoredProcedure

cmd.Parameters.Add("docId", MySqlDbType.Int32).Value = 3
cmd.Parameters.Add("Minr", MySqlDbType.Int32).Value = -1
cmd.Parameters.Add("Rev", MySqlDbType.Int32).Value = 1

dbcon.Open()
Using rdr = cmd.ExecuteReader()
    rdr.Read()
    Console.WriteLine(rdr(0))
End Using

The obvious downside is that each insert requires you to hit the DB in order to...well save to the DB. If they were int values it could be a Trigger.

Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
0

I'm a SQL developer and my VB skills are about fifteen years out of date, but instead of creating the incremented number yourself in VB just let SQL generate them with an IDENTITY field. SQL will never allow duplicates and then you just need to return the SCOPE_IDENTITY():

ALTER TABLE dbo.tbArtikel
ADD [ArtikelID] INT IDENTITY(1,1) PRIMARY KEY;
Russell Fox
  • 5,273
  • 1
  • 24
  • 28
  • Fox the product number is just varchar field... ( i know its bad etc. but how it is) –  Sep 21 '16 at 18:48
  • 1
    Ouch. Possibly the same solution, though: just have one IDENTITY field, get that value, CAST it into a VARCHAR, and store it in the product number field. Not ideal, but much safer in a multi-user environment like that. – Russell Fox Sep 21 '16 at 19:13
  • unfortunetly i cannot do this way in current implementation, is there any lcok i could deploy to my transactionor something similar? –  Sep 21 '16 at 19:19
  • 1
    I don't see how since each user is running in their own transaction. Maybe you can get creative with GUIDs? At least .Net will usually generate a unique GUID, but I'm not sure how you'd work that into your existing structure. – Russell Fox Sep 21 '16 at 19:31
  • In SQL you can use the TABLOCK table hint to keep a lock on that table which prevents others from using it (see http://stackoverflow.com/questions/3662766/sql-server-how-to-lock-a-table-until-a-stored-procedure-finishes). I just don't know how .net processes SQL transactions, sorry. – Russell Fox Sep 21 '16 at 21:27
0

I have two suggestions:

First suggestion: move your code to a stored procedure this way all your users will execute the same transaction where you can set your isolation level the way you want. Read This.

Second suggestion: I would create a unique index on your field Nummer. This way when I try to insert a duplicate value it will raise an error that I can deal with it by telling the user that he need to retry the same operation or retry it automatically.

Trying to lock the record or the table for your operation is not advisable, however you can check this article on code project you might find what you are looking for. Make sure that you provide a mechanism of releasing all locks if your program stops at the middle of the transaction.

  • Two questions: 1st: Can i make unique index for field that is type of varchar? 2nd: Having stored procedure - even if multiple users call it - does it mean they will be all working on same transaction and not own transaction for each user? How its possible? –  Sep 21 '16 at 20:53
  • 1st: Yes you can create a unique index on a varchar column provided that there are no duplicate or null values in the table. 2nd: I believe so, you can set the transaction isolation level using SET Transaction Isolation Level to the setting most suitable for you. Please Read the article on my first suggestion for detailed description of different isolation levels. – Sulieman Mansouri Sep 21 '16 at 21:24