Bidirectional relationship Using Spring boot and Spring JPA
In this post, I will try to explain many-to-many relationship at object level in a springboot application using JPA and Hibernate.
Consider the following tables where > posts and > tags exhibit a many-to-many relationship between each other -
The many-to-many relationship is implemented using a third table called > post_tags which contains the details of posts and their associated tags.
As per the below image we have > posts and > tags tables and a third table i.e. > post_tags.
After creating the project from either Springboot CLI or spring starter website https://start.spring.io/ and importing the project in your favourite IDE your project structure will look like below:
Note: I will only show the Model classes in the article, the link for the springboot application is provided at the end of the article.
We will first concentrate on creating the Model's i.e. Posts and Tags classes.
Below are the Model are classes.
Posts.java
@Entity
@Table(name="posts")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = Post.java)
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Size(max = 100)
@Column(unique = true)
private String title;
@NotNull
@Size(max = 250)
private String description;
@NotNull
@Lob
private String content;
@NotNull
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "posted_at")
private Date postedAt = new Date();
@NotNull
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "last_updated_at")
private Date lastUpdatedAt = new Date();
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
})
@JoinTable(name = "post_tags",
joinColumns = { @JoinColumn(name = "post_id") },
inverseJoinColumns = { @JoinColumn(name = "tag_id") })
private Set<Tag> tags = new HashSet<Tag>();
public Post(String title, String description, String content) {
this.title = title;
this.description = description;
this.content = content;
}
public Post(String title, String description, String content,HashSet<Tag> tags) {
this.title = title;
this.description = description;
this.content = content;
this.tags=tags;
}
}
Tag.java
@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "tags")
@JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope= Tag.class)
public class Tag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Size(max = 100)
@NaturalId
private String name;
@ManyToMany(fetch = FetchType.LAZY,
cascade = {
CascadeType.PERSIST,
CascadeType.MERGE
},
mappedBy = "tags")
private Set<Post> posts = new HashSet<>();
public Tag(String name) {
this.name = name;
}
}
Note: I have used lombok to keep the code clean and not have explicit getters, setters and constructors.
Now the point to be noted in the above two classes is the use of annotation > @JoinTable in the Posts.java and keyword > mappedBy in Tag.java.
We have @ManyToMany annotation to map or create bidirectional association between two entities. In bidirectional association we have an owner side and inverse side.
TIP: The entity which has > @JoinTable is the owner side in this case and the entity with keyword > mappedBy is the inverse side. So as per the above logic in our example Post is the owner side and Tag is the inverse side in this association.
As I mentioned at the start of the article that we achieve Many-To-Many relationship using a third table, in the class Posts.java we have @JoinTable annotation which indicate that we will associate post and tag on the field > tags and mention the table name(post_tags) which will be created in order to have a many to many relationship at database level.
This is how we instruct hibernate to create a third table for us by also giving the name of the table.
It is recommended to provide table name in the name attribute of @JoinTable annotation or else hibernate gives names according to it's own criteria.
There is one more important point which I would like to highlight in this post is the circular reference error.
This error is caused in the bidirectional association when parent references child and child references back to parent. The serializer/deserializer gets confused on the repetitive fields and we get a StackOverFlowError.
In order to fix this issue we use > @JsonIdentityInfo to handle the circular reference errors for us.
The Entity classes have @JsonIdentityInfo to help resolve this circular reference error.
As per the release documents, the @JsonIdentityInfo Annotation is used for indicating that values of annotated type or property should be serializing so that instances either contain additional object identifier (in addition actual object properties), or as a reference that consists of an object id that refers to a full serialization. In practice this is done by serializing the first instance as full object and object identity, and other references to the object as reference values.
This is how we create a Many-To-Many relationship in a springboot application using JPA and hibernate.
Interested folks can visit/clone my github repository https://github.com/frankcolaco/jpa-bidirectional-demo to go through the whole application, where you will get to see the a controller which will help you perform and/or understand the CRUD operations which we can perform with bidirectional association.
Hope you all got something to learn from this article.
Cheers!
Thank you for this great post!