Friday, November 1, 2013

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

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

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

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

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

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

Для использования Single-table стратегии данные всех типов релизов должны храниться в одной таблице.
Наша таблица будет называться releases и будет иметь колонки, которые соответствуют полям всех классов иерархии - Release, VideoRelease, AudioRelease, CDRelease и MP3Release.
Кроме того, в таблице releases будет колонка release_type, которая будет хранить тип каждого релиза. Так как у нас три типа релизов, то колонка будет хранить значения "Video", "CD" или "MP3":

















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

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

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

Создадим абстрактный класс Release с полями id, name, year и length.
Класс Release соответствует таблице releases, поэтому добавим аннотацию @Entity.
Для маппинга с использованием Single-table стратегии добавим аннотацию @Inheritance с указанием этого типа стратегии, т.е. InheritanceType.SINGLE_TABLE.
Добавим аннотацию @DiscriminatorColumn с указанием имени колонки, которая хранит тип релиза, т.е. release_type:
package com.jpa.single.table.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.SINGLE_TABLE)
@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;
 }
}

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

public enum VideoReleaseType {
 DVD,
 BD
}

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

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

@Entity
@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.single.table.strategy.mapping.tutorial.domain;

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

@MappedSuperclass
public abstract class AudioRelease extends Release {

 @Column(name = "audio_release_number_of_tracks")
 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.single.table.strategy.mapping.tutorial.domain;

public enum CDReleaseType {
 AUDIO_CD,
 SACD
}

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

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

@Entity
@DiscriminatorValue("CD")
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":
package com.jpa.single.table.strategy.mapping.tutorial.domain;

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

@Entity
@DiscriminatorValue("MP3")
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.single.table.strategy.mapping.tutorial.service;

import com.jpa.single.table.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.single.table.strategy.mapping.tutorial.service;

import com.jpa.single.table.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.single.table.strategy.mapping.tutorial.service;

import com.jpa.single.table.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="JpaSingleTableStrategyMappingTutorial" transaction-type="RESOURCE_LOCAL">
        <class>com.jpa.single.table.strategy.mapping.tutorial.domain.Release</class>
        <class>com.jpa.single.table.strategy.mapping.tutorial.domain.VideoRelease</class>
        <class>com.jpa.single.table.strategy.mapping.tutorial.domain.AudioRelease</class>
        <class>com.jpa.single.table.strategy.mapping.tutorial.domain.CDRelease</class>
        <class>com.jpa.single.table.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_single_table_strategy_mapping_tutorial"/>
            <property name="javax.persistence.jdbc.user" value="root"/>
            <property name="javax.persistence.jdbc.password" value="root"/>
        </properties>
    </persistence-unit>

</persistence>

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

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

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

public class JpaSingleTableStrategyMappingTutorial {

 public static void main(String[] args) {
  EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaSingleTableStrategyMappingTutorial");
  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, а значит мы успешно сделали маппинг иерархии классов с помощью Single-table стратегии.

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



































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

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

No comments:

Post a Comment