Wednesday, July 3, 2013

JPA-маппинг отношений "многие-ко-многим"

Многие сущности имеют отношения с другими сущностями.
Отношения бывают 4 типов:
  • One-to-One (один-к-одному)
  • One-to-Many (один-ко-многим)
  • Many-to-One (многие-к-одному)
  • Many-to-Many (многие-ко-многим)

Рассмотрим маппинг отношений типа "многие-ко-многим". Он во многом будет напоминать маппинги, описанные в постах "JPA-маппинг типа "один-к-одному"" и "JPA-маппинг типов "один-ко-многим" и "многие-к-одному"".

Возьмем за основу приложение, созданное в посте "JPA-маппинг сущности и ее свойств".

Музыкальный исполнитель может относиться ко многим жанрам, а жанр может относиться ко многим исполнителям. Опишем эти отношения в приложении.

Создадим базу данных jpa_many_to_many_mappings_tutorial и таблицы artists и genres.
Для реализации отношений типа "многие-ко-многим" создадим таблицу artists_genres с колонками artist_id и genre_id. Колонки таблицы - это внешние ключи, которые ссылаются на одноименные колонки таблиц artists и genres. Для того, чтобы данные таблицы не дублировались, ее колонки входят в состав композитного первичного ключа.
Добавим данные об исполнителях и жанрах:
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);

Однонаправленный маппинг


Если сущности типа Genre будут иметь ссылки на коллекции сущностей типа Artist, но сущности типа Artist не будут ссылаться на коллекции сущностей типа Genre, то отношения будут однонаправленными.

Создадим класс Genre с полями id, name и artists.
Для однонаправленного маппинга типа "многие-ко-многим":
  • добавим полю artists аннотацию @ManyToMany
  • добавим полю artists аннотацию @JoinTable
  • добавим элемент аннотации name c указанием имени таблицы, которая содержит данные об отношениях, т.е. artists_genres
  • добавим элементы аннотации joinColumns и inverseJoinColumns с указанием имен колонок, которые являются внешними ключами, т.е. artist_id и 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 + '\'' +
                '}';
    }
}

В отличие от приложения, описанного в посте "JPA-маппинг сущности и ее свойств", класс Artist не имеет поля genre:
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 + '\'' +
                '}';
    }
}

Создадим класс GenreService:
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();
    }
}

Добавим класс Genre в persistence unit в файле persistence.xml:
<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>

В методе main() класса JpaManyToManyMappingsTutorial создается объект типа GenreService, вызывается метод для получения сущности Genre и выводится информация о жанре и связанных с ним исполнителях:
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));
        }
    }
}

Запустим метод main():

Приложение вывело информацию о жанре и связанных с ним исполнителях.

Двунаправленный маппинг


Если сущности типов Genre и Artist будут иметь ссылки на коллекции друг друга, то отношения будут двунаправленными.

Для этого:
  • создадим поле genres типа List<Genre> в классе Artist
  • добавим полю genres аннотацию @ManyToMany
  • добавим элемент аннотации mappedBy, который будет указывать на имя поля класса Genre, которое имеет аннотации @ManyToMany и @JoinTable, т.е. 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 + '\'' +
                '}';
    }
}

Добавим в метод main() класса JpaManyToManyMappingsTutorial создание объекта типа ArtistService, вызов метода для получения сущности Artist и вывод информации об исполнителе и связанных с ним жанрах:
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));
        }
    }
}

Запустим метод main():

Приложение вывело информацию об исполнителе и связанных с ним жанрах, а значит мы успешно сделали однонаправленный и двунаправленный маппинги типа "многие-ко-многим".

Структура проекта:




Исходный код созданного приложения доступен по ссылке https://code.google.com/p/jpa-many-to-many-mappings-tutorial/.

Рекомендуемые посты:

2 comments:

  1. шедевр это public static

    ReplyDelete
    Replies
    1. А чем плох метод main() для данного примера?

      Delete