Close

JPA - Bidirectional OneToMany/ManyToOne with Join table Example

[Last Updated: Oct 29, 2025]
A quick overview of bidirectional one-to-many/many-to-one join table mapping strategy
  • By default, a bidirectional OneToMany/ManyToOne association uses a foreign key column on the side which has the single-valued reference with @ManyToOne annotation.
  • We can override this default by using an intermediate join table to persist the association.
  • To achieve that we have to use @JoinTable annotation on the both sides.
  • In this case, we don't have to use 'mappedBy' element of the @OneToMany annotation.

Example

@Entity
public class EntityA {
    @Id
    @GeneratedValue
    private int myIdA;
    private String strA;
    @OneToMany
    @JoinTable(name = "MY_JOIN_TABLE",
            joinColumns = {@JoinColumn(name = "MY_ENTITY_A_FK", referencedColumnName = "myIdA")},
            inverseJoinColumns = {@JoinColumn(name = "MY_ENTITY_B_FK", referencedColumnName = "myIdB")}
    )
    private List<EntityB> entityBList;
    .............
}
@Entity
public class EntityB {
    @Id
    @GeneratedValue
    private int myIdB;
    private String strB;
    @ManyToOne
    @JoinTable(name = "MY_JOIN_TABLE",
            joinColumns = {@JoinColumn(name = "MY_ENTITY_B_FK", insertable = false,
                    updatable = false, referencedColumnName = "myIdB")},
            inverseJoinColumns = {@JoinColumn(name = "MY_ENTITY_A_FK", insertable = false,
                    updatable = false, referencedColumnName = "myIdA")}
    )
    private EntityA refEntityA;
    .............
}

Note that we have to use insertable/updatable=false on @ManyToOne side, to avoid persisting same relationship twice from both sides. When insertable/updatable=false, the column is not included in the generated SQL INSERT/UPDATE statements.

Now let's see what table structure our association is mapped to:

public class ExampleMain {

        public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("test1");
        try {
            EntityManager em = emf.createEntityManager();
            nativeQuery(em, "SHOW TABLES");
            nativeQuery(em, "SHOW COLUMNS from EntityA");
            nativeQuery(em, "SHOW COLUMNS from EntityB");
            nativeQuery(em, "SHOW COLUMNS from MY_JOIN_TABLE");
            emf.close();
        } finally {
            emf.close();
        }
    }

    public static void nativeQuery(EntityManager em, String s) {
        System.out.printf("-----------------------------%n'%s'%n",  s);
        Query query = em.createNativeQuery(s);
        List list = query.getResultList();
        for (Object o : list) {
            if(o instanceof Object[]) {
                System.out.println(Arrays.toString((Object[]) o));
            }else{
                System.out.println(o);
            }
        }
    }
}

Here's a quick overview of the relationship:

Persisting and loading entities

public class ExampleMain2 {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("test1");
        try {
            persistEntity(emf);
            nativeQueries(emf);
            loadEntityA(emf);
            loadEntityB(emf);
        } finally {
            emf.close();
        }
    }

    private static void nativeQueries(EntityManagerFactory emf) {
        System.out.println("-- native queries --");
        EntityManager em = emf.createEntityManager();
        ExampleMain.nativeQuery(em, "Select * from EntityA");
        ExampleMain.nativeQuery(em, "Select * from EntityB");
        ExampleMain.nativeQuery(em, "Select * from MY_JOIN_TABLE");
    }

    private static void persistEntity(EntityManagerFactory emf) {
        EntityManager em = emf.createEntityManager();

        EntityB entityB1 = new EntityB();
        entityB1.setStrB("testStringB");

        EntityB entityB2 = new EntityB();
        entityB2.setStrB("testStringB2");

        EntityA entityA = new EntityA();
        entityA.setStrA("testStringA");
        entityA.setEntityBList(Arrays.asList(entityB1, entityB2));

        entityB1.setRefEntityA(entityA);
        entityB2.setRefEntityA(entityA);

        System.out.println("-- persisting entities --");
        System.out.printf(" %s%n entityA#entityBList: %s%n", entityA, entityA.getEntityBList());
        System.out.printf(" %s%n entityB1#refEntityA: %s%n", entityB1, entityB1.getRefEntityA());
        System.out.printf(" %s%n entityB2#refEntityA: %s%n", entityB2, entityB2.getRefEntityA());

        em.getTransaction().begin();
        em.persist(entityB1);
        em.persist(entityB2);
        em.persist(entityA);
        em.getTransaction().commit();

        em.close();
    }

    private static void loadEntityA(EntityManagerFactory emf) {
        System.out.println("-- loading EntityA --");
        EntityManager em = emf.createEntityManager();
        List<EntityA> entityAList = em.createQuery("Select t from EntityA t").getResultList();
        entityAList.forEach(e -> System.out.printf(" %s%n entityA#entityBList: %s%n", e, e.getEntityBList()));
        em.close();
    }

    private static void loadEntityB(EntityManagerFactory emf) {
        System.out.println("-- loading EntityB --");
        EntityManager em = emf.createEntityManager();
        List<EntityB> entityBList = em.createQuery("Select t from EntityB t").getResultList();
        entityBList.forEach(e -> System.out.printf(" %s%n entityB#refEntityA: %s%n", e, e.getRefEntityA()));
        em.close();
    }
}

Output

-- persisting entities --
EntityA{myIdA=0, strA='testStringA'}
entityA#entityBList: [EntityB{myIdB=0, strB='testStringB'}, EntityB{myIdB=0, strB='testStringB2'}]
EntityB{myIdB=0, strB='testStringB'}
entityB1#refEntityA: EntityA{myIdA=0, strA='testStringA'}
EntityB{myIdB=0, strB='testStringB2'}
entityB2#refEntityA: EntityA{myIdA=0, strA='testStringA'}
-- native queries --
-----------------------------
'Select * from EntityA'
[3, testStringA]
-----------------------------
'Select * from EntityB'
[1, testStringB]
[2, testStringB2]
-----------------------------
'Select * from MY_JOIN_TABLE'
[3, 1]
[3, 2]
-- loading EntityA --
EntityA{myIdA=3, strA='testStringA'}
entityA#entityBList: [EntityB{myIdB=1, strB='testStringB'}, EntityB{myIdB=2, strB='testStringB2'}]
-- loading EntityB --
EntityB{myIdB=1, strB='testStringB'}
entityB#refEntityA: EntityA{myIdA=3, strA='testStringA'}
EntityB{myIdB=2, strB='testStringB2'}
entityB#refEntityA: EntityA{myIdA=3, strA='testStringA'}

Example Project

Dependencies and Technologies Used:

  • h2 1.4.193: H2 Database Engine.
  • hibernate-core 5.2.8.Final: The core O/RM functionality as provided by Hibernate.
    Implements javax.persistence:javax.persistence-api version 2.1
  • JDK 1.8
  • Maven 3.3.9

Bidirectional One To Many With Join Table Example Select All Download
  • jpa-bidirectional-one-to-many-join-table
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • EntityA.java
          • resources
            • META-INF

    See Also