Enterprise Java Development@TOPIC@

Chapter 48. Primary Keys

48.1. Generated Simple Primary Keys
48.1.1. GenerationType.AUTO
48.1.2. GenerationType.IDENTITY
48.1.3. GenerationType.SEQUENCE
48.1.4. GenerationType.TABLE




GeneratedValue must be generated by provider

If an entity is defined to have its primary key automatically generated -- no matter the strategy -- the provider will insist on implementing the value. It is always an error to pass a detached/transient object to persist() with an id already assigned.

Don't default GenerationStrategy

One should always identify what the GenerationStrategy should be so that the strategy remains consistent over time. For example, Hibernate use to default to a form of IDENTITY in JavaSE environments and SEQUENCE in JavaEE environments.



Figure 48.9. IDENTITY Test (with Active Transaction)

  • Test (with Transaction Active)

    ejava.examples.orm.core.annotated.Gadget gadget = new Gadget(0);
    
    gadget.setMake("gizmo 1");
    //insert a row in the database
    //start with a tx already active
    logger.info("gadget (before persist; tx={}): {}", txActive(), gadget);
    em.persist(gadget);
    logger.info("created gadget (after persist, before flush; tx={}): {}", txActive(), gadget);
    em.flush(); 
    logger.info("created gadget (after flush; tx={}): {}", txActive(), gadget);            
    assertNotEquals(0, gadget.getId());     
  • Output (with Transaction Active)

    -gadget (before persist; tx=true): 1798443618, id=0, make=gizmo 1
    
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 1]
    -created gadget (after persist, before flush; tx=true): 1798443618, id=1, make=gizmo 1
    
    -created gadget (after flush; tx=true): 1798443618, id=1, make=gizmo 1
    
  • Follow-on IDENTITY Allocations (with Transaction Active)

    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 2]
    -created gadget(tx=true): 370055648, id=2, make=gizmo 2
    
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 3]
    -created gadget(tx=true): 911933063, id=3, make=gizmo 3
    
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 4]
    -created gadget(tx=true): 568613487, id=4, make=gizmo 4
    
    • Provider inserts row into database with null ID

    • Provider queries database (not shown in debug) for primary key generated for the row

    • Provider eagerly inserts row during persist() and before flush() when transaction is open -- to determine primary key

Using IDENTITY doubles calls to database

When using the IDENTITY strategy, the provider must make at least two calls to the database. One for the INSERT and one to determine the primary key value generated by the database.


Figure 48.10. IDENTITY Test (persist() with Inactive Transaction)

  • Test

    em.getTransaction().rollback();
    
    logger.info("rolled back tx(tx={})", txActive());
    for (int i=0; i<3; i++) {
        Gadget g = new Gadget();
        g.setMake("gizmo " + counter++);
        em.persist(g);
        logger.info("created gadget(tx={}): {}", txActive(), g);
        if (i==0) {
            gadget=g;
        }
    }
    logger.info("starting tx(tx={}): {}", txActive(), gadget);
    em.getTransaction().begin();
    logger.info("tx started, flushing (tx={}): {}", txActive(), gadget);
    em.flush();
    logger.info("cache flushed (tx={}): {}", txActive(), gadget);
    em.getTransaction().commit();
    logger.info("tx committed (tx={}): {}", txActive(), gadget);
  • Output (persist() with inactive transaction)

    -rolled back tx(tx=false)
    
    
    -created gadget(tx=false): 1372646511, id=0, make=gizmo 5
    -created gadget(tx=false): 1202178366, id=0, make=gizmo 6
    -created gadget(tx=false): 1872410525, id=0, make=gizmo 7
    -starting tx(tx=false): 1372646511, id=0, make=gizmo 5
    • Provider could not insert rows or determine primary key while transaction inactive

    • Business logic could not rely on PK values being set (e.g., send event or log)

  • Output (active transaction following persist)

    -tx started, flushing (tx=true): 1372646511, id=0, make=gizmo 5
    
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 5]
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 6]
    -insert into ORMCORE_GADGET (id, make) values (null, ?)
    -binding parameter [1] as [VARCHAR] - [gizmo 7]
    -cache flushed (tx=true): 1372646511, id=5, make=gizmo 5
    -tx committed (tx=false): 1372646511, id=5, make=gizmo 5
    • Provider waited for next flush() cycle (not tx start) to perform INSERTs and determine primary key




Figure 48.13. SEQUENCE Test (with active transaction)

  • Test (with active transaction)

    Assume.assumeTrue(Boolean.parseBoolean(System.getProperty("sql.sequences", "true")));
    
    
    ejava.examples.orm.core.annotated.Fan fan = new Fan(0);
    fan.setMake("cool runner 1");
    //insert a row in the database
    logger.info("persisting fan(tx={}): {}", txActive(), fan);
    em.persist(fan);
    logger.info("created fan (before flush, tx={}):", txActive(), fan);
    em.flush(); 
    logger.info("created fan (after flush; tx={}): {}", txActive(), fan);            
    assertNotEquals(0, fan.getId());
  • Output (with active transaction)

    -persisting fan(tx=true): 1413306467, id=0, make=cool runner 1
    -call next value for FAN_SEQ
    -call next value for FAN_SEQ   #Current value=8 after this call, next result will be 11
    
    -created fan (before flush, tx=true): 1413306467, id=5, make=cool runner 1
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 1]
    -binding parameter [2] as [BIGINT] - [5]
    -created fan (after flush; tx=true): 1413306467, id=5, make=cool runner 1
    
    
    -persisting fan(tx=true): 1413306467, id=0, make=cool runner 1
    -call next value for FAN_SEQ
    -call next value for FAN_SEQ
    
    -created fan (before flush, tx=true): 1413306467, id=5, make=cool runner 1
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 1]
    -binding parameter [2] as [BIGINT] - [5]
    -created fan (after flush; tx=true): 1413306467, id=5, make=cool runner 1
    
    • Provider obtains primary key value prior to inserting row (x2 calls addressed later)

    • First primary key value (5) corresponds with the @SequenceGenerator.initialValue property

    • Primary key value available to business logic prior to insert into database

    • Provider inserts the row during next flush cycle

    • Inspecting database server through UI -- shows current value of 8 (2x calls to next value)


SEQUENCE allows individual primary keys to be allocated in blocks

The database advances its SEQUENCE by the allocationSize on each call to next value. The provider may use the returned value and allacationSize values above the returned value before returning to the database for a new value. The database and provider *must* have the same increment/allocationSize configured.

Figure 48.14. Follow-on SEQUENCE Allocations

  • Output prior to flush/commit

    -created fan(tx=true): 1289462509, id=6, make=cool runner 2
    -created fan(tx=true): 740265405, id=7, make=cool runner 3
    -created fan(tx=true): 1439003682, id=8, make=cool runner 4
    -call next value for FAN_SEQ
    -created fan(tx=true): 578969118, id=9, make=cool runner 5
    -created fan(tx=true): 493310435, id=10, make=cool runner 6
    -created fan(tx=true): 757436159, id=11, make=cool runner 7
    -call next value for FAN_SEQ
    -created fan(tx=true): 1682973478, id=12, make=cool runner 8
    ...
    -created fan(tx=true): 1501844857, id=22, make=cool runner 18
    -created fan(tx=true): 817994751, id=23, make=cool runner 19
    -call next value for FAN_SEQ
    -created fan(tx=true): 1312250810, id=24, make=cool runner 20
    -created fan(tx=true): 1296316112, id=25, make=cool runner 21
    ...
    • Provider calling database to get next allocation prior to exhausting current allocation

    • Provider knows to self-generate next allocationSize values for value returned prior to obtaining next allocation

    • Provider waits for next flush cycle to insert rows into database

  • Output during commit

    -committing (tx=true): 1289462509, id=6, make=cool runner 2
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 2]
    -binding parameter [2] as [BIGINT] - [6]
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 3]
    -binding parameter [2] as [BIGINT] - [7]
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 4]
    ...
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 21]
    -binding parameter [2] as [BIGINT] - [25]
    -tx committed (tx=false): 1289462509, id=6, make=cool runner 2
    
    • Rows inserted into database during next flush cycle triggered by commit

    • Database makes no correlation between sequence numbers returned and primary key value assigned to a row


Increase increment/allocationSize for faster ingest

The default increment/allocationSize requires two (2) calls per insert. A larger increment/allocationSize can reduce the number of calls by up to 50%, but will potentially leave gaps and exhaust the unique sequence values earlier in the lifetime of the database if clients terminate and restart prior to exhausting an allocation.

Figure 48.15. SEQUENCE Test (persist without active transaction)

  • Test

    logger.info("tx(tx={})", txActive());
    
    for (int i=0; i<20; i++) {
        Fan f = new Fan();
        f.setMake("cool runner " + counter++);
        em.persist(f);
        logger.info("created fan(tx={}): {}", txActive(), f);
        if (i==0) {
            fan=f;
        }
    }
    logger.info("starting tx(tx={}): {}", txActive(), fan);
    em.getTransaction().begin();
    logger.info("tx started, flushing (tx={}): {}", txActive(), fan);
    em.flush();
    logger.info("cache flushed (tx={}): {}", txActive(), fan);
    em.getTransaction().commit();
    logger.info("tx committed (tx={}): {}", txActive(), fan);
  • Output while transaction inactive

    -tx(tx=false)
    -created fan(tx=false): 1451387509, id=26, make=cool runner 22
    -call next value for FAN_SEQ
    -created fan(tx=false): 1238209644, id=27, make=cool runner 23
    -created fan(tx=false): 1371953731, id=28, make=cool runner 24
    -created fan(tx=false): 1947060963, id=29, make=cool runner 25
    -call next value for FAN_SEQ
    -created fan(tx=false): 1309934743, id=30, make=cool runner 26
    -created fan(tx=false): 833420622, id=31, make=cool runner 27
    -created fan(tx=false): 1601333072, id=32, make=cool runner 28
    ...
    -call next value for FAN_SEQ
    -created fan(tx=false): 1591063329, id=42, make=cool runner 38
    -created fan(tx=false): 2129344690, id=43, make=cool runner 39
    -created fan(tx=false): 223662325, id=44, make=cool runner 40
    -call next value for FAN_SEQ
    -created fan(tx=false): 1835794313, id=45, make=cool runner 41
    
    • Same as transaction active so far

  • Output when transaction activated

    -starting tx(tx=false): 1451387509, id=26, make=cool runner 22
    
    -tx started, flushing (tx=true): 1451387509, id=26, make=cool runner 22
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 22]
    -binding parameter [2] as [BIGINT] - [26]
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 23]
    -binding parameter [2] as [BIGINT] - [27]
    ...
    -insert into ORMCORE_FAN (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [cool runner 41]
    -binding parameter [2] as [BIGINT] - [45]
    -cache flushed (tx=true): 1451387509, id=26, make=cool runner 22
    -tx committed (tx=false): 1451387509, id=26, make=cool runner 22
    
    • As with previous case -- nothing started with transaction opening

    • All inserts are delayed until next flush cycle




Figure 48.18. TABLE Test

logger.info("testTABLE");

logger.debug("table id before(tx={})={}", txActive(), getTableId());
//note that since PKs are generated, we must pass in an object that
//has not yet been assigned a PK value.
ejava.examples.orm.core.annotated.EggBeater eggbeater = new EggBeater(0);
eggbeater.setMake("done right 1");
//insert a row in the database
logger.info("persisting eggbeater (tx={}): {}", txActive(), eggbeater);
em.persist(eggbeater);
logger.info("created eggbeater (before flush; tx={}): {}", txActive(), eggbeater);
em.flush(); 
logger.info("created eggbeater (after flush; tx={}): {}", txActive(), eggbeater);
assertNotEquals(0, eggbeater.getId());   
logger.debug("table id after(tx={})={}", txActive(), getTableId());
-testTABLE
-select UID_VAL from ORMCORE_EB_UID where UID_ID='ORMCORE_EGGBEATER'
-extracted value ([UID_VAL] : [NUMERIC]) - [7]
-table id before(tx=true)=7

-persisting eggbeater (tx=true): 1049628186, id=0, make=done right 1
-select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
-update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
-select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
-update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?

-created eggbeater (before flush; tx=true): 1049628186, id=8, make=done right 1
-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
-binding parameter [1] as [VARCHAR] - [done right 1]
-binding parameter [2] as [BIGINT] - [8]

-created eggbeater (after flush; tx=true): 1049628186, id=8, make=done right 1
-select UID_VAL from ORMCORE_EB_UID where UID_ID='ORMCORE_EGGBEATER'
-extracted value ([UID_VAL] : [NUMERIC]) - [17]
-table id after(tx=true)=17
  • Provider gets a primary key value during persist

  • Provider locks row (SELECT FOR UPDATE) during transaction and updates with new value

  • Provider inserts row during flush cycle with generated primary key value

  • Cannot explain the initial 2x ID requests but both incremented the value to a result of 17 (7+5+5=17)


Figure 48.19. Follow-on TABLE Allocations

-created ehhbeater(tx=true): 1275580924, id=9, make=done right 2
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 2]
	-binding parameter [2] as [BIGINT] - [9]
-table id after[2](tx=true)=17

-created ehhbeater(tx=true): 1726759945, id=10, make=done right 3
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 3]
	-binding parameter [2] as [BIGINT] - [10]
-table id after[3](tx=true)=17
...
-created ehhbeater(tx=true): 154468798, id=13, make=done right 6
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 6]
	-binding parameter [2] as [BIGINT] - [13]
-table id after[6](tx=true)=17

	-select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
	-update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
-created ehhbeater(tx=true): 490475818, id=14, make=done right 7
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 7]
	-binding parameter [2] as [BIGINT] - [14]
-table id after[7](tx=true)=22
...
-created ehhbeater(tx=true): 360233196, id=18, make=done right 11
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 11]
	-binding parameter [2] as [BIGINT] - [18]
-table id after[11](tx=true)=22

	-select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
	-update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?	
-created ehhbeater(tx=true): 1912769093, id=19, make=done right 12
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 12]
	-binding parameter [2] as [BIGINT] - [19]
-table id after[12](tx=true)=27
...
-created ehhbeater(tx=true): 1947681232, id=23, make=done right 16
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 16]
	-binding parameter [2] as [BIGINT] - [23]
-table id after[16](tx=true)=27

	-select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
	-update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
-created ehhbeater(tx=true): 783682673, id=24, make=done right 17
	-insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
	-binding parameter [1] as [VARCHAR] - [done right 17]
	-binding parameter [2] as [BIGINT] - [24]
-table id after[17](tx=true)=32
...
-committing (tx=true): 1275580924, id=9, make=done right 2
-tx committed (tx=false): 1275580924, id=9, make=done right 2
...

TABLE Strategy Allows Clients to Self-Generate Groups of PK Values

As will SEQUENCE, the TABLE strategy allows each client to generate an allocationSize amount of primary key values before requiring a flush of the current batch or polling for a new table value.

Figure 48.20. TABLE Test (persist without active transaction)

  • Output prior to transaction

    -tx(tx=false)
    -created ehhbeater(tx=false): 150835665, id=29, make=done right 22
    -table id after[22](tx=false)=37
    -created ehhbeater(tx=false): 315885065, id=30, make=done right 23
    -table id after[23](tx=false)=37
    -created ehhbeater(tx=false): 423539130, id=31, make=done right 24
    -table id after[24](tx=false)=37
    -created ehhbeater(tx=false): 841313896, id=32, make=done right 25
    -table id after[25](tx=false)=37
    -created ehhbeater(tx=false): 1673518027, id=33, make=done right 26
    -table id after[26](tx=false)=37
    
    -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
    -update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
    -created ehhbeater(tx=false): 1042223174, id=34, make=done right 27
    -table id after[27](tx=false)=42
    ...
    -created ehhbeater(tx=false): 584643821, id=38, make=done right 31
    -table id after[31](tx=false)=42
    
    -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
    -update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
    -created ehhbeater(tx=false): 999999316, id=39, make=done right 32
    -table id after[32](tx=false)=47
    ...
    -created ehhbeater(tx=false): 1815337594, id=43, make=done right 36
    -table id after[36](tx=false)=47
    
    -select tbl.UID_VAL from ORMCORE_EB_UID tbl where tbl.UID_ID=? for update
    -update ORMCORE_EB_UID set UID_VAL=?  where UID_VAL=? and UID_ID=?
    -created ehhbeater(tx=false): 362311125, id=44, make=done right 37
    -table id after[37](tx=false)=52
    ...
    -created ehhbeater(tx=false): 1292683326, id=48, make=done right 41
    -table id after[41](tx=false)=52
    
    • Provider obtains next allocation value outside of transaction where rows inserted

    • Next allocation value requires transaction to update -- results in extra transaction per allocation

    • Provider assigns ID to object from allocation prior to transaction where rows are inserted

  • Output once transaction started

    -starting tx(tx=false): 150835665, id=29, make=done right 22
    -tx started, flushing (tx=true): 150835665, id=29, make=done right 22
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 22]
    -binding parameter [2] as [BIGINT] - [29]
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 23]
    -binding parameter [2] as [BIGINT] - [30]
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 24]
    -binding parameter [2] as [BIGINT] - [31]
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 25]
    -binding parameter [2] as [BIGINT] - [32]
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 26]
    -binding parameter [2] as [BIGINT] - [33]
    ...
    -insert into ORMCORE_EGGBEATER (make, id) values (?, ?)
    -binding parameter [1] as [VARCHAR] - [done right 41]
    -binding parameter [2] as [BIGINT] - [48]
    -cache flushed (tx=true): 150835665, id=29, make=done right 22
    -tx committed (tx=false): 150835665, id=29, make=done right 22
    
    • Provider inserts rows from cache during next flush cycle

    • Each row has a pre-assigned ID from provider

    • Database makes no correlation between ID within row and the table maintaining the IDs