Data Modeling: Many-to-Many Associations

Data modeling is a core aspect of designing effective applications that handle complex relationships between entities. In many scenarios, the need arises to represent associations where multiple instances of one entity are related to multiple instances of another. For example, a veterinarian may have multiple specialties, and each specialty could be associated with multiple veterinarians. This is known as a many-to-many association.

In a many-to-many relationship, a joining table is used to manage these connections. This table holds the primary keys of both related entities and can optionally include additional columns to store supplementary data.

Depending on the complexity of the relationship and whether extra fields are needed in the joining table, a many-to-many association can be modeled in two primary ways: directly between the entities or through an intermediary entity that adds context and attributes. In this guide, we will explore both approaches, providing practical examples to illustrate their implementation in a Jmix application.

Requirements

If you want to implement this guide step by step, you will need the following:

  1. Setup Jmix Studio

  2. Download the sample project. You can download the completed sample project, which includes all the examples used in this guide. This allows you to explore the finished implementation and experiment with the functionality right away.

Alternatively, you can start with the base Jmix Petclinic project and follow the step-by-step instructions in this guide to implement the features yourself.

What We are Going to Build

In this guide, we will enhance the Jmix Petclinic example to demonstrate different use cases of many-to-many associations. Specifically, we will cover the following use cases:

  • VeterinarianSpecialty: Modeled as a unidirectional many-to-many association, where each veterinarian can have multiple specialties without a reciprocal reference from Specialty.

  • Visit ←→ Specialty: Modeled as a bidirectional many-to-many association with Visit as the owning side, allowing each visit to track multiple specialties, and each specialty to reference the visits in which it is involved.

  • Visit ←→ TreatmentRoom: Modeled as a bidirectional many-to-many association with both sides as owning sides, allowing visits and treatment rooms to independently manage their associations.

  • Pet ←→ InsuranceProvider: Modeled as an indirect many-to-many association with an explicit entity that stores the validity range, allowing each pet to be insured by multiple companies with defined coverage periods.

Introduction

In a many-to-many association, records in one entity are related to multiple records in another entity, and vice versa. This type of relationship is common in applications and can be configured in various ways depending on the requirements of the business logic and data model.

But before we dive into the different types of many-to-many associations, let’s look briefly how Jmix by default represents a many-to-many association in the UI. Below, you can see the Treatment Room Detail View, which shows the list of associated Visits for a treatment room, allowing users to manage these associations using "Add" and "Remove" buttons.

Treatment Room Detail View with Visits Management
Figure 1. Treatment Room Detail View

Database Representation

In the database, a many-to-many relationship between two entities (like Visit and Treatment Room) is managed through a join table: PETCLINIC_VISIT_TREATMENT_ROOM_LINK. This join table contains two foreign key columns, VISIT_ID and TREATMENT_ROOM_ID, linking entries from the VISIT table to corresponding entries in the TREATMENT_ROOM table.

This approach ensures that multiple VISIT records can reference multiple TREATMENT_ROOM records and vice versa, supporting the flexibility needed for scenarios where visits span different treatment rooms. Below is a diagram illustrating this relationship and its representation in the database:

Database Representation of Visit and Treatment Room
Figure 2. Database Representation of Visit and Treatment Room Many-To-Many Association

Dimensions

There are three dimensions that categorize how many-to-many associations are managed on the application level. Let’s briefly examine these dimensions before diving into specific examples in the following sections.

  1. Direction: Unidirectional vs. Bidirectional Many-to-Many Association: This dimension addresses the accessibility of the association on the Java/JPA level. A unidirectional association allows navigation from only one entity to the other, whereas a bidirectional association enables navigation from both sides.

  2. Ownership: Ownership of the Many-to-Many Association: Ownership determines which entity manages the join table to update the records. The owning side is responsible for creating and removing relationships, while the non-owning (inverse) side simply reflects the association without directly controlling it.

  3. Mapping: Direct vs. Indirect Many-to-Many Association: This dimension distinguishes between direct associations, where the two entities are directly connected without an intermediate mapping entity, and indirect associations, where a separate mapping entity represents the relationship.

All of those dimensions have an impact on how data is accessible and managed on the Java/JPA level. But they all share a common structure at the database level. Every many-to-many association, regardless of its configuration in JPA, involves a dedicated join table in the database that holds the foreign keys for each associated entity.

The following table summarizes the main characteristics of each dimension and provides examples of common configurations in the Petclinic application:

Dimension Type Description Example

Direction ↦ From which entity/entities is traversal allowed?

Unidirectional

Only one entity references the other, without a reciprocal reference.

VeterinarianSpecialty

Bidirectional

Both entities reference each other, enabling navigation from both sides.

Visit ↦↤ Specialty

Ownership ↣ Which entity manages the relationship in the association table?

Single Owning Side

One entity (the owning side) controls updates to the relationship, while the other is non-owning.

VisitSpecialty

Both Sides Owning

Both entities manage the relationship independently, each able to update the association.

Visit ↣↢ TreatmentRoom

Mapping ⇄ Is there a dedicated JPA entity for the association table?

Direct

No intermediate mapping entity; the association is directly configured between the entities.

VisitSpecialty

Indirect

Uses an intermediate mapping entity to represent the association, allowing for additional attributes.

PetInsuranceMembershipInsuranceProvider

These three dimensions are largely independent of each other and can be combined in various ways to suit different use cases. However, there is one exception: a unidirectional association implies a single owning side, as only one entity references the other.

Now, we will look into the different dimensions and explore with an example what impact it has on the data access as well as the UI layer.

Direction

The first dimensions is Direction. Direction can be unidirectional or bidirectional. A unidirectional association allows navigation from only one entity to the other, while a bidirectional association enables navigation from both entities.

Unidirectional

A unidirectional many-to-many association is set up on only one side of the relationship. In the Petclinic example, the Veterinarian entity holds a collection of Specialties, but Specialty does not have a reference back to Veterinarian. When the application only needs to navigate from Veterinarian to Specialty unidirectional associations are the easiest solution.

Data Model

In the Veterinarian entity, the many-to-many collection of Specialties is defined as follows:

Veterinarian.java
package io.jmix.petclinic.entity.veterinarian;

import io.jmix.core.DeletePolicy;
import io.jmix.core.entity.annotation.OnDelete;
import io.jmix.core.metamodel.annotation.JmixEntity;
import java.util.List;

import io.jmix.petclinic.entity.Person;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinTable;
import jakarta.persistence.Table;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToMany;

@JmixEntity
@Table(name = "PETCLINIC_VETERINARIAN")
@Entity(name = "petclinic_Veterinarian")
public class Veterinarian extends Person {

    @OnDelete(DeletePolicy.CASCADE)
    @JoinTable(
            name = "PETCLINIC_VET_SPECIALTY_LINK",
            joinColumns = @JoinColumn(name = "VETERINARIAN_ID"),
            inverseJoinColumns = @JoinColumn(name = "SPECIALTY_ID")
    )
    @ManyToMany
    private List<Specialty> specialties;

    public List<Specialty> getSpecialties() {
        return specialties;
    }

    public void setSpecialties(List<Specialty> specialties) {
        this.specialties = specialties;
    }
}

The Veterinarian entity defines a specialties attribute, marked with @ManyToMany, to establish a many-to-many association with Specialty. This allows each Veterinarian to have multiple Specialties (and the other way around).

The @JoinTable annotation is also used for the specialties attribute. This annotation plays a role in configuring a many-to-many association, but we will only explore it the next dimension about Ownership.

On the Specialty side, however, there is no association back to Veterinarian:

Specialty.java
@JmixEntity
@Table(name = "PETCLINIC_SPECIALTY")
@Entity(name = "petclinic_Specialty")
public class Specialty extends NamedEntity {

    // ... inherits attributes from NamedEntity, mainly "name"

}

Data Access

Now we can use JPQL to access the specialties of a veterinarian, as shown below:

JPQL Query Example for Unidirectional Many-to-Many
SELECT e FROM petclinic_Veterinarian e JOIN e.specialties s WHERE s.name = :specialtyName

It is possible to navigate from Veterinarian to Specialty using the specialties collection. However, attempting to query from Specialty to Veterinarian is not possible in JPQL because Specialty has no reference to Veterinarian.

The same is true on the Java level:

Accessing Specialties from the Veterinarian in Java
Veterinarian vet = //...
List<Specialty> specialties = vet.getSpecialties();

You can call getSpecialties() method on Veterinarian to retrieve the associated specialties. But since there is no equivalent method on Specialty to retrieve veterinarians, it is not possible to access them.

UI Representation

Unidirectional many-to-many associations are correctly interpreted in Jmix Studio when generating views. It will generate a section in the VeterinarianDetailView to manage this association. As seen in the beginning, by default Jmix Studio generates a child data grid on the detail view of the entity. But it is also possible to adjust the visual representation. In this case, we used a Multi-Select Combo Box Picker in this example. This adjustment is particularly useful when the number of selectable options is small, as is the case with Specialties. The dropdown simplifies the interface and provides a cleaner, more intuitive way to manage these associations.

Veterinarian Detail View with Specialties Section
Figure 3. Veterinarian Detail View

For more information about the Multi-Select Combo Box Picker, refer to the Jmix UI Samples: Multi-Select Combo Box Picker.

Now that we’ve examined the Veterinarian side of the relationship, let’s turn our attention to the Specialty side. Since this is a unidirectional association, the SpecialtyDetailView does not include any UI components for managing this association.

Specialty Detail View without Veterinarian Section
Figure 4. Specialty Detail View without Veterinarian Section

Bidirectional

When both directions of a relationship need to be accessible through code, we use a bidirectional many-to-many association.

In our example, a Visit can involve multiple Specialties, and each Specialty can be relevant for multiple Visits. This connection is there to represent the required specialties for each visit, specifying which expertise or treatments are necessary. This could be used, for example, to match a Visit with veterinarians who have the appropriate specialties needed for that specific visit.

Data Model

In the Visit entity, we define a collection of Specialties named requiredSpecialties, representing the specialties associated with each visit:

Visit.java
@JmixEntity
@Table(name = "PETCLINIC_VISIT", indexes = {
        @Index(name = "IDX_PETCLINIC_VISIT_ASSIGNED_NURSE", columnList = "ASSIGNED_NURSE_ID"),
        @Index(name = "IDX_PETCLINIC_VISIT_PET", columnList = "PET_ID")
})
@Entity(name = "petclinic_Visit")
public class Visit {

    @OnDelete(DeletePolicy.CASCADE)
    @JoinTable(
            name = "PETCLINIC_VISIT_SPECIALTY_LINK",
            joinColumns = @JoinColumn(name = "VISIT_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "SPECIALTY_ID", referencedColumnName = "ID")
    )
    @ManyToMany
    private List<Specialty> requiredSpecialties;

    public List<Specialty> getRequiredSpecialties() {
        return requiredSpecialties;
    }

    public void setRequiredSpecialties(List<Specialty> requiredSpecialties) {
        this.requiredSpecialties = requiredSpecialties;
    }
}

On the Specialty side, a collection of Visits is defined to enable navigation back to Visit.

Specialty.java
@JmixEntity
@Table(name = "PETCLINIC_SPECIALTY")
@Entity(name = "petclinic_Specialty")
public class Specialty extends NamedEntity {

    // ... inherits attributes from NamedEntity, mainly "name"

    @ManyToMany(mappedBy = "requiredSpecialties")
    private List<Visit> visits;

    public List<Visit> getVisits() {
        return visits;
    }

    public void setVisits(List<Visit> visits) {
        this.visits = visits;
    }
}

The visits field in Specialty includes a mappedBy attribute on the @ManyToMany association. This attribute relates to the concept of ownership, which we will explore in detail in the section on Owning Side vs. Non-Owning Side.

Data Access

With a bidirectional association, it’s possible to query from both directions. For instance, if we want to retrieve all Visits associated with a specific Specialty, we could use a JPQL query like this:

JPQL Query Example for Bidirectional Many-to-Many
SELECT v FROM petclinic_Specialty s JOIN s.visits v WHERE v.visitStart = :visitStart

On the Java level, the Visit entity can directly retrieve the required specialties for a given visit through the getRequiredSpecialties() method:

Java Access Example
Visit visit = // ...
List<Specialty> requiredSpecialties = visit.getRequiredSpecialties();

Similarly, it is also possible to access visits form a Specialty via Specialty::getVisits.

UI Representation

In a bidirectional many-to-many association, Jmix Studio generates UI components on both sides of the relationship. In our example, it will create data grids in both the VisitDetailView and the SpecialtyDetailView. This enables users to see the relationship from either side in the UI.

Visit Detail View with Specialties Section
Figure 5. Visit Detail View
Specialty Detail View with Visits Section
Figure 6. Specialty Detail View

Whether the relationship can be managed via "Add" and "Remove" buttons on the data grid depends on the ownership configuration, which we will explore in the next section on Owning Side vs. Non-Owning Side.

Owning Side

The concept of the owning side in a many-to-many association determines which entity controls updates to the relationship. The owning side is responsible for managing the entries in the join table, meaning any changes to the relationship—such as adding or removing associations—are reflected in the database. The entity designated as the owning side will include the @JoinTable annotation, while the non-owning (inverse) side uses the mappedBy attribute of the @ManyToMany annotation to indicate it relies on the owning side for relationship management.

Single Owning Side

With a single owning side, only one entity is responsible for managing updates to the join table. In our previous example of the many-to-many association between Visit and Specialty, we already saw this setup in action within the UI: Jmix generated components in both the VisitDetailView and the SpecialtyDetailView, allowing users to view the association from either side. However, we focused primarily on the association itself in that context.

Now, we’ll revisit this example with a specific focus on the concept of ownership. You may have noticed that only the Visit side allowed for active management of the relationship (e.g., adding or removing Specialties for a Visit). This difference in UI behavior occurs because Visit is designated as the owning side, while Specialty is the non-owning side. Let’s examine how this is defined in the Java code for both entities.

Visit.java
@JmixEntity
@Table(name = "PETCLINIC_VISIT", indexes = {
        @Index(name = "IDX_PETCLINIC_VISIT_ASSIGNED_NURSE", columnList = "ASSIGNED_NURSE_ID"),
        @Index(name = "IDX_PETCLINIC_VISIT_PET", columnList = "PET_ID")
})
@Entity(name = "petclinic_Visit")
public class Visit {

    @OnDelete(DeletePolicy.CASCADE)
    @JoinTable(
            name = "PETCLINIC_VISIT_SPECIALTY_LINK",
            joinColumns = @JoinColumn(name = "VISIT_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "SPECIALTY_ID", referencedColumnName = "ID")
    )
    @ManyToMany
    private List<Specialty> requiredSpecialties;

    public List<Specialty> getRequiredSpecialties() {
        return requiredSpecialties;
    }

    public void setRequiredSpecialties(List<Specialty> requiredSpecialties) {
        this.requiredSpecialties = requiredSpecialties;
    }
}

The @JoinTable annotation on the Visit side establishes Visit as the owning side of the association, specifying the join table PETCLINIC_VISIT_SPECIALTY_LINK that connects Visit and Specialty in the database.

The owning side characterstic is not explicitly configured via an Annotation or attribute, but based on the fact that we use @ManyToMany and @JoinTable together on one attribute. By specifying the @JoinTable on this side, we give JPA the information that this side "is taking care of the relationship".

The joinColumns attribute defines the foreign key column VISIT_ID in the join table, referencing the ID column in the Visit entity. The inverseJoinColumns attribute defines the foreign key column SPECIALTY_ID, linking it to the ID column in the Specialty entity.

Specialty.java
package io.jmix.petclinic.entity.veterinarian;

import io.jmix.core.metamodel.annotation.JmixEntity;
import io.jmix.petclinic.entity.NamedEntity;
import io.jmix.petclinic.entity.visit.Visit;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;

import java.util.List;

@JmixEntity
@Table(name = "PETCLINIC_SPECIALTY")
@Entity(name = "petclinic_Specialty")
public class Specialty extends NamedEntity {

    // ... inherits attributes from NamedEntity, mainly "name"


    @ManyToMany(mappedBy = "requiredSpecialties")
    private List<Visit> visits;

    public List<Visit> getVisits() {
        return visits;
    }

    public void setVisits(List<Visit> visits) {
        this.visits = visits;
    }

}

In the Specialty entity, we define a @ManyToMany association, but with a special attribute: mappedBy. It references the requiredSpecialties field on the Visit side.

Unlike the Visit entity, Specialty does not specify a @JoinTable, which designates it as the non-owning side of the association. The absence of the @JoinTable indicates to JPA, that this side has no control over the join table and thus does not manage the association.

Data Access

With a single owning side, data access for querying remains possible from both entities, as it is still a bidirectional relationship. The owning side designation only determines which entity manages updates to the join table; it does not affect the ability to query from either side.

To illustrate this, imagine a service called VisitSpecialtyService that includes methods for adding Specialties to a Visit (the owning side) and adding Visits to a Specialty (the non-owning side). Here’s how the service might look:

VisitSpecialtyService.java
@Service
public class VisitSpecialtyService {

    @Autowired
    private DataManager dataManager;

    // Adds multiple Specialties to a Visit
    public void addSpecialtiesToVisit(Visit visit, List<Specialty> specialties) {
        visit.getSpecialties().addAll(specialties);
        dataManager.save(visit);
    }

    // Attempts to add multiple Visits to a Specialty
    public void addVisitsToSpecialty(Specialty specialty, List<Visit> visits) {
        specialty.getVisits().addAll(visits);

        // WARNING: this call to save will not persist changes to the association
        // because Specialty is the non-owning side of the relationship.
        dataManager.save(specialty);
    }
}

In this example, the addSpecialtiesToVisit method will successfully persist the association, as Visit is the owning side. However, if you attempt to save the relationship from the non-owning side using addVisitsToSpecialty, the changes will not be saved to the database. JPA silently ignores modifications to associations from the non-owning side without raising an exception.

Attempting to update the relationship from the non-owning side, such as by calling addVisitsToSpecialty, will result in the changes not being persisted, without any error or warning.

This behavior is by design in JPA and requires careful consideration to ensure updates are made only from the designated owning side.

UI Representation

You may have noticed that in the SpecialtyDetailView screenshot above, the table displaying associated Visits lacked "Add" and "Remove" buttons. This is because Specialty is the non-owning side, meaning that it does not control the relationship and therefore Jmix Studio does not generate options to manage it in the UI.

Specialty Detail View with Visits Display Only
Figure 7. Specialty Detail View without Add/Remove Controls

In contrast, the VisitDetailView allows users to manage Specialty associations by providing "Add" and "Remove" controls.

Visit Detail View with Specialties Management
Figure 8. Visit Detail View with Add/Remove Controls

Even if you manually add "Add" and "Remove" buttons to the SpecialtyDetailView, changes made to the association from this non-owning side will not be persisted. Due to the saving behavior described above, updates to associations can only be saved from the owning side (Visit in this case).

Both Owning Sides

Oftentimes, it’s not entirely clear which entity should manage the association, or it may be desirable to manage the relationship from both sides. Given that JPA silently ignores updates from the non-owning side, it can be challenging to work with a single owning side without careful tracking. For this reason, the default behavior in Jmix Studio when generating a bidirectional many-to-many association is to configure both sides as owning sides, allowing management of the relationship from either side.

When you create a bidirectional many-to-many association in Jmix Studio, it will by default set both entities as owning sides. This provides flexibility in relationship management, and it takes away the immediate burden to the developer to anticipate which of the sides is managing the association.

When both entities are designated as owning sides, each can independently manage updates to the join table. This setup provides greater flexibility, as it allows both entities to directly control the relationship.

In the Petclinic application, an example of this configuration is the relationship between Visit and TreatmentRoom. Both Entities include a @JoinTable annotation next to the @ManyToMany annotation, specifying the join table on each side. This means that either entity can initiate updates to the association, and changes from either side will be reflected in the database.

Visit.java
@JmixEntity
@Table(name = "PETCLINIC_VISIT", indexes = {
        @Index(name = "IDX_PETCLINIC_VISIT_ASSIGNED_NURSE", columnList = "ASSIGNED_NURSE_ID"),
        @Index(name = "IDX_PETCLINIC_VISIT_PET", columnList = "PET_ID")
})
@Entity(name = "petclinic_Visit")
public class Visit {

    @JoinTable(name = "PETCLINIC_VISIT_TREATMENT_ROOM_LINK",
            joinColumns = @JoinColumn(name = "VISIT_ID"),
            inverseJoinColumns = @JoinColumn(name = "TREATMENT_ROOM_ID"))
    @ManyToMany
    private List<TreatmentRoom> treatmentRooms;
}
TreatmentRoom.java
@JmixEntity
@Table(name = "PETCLINIC_TREATMENT_ROOM")
@Entity(name = "petclinic_TreatmentRoom")
public class TreatmentRoom {

    @JoinTable(name = "PETCLINIC_VISIT_TREATMENT_ROOM_LINK",
            joinColumns = @JoinColumn(name = "TREATMENT_ROOM_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "VISIT_ID", referencedColumnName = "ID"))
    @ManyToMany
    private List<Visit> visits;
}

Data Access

In a bidirectional many-to-many association where both sides are configured as owning, data access—both for reading and updating—can be managed from either entity. To illustrate this, let’s look at the example service VisitTreatmentRoomService from above.

@Service
public class VisitTreatmentRoomService {

    @Autowired
    private DataManager dataManager;

    // Adds multiple TreatmentRooms to a Visit
    public void addTreatmentRoomsToVisit(Visit visit, List<TreatmentRoom> treatmentRooms) {
        visit.getTreatmentRooms().addAll(treatmentRooms);
        dataManager.save(visit);
    }

    // Adds multiple Visits to a TreatmentRoom
    public void addVisitsToTreatmentRoom(TreatmentRoom treatmentRoom, List<Visit> visits) {
        treatmentRoom.getVisits().addAll(visits);
        dataManager.save(treatmentRoom);
    }
}

The source code didn’t change compared to above. The only difference is that the addVisitsToTreatmentRoom will now successfully store the visits for the treatment room.

UI Representation

Having both entities as owning sides enables full UI management of the relationship from either side. Since both Visit and TreatmentRoom independently manage the association, Jmix Studio will generate data grids and controls for adding or removing associations in both the VisitDetailView and the TreatmentRoomDetailView.

Visit Detail View with TreatmentRooms Management
Figure 9. Visit Detail View
Treatment Room Detail View with Visits Management
Figure 10. Treatment Room Detail View

This allows users to manage the association from both Visit and TreatmentRoom views, providing maximum flexibility in the UI.

However, if we take a closer look, we can see some potential limitations in this concrete business case. Simply linking Visits with TreatmentRooms directly without any additional scheduling information may lead to conflicts. Since we don’t currently account for timing, it’s likely that TreatmentRooms could be double-booked, with multiple Visits attempting to use the same room at overlapping times.

To address this, we would need a way to include temporal data with each room assignment—for example, specifying that a Visit requires Room A from 8AM to 8:30AM and Room B from 8:30 to 9:30AM. Managing such additional information within the relationship itself is not possible in a direct many-to-many association. This is where an Indirect Many-to-Many Mapping becomes valuable, allowing us to include attributes directly within the association to handle these scheduling needs.

In the next section, we’ll explore how to use indirect mapping to manage relationships with additional attributes.

Mapping

In this final section, we will discuss the concept of Direct versus Indirect mapping in many-to-many associations. Both types use a join table in the database to manage the relationship between entities. However, only the indirect association introduces an explicit JPA entity to represent the join table. This additional entity becomes essential when there is a need to store extra attributes within the relationship.

Direct Mapping

All the examples we have discussed so far demonstrate direct many-to-many associations. In each case, the entities were directly referenced from each other, without an intermediate entity representing the join table in the JPA model. Although a join table exists on the database level, it is not explicitly modeled as a JPA entity.

Because direct mapping has already been covered extensively in the previous sections, you already know everything about direct mapping. So let’s take a closer look at the other option of indirect mapping in the next section.

Indirect Mapping

In contrast to direct mapping, indirect mapping creates an explicit entity to represent the join table. As already said, this is useful when additional attributes need to be stored in the join table itself. Strictly speaking an indirect many-to-many association is not a real JPA many-to-many association, since it does not use the @ManyToMany association. You can rather think of it as a virtual or logical many-to-many association, consisting of two @ManyToOne / @OneToMany associations.

In our Petclinic example, the InsuranceCoverage entity serves as the join entity, capturing details like the validity period of the insurance policy for each pet.

For instance, a Pet may be insured by multiple InsuranceProvider entries over time, and each InsuranceProvider may cover multiple Pet entries. Here, InsuranceCoverage acts as the join entity, allowing us to record additional information, such as the start and end dates of each insurance policy.

Data Model

In the indirect mapping configuration, the InsuranceCoverage entity establishes a virtual many-to-many relationship between Pet and InsuranceProvider while also holding additional fields for the validity range. "Virtual", because there is no use of @ManyToMany annotation used. Instead, we reference to both entities from the InsuranceCoverage via two @ManyToOne associations.

Pet.java
@JmixEntity
@Table(name = "PETCLINIC_PET")
@Entity(name = "petclinic_Pet")
public class Pet extends NamedEntity {

    // ...
    @OnDelete(DeletePolicy.CASCADE)
    @Composition
    @OneToMany(mappedBy = "policyHolder")
    private List<InsuranceCoverage> insuranceCoverages;

    public List<InsuranceCoverage> getInsuranceCoverages() {
        return insuranceCoverages;
    }

    public void setInsuranceCoverages(List<InsuranceCoverage> insuranceCoverages) {
        this.insuranceCoverages = insuranceCoverages;
    }
}

In the Pet entity, the insuranceCoverages attribute is a one-to-many composition of InsuranceCoverage instances, which allows each Pet to reference multiple insurance records over time.

InsuranceProvider.java
@JmixEntity
@Table(name = "PETCLINIC_INSURANCE_PROVIDER")
@Entity(name = "petclinic_InsuranceProvider")
public class InsuranceProvider extends NamedEntity {
    @OnDelete(DeletePolicy.CASCADE)
    @Composition
    @OneToMany(mappedBy = "insuranceProvider")
    private List<InsuranceCoverage> policies;

    public List<InsuranceCoverage> getPolicies() {
        return policies;
    }
    public void setPolicies(List<InsuranceCoverage> policies) {
        this.policies = policies;
    }
}

Similarly, the InsuranceProvider entity has a one-to-many composition of InsuranceCoverage instances, enabling each company to manage a record of its insured pets.

InsuranceCoverage.java
@JmixEntity
@Table(name = "PETCLINIC_INSURANCE_COVERAGE", indexes = {
        @Index(name = "IDX_PETCLINIC_INSURANCE_COVERAGE_INSURANCE_PROVIDER", columnList = "INSURANCE_PROVIDER_ID"),
        @Index(name = "IDX_PETCLINIC_INSURANCE_COVERAGE_POLICY_HOLDER", columnList = "POLICY_HOLDER_ID")
})
@Entity(name = "petclinic_InsuranceCoverage")
public class InsuranceCoverage {

    @JoinColumn(name = "POLICY_HOLDER_ID", nullable = false)
    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Pet policyHolder;

    @JoinColumn(name = "INSURANCE_PROVIDER_ID", nullable = false)
    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private InsuranceProvider insuranceProvider;

    @Column(name = "VALID_FROM", nullable = false)
    @NotNull
    private LocalDate validFrom;

    @Column(name = "VALID_UNTIL")
    private LocalDate validUntil;

}

The InsuranceCoverage entity includes references to both Pet and InsuranceProvider and captures the validFrom and validUntil dates to represent the insurance policy’s duration.

Data Access and UI Representation

The indirect mapping affects both data access and the UI. Since InsuranceCoverage is a distinct entity, it can be managed, queried, and displayed directly. For example, in the Pet detail view, users can see the list of associated InsuranceProvider entries along with the validity range for each policy.

In Jmix Studio, setting up an indirect association like this means that you simply manage the UI of a regular entity: InsuranceCoverage. You can generate a regular detail view and with that collect all additional fields that should be recorded for the association, like the validity range in our case.

Ownership and Direction

Similar to direct many-to-many associations, indirect many-to-many relationships using a join entity also allow flexible configuration of both direction and ownership. A unidirectional relationship can be established by omitting a reference to the join entity on one side. For instance, if an InsuranceProvider should not directly access InsuranceCoverage entries, this can be achieved by simply excluding an attribute in the Java class, thereby creating a unidirectional many-to-many association from the Pet side only.

In bidirectional relationships, ownership can likewise be defined, specifying which entity "owns" and manages the association. If InsuranceCoverage entries should only be controlled from the Pet entity, this can be set up with a One-to-Many association via @Composition from the Pet side, while InsuranceProvider only contains a regular @OneToMany association. The choice between composition and association enables the desired level of control.

Summary

In this guide, we examined many-to-many relationships, a crucial aspect of data modeling when entities need to reference multiple instances of each other. Many-to-many relationships support flexible, interconnected data structures by linking entities like pets and insurance providers or veterinarians and specialties. These associations are managed with a join table, which stores the primary keys of each entity, providing a consistent structure at the database level to enable complex relationships.

We looked at three key dimensions of many-to-many relationships: Direction, Ownership and Mapping. Direction determines whether an association is unidirectional, allowing navigation from only one entity, or bidirectional, enabling access from both sides. Ownership identifies which entity controls the relationship in a bidirectional association, affecting where updates to the join table are managed. Mapping distinguishes between direct associations, where two entities are connected without an intermediary entity, and indirect associations, where a join entity allows for storing additional information about the relationship.

While these distinctions define data access and management on the Java/JPA level, the underlying join table remains structurally the same in the database.

Depending on your use case and requirements, you can choose the appropriate many-to-many configuration to ensure optimal access and management of relationships. This flexibility allows you to tailor your data model precisely, aligning it with the specific functional needs of your application while maintaining efficient control and clarity in your data interactions.

Further Information