Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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
like image 468
GoZoner Avatar asked Jan 30 '15 08:01

GoZoner


People also ask

What is identifier in Hibernate?

Identifiers in Hibernate represent the primary key of an entity. This implies the values are unique so that they can identify a specific entity, that they aren't null and that they won't be modified.

How to define primary key in Hibernate?

Mapping a primary key column with JPA and Hibernate is simple. You just need to add an attribute to your entity, make sure that its type and name match the database column, annotate it with @Column and you're done.

How SEQUENCE generator works in Hibernate?

IDENTITY: Hibernate relies on an auto-incremented database column to generate the primary key, SEQUENCE: Hibernate requests the primary key value from a database sequence, TABLE: Hibernate uses a database table to simulate a sequence.

How do you detach an object in Hibernate?

You can detach an entity by calling Session. evict() . Other options are create a defensive copy of your entity before translation of values, or use a DTO instead of the entity in that code.


1 Answers

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));
like image 179
Vlad Mihalcea Avatar answered Sep 22 '22 22:09

Vlad Mihalcea