I have a Springboot (3.4.2) application with Java 21 and I am facing an issue with JPA :
In case it can be usefull, here is my application.properties
configuration :
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/mydatabase
spring.datasource.username=****
spring.datasource.password=****
spring.jpa.properties.hibernate.dialect=.hibernate.dialect.PostgreSQLDialect
spring.datasource.driver-class-name=.postgresql.Driver
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.naming.physical-strategy=.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Here is my entity Rcon
:
@Entity
@Table(name = "rcon", uniqueConstraints = @UniqueConstraint(columnNames = { "rcon_host", "rcon_port" }))
public class Rcon{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false)
private Long id;
@Column(name = "name", columnDefinition = PostgreSqlColumn.POSTGRESQL_VARCHAR_255, nullable = false, unique = true)
private String name;
// Some other fields, nothing special
}
For each Rcon
, a Console
can be created with a FK to the Rcon
:
@Entity
@Table(name = "console")
public class Console {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false)
private Long id;
@OneToOne(optional = false, fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REFRESH}, orphanRemoval = false)
@JoinColumn(name = "rcon_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Rcon rcon;
// Some other fields, nothing special
}
The issue I am facing occurs in the following sitation :
- I save a new
Rcon
using the method below : OK
// RconServiceImpl
@Override
@Transactional
public Rcon save(Rcon rcon) {
var isNew = rcon.getId() == null;
var entity = repository.saveAndFlush(rcon);
if(isNew) {
this.eventProducer.produce(RconEvent.notifyCreated(this, entity));
} else {
this.eventProducer.produce(RconEvent.notifyUpdated(this, entity));
}
return entity;
}
// From another class (I let only usefull code to understand) :
var newRcon= new Rcon.Builder().withName("foo").build();
rconService.save(newRcon);
- After that, I save a new
Console
linked to thisRcon
using the method below : OK
// ConsoleServiceImpl
@Override
@Transactional
public Console save(Console console) {
var isNew = console.getId() == null;
var entity = repository.save(console);
if(isNew) {
this.eventProducer.produce(ConsoleEvent.notifyCreated(this, entity));
} else {
this.eventProducer.produce(ConsoleEvent.notifyUpdated(this, entity));
}
return entity;
}
// From another class :
var rcon= rconService.findByName("foo").orElseThrow(...);
var newConsole = new Console.Builder().withRcon(rcon).build();
consoleService.save(newConsole);
- After that, I update the
Rcon
using the same method as step 1 to change thename
field from 'foo' to 'bar' for exemple. It result in a RconEvent UPDATE beeing produce.
var rcon= rconService.findByName("foo").orElseThrow(...);
rcon.setName("bar");
rconService.save(rcon);
- Somewhere in the application, I have a listener of this RconEvent. When the listener consumes an event, it try to get the
Console
from the database. At this point, I expect theconsole.getRcon().getName()
to be equals to 'bar', but it's still equals to 'foo' (Rcon entity before update in step 3). This is the problem I would like to solve :
@EventListener
protected void onRconEvent(RconEvent event) {
var rcon = event.getRcon();
if(event.isUpdated()) {
rconService.findByRconId(rcon.getId()).ifPresent(console -> {
logger.error("console = {}", console); // Here, console.getRcon().getName() seems to return "bar" as expected
});
this.doSomething(rcon).thenRun(() -> rconService.findByRustServerId(rcon.getId()).ifPresent(console -> {
logger.error("console = {}", console); // Here, console.getRcon().getName() seems to return "foo" ...
}));
}
}
I have read a lot about refreshing the entity manager and tried adding it in the findByRconId method (see code below), as well as using CascadeType.REFRESH in my @OneToOne annotation, or replacing "save()" by "saveAndFlush" call for Rcon entity. However, none of my attempts have worked so far, and the Rcon reference I get in the Console is still the old one. What am I doing wrong? Thank you!
@Override
@Transactional(readOnly = true)
public Optional<Console> findByRconId(Long rconId) {
return repository.findByRconId(rconId).map(console -> {
if (entityManager.contains(console)) {
entityManager.refresh(console);
}
return console;
});
}
EDIT :
It looks like I can improve the situation by defining the findByRconId () method like the following (in this case, the entity is almost always updated, but I think I got a case where it was not ..) :
@Override
@Transactional(readOnly = true)
public Optional<Console> findByRconId(Long rconId) {
return repository.findByRconId(rconId).map(console -> {
entityManager.detach(console);
return entityManager.find(Console.class, console.getId());
});
}
I have a Springboot (3.4.2) application with Java 21 and I am facing an issue with JPA :
In case it can be usefull, here is my application.properties
configuration :
spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/mydatabase
spring.datasource.username=****
spring.datasource.password=****
spring.jpa.properties.hibernate.dialect=.hibernate.dialect.PostgreSQLDialect
spring.datasource.driver-class-name=.postgresql.Driver
spring.jpa.show-sql=false
spring.jpa.open-in-view=false
spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.naming.physical-strategy=.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Here is my entity Rcon
:
@Entity
@Table(name = "rcon", uniqueConstraints = @UniqueConstraint(columnNames = { "rcon_host", "rcon_port" }))
public class Rcon{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false)
private Long id;
@Column(name = "name", columnDefinition = PostgreSqlColumn.POSTGRESQL_VARCHAR_255, nullable = false, unique = true)
private String name;
// Some other fields, nothing special
}
For each Rcon
, a Console
can be created with a FK to the Rcon
:
@Entity
@Table(name = "console")
public class Console {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", columnDefinition = PostgreSqlColumn.POSTGRESQL_BIGINT, nullable = false)
private Long id;
@OneToOne(optional = false, fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REFRESH}, orphanRemoval = false)
@JoinColumn(name = "rcon_id", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
private Rcon rcon;
// Some other fields, nothing special
}
The issue I am facing occurs in the following sitation :
- I save a new
Rcon
using the method below : OK
// RconServiceImpl
@Override
@Transactional
public Rcon save(Rcon rcon) {
var isNew = rcon.getId() == null;
var entity = repository.saveAndFlush(rcon);
if(isNew) {
this.eventProducer.produce(RconEvent.notifyCreated(this, entity));
} else {
this.eventProducer.produce(RconEvent.notifyUpdated(this, entity));
}
return entity;
}
// From another class (I let only usefull code to understand) :
var newRcon= new Rcon.Builder().withName("foo").build();
rconService.save(newRcon);
- After that, I save a new
Console
linked to thisRcon
using the method below : OK
// ConsoleServiceImpl
@Override
@Transactional
public Console save(Console console) {
var isNew = console.getId() == null;
var entity = repository.save(console);
if(isNew) {
this.eventProducer.produce(ConsoleEvent.notifyCreated(this, entity));
} else {
this.eventProducer.produce(ConsoleEvent.notifyUpdated(this, entity));
}
return entity;
}
// From another class :
var rcon= rconService.findByName("foo").orElseThrow(...);
var newConsole = new Console.Builder().withRcon(rcon).build();
consoleService.save(newConsole);
- After that, I update the
Rcon
using the same method as step 1 to change thename
field from 'foo' to 'bar' for exemple. It result in a RconEvent UPDATE beeing produce.
var rcon= rconService.findByName("foo").orElseThrow(...);
rcon.setName("bar");
rconService.save(rcon);
- Somewhere in the application, I have a listener of this RconEvent. When the listener consumes an event, it try to get the
Console
from the database. At this point, I expect theconsole.getRcon().getName()
to be equals to 'bar', but it's still equals to 'foo' (Rcon entity before update in step 3). This is the problem I would like to solve :
@EventListener
protected void onRconEvent(RconEvent event) {
var rcon = event.getRcon();
if(event.isUpdated()) {
rconService.findByRconId(rcon.getId()).ifPresent(console -> {
logger.error("console = {}", console); // Here, console.getRcon().getName() seems to return "bar" as expected
});
this.doSomething(rcon).thenRun(() -> rconService.findByRustServerId(rcon.getId()).ifPresent(console -> {
logger.error("console = {}", console); // Here, console.getRcon().getName() seems to return "foo" ...
}));
}
}
I have read a lot about refreshing the entity manager and tried adding it in the findByRconId method (see code below), as well as using CascadeType.REFRESH in my @OneToOne annotation, or replacing "save()" by "saveAndFlush" call for Rcon entity. However, none of my attempts have worked so far, and the Rcon reference I get in the Console is still the old one. What am I doing wrong? Thank you!
@Override
@Transactional(readOnly = true)
public Optional<Console> findByRconId(Long rconId) {
return repository.findByRconId(rconId).map(console -> {
if (entityManager.contains(console)) {
entityManager.refresh(console);
}
return console;
});
}
EDIT :
It looks like I can improve the situation by defining the findByRconId () method like the following (in this case, the entity is almost always updated, but I think I got a case where it was not ..) :
@Override
@Transactional(readOnly = true)
public Optional<Console> findByRconId(Long rconId) {
return repository.findByRconId(rconId).map(console -> {
entityManager.detach(console);
return entityManager.find(Console.class, console.getId());
});
}
Share
Improve this question
edited Mar 16 at 10:26
Florian S.
asked Mar 15 at 14:43
Florian S.Florian S.
2903 silver badges5 bronze badges
1
- I added examples in the main description to show how it is used. I noticed that it sometimes works, but most of the time it doesn’t, likely due to a cache update issue or something similar I guess – Florian S. Commented Mar 15 at 21:28
2 Answers
Reset to default 2You are sending events before your transaction commits. This is a bad practice - your transaction can still rollback, yet your other processes still get the events indicating it was created or updated.
In this case, they are receiving the event before it actually committed, so they cannot see the changes (just as if it had rolled back). Send it in an after completion event, so they all go out if it is successful. Something like:
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization(){
public void afterCommit() {
this.eventProducer.produce(ConsoleEvent.notifyCreated(this, entity));
}
});
Alternatively, have a wrapper method outside the transaction call the transactional method, and have the wrapper method trigger the events after it is successful. This might allow you to handle errors with the transaction more easily as well
It looks like the problem is that you are calling save() on your Console repository instead of saveAndFlush(), like you do on your Rcon repository.
So, when you subsequently retrieve the Console, the update to it will not have been written to the database yet.