8

I have a customer and sales table

CUSTOMER
--------------
Id (int auto increment)
Name

SALES
---------------
Id (int auto increment)
CustomerId (int)
OrderTotal (decimal)

With Guid i can do this.

dbTransaction = dbContext.Database.BeginTransaction(isolationLevel);

var customer = new Customer()
{
  Id = Guid.NewGuid(),
  Name = "John Doe"
};

var sales = new Sales() 
{
  Id = Guid.NewGuid(),
  CustomerId = customer.Id,
  OrderTotal = 500
};

dbContext.SaveChanges();
dbTransaction.Commit();

How can i do this if my primary key is int (with DatabaseGeneratedOption.Identity)?

Reynaldi
  • 1,125
  • 2
  • 19
  • 41
  • This is a big issue for me too - If EF supported nested transactions (AKAIK, it doesn't), then you could SaveChanges() within a sub-TX, get the identity value, and then still have the ability to rollback the whole "parent" TX if necessary. – Jmoney38 Apr 13 '17 at 17:00
  • Possible duplicate of [Entity Framework - retrieve ID before 'SaveChanges' inside a transaction](https://stackoverflow.com/questions/17523568/entity-framework-retrieve-id-before-savechanges-inside-a-transaction) – Felix K. Jan 18 '19 at 18:48
  • This question has a strong code smell. What's the *actual* problem that must be solved here? The idea of an identity column is that generating a key value yourself is about the last thing you should want to do. – Gert Arnold Aug 20 '22 at 20:02

4 Answers4

24

You cannot. The ID that goes into a IDENTITY column is generated by the database upon insertion, and all "tricks" to circumvent that and determine the ID yourself are probably flawed.

Short answer: If you want some say in generating an ID before you save, use a GUID (UNIQUEIDENTIFIER), or a SEQUENCE (if you're working with SQL Server 2012 or newer).

Why you should not compute the next free ID yourself:

Don't even consider running a query such as context.Customers.Max(c => c.Id) + 1 as a viable solution, because there's always the possibility that you have concurrent database accesses: another process or thread might persist a new entity to the same table after you've read the next "free" ID but before you store your entity. Computing the next free ID will be prone to collisions, unless your whole operation of getting the ID, doing something with it, and storing the entity with that ID were atomic. This would likely require a table lock in the DB, which might be inefficient.

(The same problem exists even when you use SEQUENCEs, a new feature introduced in SQL Server 2012.) (I was wrong; see end of answer.)

Possible solutions:

  1. If you need to determine the ID of an object before you save it, then don't use the ID that goes in a IDENTITY column. Stay with a GUID, because you're extremely unlikely to get any collision with these.

  2. There's no need to chose between one or the other: you can actually have your cake and eat it! Nothing stops you from having two ID columns, one that you determine externally (the GUID) and one that stays internal to the DB (the IDENTITY column); see the blog article "CQS vs. server generated IDs" by Mark Seemann for a more detailed look at this idea. Here's the general idea by example:

    CREATE TABLE Foos
    (
        FooId INT IDENTITY NOT NULL PRIMARY KEY CLUSTERED,
     -- ^^^^^ assigned by the DBMS upon insertion. Mostly for DB-internal use.
        Id UNIQUEIDENTIFIER ROWGUIDCOL NOT NULL UNIQUE DEFAULT (NEWID()),
     -- ^^ can be dictated and seen by the users of your DB. Mostly for DB-external use.
        …
    );
    
    CREATE TABLE FooBars
    (
        FooId INT NOT NULL FOREIGN KEY REFERENCES Foos (FooId),
     --   use DB-internal ID in foreign key constraints ^^^^^
        …
    );
    
    CREATE VIEW PublicFoos AS
    SELECT Id, … FROM Foos;
    --     ^^ publish the public ID for users of your DB
    

    (Make sure you adhere to some convention for consistently naming internal and public ID field names.)

  3. SEQUENCEs, a feature introduced in SQL Server 2012, are a possible alternative to having an IDENTITY column. They are automatically increased and you are guaranteed a unique number when getting the next free ID using NEXT VALUE FOR SomeSequence. One of the use cases mentioned on MSDN are:

    Use sequences instead of identity columns in the following scenarios: […] The application requires a number before the insert into the table is made.

    Some caveats:

    • Getting the next sequence value will require an additional roundtrip to the database.

    • Like identity columns, sequences can be reset / re-seeded, so there is the theoretical possibility of ID collisions. Best to never re-seed identity columns and sequences if you can help it.

    • If you fetch the next free sequence value using NEXT VALUE FOR, but then decide not to use it, this will result in a "gap" in your IDs. Gaps obviously cannot happen with regular (non-sequential) GUIDs because there is no inherent ordering to them.

stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
  • But for my case, if i have 2 ids (Guid and int), what should is use for my foreign key in sales table? – Reynaldi Apr 20 '15 at 07:08
  • @Reynaldi: For the foreign key, use the DB-internal `INT IDENTITY` column. That is possible because you need to save the referenced entity before you can store the referencing entity; so at the time when you save the referencing entity, the int ID will have been generated. You can find out what the DB-internal `INT IDENTITY` is by querying the DB using the GUID entity (which you know from the start). Make sure to read Mark Seemann's blog article, it's really quite good at giving you the general idea. – stakx - no longer contributing Apr 20 '15 at 07:10
  • 1
    thanks for your link to mark seemenn post. If i read and understand his post correctly, the int id is only use for human readability. But my concern is if i want to save customer and sales as one transaction, like my examples above. So if i use int id then i should make it into 2 transactions? Save the customer first then create another transaction to save the sales? – Reynaldi Apr 20 '15 at 07:18
  • @Reynaldi: Such a transaction should be possible with plain T-SQL, but I'm not sure you can do it with Entity Framework. In that case you'd have to have two transactions; or use the GUID as a foreign key. – stakx - no longer contributing Apr 20 '15 at 07:24
1

As far as I know you can not get the ID before saving the changes in the database. The database creates the ID after the values are inserted in the database.

To add to it when you call .SaveChanges() then only it will write the changes to the database and only then the identity value will get generated.

Rahul Tripathi
  • 168,305
  • 31
  • 280
  • 331
  • I use unit of work, so it will commit after all data has been processed/saved. Will i get the value even if the data has not been committed? – Reynaldi Apr 20 '15 at 06:38
  • @Reynaldi:- It's not possible to get the Id before SaveChanges() is called. If you are using SQL Server 2012 then there is a feature of SEQUENCE type that can be used instead of an IDENTITY column, so you dont need to implement it yourself. – Rahul Tripathi Apr 20 '15 at 06:41
  • 2
    @RahulTripathi: Fetching the next ID from a sequence will not work reliably if there is the possibility that several processes / threads store stuff into the DB concurrently. For the same reason it would be a bad idea to run a query like `dbContext.Customers.Max(c => c.Id) + 1`. Ideally, fetching the ID, doing something with it, and storing the new entity to the DB should be an atomic operation (one where nothing else can intervene while it is ongoing). – stakx - no longer contributing Apr 20 '15 at 06:51
  • @stakx:- Yes I agree, the idea of Sequence just striked my mind. But didnt thought of it. Thanks for that point.! – Rahul Tripathi Apr 20 '15 at 06:54
  • @Rahul To clarify your answer. This is how i save entity to database. Create entity, SaveChanges(), then call Transaction.Commit(). At what point, the id is generated? SaveChanges() or Transaction.Commit()? – Reynaldi Apr 20 '15 at 06:54
  • @Reynaldi:- When you call the .SaveChanges then the ID is generated! – Rahul Tripathi Apr 20 '15 at 06:56
  • If for example i have 2 users doing the same thing at the same time. user 1 save changes and get id "1" and if user 2 savechanges, will user 2 get id "2" even before user 1 commit the transaction? And what happen if user 1 rollback, and user 2 do commit? – Reynaldi Apr 20 '15 at 07:00
  • @stakx Can you elaborate on your comment re: sequences? Because I've always understood that it's exactly what sequences do -- disregarding the possibility of resetting sequences, of course. – Rytmis Apr 20 '15 at 07:11
  • 1
    @Reynaldi not 100% sure here, but I believe the database would lock the table while inserting. So the process#2 would have to wait until the "SaveChangesWithRollback" or "SaveChangesSuccessfully" were complete. Thus, if process#1 failed, it would probably get id 1 (since the process#1 did not commit anything). – default Apr 20 '15 at 07:14
  • 1
    @Rytmis: Sorry, you are correct. `NEXT VALUE FOR SomeSequence` indeed guarantees all parties to receive distinct values *if* the sequences aren't reset and stuff like that. – stakx - no longer contributing Apr 20 '15 at 07:15
1

You can get that value by a small hack.

Create a function in SQL Server something like this

CREATE FUNCTION fn_getIdentity(@tbl_name varchar(30))
AS
BEGIN

IF @tbl_name = 'Employee_tbl'
   RETURN IDENT_CURRENT('Employee_tbl')
ELSE IF @tbl_name = 'Department_tbl'
   RETURN IDENT_CURRENT('Department_tbl')
ELSE
   RETURN NULL
END

Create an entity in your Entity framework to support this function and use it wherever you want.

Then use

var nextValue = dbContext.fn_getIdentity("Employee_tbl")

IDENT_CURRENT returns you last incremented value for an identity column. This doesn't mean MAX + 1 as if your previous transaction generated an identity value for this column but was rolled back then you will see next value that will be generated.

Please note, I didn't check syntax properly and this syntax is just to present an idea.

However I would go with solution provided by Stakx i.e. SEQUENCE if using SQL Server 2012 or above else creating a table to implement functionality of SEQUENCE by reserving ID once generated permanently in a table.

Shantanu Gupta
  • 20,688
  • 54
  • 182
  • 286
0

We can indeed if your ID is in integer, using SQL. The following example is for PostreSQL, please feel free to adapt it for other servers and to edit this answer.

  1. Create a virtual entity model for the database to wrap our query result and to have a fake DbSet<some virtual model> to use ef core extension method FromSqlRaw.

Define a virtual model:

public class IntReturn
{
    public int Value { get; set; }
}

Now fake a DbSet<IntReturn> it will not be really created on server:

protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            ...

            modelBuilder.Entity<IntReturn>().HasNoKey();

            base.OnModelCreating(modelBuilder);
        }
  1. Now we can get the currently used Id for Customers table in this example. The calling method is inside a Subclassed : DbContext, you'd what to instantiate your context to use it instead of this:

     public int GetNextCustomerId()
             {
    
                 //gets current id, need do +1 to get the next one 
                 var sql = "Select last_value as Value FROM \"Customers_Id_seq\";";
    
                 var i = this.Set<IntReturn>()
                 .FromSqlRaw(sql)
                 .AsEnumerable()
                 .First().Value + 1;
    
                 return i;
             }
    

Credits to: https://erikej.github.io/efcore/2020/05/26/ef-core-fromsql-scalar.html https://stackoverflow.com/a/18233089/7149454

Nick Kovalsky
  • 5,378
  • 2
  • 23
  • 50