Spring JPA / Hibernate transaction force insert instead of update

18,856

Solution 1

I never did that before, but a little hack, would maybe do the job.

There is a Persistable interface for the entities. It has a method boolean isNew() that when implemented will be used to "assess" if the Entity is new or not in the database. Base on that decision, EntityManager should decide to call .merge() or .persist() on that entity, after You call .save() from Repository.

Going that way, if You implement isNew() to always return true, the .persist() should be called no mater what, and error should be thrown after.

Correct me If I'm wrong. Unfortunately I can't test it on a live code right now.

Documentation about Persistable: http://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Persistable.html

Solution 2

Why not create a clone object which clones everything except your primary keys and then save this cloned object.

Since the PK will not be present, an insert happens, instead of an update

Share:
18,856

Related videos on Youtube

MartinS
Author by

MartinS

Updated on June 26, 2022

Comments

  • MartinS
    MartinS almost 2 years

    Edited. Whilst extending the base repository class and adding an insert method would work an more elegant solution appears to be implementing Persistable in the entities. See Possible Solution 2


    I'm creating a service using springframework.data.jpa with Hibernate as the ORM using JpaTransactionManager.

    following the basis of the tutorial here. http://www.petrikainulainen.net/spring-data-jpa-tutorial/

    My entity repositories extend org.springframework.data.repository.CrudRepository

    I'm working with a legacy database which uses meaningful primary keys rather then auto generated id's

    This situation shouldn't really occur, but I came across it due to a bug in testing. Order table has a meaningful key of OrderNumber (M000001 etc). The primary key value is generated in code and assigned to the object prior to save. The legacy database does not use auto-generated ID keys.

    I have a transaction which is creating a new order. Due to a bug, my code generated an order number which already existed in the database (M000001)

    Performing a repository.save caused the existing order to be updated. What I want is to force an Insert and to fail the transaction due to duplicate primary key.

    I could create an Insert method in every repository which performs a find prior to performing a save and failing if the row exists. Some entities have composite primary keys with a OrderLinePK object so I can't use the base spring FindOne(ID id) method

    Is there a clean way of doing this in spring JPA?

    I previously created a test service without jpa repository using spring/Hibernate and my own base repository. I implemented an Insert method and a Save method as follows.

    This seemed to work OK. The save method using getSession().saveOrUpdate gave what I'm experiencing now with an existing row being updated.

    The insert method using getSession().save failed with duplicate primary key as I want.

    @Override
    public Order save(Order bean) {
    
        getSession().saveOrUpdate(bean);
        return bean;
    }
    
    @Override
    public Order insert(Order bean) {
        getSession().save(bean);
        return bean;
    }
    

    Possible solution 1

    Based on chapter 1.3.2 of the spring docs here http://docs.spring.io/spring-data/jpa/docs/1.4.1.RELEASE/reference/html/repositories.html

    Probably not the most efficient as we're doing an additional retrieval to check the existence of the row prior to insert, but it's primary key.

    Extend the repository to add an insert method in addition to save. This is the first cut.

    I'm having to pass the key into the insert as well as the entity. Can I avoid this ?

    I don't actually want the data returned. the entitymanager doesn't have an exists method (does exists just do a count(*) to check existence of a row?)

    import java.io.Serializable;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.repository.NoRepositoryBean;
    
    /**
     *
     * @author Martins
     */
    @NoRepositoryBean
    public interface IBaseRepository <T, ID extends Serializable> extends JpaRepository<T, ID> {
    
        void insert(T entity, ID id);    
    
    }
    

    Implementation : Custom repository base class. Note : A custom exception type will be created if I go down this route..

    import java.io.Serializable;
    import javax.persistence.EntityManager;
    import org.springframework.data.jpa.repository.support.JpaEntityInformation;
    import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
    import org.springframework.transaction.annotation.Transactional;
    
    
    public class BaseRepositoryImpl<T, ID extends Serializable> 
            extends SimpleJpaRepository<T, ID> implements IBaseRepository<T, ID> {
    
        private final EntityManager entityManager;
    
        public BaseRepositoryImpl(Class<T> domainClass, EntityManager em) {
            super(domainClass, em);
            this.entityManager = em;
        }
    
    
        public BaseRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
            super (entityInformation, entityManager);
            this.entityManager = entityManager;
    
        }
    
        @Transactional
        public void insert(T entity, ID id) {
    
            T exists = entityManager.find(this.getDomainClass(),id);
    
            if (exists == null) {
              entityManager.persist(entity);
            }
            else 
              throw(new IllegalStateException("duplicate"));
        }    
    
    }
    

    A custom repository factory bean

    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
    import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
    import org.springframework.data.repository.core.RepositoryMetadata;
    import org.springframework.data.repository.core.support.RepositoryFactorySupport;
    
    import javax.persistence.EntityManager;
    import java.io.Serializable;
    
    /**
     * This factory bean replaces the default implementation of the repository interface 
     */
    public class BaseRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable>
      extends JpaRepositoryFactoryBean<R, T, I> {
    
      protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
    
        return new BaseRepositoryFactory(entityManager);
      }
    
      private static class BaseRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory {
    
        private EntityManager entityManager;
    
        public BaseRepositoryFactory(EntityManager entityManager) {
          super(entityManager);
    
          this.entityManager = entityManager;
        }
    
        protected Object getTargetRepository(RepositoryMetadata metadata) {
    
          return new BaseRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), entityManager);
        }
    
        protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
    
          // The RepositoryMetadata can be safely ignored, it is used by the JpaRepositoryFactory
          //to check for QueryDslJpaRepository's which is out of scope.
          return IBaseRepository.class;
        }
      }
    }
    

    Finally wire up the custom repository base class in the configuration

    // Define this class as a Spring configuration class
    @Configuration
    
    // Enable Spring/jpa transaction management.
    @EnableTransactionManagement
    
    @EnableJpaRepositories(basePackages = {"com.savant.test.spring.donorservicejpa.dao.repository"}, 
            repositoryBaseClass = com.savant.test.spring.donorservicejpa.dao.repository.BaseRepositoryImpl.class)
    

    Possible solution 2

    Following the suggestion made by patrykos91

    Implement the Persistable interface for the entities and override the isNew()

    A base entity class to manage the callback methods to set the persisted flag

    import java.io.Serializable;
    import javax.persistence.MappedSuperclass;
    import javax.persistence.PostLoad;
    import javax.persistence.PostPersist;
    import javax.persistence.PostUpdate;
    
    
    @MappedSuperclass
    public abstract class BaseEntity implements Serializable{
    
        protected transient boolean persisted;
    
    
        @PostLoad
        public void postLoad() {
            this.persisted = true;
        }
    
        @PostUpdate
        public void postUpdate() {
            this.persisted = true;
        }
    
        @PostPersist
        public void postPersist() {
            this.persisted = true;
        }
    
    }
    

    Then each entity must then implement the isNew() and getID()

    import java.io.Serializable; import javax.persistence.Column; import javax.persistence.EmbeddedId; import javax.persistence.Entity; import javax.persistence.Table; import javax.xml.bind.annotation.XmlRootElement; import org.springframework.data.domain.Persistable;

    @Entity
    @Table(name = "MTHSEQ")
    @XmlRootElement
    
    public class Sequence extends BaseEntity implements Serializable, Persistable<SequencePK> {
    
        private static final long serialVersionUID = 1L;
        @EmbeddedId
        protected SequencePK sequencePK;
        @Column(name = "NEXTSEQ")
        private Integer nextseq;
    
        public Sequence() {
        }
    
    
        @Override
        public boolean isNew() {
            return !persisted;
        }
    
        @Override
        public SequencePK getId() {
            return this.sequencePK;
        }
    
    
    
        public Sequence(SequencePK sequencePK) {
            this.sequencePK = sequencePK;
        }
    
        public Sequence(String mthkey, Character centre) {
            this.sequencePK = new SequencePK(mthkey, centre);
        }
    
        public SequencePK getSequencePK() {
            return sequencePK;
        }
    
        public void setSequencePK(SequencePK sequencePK) {
            this.sequencePK = sequencePK;
        }
    
        public Integer getNextseq() {
            return nextseq;
        }
    
        public void setNextseq(Integer nextseq) {
            this.nextseq = nextseq;
        }
    
        @Override
        public int hashCode() {
            int hash = 0;
            hash += (sequencePK != null ? sequencePK.hashCode() : 0);
            return hash;
        }
    
        @Override
        public boolean equals(Object object) {
            // TODO: Warning - this method won't work in the case the id fields are not set
            if (!(object instanceof Sequence)) {
                return false;
            }
            Sequence other = (Sequence) object;
            if ((this.sequencePK == null && other.sequencePK != null) || (this.sequencePK != null && !this.sequencePK.equals(other.sequencePK))) {
                return false;
            }
            return true;
        }
    
        @Override
        public String toString() {
            return "com.savant.test.spring.donorservice.core.entity.Sequence[ sequencePK=" + sequencePK + " ]";
        }
    
    
    
    }
    

    It would be nice to abstract out the isNew() but I don't think I can. The getId can't as entities have different Id's, as you can see this one has composite PK.

  • MartinS
    MartinS almost 8 years
    I may have been unclear. The primary keys are created internally and assigned to the object rather than being auto-generated on insert. Some tables have multiple primary key fields too
  • madhusudan rao Y
    madhusudan rao Y almost 8 years
    That would make it even easier. After cloning all the common fields, why not assign the required values to the entity id's and do a save. Or is this not possible?
  • MartinS
    MartinS almost 8 years
    It was only due to a bug. I was intending to insert a new order, but I assigned the wrong primary key and an existing order was updated. This is obviously really bad in a live environment so I want to eliminate the possibility of this happening. The code needs to explicitly call an insert method which should fail if the row already exists, not leave it to the spring JPA framework to decide whether to insert or update (which would work fine for database auto-generated ID primary keys)
  • madhusudan rao Y
    madhusudan rao Y almost 8 years
    The only way I can think of is to have a select/ load call yourself to check if such a record is already present in the DB and then throw a user defined exception which lets you handle the cause as you deem fit.
  • MartinS
    MartinS almost 8 years
    So there's no way of making Spring-data JPA perform a save() rather than a SaveOrUpdate() ? I will have to add an insert(order) method to my OrderRepository interface like i did before using spring-data and JPA. But now I need to code an implementation for the insert(order) which checks for the record rather than letting the database reject with 'Duplicate primary key' error. That's a shame.
  • madhusudan rao Y
    madhusudan rao Y almost 8 years
    There could be a way to do that. But, sadly I am not aware of it.
  • MartinS
    MartinS almost 8 years
    Thanks. I'll have a look. I'm currently attempting to add a custom "insert" method to a base repository to check existance before inserting - as described in spring docs docs.spring.io/spring-data/jpa/docs/1.4.1.RELEASE/reference/‌​… Unfortunately the docs seem out of date, I'm getting No suitable constructor trying to create my base repository extending SimpleJpaRepository.
  • MartinS
    MartinS almost 8 years
    Using your suggestion implementing Persistable and using callback methods @ PostLoad, @ PostUpdate, @ PostPersist seems quite an elegant way of achieving what I want. At least I got some experience at implementing custom repository base classes along the way. Thank you.
  • Nikhil Mishra
    Nikhil Mishra over 4 years
    java.sql.SQLException: Field 'id' doesn't have a default value, if I am doing record.setId(null)