Hibernate: how to map several join tables to Java objects (user's list of books)

parsecer :

I have this database structure. There are users who read books every year and compile lists of books that they've read:

users       years        books----booklists
   |           |                      |
   ------------------------------------
               |      
           userlists

I've tried to do some mappings myself, but I don't think I'm correct.

users:
+-----------+---------+
|    id     |  name   |
+-----------+---------+
@Entity
public class User  {
  @Id @GeneratedValue
  private int id;

  //?
  private List<UserList> booklists = new ArrayList<>();
}

Each user has a list of the books they've read in a certain year:

books:
+-----------+---------+
|    id     |  title  |
+-----------+---------+
@Entity
public class Book  {
  @Id @GeneratedValue
  private int id;
}

the lists for every user are inside the booklists table:

booklists:
+-----+---------+---------+
| id  | list_id | book_id |
+-----+---------+---------+
@Entity
public class BookList  {
  @Column(name="list_id")
  private int id;

  @JoinTable(
            name = "books",
            joinColumns = @JoinColumn(
                    name = "list_id",
                    referencedColumnName = "list_id"

            ),
            inverseJoinColumns = @JoinColumn(
                    name = "book_id",
                    referencedColumnName = "id"
            )

    )
    @OneToMany
    private Collection<Book> books;
}

finally the booklists and users are united inside userlists:

years:
+----+------+
| id | year |
+----+------+

userlists:
+-----+---------+---------+---------+
| id  | user_id | list_id | year_id |
+-----+---------+---------+---------+
@Entity
public class UserList  {
    @ManyToOne
    @JoinColumn(name = "user_id")
    private UserDao user;

    @JoinTable(
            name = "userlists",
            joinColumns = @JoinColumn(
                    name = "id",
                    referencedColumnName = "id"
            ),
            inverseJoinColumns = @JoinColumn(
                    name = "year_id",
                    referencedColumnName = "id"
            )
    )
    @OneToOne
    private String year;

    //?
    private BookList bookList;
}

I'm not sure how to get the private List<UserList> booklists = new ArrayList<>(); inside User. I know that one-to-many could be mapped like this:

@OneToMany
@JoinTable(joinColumns=@JoinColumn(name="user_id"),
           inverseJoinColumns=@JoinColumn(name="list_id")
List<UserList> booklists = new ArrayList<>();

But my situation has more tables than any tutorials I've seen.

EDIT: It was pointed out to me, that it's ok to get rid of the userlists table and place information about user_id and year_id inside booklists. I'm still not entirely sure how to map it properly, so any help is appreciated.

K.Nicholas :

Well, this particular problem is somewhat unique in that, for the tables you have defined, you have a list_id but no entity for it. The list_id is just a sort of sub key or something that identifies a list. I would question the normalization of the tables as you mentioned in the EDIT above, but first things first.

The essential entities are the User, the Book, and the Year entities:

@Entity
@Table(name = "users")
@Data
public class User {
   @Id
   private int id;  
   private String name;
}
@Entity
@Table(name = "books")
@Data
public class Book {
   @Id
   private int id;
   private String title;
}
@Entity
@Table(name = "years")
@Data
public class Year {
   @Id
   private int id;
   private int year;
}

As you have described it you can create a ListId, Booklist, and Userlist set of relationship tables list so:

@MappedSuperclass
@Data
@EqualsAndHashCode(of = "id")
public class ListId {
    @Column(name="list_id")
    private int listId;     
    @Id
    private int id;
}
@Entity
@Table(name = "booklists")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Booklist extends ListId {  
    @ManyToOne
    private Book book;
}
@Entity
@Table(name = "userlists")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class Userlist extends ListId {  
    @ManyToOne
    private User user;
    @ManyToOne
    private Year year;
}

There are definitely other ways to do this, but none will solve the problem of not having a real ListId entity to do JPA relationships on. However, you can use this setup like so:

List<Booklist> bls = em.createQuery("select bl from Booklist bl left outer join Userlist ul on ul.listId = bl.listId where ul.user = :user", Booklist.class)
        .setParameter("user", u1)
        .getResultList();
bls.forEach(System.out::println);

This requires a somewhat newer version of JPA, perhaps 2.2 level. At least it requires Hibernate 5.1 or greater as the implementation. If you want to add a bidirectional mapping to User for this schema, then:

@OneToMany(mappedBy = "user")
Set<Userlist> booklists;

And use it like so.

List<Booklist> bls3 = em.createQuery("select bl from User u left outer join u.booklists ul left outer join Booklist bl on ul.listId = bl.listId where u = :user and ul.year = :year", Booklist.class)
        .setParameter("user", u1)
        .setParameter("year", y1)
        .getResultList();
bls3.forEach(System.out::println);

Assuming that you have some leeway in designing the schema the way you want to for the job, then yes, you should change it.

@Entity
@Table(name = "booklists")
@Data
@ToString(exclude = "user")
public class Booklist {     
    @Id
    private int id;
    @ManyToOne
    private Book book;      
    @ManyToOne
    private User user;      
    @ManyToOne
    private Year year;
}

Using it is much easier and wouldn't require the non-related entities join from JPA 2.2 or an unmapped ListId class.

List<Booklist> bls2 = em.createQuery("select bl from Booklist bl where bl.user = :user", Booklist.class)
        .setParameter("user", u2)
        .getResultList();
bls2.forEach(System.out::println);

You can add a bidirectional mapping to User.

   @OneToMany(mappedBy = "user")
   Set<Booklist> booklists;

And use it like so:

List<Booklist> bls3 = em.createQuery("select bl from User u left outer join u.booklists bl where u = :user and bl.year = :year", Booklist.class)
        .setParameter("user", u1)
        .setParameter("year", y1)
        .getResultList();
bls3.forEach(System.out::println);

This later schema gives you the same results as the former. If it wasn't so late I'd try to figure out which of the first 3 levels of normalization your first schema was violating but perhaps that's an exercise best left to the reader.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=164452&siteId=1