Skip to content

amazon-java-03-26/HelloFatherJPA

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Learning Objectives

By the end of this session, you will be able to:

  • Explain the three JPA inheritance mapping strategies (Single Table, Table per Class, Joined)
  • Implement the JOINED strategy using @Inheritance and @PrimaryKeyJoinColumn
  • Create embeddable value objects with @Embeddable and @Embedded
  • Use @AttributeOverrides when embedding the same class multiple times
  • Build a full API with inheritance-based entities and embedded components

1. Theory — Inheritance and Component Mapping

The problem with inheritance

In Java, inheritance is natural:

public class Mammal extends Animal { ... }

But relational databases have no concept of "extends". A table can't inherit from another table. So how do we store a class hierarchy in a database?

JPA provides three strategies to solve this:

Strategy 1: Single Table

All classes in the hierarchy are stored in one table. A discriminator column tells JPA which type each row is.

Pros Cons
Fast queries — no joins Lots of NULL columns
Simple schema Not normalized
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "animal_type")
public class Animal { ... }

Strategy 2: Table per Class

Each concrete class gets its own complete table with all fields (inherited + specific). No shared parent table exists.

Pros Cons
Simple, flat tables Duplicated columns across tables
No joins needed Polymorphic queries are slow (UNION)
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Animal { ... }

Strategy 3: Joined (recommended for normalized designs)

Each class gets its own table. Subclass tables only store their unique fields and reference the parent table via a foreign key.

Pros Cons
Clean, normalized — no redundancy Requires joins to read data
Easy to add new subclasses Slightly slower for deep hierarchies
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal { ... }

When to use which?

Strategy Best for
Single Table Few subclasses, few unique fields, read-heavy
Table per Class Subclasses are very different, rarely queried together
Joined Normalized designs, many subclasses, data integrity

Embeddable classes — when a class doesn't need its own table

Sometimes you have a value object (like an address, coordinates, or contact info) that belongs inside another entity, not in its own table.

JPA handles this with @Embeddable and @Embedded:

@Embeddable
public class GpsLocation {
    private Double latitude;
    private Double longitude;
}
@Entity
public class Zoo {
    @Id
    private Long id;
    private String name;

    @Embedded
    private GpsLocation location;  // latitude and longitude stored in the zoo table
}

If you embed the same class twice, use @AttributeOverrides to rename the columns:

@Embedded
private GpsLocation mainEntrance;

@AttributeOverrides({
    @AttributeOverride(name = "latitude", column = @Column(name = "exit_lat")),
    @AttributeOverride(name = "longitude", column = @Column(name = "exit_lng"))
})
@Embedded
private GpsLocation exitGate;

2. Live-Coding Demo — Zoo Management API

We'll build an API for a zoo that tracks different types of animals using JPA inheritance (JOINED strategy) and embeds GPS coordinates into the enclosure entity.

Step 1: Database setup

CREATE DATABASE zoo_management;
USE zoo_management;

CREATE TABLE animal (
    id BIGINT NOT NULL AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    species VARCHAR(100),
    age INT,
    weight DOUBLE,
    PRIMARY KEY (id)
);

CREATE TABLE mammal (
    id BIGINT NOT NULL,
    fur_color VARCHAR(50),
    gestation_days INT,
    PRIMARY KEY (id),
    FOREIGN KEY (id) REFERENCES animal(id)
);

CREATE TABLE bird (
    id BIGINT NOT NULL,
    wingspan_cm DOUBLE,
    can_fly BOOLEAN,
    PRIMARY KEY (id),
    FOREIGN KEY (id) REFERENCES animal(id)
);

CREATE TABLE reptile (
    id BIGINT NOT NULL,
    is_venomous BOOLEAN,
    scale_type VARCHAR(50),
    PRIMARY KEY (id),
    FOREIGN KEY (id) REFERENCES animal(id)
);

CREATE TABLE enclosure (
    id BIGINT NOT NULL AUTO_INCREMENT,
    enclosure_name VARCHAR(100) NOT NULL,
    habitat_type VARCHAR(50),
    capacity INT,
    latitude DOUBLE,
    longitude DOUBLE,
    PRIMARY KEY (id)
);

Notice how mammal, bird, and reptile only store their unique columns — they reference animal(id) for shared fields. The enclosure table stores GPS coordinates directly (no separate table).

Step 2: Parent Entity — Animal

import jakarta.persistence.*;

@Entity
@Table(name = "animal")
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String species;
    private Integer age;
    private Double weight;

    public Animal() {}

    public Animal(String name, String species, Integer age, Double weight) {
        this.name = name;
        this.species = species;
        this.age = age;
        this.weight = weight;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getSpecies() { return species; }
    public void setSpecies(String species) { this.species = species; }

    public Integer getAge() { return age; }
    public void setAge(Integer age) { this.age = age; }

    public Double getWeight() { return weight; }
    public void setWeight(Double weight) { this.weight = weight; }
}
  • @Inheritance(strategy = InheritanceType.JOINED) tells JPA to use the JOINED strategy.
  • This class maps to the animal table with shared fields.

Step 3: Child Entities

@Entity
@Table(name = "mammal")
@PrimaryKeyJoinColumn(name = "id")
public class Mammal extends Animal {

    @Column(name = "fur_color")
    private String furColor;

    @Column(name = "gestation_days")
    private Integer gestationDays;

    public Mammal() {}

    public Mammal(String name, String species, Integer age, Double weight,
                  String furColor, Integer gestationDays) {
        super(name, species, age, weight);
        this.furColor = furColor;
        this.gestationDays = gestationDays;
    }

    // Getters and setters
    public String getFurColor() { return furColor; }
    public void setFurColor(String furColor) { this.furColor = furColor; }

    public Integer getGestationDays() { return gestationDays; }
    public void setGestationDays(Integer gestationDays) { this.gestationDays = gestationDays; }
}
@Entity
@Table(name = "bird")
@PrimaryKeyJoinColumn(name = "id")
public class Bird extends Animal {

    @Column(name = "wingspan_cm")
    private Double wingspanCm;

    @Column(name = "can_fly")
    private Boolean canFly;

    public Bird() {}

    public Bird(String name, String species, Integer age, Double weight,
                Double wingspanCm, Boolean canFly) {
        super(name, species, age, weight);
        this.wingspanCm = wingspanCm;
        this.canFly = canFly;
    }

    // Getters and setters
    public Double getWingspanCm() { return wingspanCm; }
    public void setWingspanCm(Double wingspanCm) { this.wingspanCm = wingspanCm; }

    public Boolean getCanFly() { return canFly; }
    public void setCanFly(Boolean canFly) { this.canFly = canFly; }
}
@Entity
@Table(name = "reptile")
@PrimaryKeyJoinColumn(name = "id")
public class Reptile extends Animal {

    @Column(name = "is_venomous")
    private Boolean isVenomous;

    @Column(name = "scale_type")
    private String scaleType;

    public Reptile() {}

    public Reptile(String name, String species, Integer age, Double weight,
                   Boolean isVenomous, String scaleType) {
        super(name, species, age, weight);
        this.isVenomous = isVenomous;
        this.scaleType = scaleType;
    }

    // Getters and setters
    public Boolean getIsVenomous() { return isVenomous; }
    public void setIsVenomous(Boolean isVenomous) { this.isVenomous = isVenomous; }

    public String getScaleType() { return scaleType; }
    public void setScaleType(String scaleType) { this.scaleType = scaleType; }
}
  • @PrimaryKeyJoinColumn(name = "id") tells JPA that the child's id is also a foreign key to the parent's id.
  • Each child calls super(...) to set shared fields.

Step 4: Embeddable class — GpsLocation

import jakarta.persistence.Embeddable;

@Embeddable
public class GpsLocation {

    private Double latitude;
    private Double longitude;

    public GpsLocation() {}

    public GpsLocation(Double latitude, Double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    // Getters and setters
    public Double getLatitude() { return latitude; }
    public void setLatitude(Double latitude) { this.latitude = latitude; }

    public Double getLongitude() { return longitude; }
    public void setLongitude(Double longitude) { this.longitude = longitude; }
}
  • @Embeddable marks this as a value object — it has no @Id, no table of its own.

Step 5: Entity with embedded component — Enclosure

import jakarta.persistence.*;

@Entity
@Table(name = "enclosure")
public class Enclosure {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "enclosure_name")
    private String enclosureName;

    @Column(name = "habitat_type")
    private String habitatType;

    private Integer capacity;

    @Embedded
    private GpsLocation location;

    public Enclosure() {}

    public Enclosure(String enclosureName, String habitatType, Integer capacity, GpsLocation location) {
        this.enclosureName = enclosureName;
        this.habitatType = habitatType;
        this.capacity = capacity;
        this.location = location;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getEnclosureName() { return enclosureName; }
    public void setEnclosureName(String enclosureName) { this.enclosureName = enclosureName; }

    public String getHabitatType() { return habitatType; }
    public void setHabitatType(String habitatType) { this.habitatType = habitatType; }

    public Integer getCapacity() { return capacity; }
    public void setCapacity(Integer capacity) { this.capacity = capacity; }

    public GpsLocation getLocation() { return location; }
    public void setLocation(GpsLocation location) { this.location = location; }
}
  • @Embedded tells JPA to map the GpsLocation fields (latitude, longitude) directly into the enclosure table — no join, no extra table.

Step 6: Repositories

@Repository
public interface AnimalRepository extends JpaRepository<Animal, Long> {}

@Repository
public interface MammalRepository extends JpaRepository<Mammal, Long> {}

@Repository
public interface BirdRepository extends JpaRepository<Bird, Long> {}

@Repository
public interface ReptileRepository extends JpaRepository<Reptile, Long> {}

@Repository
public interface EnclosureRepository extends JpaRepository<Enclosure, Long> {}

You can create a repository for the parent and for each child. Querying AnimalRepository.findAll() returns all animals (mammals, birds, reptiles) — JPA handles the joins automatically.

Step 7: Service and Controller (Mammal example)

@Service
public class MammalService {

    @Autowired
    private MammalRepository mammalRepository;

    public List<Mammal> getAllMammals() { return mammalRepository.findAll(); }
    public Optional<Mammal> getMammalById(Long id) { return mammalRepository.findById(id); }
    public Mammal createMammal(Mammal mammal) { return mammalRepository.save(mammal); }
    public void deleteMammal(Long id) { mammalRepository.deleteById(id); }
}
@RestController
@RequestMapping("/mammals")
public class MammalController {

    @Autowired
    private MammalService mammalService;

    @GetMapping
    public List<Mammal> getAll() { return mammalService.getAllMammals(); }

    @GetMapping("/{id}")
    public Optional<Mammal> getById(@PathVariable Long id) { return mammalService.getMammalById(id); }

    @PostMapping
    public Mammal create(@RequestBody Mammal mammal) { return mammalService.createMammal(mammal); }

    @DeleteMapping("/{id}")
    public void delete(@PathVariable Long id) { mammalService.deleteMammal(id); }
}

Repeat the same pattern for Bird, Reptile, and Enclosure.

Step 8: Test with Postman

Method URL Body (JSON) What it does
POST /mammals {"name":"Leo","species":"Lion","age":8,"weight":190.5,"furColor":"Golden","gestationDays":110} Creates a mammal
POST /birds {"name":"Kiwi","species":"Kiwi","age":3,"weight":2.5,"wingspanCm":0,"canFly":false} Creates a bird
POST /reptiles {"name":"Slinky","species":"King Cobra","age":5,"weight":6.0,"isVenomous":true,"scaleType":"Smooth"} Creates a reptile
GET /mammals All mammals (JPA joins animal + mammal tables)
POST /enclosures {"enclosureName":"Savanna Zone","habitatType":"Grassland","capacity":12,"location":{"latitude":40.7128,"longitude":-74.0060}} Creates enclosure with embedded GPS

Check the console — you'll see JPA generating INSERT INTO animal followed by INSERT INTO mammal for a single POST /mammals request. That's the JOINED strategy in action.


3. Guided Practice — Vehicle Dealership API

Build an API for a vehicle dealership using JPA inheritance and embedded components.

Scenario

A dealership sells cars, trucks, and motorcycles. All vehicles share common fields, but each type has unique attributes. Each vehicle also has a warranty (start date and end date) that should be embedded, not stored in a separate table.

Requirements

  1. Database: vehicle_dealership

  2. Inheritance hierarchy (JOINED strategy):

    • Parent: vehicle — columns: id (BIGINT, PK, auto-increment), make (VARCHAR), model (VARCHAR), year (INT), price (DECIMAL), color (VARCHAR)
    • Child: car — extra columns: num_doors (INT), trunk_capacity_liters (INT)
    • Child: truck — extra columns: payload_capacity_kg (DOUBLE), num_axles (INT)
    • Child: motorcycle — extra columns: engine_cc (INT), has_sidecar (BOOLEAN)
  3. Embeddable class:

    • Warranty with fields: warrantyStart (DATE), warrantyEnd (DATE)
    • Embed it into each vehicle entity
  4. Build the full stack (Entity, Repository, Service, Controller) for each vehicle type.

  5. Endpoints:

    • GET /cars, POST /cars, GET /cars/{id}, DELETE /cars/{id}
    • Same for /trucks and /motorcycles
  6. Test with Postman — create at least 2 of each vehicle type and verify that the JOINED strategy creates rows in both the parent and child tables.


Remember

  • Databases don't support inheritance — JPA provides three strategies to map it.
  • Single Table = one table, discriminator column, lots of NULLs.
  • Table per Class = separate complete tables, no joins but data redundancy.
  • Joined = parent table + child tables linked by FK — clean and normalized.
  • Use @Inheritance(strategy = InheritanceType.JOINED) on the parent entity.
  • Use @PrimaryKeyJoinColumn(name = "id") on each child entity.
  • @Embeddable marks a value object (no @Id, no own table).
  • @Embedded puts the embeddable's fields directly into the parent entity's table.
  • Use @AttributeOverrides when embedding the same class more than once.
  • Querying the parent repository returns all subtypes — JPA handles the joins.

Additional Resources

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages