Search code examples
javahibernatejoinmany-to-manyentity

Join entity in many to many not being persisted after applying "@Transient" to the corresponding entities and the join entity


the main idea of my setup is to manage the many to many relationship via repositories. That is why, in the following setup, all the relations are annotated as @Transient, since those sets should only be populated "manually", instead of letting Hibernate do that.

@Entity
@Table(name = "house")
public class House {
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private long id;

    @Transient
    private Set<Owner> owners;
}

@Entity
@Table(name = "owner")
public class Owner {
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private long id;

    @Transient
    private Set<House> houses;
}

public class HouseIdOwnerId implements Serializable {
    private long houseId;
    private long ownerId;

    public HouseIdOwnerId(final long houseId, final long ownerId) { /* ... */ }

    @Override
    public boolean equals(final Object other) { /* ... */ }
    
    @Override
    public int hashCode() { /* ... */ }
}

@Entity
@IdClass(HouseIdOwnerId.class)
@Table(name = "house_owner")
public class HouseOwner{
    @Column(name = "house_id")
    @Id
    private long houseId;

    @Column(name = "owner_id")
    @Id
    private long ownerId;

    @Transient
    private House house;

    @Transient
    private Owner owner;

    @CreatedTimestamp
    private LocalDateTime createdAt;

    public HouseOwner(final House house, final Owner owner) {
        this.houseId = house.getId();
        this.house = house;

        this.ownerId = owner.getId();
        this.owner = owner;
    }

    // ...

    @Override
    public boolean equals(final Object other) { /* ... */ }
    
    @Override
    public int hashCode() { /* ... */ } 
}

As one would expect, when trying to persist the join entity, since its properties are marked as @Transient, nothing happens:

@Test
void testInsertRelated() {
    // ...
    this.entityManager.persist(house);
    this.entityManager.persist(owner);
    this.entityManager.persist(owner2);

    final HouseOwner ho = new HouseOwner(house, owner);
    final HouseOwner ho2 = new HouseOwner(house, owner2);

    this.entityManager.persist(ho);
    this.entityManager.persist(ho2);
}

The reason I didn't do @OneToMany relationships in the dependent entities is because they would end up with ugly Set<HouseOwner> houses and Set<HouseOwner> owners properties, that of course would get ignored when persisting the main entities.

So, imagine that a request comes which creates a new house that is going to get associated with multiple owners. You would need to, somehow, map the incoming owner IDs to multiple HouseOwner objects, so these need some way of being created without the House part, because it is not yet created. Therefore, you end up with an ugly HouseOwner object.


Summarizing, I think I am mixing up concepts here. However, is it possible to do what I am trying above? Thank you in advance.


Solution

  • I'm not sure if I understood the question correctly. Why don't you use many-to-many relationships? Is it a design choice? Let me try to answer in different flavors.

    Using many-to-many relationships

    When using the 'many-to-many' relationships, you shouldn't create entity classes for join tables. In your example, the class HouseOwner shouldn't be coded. Your model contains two entities: House and Owner. The way they interact with each other is through a many-to-many relationship. Once you define this relationship in your mappings, the JPA engine will derive the join table automatically (you can use @JoinTable mapping if the defaults doesn't fit you). The code would look like this:

    @Entity
    @Table(name = "house")
    public class House {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @ManyToMany
        @JoinTable(name = "house_owner", joinColumns = {
            @JoinColumn(name = "house_id")},
                inverseJoinColumns = @JoinColumn(name = "owner_id"))
        private Set<Owner> owners;
    }
    
    @Entity
    @Table(name = "owner")
    public class Owner {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @ManyToMany(mappedBy = "owners") /* you must elect a relationship owner */
        private Set<House> houses;
    }
    

    And that's all you need to define your entities and their relationships. You can see the JPA is expecting the existence of a third table by enabling the jakarta.persistence.schema-generation.scripts.action property in your persistence.xml. Sniffing the created script, you would see something like this:

    create table house (id bigint generated by default as identity, primary key (id));
    create table house_owner (house_id bigint not null, owner_id bigint not null, primary key (house_id, owner_id));
    create table owner (id bigint generated by default as identity, primary key (id));
    alter table if exists house_owner add constraint FK96rb7vss0wg6qgwkbfmt4iw9w foreign key (owner_id) references owner;
    alter table if exists house_owner add constraint FKcb77jw3le78e333bx246ln3ap foreign key (house_id) references house;
    

    Using one-to-many relationships

    One issue with the previous approach, as you mentioned in your comment, is that it isn't possible to map the LocalDateTime createdAt field in the house_owner join table. One way around this problem is to use one-to-many mappings on House and Owner entities. The code would look like this:

    @Entity
    @Table(name = "house")
    public class House {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @OneToMany(mappedBy = "house")
        private Set<HouseOwner> owners;
    }
    
    @Entity
    @Table(name = "owner")
    public class Owner {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @OneToMany(mappedBy = "owner")
        private Set<HouseOwner> houses;
    }
    
    @Entity
    @Table(name = "house_owner")
    public public class HouseOwner {
    
        @Id
        @ManyToOne
        @JoinColumn(name = "house_id")
        private House house;
    
        @Id
        @ManyToOne
        @JoinColumn(name = "owner_id")
        private Owner owner;
    
        private LocalDateTime createdAt;
    }
    

    Again, enabling the jakarta.persistence.schema-generation.scripts.action property and looking at the generated script, you'll see:

    create table house (id bigint generated by default as identity, primary key (id));
    create table house_owner (createdAt timestamp(6), house_id bigint not null, owner_id bigint not null, primary key (house_id, owner_id));
    create table owner (id bigint generated by default as identity, primary key (id));
    alter table if exists house_owner add constraint FKcb77jw3le78e333bx246ln3ap foreign key (house_id) references house;
    alter table if exists house_owner add constraint FK96rb7vss0wg6qgwkbfmt4iw9w foreign key (owner_id) references owner;
    

    It's pretty much the same output as the previous example! But, this way, you have more control over the house_owner table.

    Using transient annotations

    If yet you want to use @Transient in your modeling, I can only imagine that you don't want the JPA engine doing the hard work for you. But, if you must, you can do the following:

    @Entity
    @Table(name = "house")
    public class House {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @Transient
        private Set<Owner> owners;
    }
    
    @Entity
    @Table(name = "owner")
    public class Owner {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;
    
        @Transient
        private Set<House> houses;
    }
    
    @Entity
    @Table(name = "house_owner")
    public class HouseOwner {
    
        @Id
        @Column(name = "house_id")
        private long houseId;
        @Id
        @Column(name = "owner_id")
        private long ownerId;
    
        @Transient
        private House house;
        @Transient
        private Owner owner;
    }
    

    Once more, sniffing the created script by enabling jakarta.persistence.schema-generation.scripts.action property, you would see something like this:

    create table house (id bigint generated by default as identity, primary key (id));
    create table house_owner (owner_id bigint not null, house_id bigint not null, primary key (house_id, owner_id));
    create table owner (id bigint generated by default as identity, primary key (id));
    

    As you can see, the same three tables - but this time without the foreign keys. This means that JPA will ignore any relationship between your entities.