Monday, January 18, 2016

JPA Single-table class hierarchy mapping tutorial

Usually there are many classes in large projects. Some of them extend others and form class hierarchies with them. Let's figure out how to map such classes.

There are three strategies to map class hierarchy:
  • Single-table
  • Joined
  • Table-per-concrete-class

Now we will use Single-table mapping strategy.

This post is available in Russian.

Music artist has video and audio releases. There are two types of last ones - CD and MP3.
All releases types can be described by next class hierarchy:

Release and AudioRelease classes are abstract, so that we actually have three release types - VideoRelease, CDRelease and MP3Release.

To use Single-table mapping strategy we have to store all releases data in one database table.
This table will be named releases and it will have columns according to properties of all hierarchy classes - Release, VideoRelease, AudioRelease, CDRelease and MP3Release.
Besides, releases table will have release_type column to store concrete release type. We have three release types so that column will store "Video", "CD" or "MP3" values:

















Create jpa_single_table_strategy_mapping_tutorial database with releases table. Add data about some releases of Korn band:
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');

Create Release abstract class with id, name, year and length properties.
Release class corresponds releases table, so add @Entity annotation for it.
To use Single-table mapping strategy add @Inheritance annotation with appropriate strategy property value, e.g. InheritanceType.SINGLE_TABLE.
Add @DiscriminatorColumn annotation and set name property value to name of column that stores release type, e.g. 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;
 }
}

There are next types of video releases - DVD, BD (Blue-ray Disc) and others.
Create VideoReleaseType enum with "DVD" and "BD" values:
package com.jpa.single.table.strategy.mapping.tutorial.domain;

public enum VideoReleaseType {
 DVD,
 BD
}

Create VideoRelease entity class with type property of VideoReleaseType type.
Add @DiscriminatorValue annotation with proper release type value, e.g. "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 +
    '}';
 }
}

Create AudioRelease abstract class. CDRelease and MP3Release classes will inherit from it.
AudioRelease class does not correspond to any database table, so that add @MappedSuperclass annotation instead of @Entity for it:
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;
 }
}

There are next CD release types - Audio CD, SACD (Super Audio CD) and others.
Create CDReleaseType enum with "AUDIO_CD" and "SACD" values:
package com.jpa.single.table.strategy.mapping.tutorial.domain;

public enum CDReleaseType {
 AUDIO_CD,
 SACD
}

Create CDRelease entity class with type property of CDReleaseType type.
Add @DiscriminatorValue annotation with proper release type value, e.g. "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 +
    '}';
 }
}

Create MP3Release entity class with bitrate property.
Add @DiscriminatorValue annotation with proper release type value, e.g. "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 +
    '}';
 }
}

Create VideoReleaseService class:
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();
 }
}

Create CDReleaseService class:
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();
 }
}

Create MP3Service class:
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();
 }
}

Add Release, VideoRelease, AudioRelease, CDRelease and MP3Release classes 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="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>

Add code that creates service objects (VideoReleaseServiceCDReleaseService and MP3ReleaseService), finds artist releases and prints their data to main() method of JpaSingleTableStrategyMappingTutorial class:
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));
  }
 }
}

Execute main() method:










Application has printed Korn band releases data, which means we have successfully created class hierarchy mapping with Single-table strategy.

Project structure:



































Application source code is available at https://code.google.com/p/jpa-single-table-strategy-mapping-tutorial/.

Recommended posts:

No comments:

Post a Comment