35
Introduction While much of the work in relational data modeling is database-agnostic, there are many database provider-specific details that need to be addressed when moving to implementation. In part, the use of ORMs reduces the variation in implementation details from database to database; however, lightweight ORMs suitable for use in code targeting browsers or mobile devices are generally not as full-featured or standards-conformant as an enterprise-capable, JPA-based ORM such as Hibernate. To help student deals with these differences—and to provide a starting point for real-world data model implementation—we are assembling a collection (still in progress) of implementation scenarios—each with a summary description of the data model, a physical entity-relationship diagram (ERD), Room entity classes, Room-generated data definition language (DDL) SQL statements for creating the database objects, JPA/Hibernate entity classes, and JPA/Hibernate-generated DDL. Platforms JPA stands for Java Persistence API. JPA is a subset of the Java Enterprise Edition—or Java EE—set of specifications, which has now been rebranded as Jakarta EE. JPA is the industry standard for Java object persistence, particularly at the enterprise level. Hibernate is the most widely used implementation of the JPA specification. To the extent that we consider reasonable, we’re avoiding Hibernate-specific extensions or additions to JPA; however, some portion of the annotations we’re using, and some of the resulting DDL-generation and application runtime behaviors, are Hibernate specific, but well supported and widely used. (Also, note that we’re assuming an Apache Derby database, which has implications for details such as varchar column collation and length.) Rather than try to distinguish between JPA standard features and the relatively small portion of Hibernate-specific features, the relevant usage examples in this document are identified by “JPA/Hibernate”. Room is the ORM supported by Google for development targeted to the Android platform. It is not based on JPA (or even JDBC), but is SQLite- and Android-specific. For further practical notes on differences in developer usage between Room and JPA/Hibernate, see “Platform notes” . Scenarios 1

Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

  • Upload
    others

  • View
    13

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

Introduction

While much of the work in relational data modeling is database-agnostic, there are many databaseprovider-specific details that need to be addressed when moving to implementation. In part, the use ofORMs reduces the variation in implementation details from database to database; however, lightweightORMs suitable for use in code targeting browsers or mobile devices are generally not as full-featured orstandards-conformant as an enterprise-capable, JPA-based ORM such as Hibernate.

To help student deals with these differences—and to provide a starting point for real-world data modelimplementation—we are assembling a collection (still in progress) of implementation scenarios—each with asummary description of the data model, a physical entity-relationship diagram (ERD), Room entity classes,Room-generated data definition language (DDL) SQL statements for creating the database objects,JPA/Hibernate entity classes, and JPA/Hibernate-generated DDL.

Platforms

JPA stands for Java Persistence API. JPA is a subset of the Java Enterprise Edition—or Java EE—set ofspecifications, which has now been rebranded as Jakarta EE. JPA is the industry standard for Java objectpersistence, particularly at the enterprise level.

Hibernate is the most widely used implementation of the JPA specification. To the extent that we considerreasonable, we’re avoiding Hibernate-specific extensions or additions to JPA; however, some portion of theannotations we’re using, and some of the resulting DDL-generation and application runtime behaviors, areHibernate specific, but well supported and widely used. (Also, note that we’re assuming an Apache Derbydatabase, which has implications for details such as varchar column collation and length.) Rather than tryto distinguish between JPA standard features and the relatively small portion of Hibernate-specificfeatures, the relevant usage examples in this document are identified by “JPA/Hibernate”.

Room is the ORM supported by Google for development targeted to the Android platform. It is not basedon JPA (or even JDBC), but is SQLite- and Android-specific.

For further practical notes on differences in developer usage between Room and JPA/Hibernate, see“Platform notes”.

Scenarios

1

Page 2: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

Student absences (one-to-many, with dependent entities)

Summary

In a data model intended for use in an application that allows a teacher or administrator to record studentfull- or partial-day absences, we will record absences by defining and implementing a Student entity andan Absence entity. Every absence is associated with exactly 1 student, and every student may have zero ormore absences recorded.

Relationships

The relationship of Student to Absence is one-to-zero-or-more. This tells us that the foreign key will bedefined in the Absence entity—though that may not be obvious when reading the code of the Absenceentity class (especially when using JPA/Hibernate).

In this case, Absence is a wholly dependent entity: an absence can’t exist independent of the associatedstudent. So the most important cascading event here is deletion: When a Student instance is deleted, all ofthe related Absence instances should also be deleted from the database. In Hibernate, the persist (save)event should also be cascaded (in fact, CascadeType.ALL is a reasonable choice here—at least for the initialimplementation), so that a student’s collection of absences will be saved to the database if the Studentinstance is updated.

ERD

2

Page 3: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

Entity Classes

Room

Student

package edu.cnm.deepdive.attendance.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Index; import androidx.room.PrimaryKey; @Entity( indices = @Index(value = {"last_name", "first_name"}, unique = true) )public class Student { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "student_id") private long id; @NonNull @ColumnInfo(name = "last_name", collate = ColumnInfo.NOCASE) private String lastName; @NonNull @ColumnInfo(name = "first_name", collate = ColumnInfo.NOCASE) private String firstName; @ColumnInfo(index = true, collate = ColumnInfo.NOCASE) private String email; public long getId() { return id; } public void setId(long id) { this.id = id; } @NonNull public String getLastName() { return lastName; } public void setLastName(@NonNull String lastName) { this.lastName = lastName; }

3

Page 4: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@NonNull public String getFirstName() { return firstName; } public void setFirstName(@NonNull String firstName) { this.firstName = firstName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }

Absence

package edu.cnm.deepdive.attendance.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; import java.util.Date; @Entity( indices = { @Index(value = {"student_id", "start"}, unique = true), @Index(value = {"start", "duration"}), @Index(value = "excused") }, foreignKeys = { @ForeignKey( entity = Student.class, childColumns = "student_id", parentColumns = "student_id", onDelete = ForeignKey.CASCADE ) } ) public class Absence { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "absence_id")

4

Page 5: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@ColumnInfo(name = "absence_id") private long id; @ColumnInfo(name = "student_id", index = true) private long studentId; @NonNull private Date start = new Date(); private Integer duration; private boolean excused; public long getId() { return id; } public void setId(long id) { this.id = id; } public long getStudentId() { return studentId; } public void setStudentId(long studentId) { this.studentId = studentId; } @NonNull public Date getStart() { return start; } public void setStart(@NonNull Date start) { this.start = start; } public Integer getDuration() { return duration; } public void setDuration(Integer duration) { this.duration = duration; } public boolean isExcused() { return excused; } public void setExcused(boolean excused) { this.excused = excused;

5

Page 6: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

this.excused = excused; } }

JPA/Hibernate

Student

package edu.cnm.deepdive.attendance.model.entity; import java.util.LinkedList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.springframework.lang.NonNull; @Entity @Table( uniqueConstraints = @UniqueConstraint(columnNames = {"lastName", "firstName"}), indexes = @Index(columnList = "email") ) public class Student { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "student_id", updatable = false, nullable = false) private Long id; @NonNull @Column(nullable = false, length = 100) private String lastName; @NonNull @Column(nullable = false, length = 100) private String firstName;

private String email; @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)

6

Page 7: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

orphanRemoval = true) @OrderBy("start, duration ASC") private List<Absence> absences = new LinkedList<>(); public Long getId() { return id; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public List<Absence> getAbsences() { return absences; } }

Absence

package edu.cnm.deepdive.attendance.model.entity; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Index; import javax.persistence.JoinColumn;

7

Page 8: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.springframework.lang.NonNull; @Entity @Table( uniqueConstraints = @UniqueConstraint(columnNames = {"student_id", "start"}), indexes = { @Index(columnList = "start, duration"), @Index(columnList = "excused") } ) public class Absence { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "absence_id", updatable = false, nullable = false) private Long id; @NonNull @Column(nullable = false) private Date start = new Date(); @NonNull @ManyToOne(fetch = FetchType.EAGER, optional = false) @JoinColumn(name = "student_id", nullable = false, updatable = false) private Student student; private Integer duration; private boolean excused; public Long getId() { return id; } public Date getStart() { return start; } public void setStart(Date start) { this.start = start; } public Student getStudent() { return student; } public void setStudent(Student student) {

8

Page 9: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

public void setStudent(Student student) { this.student = student; } public Integer getDuration() { return duration; } public void setDuration(Integer duration) { this.duration = duration; } public boolean isExcused() { return excused; } public void setExcused(boolean excused) { this.excused = excused; } }

DDL

Room

9

Page 10: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

CREATE TABLE IF NOT EXISTS `Student`( `student_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `last_name` TEXT NOT NULL COLLATE NOCASE, `first_name` TEXT NOT NULL COLLATE NOCASE, `email` TEXT COLLATE NOCASE);

CREATE UNIQUE INDEX IF NOT EXISTS `index_Student_last_name_first_name` ON `Student` (`last_name`, `first_name`);

CREATE INDEX IF NOT EXISTS `index_Student_email` ON `Student` (`email`);

CREATE TABLE IF NOT EXISTS `Absence`( `absence_id` INTEGER PRIMARY KEY AUTOINCREMENT, `student_id` INTEGER NOT NULL, `start` INTEGER NOT NULL, `duration` INTEGER, `excused` INTEGER NOT NULL, FOREIGN KEY (`student_id`) REFERENCES `Student` (`student_id`) ON UPDATE NO ACTION ON DELETE CASCADE);

CREATE UNIQUE INDEX IF NOT EXISTS `index_Absence_student_id_start` ON `Absence` (`student_id`, `start`);

CREATE INDEX IF NOT EXISTS `index_Absence_start_duration` ON `Absence` (`start`, `duration`);

CREATE INDEX IF NOT EXISTS `index_Absence_excused` ON `Absence` (`excused`);

CREATE INDEX IF NOT EXISTS `index_Absence_student_id` ON `Absence` (`student_id`);

JPA/Hibernate

10

Page 11: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

create sequence hibernate_sequence start with 1 increment by 1;

create table absence( absence_id bigint not null, duration integer, excused boolean not null, start timestamp not null, student_id bigint not null, primary key (absence_id));

create table student( student_id bigint not null, email varchar(255), first_name varchar(100) not null, last_name varchar(100) not null, primary key (student_id));

create index IDXaiq0059v382e7to0mq9x6eahx on absence (start, duration);

create index IDX6a0mokdwt4oytv1ms9xvvo6am on absence (excused);

create unique index UKll6js94oe25wf2w3j7k1ahri4 on absence (student_id, start);

create index IDXfe0i52si7ybu0wjedj6motiim on student (email);

create unique index UKrq6ii8dmivdebx7sla70hmrxv on student (last_name, first_name);

alter table absence add constraint FKlgpyjtylm8d0pocl7xo1k4thc foreign key (student_id) references student;

Volleyball league (one-to-many, with independent entities)

Summary

This data model is actually a portion of a larger model, supporting a sports league management application.In this example, we’ll be dealing with roster management—namely, teams and members; thus, our entitieswill be Team and Member . (We’re not including game schedules, team statistics, player statistics, etc. in thisexample.)

11

Page 12: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

Relationships

The data model for our theoretical volleyball league has an important difference in relationships vs. that forstudent absences: While each Member is associated with at most 1 Team at a time, and a Team can havemultiple players, the Member entity instances are actually independent of the Team instances in a criticalsense: removal of a Team should not, in general, trigger removal of the associated Member instances. Thecorollary to this is that a Member may be in the league (i.e. in the database), without being a member of aTeam .

The relationship of Team to Member is one-to-many. This tells us that the foreign key will be defined in theMember table. Once again, using JPA lets us include a collection of Member instances in the Team entityclass, but the population of that collection at runtime is done by executing a join between the tables; thatcollection is actually not an attribute of the underlying Team entity itself.

As described above, the Member entity is not wholly dependent on Team , so deletion of an instance of thelatter must not trigger deletion of the former. As a consequence of this, CascadeType.ALL is not theappropriate option in this case. We will still need to cascade some events, but not deletion; nor should wedo automatic orphan removal.

ERD

Entity classes

Room

Team

12

Page 13: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

package edu.cnm.deepdive.league.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Index; import androidx.room.PrimaryKey; @Entity( indices = @Index(value = "name", unique = true) ) public class Team { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "team_id") private long id; @NonNull @ColumnInfo(collate = ColumnInfo.NOCASE) private String name; public long getId() { return id; } public void setId(long id) { this.id = id; } @NonNull public String getName() { return name; } public void setName(@NonNull String name) { this.name = name; } }

Member

package edu.cnm.deepdive.league.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey;

13

Page 14: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@Entity( indices = { @Index(value = {"last_name", "first_name"}, unique = true), @Index(value = {"email"}, unique = true) }, foreignKeys = { @ForeignKey( entity = Team.class, childColumns = "team_id", parentColumns = "team_id" ) } ) public class Member { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "member_id") private long id; @ColumnInfo(name = "team_id", index = true) private long teamId; @NonNull @ColumnInfo(name = "last_name", collate = ColumnInfo.NOCASE) private String lastName; @NonNull @ColumnInfo(name = "first_name", collate = ColumnInfo.NOCASE) private String firstName; @NonNull private String phone; @NonNull @ColumnInfo(collate = ColumnInfo.NOCASE) private String email; public long getId() { return id; } public void setId(long id) { this.id = id; } public long getTeamId() { return teamId; } public void setTeamId(long teamId) {

14

Page 15: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

this.teamId = teamId; } @NonNull public String getLastName() { return lastName; } public void setLastName(@NonNull String lastName) { this.lastName = lastName; } @NonNull public String getFirstName() { return firstName; } public void setFirstName(@NonNull String firstName) { this.firstName = firstName; } @NonNull public String getPhone() { return phone; } public void setPhone(@NonNull String phone) { this.phone = phone; } @NonNull public String getEmail() { return email; } public void setEmail(@NonNull String email) { this.email = email; } }

JPA/Hibernate

Team

15

Page 16: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

package edu.cnm.deepdive.league.model.entity; import java.util.LinkedList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import org.springframework.lang.NonNull; @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "team_id", updatable = false, nullable = false) private Long id; @NonNull @Column(nullable = false, length = 100, unique = true) private String name; @OneToMany(mappedBy = "team", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @OrderBy("lastName, firstName") private List<Member> members = new LinkedList<>(); public Long getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Member> getMembers() { return members; } }

Member

16

Page 17: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

package edu.cnm.deepdive.league.model.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.springframework.lang.NonNull; @Entity @Table( uniqueConstraints = @UniqueConstraint(columnNames = {"lastName", "firstName"}) ) public class Member { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "member_id", updatable = false, nullable = false) private Long id; @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "team_id") private Team team; @NonNull @Column(nullable = false, length = 100) private String lastName; @NonNull @Column(nullable = false, length = 100) private String firstName; @Column(length = 50) private String phone; @Column(nullable = false, unique = true) private String email; public Long getId() { return id; } public Team getTeam() { return team; }

17

Page 18: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

} public void setTeam(Team team) { this.team = team; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }

DDL

Room

18

Page 19: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

CREATE TABLE IF NOT EXISTS `Team`( `team_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE);

CREATE UNIQUE INDEX IF NOT EXISTS `index_Team_name` ON `Team` (`name`);

CREATE TABLE IF NOT EXISTS `Member`( `member_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `team_id` INTEGER NOT NULL, `last_name` TEXT NOT NULL COLLATE NOCASE, `first_name` TEXT NOT NULL COLLATE NOCASE, `phone` TEXT NOT NULL, `email` TEXT NOT NULL COLLATE NOCASE, FOREIGN KEY (`team_id`) REFERENCES `Team` (`team_id`) ON UPDATE NO ACTION ON DELETE NO ACTION);

CREATE UNIQUE INDEX IF NOT EXISTS `index_Member_last_name_first_name` ON `Member` (`last_name`, `first_name`);

CREATE UNIQUE INDEX IF NOT EXISTS `index_Member_email` ON `Member` (`email`);

CREATE INDEX IF NOT EXISTS `index_Member_team_id` ON `Member` (`team_id`);

JPA/Hibernate

19

Page 20: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

create sequence hibernate_sequence start with 1 increment by 1;

create table member( member_id bigint not null, email varchar(255) not null, first_name varchar(100) not null, last_name varchar(100) not null, phone varchar(50), team_id bigint, primary key (member_id));

create table team( team_id bigint not null, name varchar(100) not null, primary key (team_id));

create unique index UKp4c8er0qh2kgl4lo3iar9bk8j on member (last_name, first_name);

alter table member add constraint UK_mbmcqelty0fbrvxp1q58dn57t unique (email);

alter table team add constraint UK_g2l9qqsoeuynt4r5ofdt1x2td unique (name);

alter table member add constraint FKcjte2jn9pvo9ud2hyfgwcja0k foreign key (team_id) references team;

Development teams (many-to-many)

Summary

Our data model here is again a simplified subset of a larger data model—in this case, one that might be usedas part of a project management tool for development organizations. For this example, we’re looking atroster management, as we did in the volleyball league example—but there’s a new wrinkle: a developercould be a member of multiple teams simultaneously. (It’s also possible that a developer is a member of noteam, and that a team has no members.)

Relationships

20

Page 21: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

The entities used in this example, Team and Developer , have a many-to-many relationship. The 2 entitiesare independent, in the sense that elimination of a team must not also result in the automatic eliminationthe developers who were on that team, and elimination of a developer must not result in the automaticelimination of the teams of which the developer was a member.

ERD

For reasons explained below, there are 2 different ERDs for this example: 1 for Room, and 1 forJPA/Hibernate.

Room

Room provides no direct facility for annotating, creating, or using a many-to-many relationship. Instead, itmust be implemented in the entity classes just as it will in fact be embodied in the database: as 2 entitieswith a join entity between them. This join entity, TeamDeveloper , is included in the Room-specific versionof the ERD.

JPA/Hibernate

JPA includes annotations for defining, creating, and using simple many-to-many relationships. In manycases, the join table used by the database to implement the relationship does not need to defined in ormirrored by a matching entity class; this example is one such case. Notice, however, that when we leave thejoin entity out of the ERD, there’s definitely some information missing as a result: In the ERD that includesthe join entity, it’s fairly obvious which foreign keys will be used to implement the relationship; without the

21

Page 22: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

join entity, those foreign keys don’t appear at all! For this and other reasons, we often include the joinentity in the ERD, even when we won’t be declaring it as an entity class.

Entity classes

Room

As noted above, Room provides no direct facility for annotating & creating a many-to-many relationship.So, we must define it as an entity class.

Team

22

Page 23: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

package edu.cnm.deepdive.projectmanagement.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Index; import androidx.room.PrimaryKey; @Entity( indices = @Index(value = "name", unique = true) ) public class Team { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "team_id") private long id; @NonNull @ColumnInfo(collate = ColumnInfo.NOCASE) private String name; public long getId() { return id; } public void setId(long id) { this.id = id; } @NonNull public String getName() { return name; } public void setName(@NonNull String name) { this.name = name; } }

Developer

package edu.cnm.deepdive.projectmanagement.model.entity; import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey;

23

Page 24: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@Entity( indices = { @Index(value = {"last_name", "first_name"}, unique = true), @Index(value = {"email"}, unique = true) } ) public class Developer { @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "developer_id") private long id; @NonNull @ColumnInfo(name = "last_name", collate = ColumnInfo.NOCASE) private String lastName; @NonNull @ColumnInfo(name = "first_name", collate = ColumnInfo.NOCASE) private String firstName; @NonNull private String phone; @NonNull @ColumnInfo(collate = ColumnInfo.NOCASE) private String email; public long getId() { return id; } public void setId(long id) { this.id = id; } @NonNull public String getLastName() { return lastName; } public void setLastName(@NonNull String lastName) { this.lastName = lastName; } @NonNull public String getFirstName() { return firstName; } public void setFirstName(@NonNull String firstName) {

24

Page 25: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

this.firstName = firstName; } @NonNull public String getPhone() { return phone; } public void setPhone(@NonNull String phone) { this.phone = phone; } @NonNull public String getEmail() { return email; } public void setEmail(@NonNull String email) { this.email = email; } }

TeamDeveloper

25

Page 26: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

package edu.cnm.deepdive.projectmanagement.model.entity; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.ForeignKey; @Entity( primaryKeys = {"team_id", "developer_id"}, foreignKeys = { @ForeignKey(entity = Team.class, parentColumns = "team_id", childColumns = "team_id", onDelete = ForeignKey.CASCADE), @ForeignKey(entity = Developer.class, parentColumns = "developer_id", childColumns = "developer_id", onDelete = ForeignKey.CASCADE) } ) public class TeamDeveloper { @ColumnInfo(name = "team_id", index = true) private long teamId; @ColumnInfo(name = "developer_id", index = true) private long developerId; public long getTeamId() { return teamId; } public void setTeamId(long teamId) { this.teamId = teamId; } public long getDeveloperId() { return developerId; } public void setDeveloperId(long developerId) { this.developerId = developerId; } }

JPA/Hibernate

The entity classes below do not include an entity corresponding to the physical join table that implementsthe many-to-many relationship between Team and Developer . Instead, the structure of that table iscompletely defined by information in the @JoinTable annotation in the Team entity. Also, note that sincethe join table isn’t mapped to an entity class, it is essentially transparent to the Team.developers andDeveloper.teams collections. Because of this, we have to make sure not to cascade deletions of instancesof either entity class to the other. In particular, if we used CascadeType.ALL in both entity classes’

26

Page 27: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@ManyToMany annotations, a deletion of a team (for example) would delete all of the developers associatedwith that team, then cascading to the deletion of all the other teams with which those developers areassociated, and so on.

Team

package edu.cnm.deepdive.projectmanagement.model.entity; import java.util.LinkedList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.OneToMany; import javax.persistence.OrderBy; import org.springframework.lang.NonNull; @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "team_id", updatable = false, nullable = false) private Long id; @NonNull @Column(nullable = false, length = 100, unique = true) private String name; @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @JoinTable(joinColumns = @JoinColumn(name = "team_id"), inverseJoinColumns = @JoinColumn(name = "developer_id")) @OrderBy("lastName, firstName ASC") private List<Developer> developers = new LinkedList<>(); public Long getId() { return id; } public String getName() { return name; }

27

Page 28: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

public void setName(String name) { this.name = name; } public List<Developer> getMembers() { return developers; } }

Developer

package edu.cnm.deepdive.projectmanagement.model.entity; import java.util.LinkedList; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OrderBy; import javax.persistence.Table; import javax.persistence.UniqueConstraint; import org.springframework.lang.NonNull; @Entity @Table( uniqueConstraints = @UniqueConstraint(columnNames = {"lastName", "firstName"}) ) public class Developer { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "member_id", updatable = false, nullable = false) private Long id; @NonNull @Column(nullable = false, length = 100) private String lastName; @NonNull @Column(nullable = false, length = 100)

28

Page 29: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

private String firstName; @Column(length = 50) private String phone; @Column(nullable = false, unique = true) private String email; @ManyToMany(fetch = FetchType.LAZY, mappedBy = "developers", cascade = {CascadeType.DETACH, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}) @OrderBy("name ASC") private List<Team> teams = new LinkedList<>(); public Long getId() { return id; } public List<Team> getTeams() { return teams; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) {

29

Page 30: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

public void setEmail(String email) { this.email = email; } }

DDL

Room

30

Page 31: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

CREATE TABLE IF NOT EXISTS `Team` ( `team_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE ); CREATE UNIQUE INDEX IF NOT EXISTS `index_Team_name` ON `Team` (`name`); CREATE TABLE IF NOT EXISTS `Developer` ( `developer_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `last_name` TEXT NOT NULL COLLATE NOCASE, `first_name` TEXT NOT NULL COLLATE NOCASE, `phone` TEXT NOT NULL, `email` TEXT NOT NULL COLLATE NOCASE ); CREATE UNIQUE INDEX IF NOT EXISTS `index_Developer_last_name_first_name` ON `Developer` (`last_name`, `first_name`); CREATE UNIQUE INDEX IF NOT EXISTS `index_Developer_email` ON `Developer` (`email`); CREATE TABLE IF NOT EXISTS `TeamDeveloper` ( `team_id` INTEGER NOT NULL, `developer_id` INTEGER NOT NULL, PRIMARY KEY (`team_id`, `developer_id`), FOREIGN KEY (`team_id`) REFERENCES `Team` (`team_id`) ON UPDATE NO ACTION ON DELETE CASCADE, FOREIGN KEY (`developer_id`) REFERENCES `Developer` (`developer_id`) ON UPDATE NO ACTION ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS `index_TeamDeveloper_team_id` ON `TeamDeveloper` (`team_id`); CREATE INDEX IF NOT EXISTS `index_TeamDeveloper_developer_id` ON `TeamDeveloper` (`developer_id`);

JPA/Hibernate

Note that Hibernate, in generating the DDL corresponding to the entity classes, has automaticallygenerated the name of the join table, team_developers . Had we defined the join with a @JoinTableannotation in Developer , instead of Team , it would have been named developer_teams . Alternatively,regardless of where the @JoinTable annotation was used, we could have specified the name of the jointable with the joinTable argument.

31

Page 32: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

create sequence hibernate_sequence start with 1 increment by 1; create table developer ( member_id bigint not null, email varchar(255) not null, first_name varchar(100) not null, last_name varchar(100) not null, phone varchar(50), primary key (member_id) ); create table team ( team_id bigint not null, name varchar(100) not null, primary key (team_id) ); create table team_developers ( team_id bigint not null, developer_id bigint not null ); create unique index UKq99082fio2blrywdhl575fahe on developer (last_name, first_name); alter table developer add constraint UK_chmq1wdfnhjthxo65affwdnky unique (email); alter table team add constraint UK_g2l9qqsoeuynt4r5ofdt1x2td unique (name); alter table team_developers add constraint FKe4uqipm4jvpug52fhcde8c4iv foreign key (developer_id) references developer; alter table team_developers add constraint FKge24m0vet4v491y91wdc12fr2 foreign key (team_id) references team;

Appendices

32

Page 33: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

Platform notes

Room

1. An entity field of a primitive type will be implemented as a non-nullable column in the associatedSQLite table. In other words, do not use a primitive type if you want to allow for the possibility of anull value in the database column.

2. An entity field of an object type will be implemented as a nullable column in the associated SQLitetable, unless the field is annotated with @PrimaryKey or @NonNull .

3. In a Room entity class, every non- static , non- transient field will be mapped to a column in thetable for that entity, unless the field is annotated with @Ignore .

4. The corollary to the preceding point is that the @ColumnInfo annotation is not required in manycases. However, it should be used when one or more of the following is needed for an entity field:

A column name that differs from the field name, specifiable with the name argument.(Remember that in this bootcamp, we’re strictly enforcing lower_snake_case for columnnames, and lowerCamelCase for field names. Also remember that while Hibernate automaticallytranslates lowerCamelCase field names to lower_snake_case column names, Room does not.)

A non-unique index on the column, created with the index=true argument value. This shouldbe used on any field that is used—by itself—as a foreign key, and on any field that may be usedby itself for query criteria or sorting. If a single foreign key consists of the combination ofmultiple columns, or if multiple columns are always used together in query critera or sorting, itmay be better to define a multi-column index in the @Entity annotation on the class.

A non-default collation sequence, specified with the collate argument. (In most cases wherean app will need to filter or sort on a String field in an entity, the combination of index=trueand collate=ColumnInfo.NOCASE should be used.)

A non-default mapping of the field type to a SQLite data type, using the typeAffinityargument.

5. As a general rule, every field in a Room entity class that is to be mapped to a database table columnmust either be public (which is bad for encapsulation) or have a setter and a getter that conform tothe JavaBeans naming convention. There are exceptions to this for setters—primarily involving thedefinition of constructors with parameters matching the entity fields (there are plenty of codeexamples of this online)—but these tend to make the entity classes more difficult to use and maintain.

6. While indices are almost always created on foreign key columns, and most database providers do soautomatically, SQLite does not. Thus, while we often leave out the IX notation on a foreign keyattribute in an ERD, we must remember to create such indices in Room entity classes.

7. Room entity classes, unlike JPA entities, can not include join contents. POJO classes using the

33

Page 34: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

@Relation annotation (and optionally @Embedded ) must be used to represent the join of relatedentities in Room.

8. Even many relatively simple object types in the Java standard library (e.g. Date and Enum ) require theuse of @TypeConverter methods when such types are used for entity fields.

JPA/Hibernate

1. Unlike Room, JPA requires that indices be specified at the entity class level, in the @Tableannotation, rather than at the field level. The only exception to this is single-column uniqueconstraints, which can be specified in the @Column annotation.

2. Columns specified in the indexes or uniqueConstraints arguments of the @Table annotation mustbe specified by field name, unless a column name has been set explicitly (with the name argument ofthe @Column annotation), in which case the column name must be used.

3. Multi-column unique constraints are specified with the array-valued columnNames argument of the@UniqueConstraint annotation (whichh is specified in the array-valued uniqueConstraintsargument of the @Table annotation). However, multi-column non-unique indices are specified withthe scalar-valued, commma-delimited columnList argument of the @Index annotation (which isspecified in the array-valued indexes argument of the @Table annotation).

4. String collation specification depends on the underlying database. For Derby, this is set in the JDBCconnection URL, not at the field level (as in Room/SQLite).

5. An entity field of a primitive type will be implemented as a non-nullable column in the associateddatabase table. In other words, do not use a primitive type if you want to allow for the possibility of anull value in the database column.

6. An entity field of an object type will be implemented as a nullable column in the associated table,unless the field is annotated with @Id or @Column(nullable=false) .

7. In a JPA/Hibernate entity class, every non- static , non- transient field will be mapped to a columnin the table for that entity, unless the field is annotated with @Transient , @OneToOne , @OneToMany ,@OneToAny , @ManyToOne , @ManyToMany . (For all in that list but @Transient , the annotation willeither include details on the column to be used as a foreign key, or reference the complementary fieldof the relationship, in which those details are provided.)

8. The corollary to the preceding point is that the @Column annotation is not required in many cases.However, it should be used when one or more of the following is needed for an entity field:

A column name that differs from the Hibernate-generated field name, specifiable with the nameargument. (Remember that in this bootcamp, we’re strictly enforcing lower_snake_case forcolumn names, and lowerCamelCase for field names. Also remember that Hibernateautomatically translates lowerCamelCase field names to lower_snake_case column names.)

Non-default values for updatable (defaults to true ), nullable ( true ), insertable ( true ).

34

Page 35: Introduction... · Introduction While much of the work in relational data modeling is database-agnostic, there are many database ... JPA/Hibernate entity classes, and JPA/Hibernate-generated

A unique constraint on the column, created with the unique=true argument value. If a multi-column unique constraint is needed, it must be set in the uniqueConstraints argument of the@Table annotation.

A non-default maximum length for a String field, set with the length argument. (The defaultvalue for this depends on the underlying database provider; for Derby—and several otherproviders—it is 255.)

A non-default numeric precision (maximum number of digits) or scale (placement of thedecimal point) for double , float , Double , Float , or BigDecimal fields.

A SQL-level field definition that differs from that generated by JPA/Hibernate, specified withthe columnDefinition argument.

9. As a general rule, every field in a JPA/Hibernate entity class that is to be mapped to a database tablecolumn should either be public (which is bad for encapsulation), or have a setter and a getter (if itwill be set via your application’s code), or at least have a getter (if the value will be generatedautomatically by JPA/Hibernate or the underlying database provider).

10. In JPA/Hibernate entities, foreign key columns are generally not specified as fields, but in@JoinColumn annotations, accompanying @ManyToOne or @OneToOne annotations.

11. JPA/Hibernate entity classes can (and generally do) include join contents, specified with (amongothers) the @ManyToOne , @OneToMany , @OneToOne , and @ManyToMany annotations. Keep in mind thatthese are in fact join contents, not direct attributes of the entities.

12. JPA/Hibernate can map a much wider variety of field types to SQL types than Room, withoutadditional type conversion code.

35