How to map @OneToOne by multiple columns

Hi,

I have the following DB schema:

which I’ve tried to model in Hibernate this way (irrelevant parts left out):

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class ProblematicQuery extends AbstractVersionable<Long> {

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

	@ManyToOne(optional = false, fetch = FetchType.LAZY)
	@JoinColumn(name = "tenant_id", nullable = false)
	private TenantEntity tenant;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumns(value = {
			@JoinColumn(name = "tenant_id", referencedColumnName = "tenant_id", insertable = false, updatable = false, nullable = false),
			@JoinColumn(name = "query", referencedColumnName = "query", insertable = false, updatable = false, nullable = false),
	}, foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
	private Judgment judgment;

@Entity
@AllArgsConstructor
public class Judgment extends AbstractAuditingEntity {

	@OneToOne(mappedBy = "judgment")
	private ProblematicQuery problematicQuery;

Note that ProblematicQuery is the owner and I need set insertable = false, updatable = false in the join columns in order to prevent multiple column mapping errors. However, in my business logic I’d like to do something like this:

@Transactional
public Optional<JudgmentDTO> updateJudgment(long problematicQueryId, JudgmentDTO judgmentDTO) {
		return problematicQueryRepository.findById(problematicQueryId).map(pq -> {
			var judgment = pq.getJudgment();
			if (judgment != null) {
				judgment.setApproved(judgmentDTO.approved());
			}
			else {
				// This is the problem!
				judgment = judgmentRepository.save(new Judgment(null, pq.getQuery(), judgmentDTO.approved(), pq, pq.getTenant(), null));
				pq.setJudgment(judgment);
			}
			return judgmentMapper.toDTO(judgment);
		});
	}

Please note the problem: Even though in a transaction, I need to save the judgment via repository. Even worse,I do get the following warning HHH000502: The [judgment] property of the [io.searchhub.domain.ProblematicQuery] entity was modified, but it won't be updated because the property is immutable. I do get why this is logged, but I noticed that I need to call the setter so that the Hibernate Cache is not out of sync.

Is there a better way in Hibernate to model my relation? Basically, I need the judgment independent from the ID of a ProblematicQuery as I will regularly flush the problematic query table and re-index it, and I want to keep judgments for the same query if existing. I know that I can circumvent the problem in my business logic by loading the judgment instead of the problematic query directly, but that not what I want.

Help appreciated, thanks.

Why don’t you model it this way?

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class ProblematicQuery extends AbstractVersionable<Long> {

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

	@ManyToOne(optional = false, fetch = FetchType.LAZY)
	@JoinColumn(name = "tenant_id", nullable = false, insertable = false, updatable = false)
	private TenantEntity tenant;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumns(value = {
			@JoinColumn(name = "tenant_id", referencedColumnName = "tenant_id", nullable = false),
			@JoinColumn(name = "query", referencedColumnName = "query", nullable = false),
	}, foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
	private Judgment judgment;

@Entity
@AllArgsConstructor
public class Judgment extends AbstractAuditingEntity {

	@OneToOne(mappedBy = "judgment")
	private ProblematicQuery problematicQuery;

That way, setting the judgment on the query will set both columns.

I’ve tried it but this makes other thinks more complicated as I need to access the query field of the ProblematicQuery, e.g. in some specification queries:

spec = spec.and((root, q, cb) -> cb.like(cb.lower(root.get(ProblematicQuery_.query)), "%" + query.toLowerCase() + "%"));

This one can be solved by a join, I guess but I also have projections like this:

 problematicQueryRepository.findBy(Specification.where(builderStep.build()), q -> q.project(ProblematicQuery_.ID, ProblematicQuery_.QUERY, ProblematicQuery_.RANKING)
				.as(ProblematicQueryDTO.class)
				.page(pageable));

where I don’t know how to project the query column then … I’ve also tried to use @Embeddable but it didn’t work out and I’m not sure if this would make sense for my use case anyways.

Then add the query also:

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class ProblematicQuery extends AbstractVersionable<Long> {

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

	@ManyToOne(optional = false, fetch = FetchType.LAZY)
	@JoinColumn(name = "tenant_id", nullable = false, insertable = false, updatable = false)
	private TenantEntity tenant;

	@Column(name = "query", nullable = false, insertable = false, updatable = false)
	private String query;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumns(value = {
			@JoinColumn(name = "tenant_id", referencedColumnName = "tenant_id", nullable = false),
			@JoinColumn(name = "query", referencedColumnName = "query", nullable = false),
	}, foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
	private Judgment judgment;

@Entity
@AllArgsConstructor
public class Judgment extends AbstractAuditingEntity {

	@OneToOne(mappedBy = "judgment")
	private ProblematicQuery problematicQuery;