Hibernate 6.3. OneToMany null foreign key

Hi there!
I am trying to use @OneToMany annotation, but do see some weird results, provided by EntityManager.
I use Spring Boot and code is actually run in test scope.

Here is my example:

@Data
@Entity
@Table(name = "referral_program")
public class Program {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	Long id;

	String title;

	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
	@JoinColumn(name = "referral_program_id")
	List<Bonus> bonuses = new ArrayList<>();
}

@Data
@Entity
@Table(name = "referral_bonus")
public class Bonus {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	Long id;

	@Column(name = "referral_program_id")
	Long programId;
}

@Repository
public interface ReferralProgramRepository extends JpaRepository<Program, Long>, PagingAndSortingRepository<Program, Long> {}

@Service
@AllArgsConstructor
public class ReferralService {
    private final EntityManager entityManager;

	public List<Bonus> getAllBonuses() {
		return entityManager.createQuery("from Bonus ", Bonus.class).getResultList();
	}
}

So, here is a test:

@SpringBootTest
@Transactional
public class BonusTest extends TestContainersBase {
    @Autowired
    ReferralProgramRepository referralProgramRepository;

    @Test
	@Rollback
	@DisplayName("Bonuses have Program.Id")
	void testIt() throws Exception {
		var bonus_reg_referral = new Bonus();
		var bonus_reg_referee = new Bonus();

		var prg = new Program();
		prg.setTitle("prg-1");
		prg.setBonuses(
			List.of(
				bonus_reg_referral,
				bonus_reg_referee
			));

		referralProgramRepository.save(prg);

		assertEquals(2, entityManager.createQuery("from Bonus where programId = :id", Bonus.class)
			.setParameter("id", prg.getId())
			.getResultList()
			.size());

		assertEquals(2, referralService.getAllBonuses().stream().filter(b -> b.getProgramId() != null).toList().size());
}

So, the latest assertEquals fails.
Moreover. I do not understand why the previous assertEquals works, because when I run

entityManager.createQuery("from Bonus where programId = :id", Bonus.class)
			.setParameter("id", prg.getId())
			.getResultList()

it shows me the list, but all programId are null.

Here are logs, shown by Hibernate;

Hibernate: insert into referral_program (title) values (?)
Hibernate: insert into referral_bonus (referral_program_id) values (?)
Hibernate: insert into referral_bonus (referral_program_id) values (?)
Hibernate: update referral_bonus set referral_program_id=? where id=?
Hibernate: update referral_bonus set referral_program_id=? where id=?

Please help understand what I did wrong.
Many thanks in advance!

I guess what you really want is a one-to-many with a mapped by association e.g.

@Data
@Entity
@Table(name = "referral_program")
public class Program {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	Long id;

	String title;

	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "program")
	List<Bonus> bonuses = new ArrayList<>();
}

@Data
@Entity
@Table(name = "referral_bonus")
public class Bonus {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	Long id;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "referral_program_id")
	Program program;
}

Actually I tried this approach as well. It does not work :frowning:
After calling

referralProgramRepository.save(prg);

all bonuses, retrieved by

entityManager.createQuery("from Bonus", Bonus.class).getResultList()

have null in the program field.

one more clue.
If I select existing rows from table directly, they have programId

entityManager.createNativeQuery("select * from public.referral_bonus").getResultList();

If I set the target class:

entityManager.createNativeQuery("select * from public.referral_bonus", Bonus.class).getResultList();

all programIds become null.

So, here I made isolated test


import jakarta.persistence.*;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException;
import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

@SpringBootTest
@Transactional
public class DebuggingTests extends TestContainersBase {
	@Autowired
	EntityManager entityManager;

	@Data
	@Entity
	@Table(name = "table_a")
	class Entity_A {
		@Id
		@GeneratedValue(strategy = GenerationType.IDENTITY)
		Long id;

		@OneToMany(cascade = CascadeType.ALL)
		@JoinColumn(name = "a_id")
		List<Entity_B> values = new ArrayList<>();
	}

	@Data
	@Entity
	@Table(name = "table_b")
	class Entity_B {
		@Id
		@GeneratedValue(strategy = GenerationType.IDENTITY)
		Long id;

		@Column(name = "a_id")
		Long entity_A_ID;
	}

	@Test
	@Rollback
	void test() {
		entityManager.createNativeQuery("create table public.table_a (id serial8 primary key)").executeUpdate();
		entityManager.createNativeQuery("create table public.table_b (id serial8 primary key, a_id bigint references public.table_a (id))").executeUpdate();

		var a = new Entity_A();
		var b = new Entity_B();
		a.values.add(b);

		entityManager.persist(a);
		System.out.println(a);

		ObjectMapper om = new ObjectMapper();

		entityManager.createNativeQuery("select * from public.table_b").getResultList().stream()
			.map(o -> {
				try {
					return om.writeValueAsString(o);
				} catch (JsonProcessingException e) {
					throw new RuntimeException(e);
				}
			})
			.forEach(System.out::println);

		entityManager.createNativeQuery("select * from public.table_b", Entity_B.class).getResultList().stream()
			.map(o -> {
				try {
					return om.writeValueAsString(o);
				} catch (JsonProcessingException e) {
					throw new RuntimeException(e);
				}
			})
			.forEach(System.out::println);
	}
}

and the output surprises:

I’m surprised that mapping works, it should throw an exception since the a_id column is duplicated for table_b and is not mapped as insertable = false, updatable = false. What version of Hibernate are you using?

I use 6.3.1.Final
a_id is not duplicated :wink:

The a_id column is mapped twice: once in Entity_A, through the values association’s @JoinColumn(name="a_id"), and another time in Entity_B itself with the entity_A_ID property @Column(name="a_id"). This mapping is duplicated, and either the join-column or the property column should have insertable = false, updatable = false. If this mapping does not trigger an error even with the latest Hibernate version (6.4), then I would think this is a bug.

I think the correct mapping should be something like @beikov suggested, using the mappedBy member of the #OneToMany annotation.

If that doesn’t work either, please try creating a test case that reproduces the problematic behavior you’re experiencing and attach it to a new ticket in our issue tracker, and also mention this duplicated mapping problem, so we can look into it.

1 Like

You have to set both sides of the association e.g. the program/programId as well. When you persist an entity with Hibernate ORM, it is added to the persistence context as is and does not setup bidirectional associations for you. Any following queries that return that entity will return the object as it is contained in the persistence context. So if you don’t set the association on both sides, you will see a null.

You can enable bidirectional association management when using bytecode enhancement, but this is a different programming model (your application needs to know this to avoid stack overflow errors), which is why this is not the default behavior.