JPA allocation Size causing Auto Generated Sequence to Reused after AllocationSize

1.1k views Asked by At

I have recursive parent and child relation for Graph. When I have a large Graph with 50 or more nodes on a Single hibernate session, I get an error message "A different object with the same identifier value was already associated with the session". This is due to the fact the default allocation Size in Spring JPA is 50. I have overcome this error by setting allocationSize to 100 and increment by 100. But that does not solve the root of the problem. I can have any arbitrary # nodes in ONE session. I use saveAndFlush(NodeEntity) which throws this error message.

My question is How do I force Hibernate to fetch the primary key from DB after allocation size is limit is reached and be able to generate a new set primary key in a single session?

Hibernate version: hibernate-core-5.4.30.Final.jar

Error:

A different object with the same identifier value was already associated with the session : [graph.entity.NodeEntity#53]; nested exception is javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [graph.entity.NodeEntity#53]

 //GRAPH DATABASE.
CREATE TABLE IF NOT EXISTS node
(
  id SERIAL NOT NULL,
  name character varying(255) NOT NULL,
  parent_id int,
  CONSTRAINT node_pkey PRIMARY KEY (id),
  CONSTRAINT node_parent_id_fk FOREIGN KEY (parent_id)
      REFERENCES node (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION
);

CREATE TABLE IF NOT EXISTS graph
(
  id SERIAL NOT NULL,
  name character varying(255) NOT NULL,
  node_id int,
  CONSTRAINT graph_pkey PRIMARY KEY (id),
  CONSTRAINT graph_node_fk FOREIGN KEY (node_id)
      REFERENCES node (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT graph_node_id_uk UNIQUE (node_id)
);

public class NodeEntity {
    @Id
    @SequenceGenerator(name = "node_id_seq", sequenceName = "node_id_seq",allocationSize = 50)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "node_id_seq")
    @Column(name = "id")
    int id;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,orphanRemoval=true)
@JoinColumn(name = "parent_id")
private List<NodeEntity> children = new LinkedList<NodeEntity>();

}

    public class GraphEntity{
    
        @Id
        @SequenceGenerator(name = "graph_id_seq", sequenceName = "graph_id_seq")
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "graph_id_seq")
        @Column(name = "id")
        int id;
    
        @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
        @JoinColumn(name = "NODE_ID", unique = true, nullable = true, insertable = true, updatable = true)
        private NodeEntity rootNode;
    
    }

// Way to reproduce this.
void generate150deepChild(int count,NodeEntity node){
   if(count == 100){
       return
   }else{
      NodeEntity newNode = new NodeEntity("Child " +count)
      node.getChildren().add(newNode);
      cout++;
      generate150deepChild(count,newNode);
  }
}
NodeEntity rootNode = new NodeEntity("ROOT");
// PLEASE NOT if # NodeEntity < 50 everything works fine.
generate150deepChild(0,rootNode);
// PLEASE NOT all ids are zero so they are new node.
GraphEntity graph = new GraphEntity("TEST");
graph.setRootNode(rootNode);
graphRepository.saveAndFlush(graph); 
// THIS WILL GENERATE Duplicate Primary key for NodeEntity.

Actual stack trace:

Caused by: javax.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [graph.entity.NodeEntity#131605]
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:123)
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:181)
        at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:188)
        at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:823)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:786)
        at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:261)
        at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:499)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:423)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220)
        at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:532)
        at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:463)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:426)
        at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220)
        at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153)
        at org.hibernate.event.internal.AbstractSaveEventListener.cascadeAfterSave(AbstractSaveEventListener.java:459)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:247)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:175)
        at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:104)
        at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:813)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:786)
        at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:261)
        at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:499)
        at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:423)
1

There are 1 answers

3
Ken Chan On

Change to use GenerationType.SEQUENCE strategy and it should handle for you automatically :

@Id
@SequenceGenerator(name = "node_id_seq", sequenceName = "node_id_seq",allocationSize = 50)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "node_id_seq")
@Column(name = "id")
int id;

And you just need to make sure the followings:

  1. Do not set the ID for each node manually.
  2. The related sequence in DB (i.e. the sequence specified in sequenceName in @SequenceGenerator) is really configured and aligned with what you configure in @SequenceGenerator. In case of PostgreSQL , you can do :
alter sequence node_id_seq increment by 50;

Tip:

According to this, you can change to use the pooled or pooled-lo algorithm to reduce the DB roundtrip to get the ID by configuring the following settings:

<property name="hibernate.id.optimizer.pooled.preferred" value="pooled-lo" />