Monday, November 25, 2013

JPA-маппинг иерархии классов с помощью Joined стратегии

В крупных проектах с множеством классов одни классы наследуют другие и формируют с ними иерархии. Рассмотрим маппинг таких классов.

Существует три стратегии маппинга иерархий наследования:
  • Single-table
  • Joined
  • Table-per-concrete-class

Рассмотрим маппинг при использовании Joined стратегии.

Музыкальный исполнитель может выпускать видео- и аудио-релизы. Аудио-релизы бывают двух типов - CD и MP3.
Все эти релизы можно представить следующей иерархией классов:

Классы Release и AudioRelease - абстрактные, поэтому типов релизов у нас три - VideoRelease, CDRelease и MP3Release.

Для использования Joined стратегии данные должны храниться следующим образом:
  • данные, общие для всех типов релизов, должны храниться в одной таблице - releases
  • данные конкретных типов релизов должны храниться в соответвующих таблицах - video_releases, cd_releases и mp3_releases
  • таблицы конкретных типов релизов (video_releases, cd_releases и mp3_releases) должны быть связаны с таблицей общих данных релизов (releases) с помощью внешних ключей

Создадим базу данных jpa_joined_strategy_mapping_tutorial и таблицы releases, video_releases, cd_releases и mp3_releases.
Добавим данные о некоторых релизах группы Korn:
CREATE DATABASE jpa_joined_strategy_mapping_tutorial;
USE jpa_joined_strategy_mapping_tutorial;

CREATE TABLE releases (
release_id INT PRIMARY KEY,
release_name VARCHAR(50),
release_year INT,
release_length VARCHAR(10),
release_type VARCHAR(10)
);

CREATE TABLE video_releases (
release_id INT PRIMARY KEY,
video_release_type VARCHAR(10),
FOREIGN KEY (release_id) references releases (release_id)
);

CREATE TABLE cd_releases (
release_id INT PRIMARY KEY,
cd_release_number_of_tracks INT,
cd_release_type VARCHAR(10),
FOREIGN KEY (release_id) references releases (release_id)
);

CREATE TABLE mp3_releases (
release_id INT PRIMARY KEY,
mp3_release_number_of_tracks INT,
mp3_release_bitrate INT,
FOREIGN KEY (release_id) references releases (release_id)
);

INSERT INTO releases VALUES (1, 'Live on the Other Side', 2005, '01:18:21', 'Video');
INSERT INTO releases VALUES (2, 'Live at Montreux 2004', 2008, '01:14:54', 'Video');
INSERT INTO releases VALUES (3, 'Take a Look in the Mirror', 2003, '00:56:43', 'CD');
INSERT INTO releases VALUES (4, 'See You on the Other Side', 2005, '01:01:01', 'CD');
INSERT INTO releases VALUES (5, 'The Paradigm Shift', 2013, '00:49:00', 'MP3');

INSERT INTO video_releases VALUES (1, 'DVD');
INSERT INTO video_releases VALUES (2, 'BD');

INSERT INTO cd_releases VALUES (3, 13, 'AUDIO_CD');
INSERT INTO cd_releases VALUES (4, 14, 'SACD');

INSERT INTO mp3_releases VALUES (5, 13, 320);

Создадим абстрактный класс Release с полями id, name, year и length.
Класс Release соответствует таблице releases, поэтому добавим аннотацию @Entity.
Для маппинга с использованием Joined стратегии добавим аннотацию @Inheritance с указанием этого типа стратегии, т.е. InheritanceType.JOINED.
Добавим аннотацию @DiscriminatorColumn с указанием имени колонки, которая хранит тип релиза, т.е. release_type:
package com.jpa.joined.strategy.mapping.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "releases")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "release_type")
public abstract class Release {

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

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

 @Column(name = "release_year")
 private int year;

 @Column(name = "release_length")
 private String length;

    protected Release() {

 }

 protected Release(int id, String name, int year, String length) {
  this.id = id;
  this.name = name;
  this.year = year;
  this.length = length;
 }

 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 int getYear() {
  return year;
 }

 public void setYear(int year) {
  this.year = year;
 }

 public String getLength() {
  return length;
 }

 public void setLength(String length) {
  this.length = length;
 }

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

Видео-релизы бывают различных типов - DVD, BD (Blue-ray Disc) и других.
Создадим класс перечисляемого типа VideoReleaseType с возможными значениями "DVD" и "BD":
package com.jpa.joined.strategy.mapping.tutorial.domain;

public enum VideoReleaseType {
 DVD,
 BD
}

Создадим класс сущности VideoRelease с полем type типа VideoReleaseType.
Добавим аннотацию @DiscriminatorValue с указанием типа релиза, т.е. "Video":
package com.jpa.joined.strategy.mapping.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;

@Entity
@Table(name = "video_releases")
@DiscriminatorValue("Video")
public class VideoRelease extends Release {

    @Column(name = "video_release_type")
    @Enumerated(EnumType.STRING)
    private VideoReleaseType type;

    public VideoRelease() {

    }

    public VideoRelease(int id, String name, int year, String length, VideoReleaseType type) {
        super(id, name, year, length);
        this.type = type;
    }

    public VideoReleaseType getType() {
        return type;
    }

    public void setType(VideoReleaseType type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "VideoRelease{" +
                "id=" + getId() +
                ", name='" + getName() + '\'' +
                ", year=" + getYear() +
                ", length='" + getLength() + '\'' +
                ", type=" + type +
                '}';
    }
}

Создадим абстрактный класс AudioRelease, от которого будут наследоваться классы конкретных релизов - CDRelease и MP3Release.
Класс AudioRelease не соответствует какой-либо таблице базы данных, поэтому добавим аннотацию @MappedSuperclass, а не @Entity:
package com.jpa.joined.strategy.mapping.tutorial.domain;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public abstract class AudioRelease extends Release {

    private int numberOfTracks;

    protected AudioRelease() {

    }

    protected AudioRelease(int id, String name, int year, String length, int numberOfTracks) {
        super(id, name, year, length);
        this.numberOfTracks = numberOfTracks;
    }

    public int getNumberOfTracks() {
        return numberOfTracks;
    }

    public void setNumberOfTracks(int numberOfTracks) {
        this.numberOfTracks = numberOfTracks;
    }
}

CD-релизы бывают различных типов - Audio CD, SACD (Super Audio CD) и других.
Создадим класс перечисляемого типа CDReleaseType с возможными значениями "AUDIO_CD" и "SACD":
package com.jpa.joined.strategy.mapping.tutorial.domain;

public enum CDReleaseType {
 AUDIO_CD,
 SACD
}

Создадим класс сущности CDRelease с полем type типа CDReleaseType.
Добавим аннотацию @DiscriminatorValue с указанием типа релиза, т.е. "CD".
В таблице cd_releases данные о количестве треков на альбомах хранятся в колонке cd_release_number_of_tracks. Имя колонки не совпадает с именем поля numberOfTracks. Для изменения маппинга поля добавим аннотацию @AttributeOverride с указанием имен поля и колонки таблицы, т.е. numberOfTracks и cd_release_number_of_tracks:
package com.jpa.joined.strategy.mapping.tutorial.domain;

import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Table;

@Entity
@Table(name = "cd_releases")
@DiscriminatorValue("CD")
@AttributeOverride(name = "numberOfTracks", column = @Column(name = "cd_release_number_of_tracks"))
public class CDRelease extends AudioRelease {

    @Column(name = "cd_release_type")
    @Enumerated(EnumType.STRING)
    private CDReleaseType type;

    public CDRelease() {

    }

    public CDRelease(int id, String name, int year, String length, int numberOfTracks, CDReleaseType type) {
        super(id, name, year, length, numberOfTracks);
        this.type = type;
    }

    public CDReleaseType getType() {
        return type;
    }

    public void setType(CDReleaseType type) {
        this.type = type;
    }

    @Override
    public String toString() {
        return "CDRelease{" +
                "id=" + getId() +
                ", name='" + getName() + '\'' +
                ", year=" + getYear() +
                ", length='" + getLength() + '\'' +
                ", numberOfTracks=" + getNumberOfTracks() +
                ", type=" + type +
                '}';
    }
}

Создадим класс сущности MP3Release с полем bitrate.
Добавим аннотацию @DiscriminatorValue с указанием типа релиза, т.е. "MP3".
В таблице mp3_releases данные о количестве треков на альбомах хранятся в колонке mp3_release_number_of_tracks. Как и в классе CDReleases, для изменения маппинга поля numberOfTracks добавим аннотацию @AttributeOverride с указанием имен поля и колонки таблицы, т.е. numberOfTracks и mp3_release_number_of_tracks:
package com.jpa.joined.strategy.mapping.tutorial.domain;

import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "mp3_releases")
@DiscriminatorValue("MP3")
@AttributeOverride(name = "numberOfTracks", column = @Column(name = "mp3_release_number_of_tracks"))
public class MP3Release extends AudioRelease {

    @Column(name = "mp3_release_bitrate")
    private int bitrate;

    public MP3Release() {

    }

    public MP3Release(int id, String name, int year, String length, int numberOfTracks, int bitrate) {
        super(id, name, year, length, numberOfTracks);
        this.bitrate = bitrate;
    }

    public int getBitrate() {
        return bitrate;
    }

    public void setBitrate(int bitrate) {
        this.bitrate = bitrate;
    }

    @Override
    public String toString() {
        return "MP3Release{" +
                "id=" + getId() +
                ", name='" + getName() + '\'' +
                ", year=" + getYear() +
                ", length='" + getLength() + '\'' +
                ", numberOfTracks=" + getNumberOfTracks() +
                ", bitrate=" + bitrate +
                '}';
    }
}

Создадим класс VideoReleaseService:
package com.jpa.joined.strategy.mapping.tutorial.service;

import com.jpa.joined.strategy.mapping.tutorial.domain.VideoRelease;

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

public class VideoReleaseService {

 private EntityManager em;

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

 public List<VideoRelease> findAllVideoReleases() {
  TypedQuery<VideoRelease> query = em.createQuery("SELECT v FROM VideoRelease v", VideoRelease.class);
  return query.getResultList();
 }
}

Создадим класс CDReleaseService:
package com.jpa.joined.strategy.mapping.tutorial.service;

import com.jpa.joined.strategy.mapping.tutorial.domain.CDRelease;

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

public class CDReleaseService {

 private EntityManager em;

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

 public List<CDRelease> findAllCDReleases() {
  TypedQuery<CDRelease> query = em.createQuery("SELECT c FROM CDRelease c", CDRelease.class);
  return query.getResultList();
 }
}

Создадим класс MP3Service:
package com.jpa.joined.strategy.mapping.tutorial.service;

import com.jpa.joined.strategy.mapping.tutorial.domain.MP3Release;

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

public class MP3ReleaseService {

 private EntityManager em;

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

 public List<MP3Release> findAllMP3Releases() {
  TypedQuery<MP3Release> query = em.createQuery("SELECT m FROM MP3Release m", MP3Release.class);
  return query.getResultList();
 }
}

Добавим классы Release, VideoRelease, AudioRelease, CDRelease и MP3Release в 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="JpaJoinedTableStrategyMappingTutorial" transaction-type="RESOURCE_LOCAL">
        <class>com.jpa.joined.strategy.mapping.tutorial.domain.Release</class>
        <class>com.jpa.joined.strategy.mapping.tutorial.domain.VideoRelease</class>
        <class>com.jpa.joined.strategy.mapping.tutorial.domain.AudioRelease</class>
        <class>com.jpa.joined.strategy.mapping.tutorial.domain.CDRelease</class>
        <class>com.jpa.joined.strategy.mapping.tutorial.domain.MP3Release</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_joined_strategy_mapping_tutorial"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="root"/>
        </properties>
    </persistence-unit>

</persistence>

В методе main() класса JpaJoinedStrategyMappingTutorial создаются объекты сервисов (VideoReleaseService, CDReleaseService и MP3ReleaseService), вызываются методы для получения сущностей релизов (VideoRelease, CDRelease и MP3Release) и выводится информация о них:
package com.jpa.joined.strategy.mapping.tutorial;

import com.jpa.joined.strategy.mapping.tutorial.domain.CDRelease;
import com.jpa.joined.strategy.mapping.tutorial.domain.MP3Release;
import com.jpa.joined.strategy.mapping.tutorial.domain.VideoRelease;
import com.jpa.joined.strategy.mapping.tutorial.service.CDReleaseService;
import com.jpa.joined.strategy.mapping.tutorial.service.MP3ReleaseService;
import com.jpa.joined.strategy.mapping.tutorial.service.VideoReleaseService;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import java.util.List;

public class JpaJoinedStrategyMappingTutorial {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaJoinedStrategyMappingTutorial");
        EntityManager em = emf.createEntityManager();
        VideoReleaseService videoReleaseService = new VideoReleaseService(em);
        CDReleaseService cdReleaseService = new CDReleaseService(em);
        MP3ReleaseService mp3ReleaseService = new MP3ReleaseService(em);

        System.out.println("--- Finding video releases ---");
        List<VideoRelease> videoReleases = videoReleaseService.findAllVideoReleases();
        for (VideoRelease videoRelease : videoReleases) {
            System.out.println(String.format("Found video release: %s", videoRelease));
        }

        System.out.println("\n--- Finding CD releases ---");
        List<CDRelease> cdReleases = cdReleaseService.findAllCDReleases();
        for (CDRelease cdRelease : cdReleases) {
            System.out.println(String.format("Found CD release: %s", cdRelease));
        }

        System.out.println("\n--- Finding MP3 releases ---");
        List<MP3Release> mp3Releases = mp3ReleaseService.findAllMP3Releases();
        for (MP3Release mp3Release : mp3Releases) {
            System.out.println(String.format("Found MP3 release: %s", mp3Release));
        }
    }
}

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

Приложение вывело информацию о всех релизах группы Korn, а значит мы успешно сделали маппинг иерархии классов с помощью Joined стратегии.

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



































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

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

No comments:

Post a Comment