4

I have the following statements:

int newId = db.OnlinePayments.Max(op => op.InvoiceNumber) + 1;
opi.InvoiceNumber = newId;
await db.SaveChangesAsync();

The InvoiceNumber column should be unique but this approach is dangerous because from the time I get the value for newId, another record could be added to the table. I read that locking the table would fix this but I'm not sure how I should achieve this with Entity Framework.

Also, I thought maybe doing this in a Transaction would be enough but someone said that it's not.

Update

Suppose this is the table definition in Entity Framework.

public class OnlinePaymentInfo
{
    public OnlinePaymentInfo()
    {
        Orders = new List<Order>();
    }

    [Key]
    public int Id { get; set; }

    public int InvoiceNumber { get; set; }

    public int Test { get; set; }
    //..rest of the table
}

Now, obviously Id is the primary key of the table. And I can mark InvoiceNumber as Idenity with this:

    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int InvoiceNumber { get; set; }

Now, this will work if I do something like this:

var op = new OnlinePayment { Test = 1234 };
db.OnlinePayments.Add(op);
await db.SaveChangesAsync();

And it will automatically generate a unique number for InvoiceNumber. But I need this number to change whenever I change the record. For instance:

var op = await db.OnlinePayments.FindAsync(2);
op.Test = 234243;
//Do something to invalidate/change InvoideNumber here
db.SaveChangesAsync();

I tried to set it to 0 but when I try to assign a value to it, I get an error indicating that I cannot assign to an Identity column. That's the reason I tried to do this manually.

Community
  • 1
  • 1
Alireza Noori
  • 14,961
  • 30
  • 95
  • 179
  • Correct me if I'm wrong but the actual VALUE of the identifier is irrelevant, only that you have the correct identifier; correct? You needn't worry about the ID used in the OnlinePayments table, as the db will make sure it is unique (you get the next ID in line). From that point on, you can be sure that identifier is unique, and it can be used by any other objects (e.g. the `opi.Invoicenumber`) to refer to it. – Flater Apr 13 '15 at 11:38
  • @Flater Yes, I only need a unique value. The actual value is not important. My table already has an `Id` which is marked as `Identity` and I know I can set the `InvoiceNumber` as `Identity` too but the problem is, I can't edit an `Identity` column. However I need to get a new **unique** value for this column whenever I modify a record. – Alireza Noori Apr 13 '15 at 11:50
  • but the number you enter into it, ID of the online payment, is by definition unique. Foreign keys do not require to be unique in their table, they only require that the ID they have refers to a single online payment (which you guaranted by making their ID an identity). E.g. if I have a country in my table Countries with ID 1, there can be 5 person objects with `person.CountryId = 1`. – Flater Apr 13 '15 at 12:01
  • @Flater I know how they work but for some reason I need it to change but still be unique whenever I modify the record. It's very complicated. – Alireza Noori Apr 13 '15 at 12:46
  • Don't know how I missed this before, must've glossed over it, sorry. You shouldn't guess the ID by doing +1. You should make an actual entry in the table, causing your database to assign it an ID. That ID, **once created by the database** will be unique. From then on out, you can simply used that ID as it will never be used by another entry (not even if you delete your entry from the table!). If you do it like that, you don't need to worry about other people also working in that table. The DB will make sure each of you gets a unique ID when you need it. – Flater Apr 13 '15 at 12:50
  • @Flater yes, exactly. But suppose there's an entry in the DB with the `Id` of `1` and the `InvoiceNumber` of `3`. When I modify this record, I need the `InvoiceNumber` to change to another unique number. I cannot use `3` anymore. It should be something like `4` or any number. It should only be a number and it should be unique – Alireza Noori Apr 13 '15 at 12:55
  • Let me add an answer. I think you're misinterpreting how IDs get assigned. – Flater Apr 13 '15 at 12:58
  • Does InvoiceNumber absolutely have to be an int? could it be a `Guid`, for example? Also, if you explained *why* InvoiceNumber has to change every time the invoice changes, that might help - it doesn't make any sense why an invoice number would change (in fact in many cases it's not allowed to do so once the invoice is issued for audit reasons if a 3rd party is using that InvoiceNumber as a reference). Are you using InvoiceNumber as some kind of version number or something? – Stephen Byrne Apr 16 '15 at 08:21
  • In SQL Server 2012+ use `SEQUENCE`, in previous versions emulate it with a dedicated table with `IDENTITY`. Ah, I see that @Giorgi Nakeuri has provided this answer already. The main idea is that whenever you need a new unique number for your Invoice ask the database. – Vladimir Baranov Apr 17 '15 at 01:45

6 Answers6

3

If your mission is just to keep invoice numbers unique, you can use rowversion or uniqueidentifier types for this, but I agree, this is not readable. One approach would be having saparate table like:

Create Table InvoiceNumbers(ID bigint identity(1, 1))

Then you can create stored procedure that will insert one row to this table and return inserted identity like:

Create Procedure spGenerateInvoiceNumber
@ID bigint output
As
Bebin
    Insert into InvoiceNumbers default values
    Set @ID = scope_identity()
End

Then everywhere you need to generate new number just call that proc and get next unique value for invoice number. Notice that you can get gaps with this approach.

Here is an example of how to call stored proc with code first approach and get output parameter EF4.1 Code First: Stored Procedure with output parameter

If you have version of Sql Server 2012+ then you can use new feature Sequence objects instead of creating new table for generating identity values.

Community
  • 1
  • 1
Giorgi Nakeuri
  • 35,155
  • 8
  • 47
  • 75
2

And it will automatically generate a unique number for InvoiceNumber. But I need this number to change whenever I change the record.

This functionality is in SQL Server provided by a rowversion (timestamp) column.

Just change the type of your table column to rowversion / timestamp. The rowversion column certainly cannot be used as a primary key.

With rowversion column you do not need to worry about table locking or transactions, its unique value is automatically maintained by SQL Server.

If you have a reason not to use timestamps, you can make use of SQL Server sequences combined with triggers:

CREATE SEQUENCE dbo.InvoiceNumberSeq
AS int
START WITH 1
INCREMENT BY 1 ;

GO

CREATE TRIGGER dbo.SetInvoiceNumber
ON dbo.OnlinePayments
AFTER INSERT, UPDATE 
AS
BEGIN
        UPDATE t
            SET t.InvoiceNumber = NEXT VALUE FOR dbo.InvoiceNumberSeq
            FROM dbo.OnlinePayments AS t 
            INNER JOIN inserted AS i ON t.ID = i.ID;
END
GO

If possible it is better to handle identity or timestamp inserts/updates within SQL database using built-in features as much as possible.

See SQL Fiddle.

Vojtěch Dohnal
  • 7,867
  • 3
  • 43
  • 105
2

One option you could do this is by creating relation to

public class OnlinePaymentInfo
{
    public OnlinePaymentInfo()
    {
        Orders = new List<Order>();
    }

    [Key]
    public int Id { get; set; }

    [Key, ForeignKey("InvoiceNumber")]
    public InvoiceNumber InvoiceNumber { get; set; }

    public int Test { get; set; }
    //..rest of the table
}

public class InvoiceNumber
{
    public InvoiceNumber()
    {
    }

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
}

And when you are saving the OnlinePaymentInfo, you would do

paymentInfo.InvoiceNumber = new InvoiceNumber();

And then EF would create a new identity for the InvoiceNumber and that would be unique.

ps. Not entirely sure about the syntax on code first.

Janne Matikainen
  • 5,061
  • 15
  • 21
  • This would work but how about the previous records in the database? Also how should I remove the redundant records? – Alireza Noori Apr 16 '15 at 09:45
  • You mean redundant records as in the InvoiceNumbers no longer in use? Do they really matter? Your OnlinePaymentInfo's will not have any redundancy, if you get the paymentInfo from the context rather than creating new one? – Janne Matikainen Apr 16 '15 at 10:01
  • They are not *that* important. But I'd rather don't have any redundant data on my db. – Alireza Noori Apr 16 '15 at 17:28
  • Well you could add the line db.InvoiceNumbers.Remove(paymentInfo.InvoiceNumber); before assigning the new one, thus deleting the old one when saving the changes. – Janne Matikainen Apr 17 '15 at 06:26
  • Thanks. This seems to be the best and logical way to achieve what I want, – Alireza Noori Apr 18 '15 at 14:46
1

(This answer is in relation to the comment thread above)

When adding an object to a table, it will get assigned an identity.

Person myPerson = new Person() { Name = "Flater" };

//at this point, myPerson.Id is 0 because it was not yet assigned a different value

db.People.Add(myPerson);
db.SaveChanges();

//at this point, myPerson.Id is assigned a value because it was saved!
int assignedIdentity = myPerson.Id;

Note that you do not know what the ID is until the db returns it to you. The database decided what the ID is at the time of creation.

If you preemptively check what the currently highest ID is in the table, and then do something like int myNewId = tableMaxId + 1; you will run into problem if someone else was already saving objects in the table while your code is still doing its preemptive guesses.

Never, ever guess what the ID will be. Always save the object to the database, so the database can make sure the assigned ID will never be assigned to someone else.

Note: If at this point in time, you need the ID but it might still be possible that you need to cancel the transation, have a fallback scenario where you delete the proviously created object.

Something like this:

 int the_id_that_was_assigned = 0; //currently not known yet!

try
{
        Person myPerson = new Person() { Name = "Flater" };

        db.People.Add(myPerson);
        db.SaveChanges();

        the_id_that_was_assigned = myPerson.Id; //now it is known because the object was saved.

        //do whatever processing you need to do based on that ID
        //If you want to cancel, you can throw an exception or delete the object manually
        //If you do not want to cancel, then the Person you just created is excatly what you want.
}
catch(Exception ex)
{
        if(the_id_that_was_assigned > 0)
        {
            db.People.Delete(the_id_that_was_assigned);
        }
}

This is just a quick and dirty example. I hope you get the gist of it, you need to actually create the object in order to know which identity it was assigned. Guessing the ID will lead to possible failure in cases where different applications or threads are saving object in the same database table.

Flater
  • 12,908
  • 4
  • 39
  • 62
  • Thanks for the answer. This will work **UNTIL** I need to change the ID. Please read the update above. – Alireza Noori Apr 13 '15 at 13:16
  • Why would you want to change the ID? That seems to go against the main point of an ID, being that it is an IDENTITY, and its value is irrelevant, only the consistency in its usage is what matters. Can you explain why you need to change the ID? – Flater Apr 13 '15 at 13:20
  • No, I don't want to change the `Id`. I want to change the `InvoiceNumber`. I send this number to bank and the bank needs a unique number for each payment. – Alireza Noori Apr 13 '15 at 13:26
  • You should never use your identities for anything other than PK/FK. The bank has no knowledge of your database, and they shouldn't. I'm also not sure with how unique this needs to be for the bank (unique for YOUR invoices? Unique for all of the bank's transactions?). Do not use your internal Primary Keys as data to send externally. If you ever recreate the database, identity generation will begin back at 1. This is not a good idea and should thusly be completely prevented. – Flater Apr 13 '15 at 13:30
  • In short, you're trying to use database-specific features in a way they were never intended. A foreign key (InvoiceNumber), **by definition** must reference an existing primary key in the related table. Changing it to a value of your liking is impossible and violates the main constraint and inherent nature of foreign keys. – Flater Apr 13 '15 at 13:31
  • I don't need to use the DB-specific feature (meaning Identity) I just need to generate a unique number of the `InvoiceNumber`. I don't suggest using `Identity` as I don't think it's gonna work – Alireza Noori Apr 13 '15 at 13:37
1

I would recommend you dont try and reinvent the wheel. The DB has good features that EF exposes. SQL rowversion

Properties EF Code annotation timestamp

 [Timestamp]  
 public virtual byte[] RowVersion { get; set; }

you can then use an optimistic locking approach.

If you keen to have you own locking control. Use a DB feature and call it. SQL server application level locks

Another option is just use a second table

  Public class IdPool{
    int Id {get;set}
    string bla {get; set;}
  }

And just get the next row from table This provides the Id only once and is easily built.

phil soady
  • 11,043
  • 5
  • 50
  • 95
  • My only issue with this is that I need a number. I can't really use timestamp :( – Alireza Noori Apr 16 '15 at 17:24
  • then convert the byte array to long integer. The rowversion is always 8 bytes in sql server. see https://msdn.microsoft.com/en-us/library/system.bitconverter.toint64.aspx how to. – phil soady Apr 17 '15 at 07:50
  • This could be a pretty neat solution. Let me try it and get back to you. – Alireza Noori Apr 18 '15 at 07:09
  • This works but only a few quick questions: 1. Is this number unique across all the records? I wouldn't want to have multiple numbers. 2. When I converted the number to `long` it was a really big number. Is there a way to get this sequential? (meaning starting from 1 or sth). 3. The number I'm looking for is a 32-bit integer. Is there a way to convert this number to a smaller 32 bit int? Currently the value I'm getting is very large and I can't convert it to int via casting. – Alireza Noori Apr 18 '15 at 11:56
  • Yes the rowversion is unique for the database. Its a Long 8 byte integer, not a 4 byte integer. If you need Int32 4 byte, then just create a simple table with int key and set it as DB generated and just add a row everytime you need a new int. – phil soady Apr 18 '15 at 14:13
  • In that case it seems like @JanneMatikainen's answer is the way to go. – Alireza Noori Apr 18 '15 at 14:47
  • yes that is the same as IdPool. A table that is used to allocate integers. That is good solution. – phil soady Apr 19 '15 at 09:17
  • What I meant was their solution is good. So you selected well. All Good Alireza. – phil soady Apr 20 '15 at 07:33
1

You could solve this with a stored proc:

First create a table of named sequences:

CREATE TABLE [dbo].[NamedSequence](
    [SequenceName] [nchar](10) NOT NULL,
    [NextValue] [int] NOT NULL,
 CONSTRAINT [PK_NamedSequence] PRIMARY KEY CLUSTERED 
(
    [SequenceName] ASC
)

Then seed the data (you do this one time ever)

insert into NamedSequence values ('INVOICE_NO', 1)

Then this proc:

create procedure GetNextValue 
    @SequenceName varchar(10),
    @SequenceValue int out
as
update NamedSequence 
    set @SequenceValue = NextValue, NextValue = NextValue + 1 
    where SequenceName = @SequenceName
go

Because you are setting the return value in the same statement as in the update, it will be atomic -- you will never get duplicates. If you have other cases outside of Invoice number where you need sequences, you just add a row with a new key and start value to the table.

JMarsch
  • 21,484
  • 15
  • 77
  • 125