Association Mapping in Spring Data JPA

Association Mapping in Spring Data JPA | We will discuss one-to-one, one-to-many, many-to-one, and many-to-many association mappings through examples.

Multiplicity: Connecting multiple tables using primary-key & foreign-key concepts. One table’s primary key is taken as another table’s foreign key.

There are 4 types of Multiplicity:-

  • 1 … 1 (one-to-one)
  • 1 … * (one-to-many)
  • * … 1 (many-to-one)
  • * … * (many-to-many)

All these are used best on requirement but one-to-many and many-to-many are most commonly used among them.

  • The * side / many side foreign-key Column is created.
  • For *…* (many-to-many) one extra table is created (Join Table) with 2 foreign-key columns(JoinColumn, inverserJoinColumn)
    • JoinColumn: 1st table primary key, child table 1st foreign key.
    • inverserJoinColumn: 2nd table primary key, child table 2nd foreign key.

Important points on the primary key and foreign key

  • Primary Key
    • No Duplicates are allowed in the table.
    • Value can not be null.
    • It is also called the ID Column / Unique column.
  • Foreign Key
    • This column is always used to link another table (link with primary key). One table’s primary key will be another table’s foreign key.
    • The foreign key can accept null values, even duplicates.
    • Value can not be inserted if it does not exist in its primary key column.
  • For 1…* and *…1 create a foreign key column at the many side.
  • For *…* create one extra table with two foreign key columns.
  • For 1…1 use *…1, create the foreign key column at the many side, and apply a unique condition at the foreign key column. 1 … * with the foreign key is not recommended. And if we use 1 … 1 directly then no foreign key will get created.
ExampleRelation
Employee ___ PersonIS – A
Car ___ EngineHAS – A
Company ___ BranchHAS – A
Department ___ EmployeeHAS – A
Book ___ AuthorHAS – A
Student ___ CourseHAS – A
Person ___ Credit CardHAS – A
  • One Company can have many branches and one branch can have only one Company (One to Many).
  • One Department can have many Employees & one Employee can belong to only one department (One to Many).
  • One Book can have many Authors & one Author can have written many Books (Many to Many).
  • One Student can join many courses & one course can have many students (Many to Many).
  • One Person can have many credit cards but one credit card can belong to only one person (Ont to Many).

In English, if we use having, had, has then in Java it is HAS – A relation. For example:- One Person is having many (multiple) Credit Cards.

  • 1 … * (One to Many): one row in one(parent) table is connected with multiple rows in another (Child) table.
  • * … 1 (Many to One): Many rows in the parent table are connected to one row in the child table.

Multiplicity is divided into 2 types in Java:-

  • Non-Collection based (Do not use Collection) [Child is 1]
  • Collection-based (Use Collection: List/Set) [Child is many (*)]
Non-CollectionCollection
1…11…*
*…1*…*

Association Mapping Implementation Steps:-

  1. Write Two Model classes and apply the HAS-A Relation between Model classes.
  2. Check For Collection/Non-Collection Type, if Collection type then modify HAS-A Variable
  3. as collection variable.
  4. Apply Multiplicity Annotation
    • 1…1 @ManyToOne + Unique
    • 1…* @OneToMany
    • *…1 @ManyToOne
    • *…* @ManyToMany
  5. Provide JoinColumn (foreign key Column) or JoinTable(JoinColumn + JoinColumn)

Application Properties:-

spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true

Many-To-One Example

Consider Product and Vendor. One vendor can have many products but one product can belongs to one Vendor.

  • Define Vendor Model class
  • Define Product Model class
  • Create (HAS-A) variable of Vendor inside Product
  • It is a non-collection type, keeping the HAS-A variable as it is.
  • Apply @ManyToOne Annotation on has-a variable
  • Apply @JoinColumn(name="__") on has-a variable
  • Define Repository Interfaces for both model classes
  • Define Runner class for Data Insert
  • Check database Tables finally for results.
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "prod_tab")
public class Product {

   @Id
   @Column(name = "pid_col")
   private Integer pid;

   @Column(name = "pcode_col")
   private String pcode;

   @Column(name = "pcost_col")
   private Double pcost;

   @Column(name = "pmodel_col")
   private String pmodel;

   @ManyToOne
   @JoinColumn(name = "vid_fk")
   private Vendor vob;
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ven_tab")
public class Vendor {

   @Id
   @Column(name = "vid_col")
   private Integer vid;

   @Column(name = "vcode_col")
   private String vcode;

   @Column(name = "vloc_col")
   private String vloc;
}
public interface ProductRepository extends JpaRepository<Product, Integer> { }

public interface VendorRepository extends JpaRepository<Vendor, Integer> { }
@Component
public class DataInsertRunner implements CommandLineRunner {

   @Autowired
   private ProductRepository productRepository;

   @Autowired
   private VendorRepository vendorRepository;

   @Override
   public void run(String... args) throws Exception {
      // first insert vendor
      Vendor vendor1 = new Vendor(101, "ABC", "LOC-1");
      Vendor vendor2 = new Vendor(102, "DEF", "LOC-2");
      vendorRepository.saveAll(List.of(vendor1, vendor2));

      // then insert product
      Product product1 = new Product(10, "PEN", 20.0, "A", vendor1);
      Product product2 = new Product(11, "BOOK", 40.0, "B", vendor1);
      Product product3 = new Product(12, "BTL", 80.0, "A", vendor2);
      Product product4 = new Product(13, "INK", 50.0, "B", vendor2);
      productRepository.saveAll(List.of(product1, product2, product3, product4));
   }

}

Run the application, it will create the table and insert the data.

mysql> use test;
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| prod_tab       |
| ven_tab        |
+----------------+

mysql> select * from ven_tab;
+---------+-----------+----------+
| vid_col | vcode_col | vloc_col |
+---------+-----------+----------+
|     101 | ABC       | LOC-1    |
|     102 | DEF       | LOC-2    |
+---------+-----------+----------+

mysql> select * from prod_tab;
+---------+-----------+-----------+------------+--------+
| pid_col | pcode_col | pcost_col | pmodel_col | vid_fk |
+---------+-----------+-----------+------------+--------+
|      10 | PEN       |        20 | A          |    101 |
|      11 | BOOK      |        40 | B          |    101 |
|      12 | BTL       |        80 | A          |    102 |
|      13 | INK       |        50 | B          |    102 |
+---------+-----------+-----------+------------+--------+

We can use any column name for JoinColumn (it need not be the same name as the child table primary key column). Writing @JoinColumn is optional. If we do not provide this then the Join Column name is hasAVariableName_PrimaryKeyColumnNameInChildTable.

@ManyToOne
@JoinColumn
private Vendor vob; // default column name = vob_vid_col

In the product table, if we don’t declare the name of the HAS-A variable column then the default name will be hasAVariableName_primaryKeyColumnNameOfChildTable i.e. “vob_vid_col“.

If we do not provide @ManyToOne then Exception is raised: MappingException: Could not determine type for: com.knowprogram.demo.entity.Vendor.

One-To-Many Example

Consider the Department and Employee table. One Department can have many employees but one Employee belongs to only one Department.

  • Define Employee Model class.
  • Define Dept Model class.
  • Create (HAS-A) variable of Employee inside Dept.
  • It is a collection type, modified HAS-A variable as List.
  • Apply @OneToMany Annotation on HAS-A variable.
  • Apply @JoinColumn(name="__") on HAS-A variable.
  • Define Repository Interfaces for both model classes.
  • Define Runner class for Data Insert.
  • Check Database Tables finally for results.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "depttab")
public class Department {
   @Id
   @Column(name = "did_col")
   private Integer did;

   @Column(name = "dcode_col")
   private String deptCode;

   @Column(name = "aname_col")
   private String adminName;

   @OneToMany
   @JoinColumn(name = "did_fk")
   private List<Employee> emps;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "emptab")
public class Employee {
   @Id
   @Column(name = "eid_col")
   private Integer eid;
   
   @Column(name = "ename_col")
   private String ename;
   
   @Column(name = "esal_col")
   private Double esal;
}
public interface DepartmentRepository extends JpaRepository<Department, Integer> { }

public interface EmployeeRepository extends JpaRepository<Employee, Integer> { }
@Component
public class DataInsertRunner implements CommandLineRunner {

   @Autowired
   private EmployeeRepository employeeRepository;

   @Autowired
   private DepartmentRepository departmentRepository;

   @Override
   public void run(String... args) throws Exception {
      Employee e1 = new Employee(10, "A", 3.3);
      Employee e2 = new Employee(11, "B", 4.3);
      Employee e3 = new Employee(12, "C", 5.3);
      Employee e4 = new Employee(13, "D", 6.3);
      employeeRepository.saveAll(List.of(e1, e2, e3, e4));

      Department d1 = new Department(521, "DEV", "SAM", Arrays.asList(e1, e2));
      Department d2 = new Department(522, "QA", "SYED", Arrays.asList(e3, e4));
      departmentRepository.saveAll(List.of(d1, d2));
   }

}

Table created as follows:-

Hibernate: 
    create table depttab (
        did_col integer not null,
        aname_col varchar(255),
        dcode_col varchar(255),
        primary key (did_col)
    ) engine=InnoDB
Hibernate: 
    create table emptab (
        eid_col integer not null,
        ename_col varchar(255),
        esal_col float(53),
        did_fk integer,
        primary key (eid_col)
    ) engine=InnoDB
Hibernate: 
    alter table emptab 
       add constraint FKbyy30h26o280gpceycn79rcq3 
       foreign key (did_fk) 
       references depttab (did_col)

How the data will be inserted?

  1. First, 4 columns will be inserted in the emp table with the foreign key as null.
    insert 
    into
        emptab
        (ename_col, esal_col, eid_col) 
    values
        (?, ?, ?)
+---------+-----------+----------+--------+
| eid_col | ename_col | esal_col | did_fk |
+---------+-----------+----------+--------+
|      10 | A         |      3.3 |    null |
|      11 | B         |      4.3 |    null |
|      12 | C         |      5.3 |    null |
|      13 | D         |      6.3 |    null |
+---------+-----------+----------+--------+
  1. Then 2 columns will be inserted in the dept table.
    insert 
    into
        depttab
        (aname_col, dcode_col, did_col) 
    values
        (?, ?, ?)
+---------+-----------+-----------+
| did_col | aname_col | dcode_col |
+---------+-----------+-----------+
|     521 | SAM       | DEV       |
|     522 | SYED      | QA        |
+---------+-----------+-----------+
  1. Later emp table foreign key will be updated with the dept table primary key.
    update
        emptab 
    set
        did_fk=? 
    where
        eid_col=?
+---------+-----------+----------+--------+
| eid_col | ename_col | esal_col | did_fk |
+---------+-----------+----------+--------+
|      10 | A         |      3.3 |    521 |
|      11 | B         |      4.3 |    521 |
|      12 | C         |      5.3 |    522 |
|      13 | D         |      6.3 |    522 |
+---------+-----------+----------+--------+

Aggregation And Composition

The association can be categorized into two types:-

  1. Aggregation (loose connection)
  2. Composition (tight connection)
Aggregation And Composition

Aggregation: A child can exist without parent data. Example:-

Student ------<> Course
Employee ---------<> Project

The Course table can have data without Students, Students may join later. Or if we delete one student we should not delete the course. Similarly, the project can exist even if we delete one Employee/all Employees.

Students Table

sidsnamesloc
10AABC
20BXYZ

Course Table

cidcnamecfee
1Java50
2Python50

If we delete the Student row then we need not to delete Course because that is accessed by Other students, even course can exist without Students.

Composition: Child and Parent are connected in a Strong manner. The child has no meaning without the parent.

Company -----<> Branch
Student --------<> StdMarks

Only Branch table data has no meaning without its Company data. Similarly, StdMarks table has no meaning without Student data.

StdMarks Table

SubjectMarksSID
MATH751
ENG761

Suppose we delete the record from the Student table where sid = 10 then StdMarks tables data where sid=10 will have no meaning. Marks can’t exist without Students. Therefore if we delete the Student table record then we must also delete related entries in the StdMarks table.

By default (if we don’t specify), every association is considered an aggregation (loose connection). We can achieve composition through Cascading.

More Examples:-

  • BankAccount —<> Transaction => [Composition]: If we delete the BankAccount data then there is no meaning of the Transaction table data for that bank account.
  • College —<> Branch => [Composition]: If we delete the College data then there is no meaning of the Branch table data for that College.
  • Purchase —<> PromoCode => [Aggregation]
Purchase
10 TXA 58
11 TXB 58
12 TXC 58

PromoCode
58 CB50
59 AB25

Cascading

If we define Association Mapping in Data JPA by default every type is Aggregation. To make it composition use Cascading Concept.

If we perform any operation on the parent class object and then also apply the same operation on its connected child class objects (without writing operation code over child objects) is called Cascading (Composition).

Cascading is implemented using one Enum CascadeType.

public enum CascadeType { 
    ALL, 
    PERSIST, 
    MERGE, 
    REMOVE,
    REFRESH,
    DETACH
}

Among the ALL and PERSIST are mostly used.

  • ALL => All operation
  • PERSIST => Save
  • MERGE => Update

In the ManyToOne Example code, if we remove/comment the saveAll() method call for vendors and run the application:-

// comment this code
// vendorRepository.saveAll(List.of(vendor1, vendor2));

Then it throws an exception:- jakarta.persistence.EntityNotFoundException: Unable to find com.knowprogram.demo.entity.Vendor with id 101

Now let us cascade the Vendor means when we save the Product then also save the Vendor (if already does not exist).

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "prod_tab")
public class Product {

    @Id
    @Column(name = "pid_col")
    private Integer pid;

    @Column(name = "pcode_col")
    private String pcode;

    @Column(name = "pcost_col")
    private Double pcost;

    @Column(name = "pmodel_col")
    private String pmodel;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "vid_fk")
    private Vendor vob;
}

Now when we run the application then while saving the Product data, first Spring Data JPA will insert the Vendor table data.

Similarly in the One-To-Many Example code, if we save the department then it should also save the employees. Remove the saveAll() method call for employees from the runner class, add the Cascade into the Department class and run the application.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "depttab")
public class Department {
    @Id
    @Column(name = "did_col")
    private Integer did;

    @Column(name = "dcode_col")
    private String deptCode;

    @Column(name = "aname_col")
    private String adminName;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "did_fk")
    private List<Employee> emps;
}

Comment on this line in Runner class:-

// employeeRepository.saveAll(List.of(e1, e2, e3, e4));

FetchType

This concept is used for SELECT ROW from DB using Id. When we fetch parent data, then we can get its child data (EAGER) even or only parent data (LAZY).

public enum FetchType {

    /** Defines that data can be lazily fetched. */
    LAZY,

    /** Defines that data must be eagerly fetched. */
    EAGER
}

LAZY:- On fetching Parent Data, do not fetch child data.

Hibernate: 
    select
        d1_0.did_col,
        d1_0.aname_col,
        d1_0.dcode_col 
    from
        depttab d1_0 
    where
        d1_0.did_col=?

Eager:- On fetching Parent Data, also fetch child data.

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "did_fk")
private List<Employee> emps;
Hibernate: 
    select
        d1_0.did_col,
        d1_0.aname_col,
        d1_0.dcode_col,
        e1_0.did_fk,
        e1_0.eid_col,
        e1_0.ename_col,
        e1_0.esal_col 
    from
        depttab d1_0 
    left join
        emptab e1_0 
            on d1_0.did_col=e1_0.did_fk 
    where
        d1_0.did_col=?
  • If the child is many type (1…* / *…*) then the default is LAZY.
  • If the child is one type (1…1 / *…1) then the default is EAGER.
MultiplicityDefault Fetch Type
One-To-OneEAGER
One-To-ManyLAZY
Many-To-OneEAGER
Many-To-ManyLAZY

Many-To-Many Example

One extra table is created ie called as JoinTable with 2 columns – JoinColumn, InverseJoinColumn. Example:- Student —<> Course. One Student can join many Courses and one Course can have many Students. Implementation Steps:-

  1. Write Two Model classes and apply HAS-A Relation between Model classes.
  2. Check For Collection/Non-Collection Type. If Collection type then modify HAS-A Variable as Collection variable.
  3. Apply Multiplicity Annotation @ManyToMany.
  4. Provide JoinColumn (FK Column) or JoinTable(JoinColumn + JoinColumn)
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Course {
    @Id
    private Integer cid;
    private String cname;
    private Double cfee;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student {
    @Id
    private Integer sid;
    private String sname;
    private String sloc;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "stdcrstb", 
        joinColumns = @JoinColumn(name = "sidFk"), 
        inverseJoinColumns = @JoinColumn(name = "cidFk")
    )
    private List<Course> cobs;
}
public interface CourseRepository extends JpaRepository<Course, Integer> { }

public interface StudentRepository extends JpaRepository<Student, Integer> { }
@Component
public class DataInsertRunner implements CommandLineRunner {

    @Autowired
    private StudentRepository studentRepository;

    @Override
    public void run(String... args) throws Exception {
        Course c1 = new Course(58, "Java", 500.0);
        Course c2 = new Course(59, "UI", 400.0);
        Course c3 = new Course(60, "AWS", 300.0);

        Student s1 = new Student(1, "A", "ABC", List.of(c1, c2));
        Student s2 = new Student(2, "B", "XYZ", List.of(c2, c3));

        studentRepository.saveAll(List.of(s1, s2));
    }
}

Tables are created as follows:-

Hibernate: 
    create table course (
        cid integer not null,
        cfee float(53),
        cname varchar(255),
        primary key (cid)
    ) engine=InnoDB
Hibernate: 
    create table stdcrstb (
        sid_fk integer not null,
        cid_fk integer not null
    ) engine=InnoDB
Hibernate: 
    create table student (
        sid integer not null,
        sloc varchar(255),
        sname varchar(255),
        primary key (sid)
    ) engine=InnoDB
Hibernate: 
    alter table stdcrstb 
       add constraint FK6s2ki7ej66jt5s6igq6i430ia 
       foreign key (cid_fk) 
       references course (cid)
Hibernate: 
    alter table stdcrstb 
       add constraint FKcewl79jgi749y64i75kkpgy7a 
       foreign key (sid_fk) 
       references student (sid)

First, it will insert data into the Student and Course table then it will insert in the stdcrstb table.

mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| course         |
| stdcrstb       |
| student        |
+----------------+

mysql> select * from course;
+-----+------+-------+
| cid | cfee | cname |
+-----+------+-------+
|  58 |  500 | Java  |
|  59 |  400 | UI    |
|  60 |  300 | AWS   |
+-----+------+-------+

mysql> select * from student;
+-----+------+-------+
| sid | sloc | sname |
+-----+------+-------+
|   1 | ABC  | A     |
|   2 | XYZ  | B     |
+-----+------+-------+

mysql> select * from stdcrstb;
+--------+--------+
| sid_fk | cid_fk |
+--------+--------+
|      1 |     58 |
|      1 |     59 |
|      2 |     59 |
|      2 |     60 |
+--------+--------+

One-To-One Example

Parent table one row is connected to Child table one row (at max). Example:- Passenger ——-<> BusTicket

Use Many-To-One and apply unique Condition (unique=true).
1…1 = (unique) *…1

Why can’t we take 1…* and apply unique for 1…1? Yes, We can do that. One-To-One & Many-To-One are non-collection based whereas One-To-Many is collection-based, therefore we have to use a List that will hold only one value. It becomes a memory waste.

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class BusTicket {
    @Id
    private Integer btid;
    private String seatNo;
    private String bfrom;
    private String bto;
    private Double cost;
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Passenger {
    @Id
    private Integer pid;
    private String pname;
    private String gender;
    private Integer age;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "tckt_id_fk", unique = true)
    private BusTicket ticket;
}
public interface BusTicketRepository extends JpaRepository<BusTicket, Integer> { }

public interface PassengerRepository extends JpaRepository<Passenger, Integer> { }
@Component
public class DataInsertRunner implements CommandLineRunner {

    @Autowired
    private PassengerRepository passengerRepository;

    @Override
    public void run(String... args) throws Exception {
        BusTicket busTicket1 = new BusTicket(110, "A01", "ABC", "XYZ", 50.0);
        BusTicket busTicket2 = new BusTicket(111, "A02", "ABC", "XYZ", 60.0);
        BusTicket busTicket3 = new BusTicket(112, "A03", "ABC", "XYZ", 70.0);

        Passenger passenger1 = new Passenger(85, "A", "M", 21, busTicket1);
        Passenger passenger2 = new Passenger(86, "B", "M", 22, busTicket2);
        Passenger passenger3 = new Passenger(87, "C", "W", 23, busTicket3);

        passengerRepository.saveAll(List.of(passenger1, passenger2, passenger3));
    }
}

Table is created as follows:-

Hibernate: 
    create table bus_ticket (
        btid integer not null,
        bfrom varchar(255),
        bto varchar(255),
        cost float(53),
        seat_no varchar(255),
        primary key (btid)
    ) engine=InnoDB
Hibernate: 
    create table passenger (
        pid integer not null,
        age integer,
        gender varchar(255),
        pname varchar(255),
        tckt_id_fk integer,
        primary key (pid)
    ) engine=InnoDB
Hibernate: 
    alter table passenger 
       drop index UKetijr8ax2svla8rlih5r7s7hy
Hibernate: 
    alter table passenger 
       add constraint UKetijr8ax2svla8rlih5r7s7hy unique (tckt_id_fk)
Hibernate: 
    alter table passenger 
       add constraint FKtenmo5unfkkef877g6bj0bt9f 
       foreign key (tckt_id_fk) 
       references bus_ticket (btid)
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| bus_ticket     |
| passenger      |
+----------------+

mysql> select * from passenger;
+-----+------+--------+-------+------------+
| pid | age  | gender | pname | tckt_id_fk |
+-----+------+--------+-------+------------+
|  85 |   21 | M      | A     |        110 |
|  86 |   22 | M      | B     |        111 |
|  87 |   23 | W      | C     |        112 |
+-----+------+--------+-------+------------+

mysql> select * from bus_ticket;
+------+-------+------+------+---------+
| btid | bfrom | bto  | cost | seat_no |
+------+-------+------+------+---------+
|  110 | ABC   | XYZ  |   50 | A01     |
|  111 | ABC   | XYZ  |   60 | A02     |
|  112 | ABC   | XYZ  |   70 | A03     |
+------+-------+------+------+---------+

If you enjoyed this post, share it with your friends. Do you want to share more information about the topic discussed above or do you find anything incorrect? Let us know in the comments. Thank you!

Leave a Comment

Your email address will not be published. Required fields are marked *