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 :
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
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.
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 .
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With