2

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;
...
parsecer
  • 4,758
  • 13
  • 71
  • 140
  • Why do you want to have both `OneToOne` as bidirectional if as it stated in your question both of these references should refer to the same `Person`? – SternK Sep 17 '20 at 11:20
  • @SternK Then what is the alternative? – parsecer Sep 23 '20 at 04:23
  • _"A Person can have **two** favourite animals [...] It is a @OneToOne mapping"_ → This is clearly a contradiction. But if you cannot see why, think about this: mappings reflect the relationships between _entities_, not between specific fields. If **one** `Person` relates to **more than one** ("many") `Animal`s, i.e. if `SELECT * FROM ANIMAL WHERE FIRSTPERSON = xyz` _may_ (and usually _will_) return "many" rows, then the relationship between `Person` and `Animal` is **One to Many**. (I won't explain how to reflect a @OneToMany relationship, since you already commented you know how.) – walen Sep 23 '20 at 08:31
  • But in my example `SELECT * FROM ANIMAL WHERE FIRSTPERSON = xyz` would return 1 row. Say, the `animals` table would have two columns - `firstperson` and `secondperson`. If a particular animal with id `4` has a person `57` as `firstperson` then no other animal can have that person as the `firstperson`. At the same time the `57` person would have the animal `4` in the `firstanimal` column, – parsecer Sep 29 '20 at 03:13
  • @walen and no other person would have that animal as the `firstanimal`: `select * from persons where firstanimal = 4` would return one row with id = 57 and `select * from animals where firstperson = 57` would return one row with id = 4 – parsecer Sep 29 '20 at 03:14
  • @parsecer Yes, sorry, the actual query would be `SELECT * FROM ANIMAL WHERE PERSON = xyz` because you wouldn't have `firstPerson`and `secondPerson` anymore, since it's always one and the same Person. | _"no other person would have that animal as the firstanimal"_ → Cool, that's the "one" in **One**ToMany. The question is, would the same person have more than one animal? Because that's the "many" in OneTo**Many**. – walen Sep 29 '20 at 06:39
  • @walen `because you wouldn't have firstPersonand secondPerson anymore, since it's always one and the same Person` how come? I have two columns - columns `firstperson` and `secondperson`. `The question is, would the same person have more than one animal? Because that's the "many" in OneToMany.` If an animal is taken as `firstanimal` by some person, **other persons can't have that animal as first animal.** At the same time that person is the animal's `firstperson` and that person **also** can't be taken as `firstperson` **by other animals**. So it's mutually one-to-one – parsecer Sep 29 '20 at 16:50

5 Answers5

4

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.

Hoang Vu
  • 66
  • 2
2

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;

    ...
}
M A
  • 71,713
  • 13
  • 134
  • 174
2

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.

Nathan
  • 1,576
  • 8
  • 18
1

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 ;)

Watachiaieto
  • 417
  • 3
  • 10
0

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

Braian Coronel
  • 22,105
  • 4
  • 57
  • 62