Mapping unidirectional one-to-one relations correctly

I have what I thought was a pretty basic database schema and yet it seems I cannot model it correctly in Hibernate.

I’ve searched this forum and found an old thread discussing something similar, 0 answers.
I searched the web and hibernate’s issue tracker, I only found out what does not work but no solution.

So any help would be appreciated.

My database-schema, witten as a postgresql flyway script:

create table parent (
    id uuid primary key
)

create table child (
    id uuid primary key,
    parent_id uuid not null reference parent (id)
)

My current approach as entities, which do not quite work as they are:

@Entity
class Parent {
    @Id
    private UUID id;
    @OneToOne(cascade = ALL)
    private Child child;
}

@Entity
class Child {
    @Id
    private UUID id;
    private UUID parentId;
}

The issue is saving a parent with a child does not work: Hibernate attempts to save the child before it saves the parent, leading to my db throwing an exception, as it should.

Now, I know one, maybe 2 ways to convince hibernate to save the parent first:

  1. I could replace the field child.parentId with parent.childId.
  • Pros: Java-model can stay as-is (minor changes to mapping annotations perhaps)
  • Cons: I have use cases where other entities want to join the child by parentId. In this version I would need to join other entity -> parent -> child which seems quite uncessary
  1. model the relation in java as a bidirectional relation
  • Pros: I can join other entity -> child directly
  • Cons: Bidirectional relations are annoying to manage. I have 0 actual use cases for the field child.parent but a bunch of extra work for the sole purpose of convincing Hibernate to save the parent, child entities in the correct order.

So I’m looking for a way to do this correctly, ideally without a) turning the relation bidirectional and b) moving the foreign key from the child to the parent table in the db. Alternatively, an explanation for why it does not work / is a bad idea would be appreciated also.

The Parent entity does not have a foreign key column, so you must use mappedBy. Either referring to an association:

@Entity
class Parent {
    @Id
    private UUID id;
    @OneToOne(mappedBy = "parent", cascade = ALL)
    private Child child;
}

@Entity
class Child {
    @Id
    private UUID id;
    @OneToOne(cascade = ALL)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

Or to a field that maps the id:

@Entity
class Parent {
    @Id
    private UUID id;
    @OneToOne(mappedBy = "parentId", cascade = ALL)
    private Child child;
}

@Entity
class Child {
    @Id
    private UUID id;
    private UUID parentId;
}

Note though that for this mapping to be 100% correct, you would have to mark the parent_id column as unique.

Thanks for the quick response :slight_smile:

Something in the style of the second option is what I am after. And in fact for some unidirectional @OneToMany associations I have your proposal is exactly how I do it and it works.
However, when I try to use mappedBy = "<foreign key field name>" with @OneToOne associations I always end up with this exception:

Association ‘com.example.onetoone.Parent.child’ is ‘mappedBy’ a property named ‘parentId’ of the target entity type ‘com.example.onetoone.Child’ which is not a ‘@OneToOne’ or ‘@ManyToOne’ association

I had understood the exception message to mean, basically, “you cannot just reference a field, you need to make it bidirectional and reference the other side”. But I would like to avoid making the relation bidirectional, so I’ve been looking for other solutions.
That said, maybe I’m just not doing it right? If there is a way to make it work via mappedBy referencing the foreign key in the Child table then that’d be great :slight_smile:

My current classes / schema

Entities:

@Entity
@NoArgsConstructor
@AllArgsConstructor
class Parent {
	@Id
	private UUID id;

	@OneToOne(mappedBy = "parentId", cascade = CascadeType.ALL)
	private Child child;
}

@Entity
@NoArgsConstructor
@AllArgsConstructor
class Child {
	@Id
	private UUID id;

	private UUID parentId;
}

DB:

-- h2 dialect as the sample project I threw together to run the simplified example uses h2
create table parent
(
    id uuid primary key
);

create table child
(
    id        uuid primary key,
    parent_id uuid unique not null,
    foreign key (parent_id) references parent (id)
);

Testing setup:

@SpringBootTest
class OneToOneApplicationTests {

	@Autowired
	private EntityManager entityManager;

	@Test
	@Transactional
	void contextLoads() {
		Session session = entityManager.unwrap(Session.class);
		UUID parentId = randomUUID();
		UUID childId = randomUUID();
		session.persist(new Parent(parentId, new Child(childId, parentId)));

		session.flush(); // this is where it errors 
	}

}

This was fixed in recent Hibernate ORM version. Please upgrade to the latest version e.g. 6.6.0.Final

Was really excited to hear that, just got around to try it out but sadly no changes :frowning:

Did the whole gradle clean, clean rebuild in the IDE, check the dependency graph really shows 6.6.0.Final but all to no use. Even on 6.6.0.Final I still get the above error when attempting to point OneToOne.mappedBy to a foreign key that is not annotated with @OneToOne ifself (because it’s unidirectional).

Well, seems I mixed that up with something else. It works for @OneToMany though.
You should be able to annotate it like this though:

@Entity
class Parent {
    @Id
    private UUID id;
    @OneToOne(cascade = ALL)
    @JoinColumn(name = "id", referenceColumnName = "parent_id", insertable = false, updatable = false)
    private Child child;
}

@Entity
class Child {
    @Id
    private UUID id;
    private UUID parentId;
}

Doing it via @JoinColumn is one more option that looks like it should work yet does not :frowning: Instead hibernate attemps to save the child first again. Approximately this works for @OneToMany though.

The fact that all of this works with @OneToMany always makes me think it should work with @OneToOne, somehow, and I’ve just not found the right way to do it yet…

Please try to create a reproducer with our test case template and if you are able to reproduce the issue, create a bug ticket in our issue tracker and attach that reproducer.

The last thing that you could try is also annotate @MapsId on the @OneToOne

Tried @MapsId with no luck either. But I’ve finally taken the time to build a reproducer using your template and opened a bug issue.

Thanks for your time and patience :slight_smile:

1 Like