@IdClass Produces 'Identifier of an Instance was Altered' with JPA and Hibernate

15,295

The reason for this error is due to changing the entity identifier of a managed entity.

During the life-time of a PersistenceContext, there can be one and only one managed instance of any given entity. For this, you can't change an existing managed entity identifier.

In you example, even if you start a new transaction, you must remember that the PersistenContext has not been closed, so you still have a managed c1 entity attached to the Hibernate Session.

When you try to find the Company:

c1 = em.find (Company.class, new Company.Identity("ACURA"));

The identifier doesn't match the one for the Company that's being attached to the current Session, so a query is issued:

Hibernate: select company0_.name as name1_0_0_ from Company company0_ where company0_.name=?

Because SQL is CASE INSENSITIVE, you will practically select the same database row as the current managed Company entity (the persisted c1).

But you can have only one managed entity for the same database row, so Hibernate will reuse the managed entity instance, but it will update the identifier to:

new Company.Identity("ACURA");

You can check this assumptions with the following test:

String oldId = c1.name;
Company c2 = em.find (Company.class, new Company.Identity("ACURA"));
assertSame(c1, c2);
assertFalse(oldId.equals(c2.name));

When the second transaction is committed, the flush will try to update the entity identifier (which changed from 'Acura' to 'ACURA') and so the DefaultFlushEntityEventListener.checkId() method will fail.

According to teh JavaDoc, this check is for:

make(ing) sure (the) user didn't mangle the id

To fix it, you need to remove this find method call:

c1 = em.find (Company.class, new Company.Identity("ACURA"));

You can check that c1 is already attached:

assertTrue(em.contains(c1));
Share:
15,295

Related videos on Youtube

GoZoner
Author by

GoZoner

My fear is that I'll look back upon this time as the best time of my life.

Updated on September 18, 2022

Comments

  • GoZoner
    GoZoner over 1 year

    For a JPA entity model using a case-insensitive database schema, when I use a @IdClass annotation I consistently get 'identifier of an instance was altered' exception. For an object with a 'string' primary key, the error occurs when an string of one case exists in the database and a query is performed with the same string differing only in case.

    I've looked at other SO answers and they are of the form: a) don't modify the primary key (I'm not) and b) your equals()/hashCode() implementations are flawed. For 'b' I've tried using toLowerCase() and equalsIgnoringCase() but to no avail. [Additionally, it seems the Hibernate code is directly setting properties, rather than calling property setters when the 'altering' occurs.]

    Here is the specific error:

    Caused by: javax.persistence.PersistenceException: org.hibernate.HibernateException: 
    identifier of an instance of db.Company was altered 
     from {Company.Identity [62109154] ACURA}
       to {Company.Identity [63094242] Acura}
    

    Q: For a case insensitive DB containing a company 'Acura' (as primary key), using @IdClass how do I subsequently find other capitalizations?

    Here is the offending code (starting with an empty database):

    public class Main {    
        public static void main(String[] args) {
            EntityManagerFactory emf =
                    Persistence.createEntityManagerFactory("mobile.mysql");
            EntityManager em = emf.createEntityManager();
    
            em.getTransaction().begin();
    
            Company c1 = new Company ("Acura");
            em.persist(c1);
    
            em.getTransaction().commit();
            em.getTransaction().begin();
    
            c1 = em.find (Company.class, new Company.Identity("ACURA"));
    
            em.getTransaction().commit();
            em.close();
            System.exit (0);    
        }
    }
    

    and here is the 'db.Company' implementation:

    @Entity
    @IdClass(Company.Identity.class)
    public class Company implements Serializable {
    
        @Id
        protected String name;
    
        public Company(String name) {
            this.name = name;
        }
    
        public Company() { }
    
        @Override
        public int hashCode () {
            return name.hashCode();
        }
    
        @Override
        public boolean equals (Object that) {
            return this == that ||
                    (that instanceof Company &&
                            this.name.equals(((Company) that).name));}
    
        @Override
        public String toString () {
            return "{Company@" + hashCode() + " " + name + "}";
        }
    
        //
    
        public static class Identity implements Serializable {
            protected String name;
    
            public Identity(String name) {
                this.name = name;
            }
    
            public Identity() { }
    
            @Override
            public int hashCode () {
                return name.hashCode();
            }
    
            @Override
            public boolean equals (Object that) {
                return this == that ||
                        (that instanceof Identity &&
                            this.name.equals(((Identity)that).name));
            }
    
            @Override
            public String toString () {
                return "{Company.Identity [" + hashCode() + "] " + name + "}";
            }
        }
    }
    

    Note: I know using @IdClass isn't needed when there is a single primary key; the above is the simplest example of the problem.

    As I said, I believe this problem persists even when the hashCode()/equals() methods are made case insensitive; however, suggestions taken.

    ...
    INFO: HHH000232: Schema update complete
    Hibernate: insert into Company (name) values (?)
    Hibernate: select company0_.name as name1_0_0_ from Company company0_ where company0_.name=?
    Exception in thread "main" javax.persistence.RollbackException: Error while committing the transaction
        at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:94)
        at com.lambdaspace.Main.main(Main.java:24)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:483)
        at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
    Caused by: javax.persistence.PersistenceException: org.hibernate.HibernateException: identifier of an instance of db.Company was altered from {Company.Identity [62109154] ACURA} to {Company.Identity [63094242] Acura}
        at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1763)
        at org.hibernate.jpa.spi.AbstractEntityManagerImpl.convert(AbstractEntityManagerImpl.java:1677)
        at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:82)
        ... 6 more
    Caused by: org.hibernate.HibernateException: identifier of an instance of db.Company was altered from {Company.Identity [62109154] ACURA} to {Company.Identity [63094242] Acura}
        at org.hibernate.event.internal.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:80)
        at org.hibernate.event.internal.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:192)
        at org.hibernate.event.internal.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:152)
        at org.hibernate.event.internal.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:231)
        at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:102)
        at org.hibernate.event.internal.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:55)
        at org.hibernate.internal.SessionImpl.flush(SessionImpl.java:1222)
        at org.hibernate.internal.SessionImpl.managedFlush(SessionImpl.java:425)
        at org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction.beforeTransactionCommit(JdbcTransaction.java:101)
        at org.hibernate.engine.transaction.spi.AbstractTransactionImpl.commit(AbstractTransactionImpl.java:177)
        at org.hibernate.jpa.internal.TransactionImpl.commit(TransactionImpl.java:77)
        ... 6 more