Skip to content

Latest commit

 

History

History
596 lines (509 loc) · 15.5 KB

README.md

File metadata and controls

596 lines (509 loc) · 15.5 KB

A compendium of knowledge about one to many connections in JPA

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.

Product

Class description

A class that is widely understood as a reflection of a product or service in the system.

Fields description

  • 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 .

Basic implementation of entitity

@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();
    }

}

SQL code that is generated by JPA

    create table product (
       id int4 not null,
        name varchar(255),
        primary key (id)
    )

Review

Class description

A class that is a review model for e.g. a product that is being sold.

Fields description

  • id - identifier, annotation @Id is required by JPA
  • text - review text

Basic implementation of entitity

@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();
    }

}

SQL code that is generated by JPA

    create table review (
       id int4 not null,
        text varchar(255),
        primary key (id)
    )

#J PA Behavior Tests for various annotations and settings

Test code - unidirectional

    @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.

Use EAGER - unidirectional with linking table

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)
    )

Generated query

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=?

Conclusion

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;

Use LAZY - unidirectional with linking table

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)
    )

Generatedd 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_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;

Conclusion

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.

Use EAGER - unidirectional using join

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)
    )

Generated query

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=?

Conclusion

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;

Use LAZY - unidirectional using join

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)
    )

Generated query

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=?

Conclusion

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;

Use EAGER and LAZY - bidirectional using join

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)
    )

Test

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();

Generated query

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=?

Conclusion

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.