1:N with Publisher
We created the Book entity that mapped the rows in our book table, and we created a controller that allowed for managing these Books via HTTP requests.
However, we also need to deal with other entities that may be related to books.
For instance:
- Each
Bookis published by aPublisher(a “many-to-one” relationship). - A
Publisherpublishes manyBooks (a “one-to-many” relationship).
In this section, we will practice with implementing such a relationship in our application.
Migrating the database
We need to add a Flyway database migration that will create the appropriate publisher table, and add the publisher_id foreign key to the book table.
Create a src/main/resources/db/migration/V2__add-publisher.sql file with this content:
# Creates the table for our publishers
create table publisher (
id bigint primary key not null,
name varchar(255) unique not null
);
# Adds the foreign key from a book to its publisher
alter table book
add publisher_id bigint references publisher (id);
Basic functionality for publishers
Create the Publisher entity in your application and its repository.
Add a controller that allows you to create, list, update the name, and delete its entries, by handling these HTTP requests:
GET /publishers: list all the publishersPOST /publishers: add a publisherGET /publishers/{id}: get a specific publisher by IDPUT /publishers/{id}: update the name of a publisher by IDDELETE /publishers/{id}: delete a publisher
As these steps will be the same as for Book, we will not provide detailed step-by-step instructions on how to do it.
Ignore the relationship between Book and Publisher for now.
Don’t forget to write the appropriate tests for it!
Adding the relationship to the entities
Let’s extend Book and Publisher so they know about each other.
First, add this to Book:
@ManyToOne
private Publisher publisher;
Generate the getter and setter methods (getPublisher / setPublisher) as usual in your IDE.
In addition, if your Book domain entity is @Serdeable, add @JsonIgnore to the new publisher field, as we want to avoid a scenario where we serialise the entire Publisher and all their books when we’re trying to send a single Book over the network.
@JsonIgnore means that the field will not be serialised via JSON.
Likewise, add this to Publisher, as well as the appropriate getter and setter methods:
@OneToMany(mappedBy = "publisher")
private Set<Book> books = new HashSet<>();
If you remember from the lecture, in bidirectional relationships like this one, there is one side that owns the relationship: in other words, the side that you should change - the other side is only for reading.
In the case of one-to-many + many-to-one relationships, the owning side is always many-to-one.
In other words, Book is the owner of this relationship.
The side that does not own the relationship (Publisher) indicates via mappedBy the name of the property on the other side that owns the relationship (in this case, publisher in Book).
Likewise, if you made your Publisher to be @Serdeable, add the @JsonIgnore annotation to your books field.
Before we move on, check that all your tests are still passing.
Extending the controllers
We want to add support for this Book->Publisher relationship to our controllers.
Specifically, we want to support these features:
POST /books/{id}should allow for specifying the publisher.PUT /books/{id}should allow for setting and unsetting the publisher of aBook.GET /books/{id}/publishershould return the publisher of aBook.GET /publishers/{id}/booksshould list theBooks of aPublisher.
You should be able to implement these yourself with what you have learned so far for the most part. There are a few things to take into account, though:
- For specifying the publisher while creating or updating a
Book, you may now need to create aBookCreateDTOwith a dedicatedLong publisherIdfield, asBookitself will not allow you to specify that information (since it will just have aPublisher publisherfield). - Fetching the publisher of a
Bookcan be done in two main ways:-
If you use
repo.findById(id)to get theBookand then usebook.getPublisher()to get thePublisher, you will need to add the@Transactionalannotation to the controller method so both queries will run in the same transaction. Otherwise, you may get an error message on thebook.getPublisher()call, asbookwill no longer be connected to a database session. -
Alternatively, you can add a custom query method to your
PublisherRepositoryand retrieve the appropriatePublisherin one call, like this one - we picked this name specifically so we’d obtain thePublisherthat has the givenidamong itsbooks:Optional<Publisher> findByBooksId(Long id);
-
- When fetching the
Books of aPublisher, you can follow two approaches:- Use a
@Transactionalcontroller method that first finds thePublisher, copiespublisher.getBooks()to a newList<Book>(to avoid any issues with lazy collections) which it then returns. - Use a custom query method in your
BookRepositorywhich fetches thoseBooks directly - again, we picked the name specifically to find theBooks whosepublisherhas the given ID:List<Book> findByPublisherId(long publisherId);
- Use a
When testing, consider that you will need to modify the @BeforeEach method so it deletes all the Books first, and then all the Publishers.
If you try to delete all the Publishers first, you may see errors as some Books may still be pointing at them.
Once you are done with the above functionality and your tests pass, move on to the next section.