In this article, we will learn how to implement different types of relationships between entities using Spring Boot JPA. We will walk through all three major relationship types:
- One-to-One
- One-to-Many / Many-to-One
- Many-to-Many
All examples use Lombok to reduce boilerplate code. If you’re not familiar with it, check out the Lombok documentation.
One-to-One Relationship
A one-to-one relationship means a record in one table is linked to exactly one record in another table.
Example: A Person entity has one-to-one mapping with a Passport entity.
Let’s see the code example.
DB ER diagram for the relationship would look as below:

PersonEntity.java
@Entity
@Table(name = "person")
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class PersonEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "person_id")
private Integer personId;
@Column(name = "person_name")
private String personName;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "passport_number", unique = true)
private PassportEntity passport;
}
PassportEntity.java
@Entity
@Table(name = "passport")
@Getter @Setter @Builder
@NoArgsConstructor @AllArgsConstructor
public class PassportEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "passport_id")
private Integer passportId;
@Column(name = "passport_number")
private String passportNumber;
@Column(name = "country")
private String country;
@Column(name = "passport_type")
private String passportType;
@OneToOne(mappedBy = "passport")
private PersonEntity person;
}Key Annotations
@OneToOneestablishes the one-to-one relation.@JoinColumnsets the foreign key and enforces uniqueness.mappedByindicates the inverse side of the relationship.cascade = CascadeType.ALLpropagates all operations.
One-to-Many Relationship
A one-to-many relationship is a type of cardinality that refers to the relationship between two entities A and B in which an element of A may be linked to many elements of B, but a member of B is linked to only one element of A.
Example: A person can have multiple credit cards and at the same time a credit card is linked to one person.
DB ER diagram for the relationship would look as below:

Person.java
@Entity
@Table(name = "person")
@Getter @Setter @Builder
@NoArgsConstructor
@AllArgsConstructor
public class Person {
@Id
@Column(name = "person_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer personId;
@Column(name = "person_name")
private String personName;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "person_card_mapping",
joinColumns = @JoinColumn(name = "person_id"),
inverseJoinColumns = @JoinColumn(name = "card_id"))
private List<Card> cards;
}Card.java
@Entity
@Table(name = "card")
@Getter @Setter @Builder
@NoArgsConstructor
@AllArgsConstructor
public class Card {
@Id
@Column(name = "card_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer cardId;
@Column(name = "card_no")
private String cardNo;
@Column(name = "cardholder_name")
private String cardholderName;
@Column(name = "expiry_month")
private String expiryMonth;
@Column(name = "expiry_year")
private String expiryYear;
}Key Annotations
@OneToManyonPersonmakes it the owner of multipleCardinstances.@JoinTablecreates an intermediate mapping table (person_card_mapping).
Many-to-Many Relationship
A Many-to-Many relationship is a type of cardinality that refers to the relationship between two entities, say, A and B, where A may contain a parent instance for which there are many children in B and vice versa.
Example: Let’s take an example of a Merchant and service provided by them. A merchant could provide a variety of services at the same time the same service could be provided by various other merchants.
DB ER diagram for the relationship would look as below:

MerchantEntity.java
@Entity
@Table(name = "merchant")
@Getter @Setter @Builder
@NoArgsConstructor
@AllArgsConstructor
public class MerchantEntity {
@Id
@Column(name = "merchant_id")
private Integer merchantId;
@Column(name = "merchant_name")
private String merchantName;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "merchant_service_mapping",
joinColumns = @JoinColumn(name = "merchant_id", referencedColumnName = "merchant_id"),
inverseJoinColumns = @JoinColumn(name = "service_id", referencedColumnName = "service_id"))
private List<ServiceEntity> merchantService;
}ServiceEntity.java
@Entity
@Table(name = "service")
@Getter @Setter @Builder
@NoArgsConstructor
@AllArgsConstructor
public class ServiceEntity {
@Id
@Column(name = "service_id")
private Integer serviceId;
@Column(name = "service_name")
private String serviceName;
@ManyToMany(mappedBy = "merchantService", fetch = FetchType.EAGER)
private List<MerchantEntity> merchants;
}Key Annotations
@ManyToManyon both sides defines bidirectional many-to-many.@JoinTableestablishes the mapping table with foreign keys from both entities.referencedColumnNameensures explicit column mapping.
Testing
All the relationships can be tested with simple JUnit code.
One-to-One Testing
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class EntityMappingApplicationTests {
@Autowired
private PersonRepository personRepo;
@Autowired
private PassportRepository passportRepository;
private PersonEntity person = null;
private PassportEntity passport = null;
private Integer personId;
@BeforeAll
public void setup() {
passport = PassportEntity.builder()
.passportNumber("A1234567")
.country("India")
.passportType("Diplomatic")
.build();
person = PersonEntity.builder()
.personName("John Doe")
.passport(passport)
.cards(cards)
.build();
person = personRepo.save(person);
personId = person.getPersonId();
}
@Test
public void testOneToOne() {
PersonEntity person = personRepo.findById(personId).orElse(null);
assertNotNull(person.getPassport());
assertEquals("A1234567", person.getPassport().getPassportNumber());
PassportEntity passport = passportRepository.findByPassportNumber("A1234567");
assertNotNull(passport.getPerson());
assertEquals("John Doe", passport.getPerson().getPersonName());
}
}One-to-Many Testing
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class OneToManyMappingTests {
@Autowired
private PersonRepository personRepo;
private Person person = null;
private Card card1 = null;
private Card card2 = null;
private Integer personId;
@BeforeAll
public void setup() {
card1 = Card.builder()
.cardholderName("John Doe")
.cardNo("4578963214502354")
.expiryMonth("12")
.expiryYear("2022")
.build();
card2 = Card.builder()
.cardholderName("John Doe")
.cardNo("4111111111111111")
.expiryMonth("02")
.expiryYear("2025")
.build();
List<Card> cards = new ArrayList<>();
cards.add(card1);
cards.add(card2);
person = Person.builder()
.personName("John Doe")
.cards(cards)
.build();
person = personRepo.save(person);
personId = person.getPersonId();
}
@Test
public void testOneToMany() {
Person person = personRepo.findById(personId).orElse(null);
assertNotNull(person.getCards());
assertEquals("4578963214502354", person.getCards().get(0).getCardNo());
assertEquals("4111111111111111", person.getCards().get(1).getCardNo());
}
}Many-to-Many Testing
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ManyToManyMappingTests {
@Autowired
private MerchantRepository merchantRepository;
@Autowired
private ServiceRepository serviceRepository;
private ServiceEntity serviceEntity1 = null;
private ServiceEntity serviceEntity2 = null;
private ServiceEntity serviceEntity3 = null;
private MerchantEntity merchantEntity1 = null;
private MerchantEntity merchantEntity2 = null;
@BeforeAll
public void setup() {
serviceEntity1 = ServiceEntity.builder()
.serviceId(1)
.serviceName("Plumbing")
.build();
serviceEntity2 = ServiceEntity.builder()
.serviceId(2)
.serviceName("Home Renovation")
.build();
serviceEntity3 = ServiceEntity.builder()
.serviceId(3)
.serviceName("Carpenting")
.build();
List<ServiceEntity> serviceList1 = new ArrayList<>();
serviceList1.add(serviceEntity1);
serviceList1.add(serviceEntity2);
List<ServiceEntity> serviceList2 = new ArrayList<>();
serviceList2.add(serviceEntity1);
serviceList2.add(serviceEntity3);
merchantEntity1 = MerchantEntity.builder()
.merchantId(1)
.merchantName("John's Builder")
.merchantService(serviceList1)
.build();
merchantEntity2 = MerchantEntity.builder()
.merchantId(2)
.merchantName("Jack Repairs")
.merchantService(serviceList2)
.build();
merchantEntity1 = merchantRepository.save(merchantEntity1);
merchantEntity2 = merchantRepository.save(merchantEntity2);
List<MerchantEntity> me1 = new ArrayList<>();
me1.add(merchantEntity1);
me1.add(merchantEntity2);
List<MerchantEntity> me2 = new ArrayList<>();
me2.add(merchantEntity1);
List<MerchantEntity> me3 = new ArrayList<>();
me3.add(merchantEntity2);
serviceEntity1.setMerchants(me1);
serviceEntity2.setMerchants(me2);
serviceEntity3.setMerchants(me3);
serviceRepository.save(serviceEntity1);
serviceRepository.save(serviceEntity2);
}
@Test
public void testManyToMany() {
MerchantEntity merchant1 = merchantRepository.findById(1).orElse(null);
assertNotNull(merchant1.getMerchantService());
assertEquals("Plumbing", merchant1.getMerchantService().get(0).getServiceName());
assertEquals("Home Renovation", merchant1.getMerchantService().get(1).getServiceName());
MerchantEntity merchant2 = merchantRepository.findById(2).orElse(null);
assertNotNull(merchant2.getMerchantService());
assertEquals("Plumbing", merchant2.getMerchantService().get(0).getServiceName());
assertEquals("Carpenting", merchant2.getMerchantService().get(1).getServiceName());
ServiceEntity serviceEntity1 = serviceRepository.findByServiceName("Plumbing");
assertNotNull(serviceEntity1.getMerchants());
assertEquals("John's Builder", serviceEntity1.getMerchants().get(0).getMerchantName());
assertEquals("Jack Repairs", serviceEntity1.getMerchants().get(1).getMerchantName());
ServiceEntity serviceEntity2 = serviceRepository.findByServiceName("Carpenting");
assertNotNull(serviceEntity2.getMerchants());
assertEquals("Jack Repairs", serviceEntity2.getMerchants().get(0).getMerchantName());
}
}Summary
| Relationship Type | Cardinality | Join table needed |
| One-to-One | A ↔ B (1:1) | No |
| One-to-Many | A → B (1:N) | Optional |
| Many-to-Many | A ↔ B (N:M) | Yes |
- Use
cascadeto control lifecycle propagation. - Use
mappedByon the inverse side to avoid duplicate mapping. - Use
@JoinTableonly when needed for relationship tables.
Source Code
You can find the full working examples and test cases on GitHub: Entity Mapping