JPA create @ManyToMany table name not by 'type_type' but by 'type_varname'

Viewed 1299

Suppose we have two (or more) classes, where one is @ManyToMany-referencing other classes: (I leave out a lot of annotations here, to simplify)

@Entity
class Newspaper {
    @Id long                    id;
    @ManyToMany Set<Author>     authors     = new HashSet<>();
    @ManyToMany Set<Article>    oldArticles = new HashSet<>();
    @ManyToMany Set<Article>    newArticles = new HashSet<>();
}

@Entity
class Article {
    @Id long id;
}

@Entity
class Author {
    @Id long id;
}

Now by default, JPA would create two tables:

Newspaper_Author
Newspaper_Article

and even mixing up oldArticles and newArticles into the same table, creating interesting results ;-)

Now this problem can easily be fixed by defining the @JoinTable on at least one or all member variables:

    @Entity
class Newspaper {
    @Id long                                                            id;
    @ManyToMany Set<Author>                                             authors     = new HashSet<>();
    @ManyToMany Set<Article>                                            oldArticles = new HashSet<>();
    @ManyToMany @JoinTable(name = "Newspaper_newArticles") Set<Article> newArticles = new HashSet<>();
}

So, finally getting to my question: When only having the class defined like this

@Entity
class Newspaper {
    @Id long                    id;
    @ManyToMany Set<Author>     authors     = new HashSet<>();
    @ManyToMany Set<Article>    oldArticles = new HashSet<>();
    @ManyToMany Set<Article>    newArticles = new HashSet<>();
}

is there any way to make JPA automatically create the tables

Newspaper_authors
Newspaper_oldArticles
Newspaper_newArticles

UPDATE: ... and to make problems really crazy:

@Entity
class Newspaper {
    @Id long                    id;
    @ManyToMany Set<Author>     authors     = new HashSet<>();
    @ManyToMany Set<OldArticle> oldArticles = new HashSet<>();
    @ManyToMany Set<NewArticle> newArticles = new HashSet<>();
}

@MappedSuperclass
class Article {
    @Id long                id;
    @ManyToMany Set<Author> authors = new HashSet<>();
}

@Entity
class OldArticle extends Article { /* */ }

@Entity
class NewArticle extends Article { /* */ }

@Entity
class Author {
    @Id long id;
}

how could I possibly define the name(s) for Article.authors here?

1 Answers

In your case, I would recommend to use @Inheritance with a single table for all type of articles instead of @MappedSuperclass:

@Data
@NoArgsConstructor
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
public abstract class Article {
    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany
    private Set<Newspaper> newspapers;

    @ManyToMany
    private Set<Author> authors;

    public Article(String name, Set<Newspaper> newspapers, Set<Author> authors) {
        this.name = name;
        this.newspapers = newspapers;
        this.authors = authors;
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

All types of articles will be stored in the single table and their types you can determine with column type which you set in @DiscriminatorColumn annotation.

Then you will be able to use a single set of articles in the Newspaper entity:

@Data
@NoArgsConstructor
@Entity
public class Newspaper {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany(mappedBy = "newspapers")
    private Set<Author> authors;

    @ManyToMany(mappedBy = "newspapers")
    private Set<Article> articles;

    public Newspaper(String name) {
        this.name = name;
    }
}

Pay attention to parameter mappedBy which you have to use in case of using bi-directional ManyToMany.

Concrete articles:

@NoArgsConstructor
@Entity
@DiscriminatorValue("FIRST")
public class FirstTypeArticle extends Article {
    public FirstTypeArticle(String name, Set<Newspaper> newspapers, Set<Author> authors) {
        super(name, newspapers, authors);
    }
}

@NoArgsConstructor
@Entity
@DiscriminatorValue("SECOND")
public class SecondTypeArticle extends Article {
    public SecondTypeArticle(String name, Set<Newspaper> newspapers, Set<Author> authors) {
        super(name, newspapers, authors);
    }
}

Note to the annotation @DiscriminatorValue, it used to set a value of discriminator column.

The author entity (also with a single set of articles):

@Data
@NoArgsConstructor
@Entity
public class Author {
    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany
    private Set<Newspaper> newspapers;

    @ManyToMany(mappedBy = "authors")
    private Set<Article> articles;

    public Author(String name, Set<Newspaper> newspapers) {
        this.name = name;
        this.newspapers = newspapers;
    }
}

For these set of entities in my Spring Boot 2.1.1 demo project (with H2 database) Hibernate has created the following tables without any additional settings:

ARTICLE
ARTICLE_AUTHORS
ARTICLE_NEWSPAPERS
AUTHOR
AUTHOR_NEWSPAPERS
NEWSPAPER

Repositories:

public interface ArticleRepo extends JpaRepository<Article, Integer> {
}

public interface AuthorRepo extends JpaRepository<Author, Integer> {
}

public interface NewspaperRepo extends JpaRepository<Newspaper, Integer> {
}

Usage example:

@RunWith(SpringRunner.class)
@DataJpaTest
public class ArticleRepoTest {

    @Autowired private ArticleRepo articleRepo;
    @Autowired private AuthorRepo authorRepo;
    @Autowired private NewspaperRepo newspaperRepo;

    private List<Article> articles;

    @Before
    public void setUp() throws Exception {
        List<Newspaper> newspapers = newspaperRepo.saveAll(List.of(
                new Newspaper("newspaper1"),
                new Newspaper("newspaper2")
        ));

        List<Author> authors = authorRepo.saveAll(List.of(
                new Author("author1", new HashSet<>(newspapers)),
                new Author("author2", new HashSet<>(newspapers))
        ));

        articles = articleRepo.saveAll(List.of(
                new FirstTypeArticle("first type article", new HashSet<>(newspapers), new HashSet<>(authors)),
                new SecondTypeArticle("second type article", new HashSet<>(newspapers), new HashSet<>(authors))
        ));
    }

    @Test
    public void findAll() {
        List<Article> result = articleRepo.findAll();
        result.forEach(System.out::println);
        assertThat(result)
                .hasSize(2)
                .containsAll(articles);
    }
}

UPDATE

I personally do not like using @Inheritance...
I also tried to avoid the mappedBy functionality, because I neither need bidirectional addressing...

Of cause, you can use @MappedSuperclass instead of @Inheritance in Article. And you can avoid mappedBy and use unidirectional ManyToMany.

But in this case, you will have to save independent entities such Author and Article through Newspaper only (see cascade = CascadeType.MERGE parameter). As for me it's rather inconvenient (I tried to neutralize it with utility methods addAuthors and addArticles):

@Data
@NoArgsConstructor
@Entity
public class Newspaper {

    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany(cascade = CascadeType.MERGE)
    private Set<Author> authors = new HashSet<>();

    @ManyToMany(cascade = CascadeType.MERGE)
    private Set<FirstTypeArticle> firstTypeArticles = new HashSet<>();

    @ManyToMany(cascade = CascadeType.MERGE)
    private Set<SecondTypeArticle> secondTypeArticles = new HashSet<>();

    public Newspaper(String name) {
        this.name = name;
    }

    public Newspaper addAuthors(Author... authors) {
        if (authors != null) {
            this.authors.addAll(Set.of(authors));
        }
        return this;
    }

    public Newspaper addArticles(Article... articles) {
        for (Article article : articles) {
            if (article instanceof FirstTypeArticle) {
                this.firstTypeArticles.add((FirstTypeArticle) article);
            }
            if (article instanceof SecondTypeArticle) {
                this.secondTypeArticles.add((SecondTypeArticle) article);
            }
        }
        return this;
    }
}
@Data
@NoArgsConstructor
@Entity
public class Author {
    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    public Author(String name) {
        this.name = name;
    }
}
@Data
@NoArgsConstructor
@MappedSuperclass
public abstract class Article {
    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    @ManyToMany(cascade = CascadeType.MERGE)
    private Set<Author> authors = new HashSet<>();

    public Article(String name, Author... authors) {
        this.name = name;
        addAuthors(authors);
    }

    public void addAuthors(Author... authors) {
        if (authors != null) {
            this.authors.addAll(Set.of(authors));
        }
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + "{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

@NoArgsConstructor
@Entity
public class FirstTypeArticle extends Article {
    public FirstTypeArticle(String name, Author... authors) {
        super(name, authors);
    }
}

@NoArgsConstructor
@Entity
public class SecondTypeArticle extends Article {
    public SecondTypeArticle(String name, Author... authors) {
        super(name, authors);
    }
}

Then you will get the following autogenerated tables:

AUTHOR
FIRST_TYPE_ARTICLE
FIRST_TYPE_ARTICLE_AUTHORS
SECOND_TYPE_ARTICLE
SECOND_TYPE_ARTICLE_AUTHORS
NEWSPAPER
NEWSPAPER_AUTHORS
NEWSPAPER_FIRST_TYPE_ARTICLES
NEWSPAPER_SECOND_TYPE_ARTICLES

Usage example

Adding newspapers:

newspapers = newspaperRepo.saveAll(List.of(
        new Newspaper("newspaper1"),
        new Newspaper("newspaper2")
));

Adding authors:

newspaperRepo.save(newspapers.get(0).addAuthors(
        new Author("author1"),
        new Author("author2")
));

Getting authors:

authors = authorRepo.findAll();

Adding articles:

newspaperRepo.save(newspapers.get(0).addArticles(
        new FirstTypeArticle("article1", authors.get(0), authors.get(1)),
        new SecondTypeArticle("article2", authors.get(1))
));
Related