@OneToOne relation mappedBy @Any?

Perhaps a strange modelling question: I would like to define a OneToOne relation mapped by an Any relation. I’m modelling CRUD Actions, where Creation and Update each have one Stuff attached, but Read has many Stuffs. This means I can’t just put the foreign keys in the Action tables. What I had in mind would look like this:

@Entity
public class ActionCreate extends Action {
	@OneToOne(mappedBy = "action")
	private Stuff stuff;
}

// ActionUpdate like ActionCreate

@Entity
public class ActionRead extends Action {
	@OneToMany(mappedBy = "action")
	private Set<Stuff> stuffs = new HashSet<Stuff>();
}

@Entity
public class Stuff {
	@Any
	@AnyDiscriminator(DiscriminatorType.STRING)
	@AnyDiscriminatorValue(discriminator = "ActionCreate",entity = ActionCreate.class)
	@AnyDiscriminatorValue(discriminator = "ActionRead", entity = ActionRead.class)
	@AnyDiscriminatorValue(discriminator = "ActionUpdate", entity = ActionUpdate.class)
	@AnyKeyJavaClass(Long.class)
	@Column(name = "action_type")
	@JoinColumn(name = "action_id")
	private Action action;
}

Hibernate doesn’t support this at the moment, but should it? I mean, is that a reasonable feature? OneToMany mapped by Any is supported now (thanks for that by the way :grin:), but I have no idea if implementing this would be at all similar.

Either way, for the time being it seems that I need something like Mapping unidirectional one-to-one relations correctly, I’ll also ask there.

I don’t think that @Any is the “right” target for a @OneToOne(mappedBy = "..."), because it has no unique constraint on (action_type, action_id). You can’t map that constraint though, since you use the Stuff#action relationship also for your @OneToMany, so in your particular use case, this would IMO not make sense or be unsafe.

You could map it differently though, if you don’t care about safety:

@Entity
public class ActionCreate extends Action {
	@MapsId
	@OneToOne
	@JoinColumn(name = "id", referencedColumnName = "action_id")
	private Stuff stuff;
}

I would stick to mapping it with an explicit FK join column though:

@Entity
public class ActionCreate extends Action {
	@OneToOne
	@JoinColumn(name = "stuff_id")
	private Stuff stuff;
}

I had started a discussion on the subject on Zulip:

(29001) #hibernate-user > @OneToOne(mappedby) with a @Any KO - Hibernate - Zulip

Indeed, this relation doesn’t feel very clean. It was first defined in Laravel/PHP, which has higher general YOLO-levels; if you want safety you often have to implement it yourself.

I tried setting the referencedColumnName as you suggested, that would be a good solution. But the executed queries still show (paraphrasing): [...] LEFT JOIN stuff ON action_create.id = stuff.id [...] . That appears to be this bug. Also, I took a dive in the source with a debugger, and IIUC toOne#referenceToPrimaryKey is often (always?) true for OneToOne relations without a mappedBy specified.

So there’s a bug here, but I can understand that it’s not exactly high priority. If it’s hard to fix cleanly, would a warning be a good idea, if the user tries this unsupported combination?

--

@beikov : do I understand your second proposal correctly as a foreign key in the ActionCreate table pointing to its particular Stuff? Cleaner indeed, but that’s not going to work for the PHP side of our application, which uses the same database. Wouldn’t it complicate the Stuff#action relationship as well, with possibly redundant information in the database?

--

Thanks @boutss for the link (first time I see Zulip in action, looks nice). I see you’re wrestling with your share of polymorphic relations yourself, good to know i’m not alone! :sweat_smile:

If you want to ensure that an ActionCreate can only have a single Stuff associated through this dedicated relationship, then yes, having an FK column there would be better IMO.

Now that I’m thinking more about it, maybe a unique constraint like alter table stuff add unique constraint stuff_action_create (case when action_type = 'ActionCreate' then action_id end) would be good enough to make @OneToOne target @Any safe…

If you add this unique constraint, you should be able to map this way:

@Entity
public class ActionCreate extends Action {
	@MapsId
	@OneToOne
	@JoinColumn(name = "id", referencedColumnName = "action_id")
	private Stuff stuff;
}

Hm, I have the impression that our database (Mariadb/Mysql) doesn’t support the case when construction. But I could define an index with a generated column, more or less as follows. NULL values are ignored for unique-indices, so it still works for ActionRead#stuffs.

ALTER TABLE stuff
ADD COLUMN `action_type_ind` char(42) GENERATED ALWAYS AS
 (IF(`action_type` in ('ActionCreate','ActionUpdate'),`action_type`,NULL)) VIRTUAL,
ADD UNIQUE `action_type_ind` (`action_type_ind`, `action_id`);

Even with @mapsId, the query still joins Stuff on its id instead of action_id. Also, I didn’t mention earlier: I added @SQLRestriction(value = “action_type = ‘ActionCreate’”) on ActionCreate#stuff, but that is also not honoured. Possibly for the same reason the referencedColumnName is ignored?

Unless you have another trick up your sleeve, I’ll probably use the hack I mentioned in the linked thread. But the unique constraint would still be useful, so that’s something.

If the referenceColumnName is ignored, that would be a bug, but HHH-18495 seems to suggest that this works. I’d appreciate if you could create a reproducer for this and attach it to a new bug report.

Alternatively, you could also map it as @ManyToOne, even though that’s not totally correct, the unique constraint will protect your data.

This is a known limitation. You can track HHH-2737 if you want to be notified, once we add support for this.