Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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

like image 436
Aurélien Audelin Avatar asked Aug 27 '19 15:08

Aurélien Audelin


People also ask

How can we save entity in JPA without primary key?

Sometimes your object or table has no primary key. The best solution in this case is normally to add a generated id to the object and table. If you do not have this option, sometimes there is a column or set of columns in the table that make up a unique value. You can use this unique set of columns as your id in JPA.

Which of the following values can be used to configure a BootstrapMode with JPA considering JPA 2.1 or above?

As of Spring Data JPA 2.1 you can now configure a BootstrapMode (either via the @EnableJpaRepositories annotation or the XML namespace) that takes the following values: DEFAULT (default) — Repositories are instantiated eagerly unless explicitly annotated with @Lazy .

What is @ID in JPA?

In JPA the object id is defined through the @Id annotation and should correspond to the primary key of the object's table. An object id can either be a natural id or a generated id. A natural id is one that occurs in the object and has some meaning in the application.


1 Answers

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.

like image 163
Oliver Drotbohm Avatar answered Sep 21 '22 23:09

Oliver Drotbohm