1

I am currently writing a fairly "dumb" ETL process. I call it dumb because the process runs nightly, digest all of the data (even if it hasn't change), and persist it. My hope is to keep it simple.

I've been using Hibernate v6.1.7 to define my entities and persist them. Since the entities may already exist in my database, I'm using merge to persist the entity.

public static void main(String[] args){
    BaseballGame game = new BaseballGame();
    BaseballWeather weather = new BaseballWeather();

    game.setGameId(12345L);
    game.setWeather(weather);

    weather.setTemperature(70.0);
    weather.setGame(game);

    Session session = Hibernate.getSessionFactory().openSession();
    Transaction transaction = session.beginTransaction();
    session.merge(game);
    transaction.commit();
    session.close();
}

This works fine for simple entities that don't have any complex relationship and for entities that have a ManyToOne relationship.

I'm running into problems when persisting an entity that has a OneToOne relationship where the child owns the key. Here are some trivial examples of entities that exhibit this problem.

@Entity
@Table(name = "baseball_game")
public class BaseballGame {

    @Id
    private Long gameId;

    @OneToOne(mappedBy = "game", cascade = CascadeType.MERGE)
    private BaseballWeather weather;

    public void setGameId(Long gameId) {
        this.gameId = gameId;
    }

    public void setWeather(BaseballWeather weather) {
        this.weather = weather;
    }
}

@Entity
@Table(name = "baseball_weather")
public class BaseballWeather {

    @Id
    private Long id;

    private Double temperature;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "game_id")
    @MapsId
    private BaseballGame game;

    public void setTemperature(Double temperature) {
        this.temperature = temperature;
    }

    public void setGame(BaseballGame game) {
        this.game = game;
    }
}

If I run the main code from above, it will succeed on the first run. But once the baseball_weather exists, running merge produces the following error.

Exception in thread "main" jakarta.persistence.EntityExistsException: A different object with the same identifier value was already associated with the session : [x.y.z.BaseballWeather#12345]

The error message is correct, I'm manually creating a version of the BaseballWeather class that contains the same ID as an object already stored in the session. But I'm not seeing this behavior with the parent class or with other entities that don't have this OneToOne relationship. In those cases objects I create manually will still be merged without error.

I was under the impression that specifying the cascade type to "Merge" would, well, cascade the merge operation. I've tried substituting in the other cascade types (Persist, All, etc..) and none have been able to persist the object if it doesn't exist or update if it does.

Is anyone aware of how I can update my entities to support both persisting and updating via the merge method without running into errors?

Staros
  • 3,232
  • 6
  • 30
  • 41

1 Answers1

2

OK, I see. So here is the full code required to reproduce the error:

@SessionFactory
@DomainModel(annotatedClasses = {SOTest.BaseballGame.class, SOTest.BaseballWeather.class})
public class SOTest {

    @Test void test(SessionFactoryScope scope) {
        BaseballGame game = new BaseballGame();
        BaseballWeather weather = new BaseballWeather();

        game.setGameId(12345L);
        game.setWeather(weather);

        // Note that the id field was never set!
        //weather.id = 12345L;

        weather.setTemperature(70.0);
        weather.setGame(game);

        scope.inSession(session-> {
            Transaction transaction = session.beginTransaction();
            session.merge(game);
            transaction.commit();
        });
        scope.inSession(session-> {
            Transaction transaction = session.beginTransaction();
            session.merge(game);
            transaction.commit();
        });
    }

    @Entity
    @Table(name = "baseball_game")
    static public class BaseballGame {

        @Id
        private Long gameId;

        @OneToOne(mappedBy = "game", cascade = CascadeType.MERGE)
        private BaseballWeather weather;

        public void setGameId(Long gameId) {
            this.gameId = gameId;
        }

        public void setWeather(BaseballWeather weather) {
            this.weather = weather;
        }
    }

    @Entity
    @Table(name = "baseball_weather")
    static public class BaseballWeather {

        @Id
        private Long id;

        private Double temperature;

        @OneToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "game_id")
        @MapsId
        private BaseballGame game;

        public void setTemperature(Double temperature) {
            this.temperature = temperature;
        }

        public void setGame(BaseballGame game) {
            this.game = game;
        }
    }

}

So the issue here resulted from the commented line added by me. Hibernate inferred that the detached BaseballWeather was a new/transient object (not existing in the database) because it had a null @Id property.

This is a situation that I actually didn't realize can happen. It only happens when you have these one-to-one associations with @MapsId.

Anyway I will look into this further to see if we can report a better error here, but to be clear, this was actually your fault for forgetting to set the id field.

Gavin King
  • 3,182
  • 1
  • 13
  • 11
  • 1
    Thanks Gavin! I confirmed that setting the child object id via the constructor/setter fixes the issue. I think my confusion came from the fact that on the initial persist Hibernate seems to map the child's id from the parent object automatically (via the @MapsId annotation) but on subsequent persists I need to set it manually. That said, your comment about Hibernate assuming it's a new object because the ID is null makes sense. – Staros May 11 '23 at 12:04