Document is on post which shows the different ways of mapping @oneToMany
. The entry does not cover all the possibilities of creating this type of relationship and the mode of operation, which I intend to expand in this document.
In my examples, I will use the relationship between the entities Product
and Review
.
A class that is widely understood as a reflection of a product or service in the system.
- id - identifier, annotation
@Id
is required by JPA - name - description of an item
- reviews - these are reviews of a given product, one product may have multiple reviews
The product is a unique entity, therefore the equals
and hashCode
methods have been implemented as suggestedVlad Michalcea
.
@Entity
public class Product {
@Id
private Integer id;
private String name;
@OneToMany
private Set<Review> reviews = new HashSet<>();
protected Product() {
}
public Product(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void addReview(Review review){
reviews.add(review);
}
public void removeReview(Review review){
reviews.remove(review);
}
public Set<Review> getReviews() {
return reviews;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Product)) {
return false;
}
return id != null && id.equals(((Product) o).id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
create table product (
id int4 not null,
name varchar(255),
primary key (id)
)
A class that is a review model for e.g. a product that is being sold.
- id - identifier, annotation
@Id
is required by JPA - text - review text
@Entity
public class Review {
@Id
private Integer id;
private String text;
protected Review() {
}
public Review(String text) {
this.text = text;
}
public Integer getId() {
return id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Review)) {
return false;
}
return id != null && id.equals(((Review) o).id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
create table review (
id int4 not null,
text varchar(255),
primary key (id)
)
#J PA Behavior Tests for various annotations and settings
@Test
@Transactional
void test01() throws ExecutionException, InterruptedException {
//GIVEN
Iterable<Review> savedReviews = createReviews();
Product product = createProduct(savedReviews);
//WHEN
Optional<Product> byId = productRepository.findById(product.getId());
//THEN
byId.get().getReviews()
.forEach(Review::getText);
assertThat(reviews).hasSize(3);
}
private Product createProduct(Iterable<Review> savedReviews) throws InterruptedException, ExecutionException {
return CompletableFuture.supplyAsync(() -> {
Product item = new Product("item");
savedReviews.forEach(item::addReview);
return productRepository.save(item);
}, executor)
.get();
}
private Iterable<Review> createReviews() throws InterruptedException, ExecutionException {
return CompletableFuture.supplyAsync(() -> reviewRepository.saveAll(List.of(
new Review("first review"),
new Review("second review"),
new Review("third review")
)), executor)
.get();
}
The test looks very complex, but in fact it creates three Reviews
with text and writes in a dedicated thread and then we wait for the return of the saved entities from the database. The next step is to create a Product
in a dedicated thread, additionally linking the already saved Review
to the object. We wait for the return from the database, then we download the Product
from the database and iterate through the Review
it has when reading the text.
Why did I use CompletableFuture
? This is a way to bypass the JPA level 1 cache. If I hadn't, the results of my tests would not show all the queries because JPA would be accessing the cache instead of accessing the data.
In the Product
class, use the binding as below
@OneToMany(fetch = FetchType.EAGER)
private Set<Review> reviews = new HashSet<>();
The sql code creates an additional join table
create table product_reviews (
product_id int4 not null,
reviews_id int4 not null,
primary key (product_id, reviews_id)
)
We have 3x review
creation as inserts
insert
into
review
(text, id)
values
(?, ?)
Commit and then we create
insert
into
product
(name, id)
values
(?, ?)
And 3 times we insert into lining table
insert
into
product_reviews
(product_id, reviews_id)
values
(?, ?)
To select we use
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_,
reviews1_.product_id as product_1_1_1_,
review2_.id as reviews_2_1_1_,
review2_.id as id1_2_2_,
review2_.text as text2_2_2_
from
product product0_
left outer join
product_reviews reviews1_
on product0_.id=reviews1_.product_id
left outer join
review review2_
on reviews1_.reviews_id=review2_.id
where
product0_.id=?
Query is extensive. We still use the link table, which is completely redundant because it does not carry additional information and we have a direct link between Product
and Review
. You can see that the query fields belong to both objects.
This form of the query should have been expected because we explicitly declared that all data should be retrieved and that we would map it to the objects appropriately.
Statistics
31851 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
342820 nanoseconds spent preparing 1 JDBC statements;
6296147 nanoseconds spent executing 1 JDBC statements;
In the Product
class, use the binding as below
@OneToMany(fetch = FetchType.LAZY)
private Set<Review> reviews = new HashSet<>();
The sql code creates an additional join table
create table product_reviews (
product_id int4 not null,
reviews_id int4 not null,
primary key (product_id, reviews_id)
)
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_
from
product product0_
where
product0_.id=?
select
reviews0_.product_id as product_1_1_0_,
reviews0_.reviews_id as reviews_2_1_0_,
review1_.id as id1_2_1_,
review1_.text as text2_2_1_
from
product_reviews reviews0_
inner join
review review1_
on reviews0_.reviews_id=review1_.id
where
reviews0_.product_id=?
Statistics
29033 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
247217 nanoseconds spent preparing 2 JDBC statements;
20233953 nanoseconds spent executing 2 JDBC statements;
The result of calling query is as expected. It will be a bit more optimal because JPA tries to optimize the use of the database, and thus the minimum necessary amount of data was collected when searching for an object. This was the first query. Proxy objects have been created for the data marked as LAZY
.
The test is intentionally designed to force the download of lazy objects. It can be seen that another query is created to retrieve the missing Review
. I do not focus on query execution time because this test is performed individually in a changing environment.
In the Product
class, use the binding as below
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
The sql code creates additional column in related object
create table product_reviews (
product_id int4 not null,
reviews_id int4 not null,
primary key (product_id, reviews_id)
)
When we create product and attach reviews on it it create's query
insert
into
product
(name, id)
values
(?, ?)
And 3 times:
update
review
set
product_id=?
where
id=?
Transaction is commited and then we have query
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_,
reviews1_.product_id as product_3_1_1_,
reviews1_.id as id1_1_1_,
reviews1_.id as id1_1_2_,
reviews1_.text as text2_1_2_
from
product product0_
left outer join
review reviews1_
on product0_.id=reviews1_.product_id
where
product0_.id=?
Select is the same as when we used lining table. Difference is with missing linking table. This should inprove performace as we have one join less. Necessary data is stored on Review
directly.
Also on creatio of the Product
we can see that we update Review
not insert into linking table.
Statistics
25744 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
135102 nanoseconds spent preparing 1 JDBC statements;
224920536 nanoseconds spent executing 1 JDBC statements;
In the Product
class, use the binding as below
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
The sql code creates additional column in related object
create table product_reviews (
product_id int4 not null,
reviews_id int4 not null,
primary key (product_id, reviews_id)
)
When we create product and attach reviews on it it create's query
insert
into
product
(name, id)
values
(?, ?)
And 3 times:
update
review
set
product_id=?
where
id=?
Transaction is commited and then we have query
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_
from
product product0_
where
product0_.id=?
select
reviews0_.product_id as product_3_1_0_,
reviews0_.id as id1_1_0_,
reviews0_.id as id1_1_1_,
reviews0_.text as text2_1_1_
from
review reviews0_
where
reviews0_.product_id=?
Select is the same as when we used linking table. Difference is with missing linking table. This should inprove performace as we have one join less. Necessary data is stored on Review
directly. To select Product
and related objects we use two queries
Also on creatio of the Product
we can see that we update Review
not insert into linking table.
Statistics
28093 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
194543 nanoseconds spent preparing 2 JDBC statements;
9282998 nanoseconds spent executing 2 JDBC statements;
In the Product
class, use the binding as below. Depending on FetchType
use EAGER
or LAZY
@OneToMany(
fetch = FetchType.EAGER,
mappedBy = "product",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private Set<Review> reviews = new HashSet<>();
Additional mthods to bind Product
with Review
public void addReview(Review review) {
reviews.add(review);
review.setProduct(this);
}
public void removeReview(Review review) {
reviews.remove(review);
review.setProduct(null);
}
To make bidirectional relation we have to add additional field onReview
@ManyToOne(fetch = FetchType.EAGER)
private Product product;
The sql code creates additional column in related object
create table review (
id int4 not null,
text varchar(255),
product_id int4,
primary key (id)
)
To make bidirectional mapping work Review
and Product
is created in one transaction
Product savedProduct = CompletableFuture.supplyAsync(() -> {
List<Review> reviews = List.of(
new Review("first review"),
new Review("second review"),
new Review("third review"));
Product product = new Product("item");
reviews.forEach(product::addReview);
productRepository.save(product);
return product;
}, executor).get();
When we create product and attach reviews on it it create's query
insert
into
product
(name, id)
values
(?, ?)
And 3 times:
insert
into
review
(product_id, text, id)
values
(?, ?, ?)
Transaction is commited and then we have query
for EAGER
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_,
reviews1_.product_id as product_3_1_1_,
reviews1_.id as id1_1_1_,
reviews1_.id as id1_1_2_,
reviews1_.product_id as product_3_1_2_,
reviews1_.text as text2_1_2_
from
product product0_
left outer join
review reviews1_
on product0_.id=reviews1_.product_id
where
product0_.id=?
for LAZY
select
product0_.id as id1_0_0_,
product0_.name as name2_0_0_
from
product product0_
where
product0_.id=?
select
reviews0_.product_id as product_3_1_0_,
reviews0_.id as id1_1_0_,
reviews0_.id as id1_1_1_,
reviews0_.product_id as product_3_1_1_,
reviews0_.text as text2_1_1_
from
review reviews0_
where
reviews0_.product_id=?
If we take a look at unidirectional and bidirectional mapping we can see that query from database perspective are the same. Bidirectional mapping gives us option to get Product
from Review
and code is slightly different so the performance is the same.