最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

java - How to refresh a OneToOne relation after update - Stack Overflow

programmeradmin2浏览0评论

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 :

  1. 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);
  1. After that, I save a new Console linked to this Rcon 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);
  1. After that, I update the Rcon using the same method as step 1 to change the name 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);
  1. 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 the console.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 :

  1. 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);
  1. After that, I save a new Console linked to this Rcon 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);
  1. After that, I update the Rcon using the same method as step 1 to change the name 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);
  1. 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 the console.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
Add a comment  | 

2 Answers 2

Reset to default 2

You 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.

发布评论

评论列表(0)

  1. 暂无评论