How to define Friendship relationship using Hibernate?

点点圈 提交于 2019-12-22 03:38:11

问题


I need to have "FriendRequest" and "ListOfFriends" functionalities.Similar to facebook, which shows number of received friend requests and number of approved friends.

By "FriendRequest", I mean to have a list of friend requests that the user receives.

By "ListOfFriends", I mean to have a list of friends of the user.

To achieve that, I defined following class but when I try to retrieve a user using its username, a "Stackoverflow" exception message will be thrown. It seems it goes into an indefinite loop.

When I remove "FriendRequests" and "Friends" from my toString method it stops throwing the exception.

This question is similar to what I am going to achieve but the difference is I do not what to have a separate Credential class.

Member

@Entity
public class Member implements Serializable {
    @Id
    @GeneratedValue
    private long id;
    @Column(name = "username", nullable = false, unique = true)
    private String username;
    @Column(nullable = false)
    private String fname;
    @Column(nullable = false)
    private String lname;
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "requester")
    private Set<Friendship> friendRequests = new HashSet<Friendship>();
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "friend")
    private Set<Friendship> friends = new HashSet<Friendship>();

    getters and setters

    @Override
    public String toString() {
       return "Member [
                .....                 
                , fname=" + fname + ", lname=" + lname
                //  + friendRequests.size() + ", friends=" + friends.size() + 
       "]";
    }
}

Friendship

@Entity
public class Friendship implements Serializable {
    @Id
    @ManyToOne
    @JoinColumn(referencedColumnName = "username")
    Member requester;
    @Id
    @ManyToOne
    @JoinColumn(referencedColumnName = "username")
    Member friend;
    @Temporal(javax.persistence.TemporalType.DATE)
    Date date;
    @Column(nullable = false)
    boolean active;

回答1:


I would like to suggest DB Design.

Right now, You are having a friendship table and friends table as per POJO classes. Instead of that, you can just only have friendship table with one more column boolean isAccepted;(variable in POJO class).

If boolean value is true, It means that that member(friend) is a friend. When you want to get all the friends, retrieve the rows of friendship with isAccepted set to true; If you want to get only friendrequests(Or pending requests to be Approved), get all the rows with isAccepted set to false.

With the present DB design or ER model, you are using. You need to delete the row from the friendship table (as It is no more a friend request), If the person accepts the friend request and store somewhere. Where do you store the friends . According to your present design, It stores in the friends table. But there is no column saying that the row indicates a friend of someone. It is just like adding a row(friend) to the friends table every time one member accepts the request of other member and just making redundant rows.




回答2:


I don't get the problem that you have, though I did very few modifications to your code so it could work with my setup.

/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="hibernate-entitymanager-demo" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <properties>
      <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/hibernate4?zeroDateTimeBehavior=convertToNull"/>
      <property name="javax.persistence.jdbc.password" value="root"/>
      <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
      <property name="javax.persistence.jdbc.user" value="root"/>
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.NoCacheProvider"/>
      <!-- property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/ -->
    </properties>
  </persistence-unit>
</persistence>

Main.java (two use cases here: either createFriendships to initialize some data, or findMembers for the problematic use case. Pay attention to the javax.persistence.schema-generation.database.action property in persistence.xml as you'll want to create the database in the first, but not in the latter)

public class Main {

    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hibernate-entitymanager-demo");
        EntityManager em = emf.createEntityManager();

        EntityTransaction transaction = em.getTransaction();
        transaction.begin();

        // createFriendships(em);

        findMembers(em);

        transaction.commit();

        em.close();
        emf.close();
    }

    private static void findMembers(EntityManager em) {
        List<Member> list = em.createQuery("from Member").getResultList();
        for (Member m : list) {
            System.out.println(m);
        }
    }

    private static void createFriendships(EntityManager em) {
        List<Member> members = createMembers(em);

        for (int i = 0; i < members.size(); i++) {
            for (int j = 0; j < members.size(); j++) {
                if (i != j) {
                    createFriendship(em, members.get(i), members.get(j));
                }
            }
        }
    }

    private static List<Member> createMembers(EntityManager em) {
        List<Member> members = new ArrayList<>();
        members.add(createMember(em, "Roberta", "Williams", "rwilliams"));
        members.add(createMember(em, "Ken", "Williams", "kwilliams"));
        members.add(createMember(em, "Dave", "Grossman", "dgrossman"));
        members.add(createMember(em, "Tim", "Schafer", "tschafer"));
        members.add(createMember(em, "Ron", "Gilbert", "rgilbert"));
        return members;
    }

    private static Member createMember(EntityManager em, String fname, String lname, String username) {
        Member m = new Member();
        m.setFname(fname);
        m.setLname(lname);
        m.setUsername(username);
        em.persist(m);
        return m;
    }

    private static void createFriendship(EntityManager em, Member requester, Member friend) {
        Friendship f = new Friendship();
        f.setActive(true);
        f.setDate(new Date());
        f.setRequester(requester);
        f.setFriend(friend);
        em.persist(f);
    }

}

Main produces:

Member [fname = Roberta, lname = Williams, requests = 4, friends = 4]
Member [fname = Ken, lname = Williams, requests = 4, friends = 4]
Member [fname = Dave, lname = Grossman, requests = 4, friends = 4]
Member [fname = Tim, lname = Schafer, requests = 4, friends = 4]
Member [fname = Ron, lname = Gilbert, requests = 4, friends = 4]

Friendship.java The real actual change was the referenced column name which I changed from username to id as I got a could not get a field value by reflection kind of exception. Beside I think it's better in terms of database normalization:

@Entity
@Table(name = "FRIENDSHIPS")
public class Friendship implements Serializable {

    @Id
    @ManyToOne
    @JoinColumn(referencedColumnName = "id")
    Member requester;

    @Id
    @ManyToOne
    @JoinColumn(referencedColumnName = "id")
    Member friend;

    @Temporal(javax.persistence.TemporalType.DATE)
    Date date;

    @Column(nullable = false)
    boolean active;

    // getters & setters

}

Member.java (no change here, except for toString())

@Entity
@Table(name = "MEMBERS")
public class Member implements Serializable {

    @Id
    @GeneratedValue
    private long id;

    @Column(name = "username", nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String fname;

    @Column(nullable = false)
    private String lname;

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "requester")
    private Set<Friendship> friendRequests = new HashSet<>();

    @OneToMany(fetch = FetchType.EAGER, mappedBy = "friend")
    private Set<Friendship> friends = new HashSet<>();

    // getters & setters

    @Override
    public String toString() {
        return "Member [fname = " + fname + ", lname = " + lname
                + ", requests = " + friendRequests.size()
                + ", friends = " + friends.size() + "]";
    }

}



回答3:


I emulated your situation with latest version of Hibernate 5.2.2.Final. I get same exception. I tried to substitute EAGER fetching to LAZY and it started to work fine. StackOverFlowError means that Hibernate didn't recognize object in cyclic dependency, and get started to fetch referenced rows again and again creating new and new objects until the Stack Memory runs away.

Sane situation:

Member[1] --ref-- Friendship[1] --ref-- Member[1] --ref-- ... 
(Here Hibernate recognizes cyclic dependency and creates only two objects )

Bug situation (our case):

Member[1] --ref-- Friendship[1] --ref-- Member[1] --ref-- ... 
(Here Hibernate doesn't recognize cyclic dependency and creates plenty of objects until Stack Memory licks, which cause StackOverFlowError)

Googling and asking gave me this answer:

All JPA providers should be able to cope with eager loading of cyclic relations. Any problem should mean you raise a bug against the JPA provider.

– Neil Stockton

First solution:

So I suggest you using LAZY initialization. And handle fetch operations manually by FETCH JOIN. See please this answer.

Second solution:

Avoid cyclic references. If primary object you will firstly call is Friendship then comment out Friendship objects in object, so that Friendship will fetch Member and Member will know nothing about Friendship. It works for me, I've tested.

Unfortunately you architecture doesn't allow making the opposite Friendship doesn't know about Member since Member is primary key. I suggest you change your architecture it's very strong coupling - bad practice for programming.


References:

  • I asked relative question.
  • Source of example emulation with LAZY fetching is here.
  • Reported bug against Hibernate.



回答4:


Actually, the difference from the other questions is not only dropped Credentials object. Your Member class has backward relationships to Friendships, which causes the cyclic dependencies. In the other example the "root" object is a friendship, which makes join with members. If you want to make a Member to be the "root", you should design a different model.



来源:https://stackoverflow.com/questions/38874609/how-to-define-friendship-relationship-using-hibernate

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!