Hibernate: OneToOne: two fields of the same type

Viewed 687

Suppose I have a Person entity and an Animal entity; A Person can have two favourite animals and an Animal can only have one Person that likes them (one person liking an animal makes it no longer possible for other people to like/see that animal). It is a @OneToOne mapping: for the two Animal fields in Person and for the two Person fields in Animal. However, in Animal firstPerson should == to secondPerson. Is there a way to do the following with only one Person field in the Animal class?

Person.java:

@Entity
@SequenceGenerator(name="PERSON_SEQ", sequenceName="person_sequence")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="PERSON_SEQ")
    private Long id;

    @Column
    private String name;

    public Person()  {}

    @OneToOne
    @JoinColumn(name = "firstAnimal")
    private Animal firstAnimal;
    @OneToOne
    @JoinColumn(name = "secondAnimal")
    private Animal secondAnimal;

    //getters and setters

Animal.java:

@Entity
@SequenceGenerator(name="PRIVATE_SEQ", sequenceName="private_sequence")
public class Animal {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="PRIVATE_SEQ")
    private Long id;

    @Column
    private String name;

   @OneToOne(mappedBy = "firstAnimal")
   private Person firstPerson;

   @OneToOne(mappedBy = "secondAnimal")
   private Person secondPerson;
...
5 Answers

You might want to use @OneToMany / @ManyToOne in this scenario for a perpetual solution.

Person.java:

@Entity
@Table(name = "...")
public class Person {

    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name="person_id")
    private Long id;

    @Column(name="...")
    private String name;

    @OneToMany(mappedBy = "owner", cascade = CascadeType.?)
    private List<Animal> animals;
    
}

Animal.java:

@Entity
@Table(name = "...")
public class Animal {
    @Id
    @GeneratedValue(generator = "uuid")
    @GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
    @Column(name = "...")
    private Long id;

    @Column(name = "...")
    private String name;

    @ManyToOne
    @JoinColumn(name="columnName", referencedColumnName="person_id")
    private Person owner;

}

For further constrains such as one person can only have maximum 2 favourite animals or one animal can only be owned by 1 person, you can have them checked using code logic.

I don't think there is an easy way to implement this bidirectional mapping with a OneToOne (as the others pointed out, this is not really a one-to-one association). Even if there was, I would argue your model would not be readable or easy to understand given the domain model requirements: when you say "an Animal can only have one Person that likes them", we shouldn't expect to have two Person fields in the Animal class, we should expect simply one.

You can drop the bidirectional mapping and opt for a simple unidirectional association for each Animal in the Person:

@Entity
@SequenceGenerator(name="PERSON_SEQ", sequenceName="person_sequence")
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="PERSON_SEQ")
    private Long id;

    @Column
    private String name;

    public Person()  {}

    @OneToOne
    @JoinColumn(name = "firstAnimal")
    private Animal firstAnimal;

    @OneToOne
    @JoinColumn(name = "secondAnimal")
    private Animal secondAnimal;

    ...
}

@Entity
@SequenceGenerator(name="PRIVATE_SEQ", sequenceName="private_sequence")
public class Animal {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="PRIVATE_SEQ")
    private Long id;

    @Column
    private String name;

    @OneToOne
    @JoinColumn(name = "person")
    private Person person;

    ...
}

The short answer is that JPA and Hibernate aren't designed to do what you are asking. There is no way to configure this via annotations and mappings.

The longer answer is that you can kind of get the behavior that you are looking for using a combination of transient fields and database configuration...

If you mapped 2 different one-to-one fields, you could add a transient field inside of the Animal that checks first one field and then the other. If the first field is not-null, return it. Otherwise, return the second field (no need for a second null-check).

If you want to enforce that an Animal is only liked by one person, there is also no way to represent this via standard JPA annotations or via hibernate-specific annotations. It would be possible to add some check-constraints, triggers, and indexes that would guarantee the uniqueness of "like" per Animal, but you don't get that out-of-the-box with any of the annotations.

Like everyone has said - If you have one person to an animal, but multiple animals to a person it cannot be a one-to-one relationship. That is a two-to-one relationship.

If the animals first person is person a, and their second person is person a, then both connectios in person a have been used too.

Alternatively, you could force that second person is null if first person is set - but then just make it a many to 1 relationship like it should be ;)

That design in both the database or OOP paradigm is not scalable at all. In the domain of reality you could have as many columns in the db or fields in your entity as there are animals a person has regardless of the type (favorite).

In databases you are not complying with even the first normal formal. Also in terms of cardinalities you always have 0, 1 or n. There is no such thing as 2, 2 is n.

You could redesign your model thinking about relating the person with a list of animals with the annotation @OneToMany

In Kotlin it would be:

@OneToMany(cascade = [CascadeType.ALL])
@JoinColumn(name = "animal_id", referencedColumnName = "animal_id")
val animals: List<Animal>

On the other hand, you can add an enum to the animal that represents the type.

enum class TypeAnimal {
        FAVOURITE
}

GL

Related