What is JPA?
JPA is a Java framework and specification that stands for Java Persistence API.
As its name suggests, it offers APIs to help developers in data persistence operations on a
relational database. In particular:
- it provides a mapping between Java classes and database tables
- it provides a language to perform SQL queries, called JPQL (Java Persistence Query Language), which is independent of the used DBMS
- it provides various APIs for managing and manipulating Java objects that map database tables.
These three features can be summarized in one sentence: JPA is a framework that uses the ORM (Object-Relational Mapping) technique.
We also said that it provides API, so interfaces: there are then several providers that implement this specification; the most famous are Hibernate and EclipseLink.
In this first article we will see how to map OneToMany, OneToOne, ManyToMany relationships on JPA.
Prerequisites
- Aver installato una jdk (useremo la versione 8 ma va bene anche una successiva).
- Aver installato maven (https://maven.apache.org/install.html).
First step: import the following dependencies
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.24.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.javax.persistence</groupId>
<artifactId>hibernate-jpa-2.1-api</artifactId>
<version>1.0.2.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
Note that we will use version 2.1 of JPA, use Hibernate as JPA provider and use H2 as in-memory database.
Second step: create the entity UserEntity (one to many, one to one and many to many relationship)
We will have these tables in the DB:
So, the table USERS is in relation 1:1 with CONTACTS, it is in relation 1:N with the table CARS and finally it is in relation N:N with the table ARTICLES, then there is an association table AUTHOR_ARTICLES.
We create a subpackage entities and there we write the Java classes that map these tables. We will create bi-directional relationships, i.e. the associations will be mapped with JPA by both father and daughter classes.
In addition, each entity will implement an interface created by us, JpaEntity: this can bring advantages for developments that we will do in the future:
public interface JpaEntity extends Serializable {
}
UserEntity:
@Entity
@Table(name = "USERS")
public class UserEntity implements JpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
private String name;
private String surname;
private String code;
@OneToMany(mappedBy = "userEntity")
private Set<CarEntity> cars;
@OneToOne(mappedBy = "userEntity", cascade = CascadeType.ALL, orphanRemoval = true)
@PrimaryKeyJoinColumn
private ContactEntity contactEntity;
@ManyToMany(mappedBy = "authors")
private Set<ArticleEntity> articles;
public void addCar(CarEntity carEntity) {
carEntity.setUserEntity(this);
if(this.cars == null) {
this.cars = new HashSet<>();
}
this.cars.add(carEntity);
}
public void removeCar(CarEntity carEntity) {
this.cars.remove(carEntity);
carEntity.setUserEntity(null);
}
public void addArticle(ArticleEntity articleEntity) {
if(articleEntity.getAuthors() == null) {
articleEntity.setAuthors(new HashSet<>());
}
if(this.articles == null) {
this.articles = new HashSet<>();
}
articleEntity.getAuthors().add(this);
this.articles.add(articleEntity);
}
public void removeArticle(ArticleEntity articleEntity) {
this.articles.remove(articleEntity);
articleEntity.getAuthors().remove(this);
}
//getters, setters, equals, hashcode
}
Let's analyze the code:
- With @Entity we are indicating to JPA that this Java class maps a table in the database.
- With @Table(name = "USERS") we indicate to JPA that the name of the mapped table is USERS. Without this annotation, it would look for
a table with the same name as the Java class.
- With @Id we indicate that the field noted is a primary key.
- With @GeneratedValue(strategy = GenerationType.IDENTITY) we indicate that the id is an IDENTITY (i.e. it is a field that is auto-incremented
from DBMS).
- With @Column(name = "user_id"), we tell JPA that this Java field maps the user_id column. Without this annotation, JPA
would try to map a column with the same name as the Java field.
- With @OneToMany(mappedBy = "userEntity") we are telling JPA that this table is in relation 1:N with the class CarEntity
which maps the table CARS. Also, with mappedBy we tell JPA that CarEntity has an attribute called userEntity to map the relation
reverse N:1.
- With @OneToOne(mappedBy = "userEntity", cascade = CascadeType.ALL, orphanRemoval = true) we are indicating to JPA that there is a 1:1 relation
with the ContactEntity class that maps the table CONTACTS. The speech of mappedBy is equal to point 6. Also, with cascade = CascadeType.ALL,
we indicate that the cascade is active on all operations of INSERT, UPDATE, DELETE (it is not mandatory to have the cascade side db). Finally, with
orphanRemoval = true we indicate that if a son, ContactEntity, remains "orphaned" of his father, UserEntity, (i.e. he has foreign key null), it has to
be deleted automatically.
- With @PrimaryKeyJoinColumn, we indicate to JPA that the table CONTACTS has as primary key the same as the table USERS.
- With @ManyToMany(mappedBy = "authors") we tell JPA that there is an N:N relationship with the ArticleEntity class and that ArticleEntity has a collection of UserEntity with authors name.
It is also good practice writing add and remove methods such as addCar and removeCar that in addition to add/remove a Car from the list, reference/de-reference also in the child class the father's instance (carEntity.setUserEntity(this) and carEntity.setUserEntity(null)).
Another good rule is to set the name of the tables in plural (USERS) and those of the Java classes in singular (User or UserEntity). Obviously, neither of these two indications are mandatory.
Third step: create the entity CarEntity (many to one relationship)
@Entity
@Table(name = "CARS")
public class CarEntity implements JpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "car_id")
private Long id;
private String company;
private String name;
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity userEntity;
//getters, setters, equals, hashcode
}
Let's analyze the code:
- With @ManyToOne we tell JPA that CarEntity is in relationship N:1 with UserEntity. So, the userEntity field on DB side will be a FK.
- With @JoinColumn(name = "user_id") we tell JPA what is the name of the column that is FK. Since both in the table CAR and in the table USERS the column that is FK has the same name, just use name = "user_id", otherwise, if the table USERS had as column name PK "id", we would be forced to write like this @JoinColumn(name = "user_id", referencedColumnName = "id").
Fourth step: create the entity ContactEntity (one to one relationship)
@Entity
@Table(name = "CONTACTS")
public class ContactEntity implements JpaEntity {
@Id
@Column(name = "user_id")
private Long id;
private String city;
@Column(name = "telephone_number")
private String telephoneNumber;
@OneToOne
@JoinColumn(name = "user_id")
@MapsId
private UserEntity userEntity;
//getters, setters, equals, hashcode
}
Let's analyze the code:
- With @OneToOne and @JoinColumn(name = "user_id") we are telling JPA that the FK column of the 1:1 relationship is mapped on the table side CONTACTS.
- With @MapsId we are also saying that the FK is also PK of the table CONTACTS.
Fifth step: create the entity ArticleEntity (many to many relationship)
@Entity
@Table(name = "ARTICLES")
public class ArticleEntity implements JpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "article_id")
private Long id;
private String title;
@ManyToMany
@JoinTable(name = "AUTHOR_ARTICLES", joinColumns = {
@JoinColumn(name = "article_id")
}, inverseJoinColumns = @JoinColumn(name = "user_id"))
private Set<UserEntity> authors;
//getters, setters, equals, hashcode
}
Let's analyze the code:
- With @JoinTable(...) we are indicating to JPA the name of the association table (AUTHOR_ARTICLES) and which are the PK/FK that compose it.
Note that for N:N relations, JPA side we don't have to map the association table with a Java class.
What if we want to add more fields in the AUTHOR_ARTICLES association table? In this case we can't talk more about a real table of association, as the latter only serves to store associations between several tables. However, we can break down a many to many relationship in two one to many relationships with a third "concrete" table (i.e. AUTHOR_ARTICLES):
At this point, you need to create a new class that will map the AUTHOR_ARTICLES table:
@Entity
@Table(name = "AUTHOR_ARTICLES")
public class AuthorArticlesEntity implements JpaEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity userEntity;
@ManyToOne
@JoinColumn(name = "article_id")
private ArticleEntity articleEntity;
//getters, setters, equals, hashcode
}
The UserEntity class will have a collection of type AuthorArticlesEntity and no more of type ArticleEntity, in relation to one to many instead of many to many. The same goes for ArticleEntity.
The same strategy can also be adopted when we need to map an association table that relates more than two tables.
Conclusions
In this first article on JPA we have seen how to map all relationships with JPA in the right way in a bi-directional way.
You can find the complete project on my github at this link: JPA Project
Articles about JPA: JPA