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:
-
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.
-
Download and unzip the source repository
-
or clone it using git:
git clone https://github.com/jmix-framework/jmix-data-modeling-many-to-many-sample.git
-
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:
-
Veterinarian
→Specialty
: Modeled as a unidirectional many-to-many association, where each veterinarian can have multiple specialties without a reciprocal reference fromSpecialty
. -
Visit
←→Specialty
: Modeled as a bidirectional many-to-many association withVisit
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.
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:
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.
-
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.
-
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.
-
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. |
|
Bidirectional |
Both entities reference each other, enabling navigation from both sides. |
|
|
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. |
|
Both Sides Owning |
Both entities manage the relationship independently, each able to update the association. |
|
|
Mapping ⇄ Is there a dedicated JPA entity for the association table? |
Direct |
No intermediate mapping entity; the association is directly configured between the entities. |
|
Indirect |
Uses an intermediate mapping entity to represent the association, allowing for additional attributes. |
|
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:
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
:
@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:
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:
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.
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.
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:
@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
.
@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:
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:
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.
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.
@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.
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:
@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 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.
In contrast, the VisitDetailView
allows users to manage Specialty
associations by providing "Add" and "Remove" controls.
Even if you manually add "Add" and "Remove" buttons to the |
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.
@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;
}
@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
.
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.
@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.
@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.
@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.