Karim Stekelenburg
Karim Stekelenburg

Reputation: 643

Many to Many relationships using Spring Boot, Jackson and Hibernate

I'm working on a rest project using Spring Boot and Hibernate and am currently trying to figure out how to handle my json-serialization.

enter image description here

The schema shown in the ERD above is mapped by Hibernate and works fine.

The problem arises when I make a get request to a controller. My understanding is that Spring now tries to serialize the object-chain using Jackson. Because both the parent and child objects have one another as an attribute, we find ourselves hitting an infinite recursion loop.

Now I've looked into @JsonIgnore, @JsonView, @JsonManagedReference and @JsonBackReference but these only seem to work for one-to-many relationships.

What I'm looking for is a situation where when I for instance make a GET request to /users/{id}, I get the user object including all it's relationship attributes (let's call it the full object), but the relationship attributes themselves don't show their relationship-attributes (minimized objects). This works fine with the annotations mentioned above, but how do I make this work the other way as well?

Desired response for: /users/{id}

{   // full user object
    id: 1,
    username: 'foo',
    // password can be JsonIgnored because of obvious reasons
    role: { // minimized role object
        id: 1,
        name: 'bar'
        // NO USERS LIST
    }
    area: { //minimized area object
        id: 2,
        name: 'some val'
        // NO USERS LIST
        // NO TABLES LIST
    }
}

Desired response for /userrole/{id}

{ // full role object
    id: 1,
    name: 'waiter'
    users: [
        {   // minmized user object
            id: 1,
            username: 'foo'
            // password can be JsonIgnored because of obvious reasons
            // NO ROLE OBJECT
            // NO AREA OBJECT
        },
        {   // minmized user object
            id: 1,
            username: 'foo'
            // password can be JsonIgnored because of obvious reasons
            // NO ROLE OBJECT
            // NO AREA OBJECT
        }
    ]
}

In general: I'd like a full object when the request is made to the entity directly and a minimized object when requested indirectly.

Any Ideas? I hope my explanation is clear enough.


UPDATE

The Area, User and UserRole POJO's as requested in the comment sections.

User

@Entity
@Table(name = "users", schema = "public", catalog = "PocketOrder")

public class User {
    private int id;
    private String username;
    private String psswrd;
    private List<Area> areas;

    private UserRole Role;

    @Id
    @Column(name = "id", nullable = false)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Basic
    @Column(name = "username", nullable = false, length = 20)
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Basic
    @JsonIgnore
    @Column(name = "psswrd", nullable = true, length = 40)
    public String getPsswrd() {
        return psswrd;
    }

    public void setPsswrd(String psswrd) {
        this.psswrd = psswrd;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        User user = (User) o;

        if (id != user.id) return false;
        if (username != null ? !username.equals(user.username) : user.username != null) return false;
        if (psswrd != null ? !psswrd.equals(user.psswrd) : user.psswrd != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (username != null ? username.hashCode() : 0);
        result = 31 * result + (psswrd != null ? psswrd.hashCode() : 0);
        return result;
    }

    @ManyToMany(mappedBy = "users")
    public List<Area> getAreas() {
        return areas;
    }

    public void setAreas(List<Area> areas) {
        this.areas = areas;
    }

    @ManyToOne
    @JoinColumn(name = "role_fk", referencedColumnName = "id", nullable = false)
    public UserRole getRole() {
        return Role;
    }

    public void setRole(UserRole role) {
        Role = role;
    }
}

UserRole

@Entity
@javax.persistence.Table(name = "userroles", schema = "public", catalog = "PocketOrder")
public class UserRole {
    private int id;
    private String name;
    private List<User> users;


    @Id
    @Column(name = "id", nullable = false)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Basic
    @Column(name = "name", nullable = false, length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        UserRole userRole = (UserRole) o;

        if (id != userRole.id) return false;
        if (name != null ? !name.equals(userRole.name) : userRole.name != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @OneToMany(mappedBy = "role")
    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        users = users;
    }
}

Area

@Entity
@javax.persistence.Table(name = "areas", schema = "public", catalog = "PocketOrder")
public class Area {
    private int id;
    private String name;
    private List<User> users;


    private List<Table> tables;

    @Id
    @Column(name = "id", nullable = false)
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Basic
    @Column(name = "name", nullable = false, length = 20)
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Area area = (Area) o;

        if (id != area.id) return false;
        if (name != null ? !name.equals(area.name) : area.name != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (name != null ? name.hashCode() : 0);
        return result;
    }

    @ManyToMany
    @JoinTable(name = "areas_users", catalog = "PocketOrder", schema = "public", joinColumns = @JoinColumn(name = "area_fk", referencedColumnName = "id", nullable = false), inverseJoinColumns = @JoinColumn(name = "user_fk", referencedColumnName = "id", nullable = false))
    public List<User> getUsers() {
        return users;
    }

    public void setUsers(List<User> users) {
        this.users = users;
    }

    @OneToMany(mappedBy = "area")
    public List<Table> getTables() {
        return tables;
    }

    public void setTables(List<Table> tables) {
        this.tables = tables;
    }
}

Upvotes: 4

Views: 4278

Answers (3)

dragsu
dragsu

Reputation: 129

@JsonIgnoreProperties should achieve what you are after. According to the doco this annotation can be used to,

either suppress serialization of properties (during serialization), or ignore processing of JSON properties read (during deserialization).

A simple example is below.

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false)
    private String firstName;

    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinTable(
        name="user_roles",
        joinColumns=
            @JoinColumn(name="USER_ID", referencedColumnName="ID"),
        inverseJoinColumns=
            @JoinColumn(name="ROLE_NAME", referencedColumnName="NAME")
    )
    @JsonIgnoreProperties("users")
    private List<Role> roles;
}
public class Role {

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

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "roles")
    @JsonIgnoreProperties("roles")
    public List<User> users;
}

Upvotes: 0

Renis1235
Renis1235

Reputation: 4710

The way I worked around this on a Many-to-Many relationship is by using

@JsonIgnore

On one of the Entities. For example we have Person and Child entities. One Person can have many children and vice-versa. On Person we have :

public class Person
{
  //Other fields ommited
     @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
     @JoinTable(name = "person_child",
                    joinColumns = {
        @JoinColumn(name = "person_id", referencedColumnName = "id", nullable = false, 
        updatable = false)
         },
                    inverseJoinColumns = {
                @JoinColumn(name = "child_id", referencedColumnName = "id", nullable = 
                false, updatable = false)
        })
private Set<Child> children = new HashSet<>() ;

}

And on Child we have :

public class Child
{
      @JsonIgnore
      @ManyToMany(mappedBy = "children", fetch = FetchType.LAZY)
      private Set<Person> people = new HashSet<>() ;
}

Now when we get a Person, we also get all his connected children. But when we get a Child then we don't get all People because we have @JsonIgnore annotation on it. This fixes the Infinite Recursion problem, and raises this one.

My workaround was by writing a query to get me all the People connected to a specific child_id. Below you may see my code:

public interface PersonDAO extends JpaRepository<Person, Long>
{

        @Query(value = "SELECT * " +
        " FROM person p INNER JOIN person_child j " +
        "ON p.id = j.person_id WHERE j.child_id = ?1 ", nativeQuery = true)
         public List<Person> getPeopleViaChildId(long id);
}

And i use it whenever I want to get all People from a child.

Upvotes: 1

Ady Junior
Ady Junior

Reputation: 1080

Try use @JsonSerialize on specific points:

For sample:

1 - Map your field

@JsonSerialize(using = ExampleSampleSerializer.class)
@ManyToOne
private Example example;

2 - Create custom jackson serializer (Here you can control the serialization)

public class ExampleSampleSerializer extends JsonSerializer<Example> {
    @Override
    public void serialize(Example value, JsonGenerator jsonGenerator, SerializerProvider serializers) throws IOException, JsonProcessingException {

        jsonGenerator.writeStartObject();
        jsonGenerator.writeFieldName("first");
        jsonGenerator.writeNumber(value.getFirstValue());
        jsonGenerator.writeFieldName("second");
        jsonGenerator.writeNumber(value.getSecondValue());
        jsonGenerator.writeFieldName("third");
        jsonGenerator.writeNumber(value.getAnyAnotherClass().getThirdValue());
        jsonGenerator.writeEndObject();
    }
}

Upvotes: 4

Related Questions