Hibernate OneToMany difference in produced queries when migrating from 5 to 6

I seem to have stumbled on a weird issue when migrating a project from hibernate 5 to 6.

The same code on both produces different queries and consequently different results. Hibernate 6 produces an extra “delete” query that deletes the record right after insertion, while hibernate 5 does not.

Hibernate 5 produced queries:

Hibernate: select hibernate_sequence.nextval from dual
Hibernate: insert into "user" (name, surname, id) values (?, ?, ?)
Hibernate: select count(*) as col_0_0_ from "user" user0_
// User count: 1
Hibernate: select hibernate_sequence.nextval from dual
Hibernate: insert into device (name, id) values (?, ?)
Hibernate: select count(*) as col_0_0_ from device device0_
// Device count: 1
Hibernate: select user0_.id as id1_2_, user0_.name as name2_2_, user0_.surname as surname3_2_ from "user" user0_ where user0_.name=?
Hibernate: select hibernate_sequence.nextval from dual
Hibernate: insert into provisioning_info (device_id, user_id, id) values (?, ?, ?)
Hibernate: select count(*) as col_0_0_ from provisioning_info provisioni0_
// ProvisioningInfo count: 1

Hibernate 6 produced queries:

Hibernate: select user_seq.nextval from dual
Hibernate: insert into "user" (name, surname, id) values (?, ?, ?)
Hibernate: select count(*) from "user" u1_0
// User count: 1
Hibernate: select device_seq.nextval from dual
Hibernate: insert into device (name, id) values (?, ?)
Hibernate: select count(*) from device d1_0
// Device count: 1
Hibernate: select u1_0.id,u1_0.name,u1_0.surname from "user" u1_0 where u1_0.name=?
Hibernate: select provisioning_info_seq.nextval from dual
Hibernate: insert into provisioning_info (device_id, user_id, id) values (?, ?, ?)
Hibernate: delete from provisioning_info where user_id=?
Hibernate: select count(*) from provisioning_info p1_0
// ProvisioningInfo count: 0

Now, why does hibernate 6 produce an extra query delete from provisioning_info where user_id=??
Are we using hibernate incorrectly? What can I do to make it work on hibernate 6?

Models:

@Entity
@Table(name = "\"user\"")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    private String surname;

    @OneToMany(fetch = FetchType.LAZY)
    @JoinTable(
        name = "provisioning_info",
        joinColumns = @JoinColumn(name = "user_id", nullable = false, updatable = false),
        inverseJoinColumns = @JoinColumn(name = "device_id", nullable = false, updatable = false)
    )
    private Set<Device> devices;
	
	// Omitted getters and setters
}

@Entity
@Table(name = "device")
public class Device {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;

    // Omitted getters and setters
}

@Entity
@Table(name = "provisioning_info")
public class ProvisioningInfo {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "device_id")
    private Device device;

    // Omitted getters and setters	
}

Repositories:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByName(String name);
}

public interface DeviceRepository extends JpaRepository<Device, Long> {
    Optional<Device> findByName(String name);
}

public interface ProvisioningInfoRepository extends JpaRepository<ProvisioningInfo, Long> {
}

Test code:

@Transactional
public void test() {
	User user = new User();
	user.setName("name");
	user.setSurname("surname");

	userRepository.save(user);

	System.out.println("User count: " + userRepository.count());

	Device device = new Device();
	device.setName("device");

	deviceRepository.save(device);

	System.out.println("Device count: " + deviceRepository.count());

	user = userRepository.findByName(user.getName()).orElseThrow();
	device = deviceRepository.findById(device.getId()).orElseThrow();

	ProvisioningInfo pi = new ProvisioningInfo();
	pi.setUser(user);
	pi.setDevice(device);

	provisioningInfoRepository.save(pi);

	System.out.println("ProvisioningInfo count: " + provisioningInfoRepository.count());
}

Database creation sql (Oracle):

create sequence hibernate_sequence start with 1 increment by 1;
create table device (id number(19,0) not null, name varchar2(255), primary key (id));
create table provisioning_info (id number(19,0) not null, device_id number(19,0) not null, user_id number(19,0) not null, primary key (user_id, device_id));
create table "user" (id number(19,0) not null, name varchar2(255), surname varchar2(255), primary key (id));
alter table provisioning_info add constraint FKaa9goxi73eawrckas10a5u3r8 foreign key (device_id) references device;
alter table provisioning_info add constraint FKht6iwjjedykqjupratrdgpf6d foreign key (user_id) references "user";

Thank you in advance.

Mapping a join table of an association as an entity and trying to manage both in one transaction is generally a recipe for disaster. If you have an entity for the join table, then use that. Use this mapping:

@Entity
@Table(name = "\"user\"")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String name;
    private String surname;

    @OneToMany(mappedBy = "user")
    private Set<ProvisioningInfo> devices;
}
1 Like

Thank you, that indeed does work.

Might I just ask why the “delete sql” disappears, if I simply replace
user = userRepository.findByName(user.getName()).orElseThrow();
with
user = userRepository.findById(user.getId()).orElseThrow();

or if you could explain why this extra “delete sql” is even generated. I’m trying to understand it better, to never make this mistake again.

Best regards and thank you again.

Calling userRepository.count() triggers Hibernate to flush/persist the User, so after this point, it is managed, but with an empty devices collection.
When you save a ProvisioningInfo, the collection would actually contain data, but the managed collection is not initialized yet, so at the end of the transaction, when Hibernate flushes data, it sees that the devices of the managed entity is an empty collection, so it issues a delete statement.

This kind of relates to [Closed] ignoring collections on merge() · Discussion #4398 · hibernate/hibernate-orm · GitHub in the sense that you should actually initialize a lazy collection instead of an empty one to avoid this case, but then you also fail to maintain “both sides” of the relationship, i.e. when you persist a ProvisioningInfo entity, you should also add that to the respective collection of the User.

By using the mapping I proposed to you, a flush of a User now doesn’t manage that join table provisioning_info anymore, so there are no surprises.
TLDR, just don’t use a @JoinTable when you manage the contents of that table through an entity.

1 Like