Monday, July 15, 2013

JPA Many-to-Many mappings tutorial

Most of entities have relationships with other entities.
There are four types of relationships:
  • One-to-One
  • One-to-Many
  • Many-to-One
  • Many-to-Many

Let's look at many-to-many relationship mappings. They are similar to mappings described in "JPA One-to-One mappings tutorial" post.

This post is available in Russian.

Let's modify the application created in "JPA table and column mappings tutorial" post.

Music artists might relate to many genres, and genres might relate to many artists. Let's describe such relationship in our application.

Create jpa_many_to_many_mappings_tutorial database with artists and genres tables.
Create table artists_genres with artist_id and genre_id columns to implement many-to-many relationship.
These columns are foreign keys that reference artists and genres tables. They constitute a composite primary key, so that table data is not duplicated.
Add artists and genres data:
CREATE DATABASE jpa_many_to_many_mappings_tutorial;
USE jpa_many_to_many_mappings_tutorial;

CREATE TABLE artists (
artist_id INT PRIMARY KEY,
artist_name VARCHAR(30)
);

CREATE TABLE genres (
genre_id INT PRIMARY KEY,
genre_name VARCHAR(30)
);

CREATE TABLE artists_genres (
artist_id INT,
genre_id INT,
PRIMARY KEY (artist_id, genre_id),
FOREIGN KEY (artist_id) REFERENCES artists (artist_id),
FOREIGN KEY (genre_id) REFERENCES genres (genre_id)
);

INSERT INTO artists VALUES (1, 'Franz Ferdinand');
INSERT INTO artists VALUES (2, 'System of a Down');

INSERT INTO genres VALUES (1, 'Rock');
INSERT INTO genres VALUES (2, 'Indie Rock');
INSERT INTO genres VALUES (3, 'Alternative Rock');

INSERT INTO artists_genres VALUES (1, 1);
INSERT INTO artists_genres VALUES (1, 2);
INSERT INTO artists_genres VALUES (2, 1);
INSERT INTO artists_genres VALUES (2, 3);

Unidirectional mapping


If Genre entities have references to collections of Artist entities, but Artist entities don't have references to collections of Genre entities, then such relationship is unidirectional.

Create Genre class with id, name and artists properties.
To create unidirectional many-to-many mapping:
  • add @ManyToMany annotation to artists property
  • add @JoinTable annotation to artists property
  • add name annotation element that references table with relationship data, e.g. artists_genres
  • add joinColumns and inverseJoinColumns annotation elements that reference foreign key columns, e.g. artist_id and genre_id
package com.jpa.many.to.many.mappings.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.List;

@Entity
@Table(name = "genres")
public class Genre {

    @Id
    @Column(name = "genre_id")
    private int id;

    @Column(name = "genre_name")
    private String name;

    @ManyToMany
    @JoinTable(name = "artists_genres",
            joinColumns = @JoinColumn(name = "artist_id"),
            inverseJoinColumns = @JoinColumn(name = "genre_id"))
    private List<Artist> artists;

    public Genre() {

    }

    public Genre(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Artist> getArtists() {
        return artists;
    }

    public void setArtists(List<Artist> artists) {
        this.artists = artists;
    }

    @Override
    public String toString() {
        return "Genre{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Unlike the application described in "JPA table and column mappings tutorial", Artist class doesn't have a genre property:
package com.jpa.many.to.many.mappings.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.List;

@Entity
@Table(name = "artists")
public class Artist {

    @Id
    @Column(name = "artist_id")
    private int id;

    @Column(name = "artist_name")
    private String name;   

    public Artist() {

    }

    public Artist(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }    

    @Override
    public String toString() {
        return "Artist{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Create GenreService class:
package com.jpa.many.to.many.mappings.tutorial.service;

import com.jpa.many.to.many.mappings.tutorial.domain.Genre;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;

public class GenreService {

    private EntityManager em;

    public GenreService(EntityManager em) {
        this.em = em;
    }

    public Genre createGenre(int id, String name) {
        Genre genre = new Genre(id, name);
        em.persist(genre);

        return genre;
    }

    public void removeGenre(int id) {
        Genre genre = em.find(Genre.class, id);

        if (genre != null) {
            em.remove(genre);
        }
    }

    public Genre changeGenreName(int id, String name) {
        Genre genre = em.find(Genre.class, id);

        if (genre != null) {
            genre.setName(name);
        }

        return genre;
    }

    public Genre findGenre(int id) {
        return em.find(Genre.class, id);
    }

    public List<Genre> findAllGenres() {
        TypedQuery<Genre> query = em.createQuery("SELECT g FROM Genre g", Genre.class);
        return query.getResultList();
    }
}

Add Genre class to persistence unit in persistence.xml file:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">

    <persistence-unit name="JpaManyToManyMappingsTutorial" transaction-type="RESOURCE_LOCAL">
        <class>com.jpa.many.to.many.mappings.tutorial.domain.Artist</class>
        <class>com.jpa.many.to.many.mappings.tutorial.domain.Genre</class>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
            <property name="javax.persistence.jdbc.url"
                      value="jdbc:mysql://localhost:3306/jpa_many_to_many_mappings_tutorial"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="root"/>
        </properties>
    </persistence-unit>

</persistence>

Add code that creates GenreService object, finds a Genre entity and prints genre and its related artists data to main() method of JpaManyToManyMappingsTutorial class:
package com.jpa.many.to.many.mappings.tutorial;

import com.jpa.many.to.many.mappings.tutorial.domain.Artist;
import com.jpa.many.to.many.mappings.tutorial.domain.Genre;
import com.jpa.many.to.many.mappings.tutorial.service.GenreService;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class JpaManyToManyMappingsTutorial {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaManyToManyMappingsTutorial");
        EntityManager em = emf.createEntityManager();
        GenreService genreService = new GenreService(em);

        System.out.println("--- Finding genre ---");
        Genre genre = genreService.findGenre(1);
        System.out.println(String.format("Found genre: %s", genre));
        for (Artist artist : genre.getArtists()) {
            System.out.println(String.format("Genre artist: %s", artist));
        }
    }
}

Execute main() method:

Application has printed genre and its related artists data as expected.

Bidirectional mapping


If Genre and Artist entities have references to collections of each other, then such relationship is bidirectional.

To create it:
  • add genres property of List<Genre> type in Artist class
  • add @ManyToMany annotation to genres property
  • add mappedBy annotation element, which references to Genre entity property with @ManyToMany and @JoinTable annotations, e.g. artists
package com.jpa.many.to.many.mappings.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.List;

@Entity
@Table(name = "artists")
public class Artist {

    @Id
    @Column(name = "artist_id")
    private int id;

    @Column(name = "artist_name")
    private String name;

    @ManyToMany(mappedBy = "artists")
    private List<Genre> genres;

    public Artist() {

    }

    public Artist(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Genre> getGenres() {
        return genres;
    }

    public void setGenres(List<Genre> genres) {
        this.genres = genres;
    }

    @Override
    public String toString() {
        return "Artist{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Add code that creates ArtistService object, finds an Artist entity and prints artist and its related genres data to main() method of JpaManyToManyMappingsTutorial class:
package com.jpa.many.to.many.mappings.tutorial;

import com.jpa.many.to.many.mappings.tutorial.domain.Artist;
import com.jpa.many.to.many.mappings.tutorial.domain.Genre;
import com.jpa.many.to.many.mappings.tutorial.service.ArtistService;
import com.jpa.many.to.many.mappings.tutorial.service.GenreService;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

public class JpaManyToManyMappingsTutorial {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaManyToManyMappingsTutorial");
        EntityManager em = emf.createEntityManager();
        GenreService genreService = new GenreService(em);
        ArtistService artistService = new ArtistService(em);

        System.out.println("--- Finding genre ---");
        Genre genre = genreService.findGenre(1);
        System.out.println(String.format("Found genre: %s", genre));
        for (Artist artist : genre.getArtists()) {
            System.out.println(String.format("Genre artist: %s", artist));
        }
        System.out.println();

        System.out.println("--- Finding artist ---");
        Artist artist = artistService.findArtist(1);
        System.out.println(String.format("Found artist: %s", artist));
        for (Genre artistGenre : artist.getGenres()) {
            System.out.println(String.format("Artist genre: %s", artistGenre));
        }
    }
}

Execute main() method:

Application has printed artist and its related genres data, which means we have successfully created unidirectional and bidirectional many-to-many mappings during application development.

Folder structure:




Application source code is available at https://code.google.com/p/jpa-many-to-many-mappings-tutorial/.

Recommended posts:

6 comments:

  1. In Bidirectional mapping...

    you just have given like this

    @ManyToMany(mappedBy = "artists")
    private List genres;

    here what exactly mappedBy attribute does?
    no need to give like Genre class ?

    ReplyDelete
    Replies
    1. Actually mapping looks like this:
      @ManyToMany(mappedBy = "artists")
      private List<Genre> genres;

      So, list elements have Genre type.
      mappedBy annotation element is needed to make a bidirectional mapping, without it Artist entity wouldn't have access to related Genre entities.

      You can do the same mapping without specifying Genre type in List collection using targetEntity annotation element:
      @ManyToMany(targetEntity = Genre.class, mappedBy = "artists")
      private List genres;

      Delete
  2. Alex please can you suggest me above many to many relationship through dao and service layer ...

    ReplyDelete
  3. Thanks a lot! You made a new blog entry to answer my question; I really appreciate your time and effort.
    java training in chennai |
    java training institutes in chennai

    ReplyDelete