How to save entities with manually assigned identifiers using Spring Data JPA?

Aurélien Audelin :

I'm updating an existing code that handles the copy or raw data from one table into multiple objects within the same database.

Previously, every kind of object had a generated PK using a sequence for each table.

Something like that :

@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

In order to reuse existing IDs from the import table, we removed GeneratedValue for some entities, like that :

@Id
@Column(name = "id")
private Integer id;

For this entity, I did not change my JpaRepository, looking like this :

public interface EntityRepository extends JpaRepository<Entity, Integer> {
    <S extends Entity> S save(S entity);
}

Now I'm struggling to understand the following behaviour, within a spring transaction (@Transactional) with the default propagation and isolation level :

  • With the @GeneratedValue on the entity, when I call entityRepository.save(entity) I can see with Hibernate show sql activated that an insert request is fired (however seems to be only in the cache since the database does not change)
  • Without the @GeneratedValue on the entity, only a select request is fired (no insert attempt)

This is a big issue when my Entity (without generated value) is mapped to MyOtherEntity (with generated value) in a one or many relationship.

I thus have the following error :

ERROR: insert or update on table "t_other_entity" violates foreign key constraint "other_entity_entity"
Détail : Key (entity_id)=(110) is not present in table "t_entity"

Seems legit since the insert has not been sent for Entity, but why ? Again, if I change the ID of the Entity and use @GeneratedValue I don't get any error.

I'm using Spring Boot 1.5.12, Java 8 and PostgreSQL 9

Oliver Drotbohm :

You're basically switching from automatically assigned identifiers to manually defined ones which has a couple of consequences both on the JPA and Spring Data level.

Database operation timing

On the plain JPA level, the persistence provider doesn't necessarily need to immediately execute a single insert as it doesn't have to obtain an identifier value. That's why it usually delays the execution of the statement until it needs to flush, which is on either an explicit call to EntityManager.flush(), a query execution as that requires the data in the database to be up to date to deliver correct results or transaction commit.

Spring Data JPA repositories automatically use default transactions on the call to save(…). However, if you're calling repositories within a method annotated with @Transactional in turn, the databse interaction might not occur until that method is left.

EntityManager.persist(…) VS. ….merge(…)

JPA requires the EntityManager client code to differentiate between persisting a completely new entity or applying changes to an existing one. Spring Data repositories w ant to free the client code from having to deal with this distinction as business code shouldn't be overloaded with that implementation detail. That means, Spring Data will somehow have to differentiate new entities from existing ones itself. The various strategies are described in the reference documentation.

In case of manually identifiers the default of inspecting the identifier property for null values will not work as the property will never be null by definition. A standard pattern is to tweak the entities to implement Persistable and keep a transient is-new-flag around and use entity callback annotations to flip the flag.

@MappedSuperclass
public abstract class AbstractEntity<ID extends SalespointIdentifier> implements Persistable<ID> {

  private @Transient boolean isNew = true;

  @Override
  public boolean isNew() {
    return isNew;
  }


  @PrePersist
  @PostLoad
  void markNotNew() {
    this.isNew = false;
  }

  // More code…
}

isNew is declared transient so that it doesn't get persisted. The type implements Persistable so that the Spring Data JPA implementation of the repository's save(…) method will use that. The code above results in entities created from user code using new having the flag set to true, but any kind of database interaction (saving or loading) turning the entity into a existing one, so that save(…) will trigger EntityManager.persist(…) initially but ….merge(…) for all subsequent operations.

I took the chance to create DATAJPA-1600 and added a summary of this description to the reference docs.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=139523&siteId=1