Reputation: 2461
I am working through this tutorial using Spring Boot starter spa, hibernate, and Postgres trying to figure out how to insert data cross a relation using a many to many relation. I followed this tutorial: many-to-many tutorial but it seems that after the running the example the publisher table has duplicate publisher entries for publisher a and publisher b. can someone explain how to stop this from happening? I have read that implementing. a hashcode and equals method for the entities would solve this issue, but after implanting the following code for the entities, I still get the same issue.
@Entity
public class Publisher {
private int id;
private String name;
private Set<Book> books;
public Publisher(){
}
public Publisher(String name){
this.name = name;
}
public Publisher(String name, Set<Book> books){
this.name = name;
this.books = books;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToMany(mappedBy = "publishers")
public Set<Book> getBooks() {
return books;
}
public void setBooks(Set<Book> books) {
this.books = books;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Publisher publisher = (Publisher) o;
return name.equals(publisher.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
and
@Entity
public class Book {
private int id;
private String name;
private Set<Publisher> publishers;
public Book() {
}
public Book(String name) {
this.name = name;
}
public Book(String name, Set<Publisher> publishers){
this.name = name;
this.publishers = publishers;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(name = "book_publisher",
joinColumns = @JoinColumn(name = "book_id",
referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "publisher_id", referencedColumnName = "id"))
public Set<Publisher> getPublishers() {
return publishers;
}
public void setPublishers(Set<Publisher> publishers) {
this.publishers = publishers;
}
@Override
public String toString() {
String result = String.format(
"Book [id=%d, name='%s']%n",
id, name);
if (publishers != null) {
for(Publisher publisher : publishers) {
result += String.format(
"Publisher[id=%d, name='%s']%n",
publisher.getId(), publisher.getName());
}
}
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return name.equals(book.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}
the output from select * from publisher
[
{"id": 2,"name": "Publisher B"},
{"id": 3,"name": "Publisher A"},
{"id": 5,"name": "Publisher C"},
{"id": 6,"name": "Publisher B"},
{"id": 7,"name": "Publisher A"}
]
why is there a id = 6 and id = 7?
Upvotes: 1
Views: 74
Reputation: 42184
To answer your question we will have to take a closer look to the public void run(String... strings)
method that triggers all queries to the database - https://github.com/hellokoding/jpa-manytomany-springboot-hsql/blob/master/src/main/java/com/hellokoding/jpa/HelloJpaApplication.java
Between lines 35 and 49 there is:
final Publisher publisherA = new Publisher("Publisher A");
final Publisher publisherB = new Publisher("Publisher B");
final Publisher publisherC = new Publisher("Publisher C");
bookRepository.save(new HashSet<Book>(){{
add(new Book("Book A", new HashSet<Publisher>(){{
add(publisherA);
add(publisherB);
}}));
add(new Book("Book B", new HashSet<Publisher>(){{
add(publisherA);
add(publisherC);
}}));
}});
This part is responsible for persisting first 3 publishers: Publisher A, Publisher B and Publisher C. First Book
, "Book A" creates Publisher A and Publisher B. And because those publishers are assigned to a value, Hibernate marks those entities as "persisted". Thanks to this when "Book B" is being saved, only Publisher C is created and "Book B" uses existing reference to Publisher A and does not create a new object.
Now let's take a look what happens between lines 56 and 70:
// save a couple of publishers
final Book bookA = new Book("Book A");
final Book bookB = new Book("Book B");
publisherRepository.save(new HashSet<Publisher>() {{
add(new Publisher("Publisher A", new HashSet<Book>() {{
add(bookA);
add(bookB);
}}));
add(new Publisher("Publisher B", new HashSet<Book>() {{
add(bookA);
add(bookB);
}}));
}});
Here we can see an opposite situation. Instead of saving Book
and associating Publisher
s with every book, here Publisher
object is being saved with associated Book
s. The crucial information is that both publishers that are being saved through publisherRepository.save()
call are completely new objects - both objects are instantiated with the constructor inside Set.add()
method. These two publishers are completely new objects, they only share the same name with Publisher A and Publisher B. But from the object identity perspective, they are not the same objects. This block of code will also create two Book
objects. And only two, because both publishers share references to the same Book A and Book B objects.
And that's why when you list all Publishers you will get something like this:
Publisher A -|
Publisher C |----- created in bookRepository.save() call
Publisher B -|
Publisher A -|----- created in publisherRepository.save() call
Publisher B -|
I hope it helps.
Upvotes: 2