Issue
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.
Solution
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
Answered By - patrykos91
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)