Thursday, April 17, 2014

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

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

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

Рассмотрим маппинг при использовании Table-per-concrete-class стратегии.

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

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

Для использования Table-per-concrete-class стратегии все данные каждого типа релиза должны храниться в отдельной таблице:

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

CREATE TABLE video_releases (
video_release_id INT PRIMARY KEY,
video_release_name VARCHAR(50),
video_release_year INT,
video_release_length VARCHAR(10),
video_release_type VARCHAR(10)
);

CREATE TABLE cd_releases (
cd_release_id INT PRIMARY KEY,
cd_release_name VARCHAR(50),
cd_release_year INT,
cd_release_length VARCHAR(10),
cd_release_number_of_tracks INT,
cd_release_type VARCHAR(10)
);

CREATE TABLE mp3_releases (
mp3_release_id INT PRIMARY KEY,
mp3_release_name VARCHAR(50),
mp3_release_year INT,
mp3_release_length VARCHAR(10),
mp3_release_number_of_tracks INT,
mp3_release_bitrate INT
);

INSERT INTO video_releases VALUES (1, 'Live on the Other Side', 2005, '01:18:21', 'DVD');
INSERT INTO video_releases VALUES (2, 'Live at Montreux 2004', 2008, '01:14:54', 'BD');

INSERT INTO cd_releases VALUES (1, 'Take a Look in the Mirror', 2003, '00:56:43', 13, 'AUDIO_CD');
INSERT INTO cd_releases VALUES (2, 'See You on the Other Side', 2005, '01:01:01', 14, 'SACD');

INSERT INTO mp3_releases VALUES (1, 'The Paradigm Shift', 2013, '00:49:00', 13, 320);

Создадим абстрактный класс Release с полями id, name, year и length.
Класс Release не соответствует какой-либо таблице базы данных, поэтому добавим аннотацию @MappedSuperclass, а не @Entity.
Для маппинга с использованием Table-per-concrete-class стратегии добавим аннотацию @Inheritance с указанием этого типа стратегии, т.е. InheritanceType.TABLE_PER_CLASS:
package com.jpa.table.per.concrete.klass.strategy.mapping.tutorial.domain;

import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Release {

 @Id
 private int id;

 private String name;
 private int year;
 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.table.per.concrete.klass.strategy.mapping.tutorial.domain;

public enum VideoReleaseType {
 DVD,
 BD
}

Создадим класс сущности VideoRelease с полем type типа VideoReleaseType.
В таблице video_releases идентификатор релиза хранится в колонке video_release_id, а не id. Аналогичная ситуация и с другими колонками.
Для изменения маппинга полей сущности VideoRelease добавим аннотацию @AttributeOverrides и аннотации @AttributeOverride для полей объекта:
package com.jpa.table.per.concrete.klass.strategy.mapping.tutorial.domain;

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

@Entity
@Table(name = "video_releases")
@AttributeOverrides({
  @AttributeOverride(name = "id", column = @Column(name = "video_release_id")),
  @AttributeOverride(name = "name", column = @Column(name = "video_release_name")),
  @AttributeOverride(name = "year", column = @Column(name = "video_release_year")),
  @AttributeOverride(name = "length", column = @Column(name = "video_release_length"))
})
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.table.per.concrete.klass.strategy.mapping.tutorial.domain;

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.table.per.concrete.klass.strategy.mapping.tutorial.domain;

public enum CDReleaseType {
 AUDIO_CD,
 SACD
}

Создадим класс сущности CDRelease с полем type типа CDReleaseType.
Аналогично, изменим маппинг полей сущности CDRelease:
package com.jpa.table.per.concrete.klass.strategy.mapping.tutorial.domain;

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

@Entity
@Table(name = "cd_releases")
@AttributeOverrides({
  @AttributeOverride(name = "id", column = @Column(name = "cd_release_id")),
  @AttributeOverride(name = "name", column = @Column(name = "cd_release_name")),
  @AttributeOverride(name = "year", column = @Column(name = "cd_release_year")),
  @AttributeOverride(name = "length", column = @Column(name = "cd_release_length")),
  @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();
  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.
Изменим маппинг полей сущности MP3Release:
package com.jpa.table.per.concrete.klass.strategy.mapping.tutorial.domain;

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

@Entity
@Table(name = "mp3_releases")
@AttributeOverrides({
  @AttributeOverride(name = "id", column = @Column(name = "mp3_release_id")),
  @AttributeOverride(name = "name", column = @Column(name = "mp3_release_name")),
  @AttributeOverride(name = "year", column = @Column(name = "mp3_release_year")),
  @AttributeOverride(name = "length", column = @Column(name = "mp3_release_length")),
  @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.table.per.concrete.klass.strategy.mapping.tutorial.service;

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

import com.jpa.table.per.concrete.klass.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();
 }
}

Создадим класс MP3ReleaseService:
package com.jpa.table.per.concrete.klass.strategy.mapping.tutorial.service;

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

</persistence>

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

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

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

public class JpaTablePerConcreteClassStrategyMappingTutorial {

 public static void main(String[] args) {
  EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaTablePerConcreteClassStrategyMappingTutorial");
  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():
--- Finding video releases ---
Found video release: VideoRelease{id=1, name='Live on the Other Side', year=2005, length='01:18:21', type=DVD}
Found video release: VideoRelease{id=2, name='Live at Montreux 2004', year=2008, length='01:14:54', type=BD}

--- Finding CD releases ---
Found CD release: CDRelease{id=1, name='Take a Look in the Mirror', year=2003, length='00:56:43', numberOfTracks=13, type=AUDIO_CD}
Found CD release: CDRelease{id=2, name='See You on the Other Side', year=2005, length='01:01:01', numberOfTracks=14, type=SACD}

--- Finding MP3 releases ---
Found MP3 release: MP3Release{id=1, name='The Paradigm Shift', year=2013, length='00:49:00', numberOfTracks=13, bitrate=320}

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

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

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

No comments:

Post a Comment