The Nightmare
The Nightmare

Reputation: 701

How to create DTO object with collections

I am writing application using Spring framework. I want to create dto model for my entity object and using it in spring controller. It is not difficult, but I have a relation between my table in database and I must have set in my entity object.

User entity

    @Entity
@Table(name = "users")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    @Column(name = "id")
    private Integer id;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    @Column(name = "username")
    private String username;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    @Column(name = "password")
    private String password;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    @Column(name = "firstName")
    private String firstName;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    @Column(name = "lastName")
    private String lastName;
    @Size(min = 11, max = 11)
    @Column(name = "personalId")
    private String personalId;
    @Size(max = 40)
    @Column(name = "city")
    private String city;
    @Size(max = 40)
    @Column(name = "address")
    private String address;
    @NotBlank
    @NotNull
    @Email
    @Size(max = 40)
    @Column(name = "email")
    private String email;
    @Size(min = 9, max = 9)
    @Column(name = "phone")
    private String phone;
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private Set<UserRole> userRoleSet;

user_roles entity

@Entity
@Table(name = "user_roles")
public class UserRole implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "id")
    private Integer id;
    @NotNull
    @Size(max = 40)
    @Column(name = "name")
    private String name;
    @JoinColumn(name = "userId")
    @ManyToOne(targetEntity = User.class)
    private User user;

Table user have a relation one to many to user_role table. I create a DTO object for this entities:

userDto

public class UserDto {

    private Integer id;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    private String username;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    private String password;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    private String firstName;
    @NotBlank
    @NotNull
    @Size(min = 3, max = 40)
    private String lastName;
    @Size(min = 11, max = 11)
    private String personalId;
    @Size(min = 3, max = 40)
    private String city;
    @Size(min = 3, max = 40)
    private String address;
    @NotBlank
    @NotNull
    @Email
    @Size(max = 40)
    private String email;
    @Size(min = 9, max = 9)
    private String phone;
    private Set<UserRoleDto> userRoleSetDto;

userRoleDto

public class UserRoleDto {

    private Integer id;
    @NotNull
    @Size(min = 3, max = 40)
    private String name;
    private UserDto userDto;

My controller which redirects to a page with a list of users

@SuppressWarnings("unchecked")
@RequestMapping(value = "/admin/adminlist", method = RequestMethod.GET)
public ModelAndView goAdminList() {
    ModelAndView mav = new ModelAndView("admin/adminlist");
    List<UserDto> admins = prepareUserListDto(userService.getAdminList());
    mav.addObject("admins", admins);
    return mav;
}

In this controller I prepare user list to userdto list, using method prepareUserListDto:

private List<UserDto> prepareUserListDto(List<User> users) {
    List<UserDto> userDtoList = null;
    if (users != null && !users.isEmpty()) {
        userDtoList = new ArrayList<UserDto>();
        UserDto userDto = null;
        for (User user : users) {
            userDto = new UserDto();
            userDto.setId(user.getId());
            userDto.setUsername(user.getUsername());
            userDto.setPassword(user.getPassword());
            userDto.setFirstName(user.getFirstName());
            userDto.setLastName(user.getLastName());
            userDto.setEmail(user.getEmail());
            userDto.setUserRoleSetDto(prepareUserRoleDtoSet(user
                    .getUserRoleSet()));
            userDtoList.add(userDto);
        }
    }
    return userDtoList;
}

this method invoke to method to prepare userRole to userRoleDto

private Set<UserRoleDto> prepareUserRoleDtoSet(Set<UserRole> userRoles) {
    Set<UserRoleDto> userRoleDtoSet = null;
    if (userRoles != null && !userRoles.isEmpty()) {
        userRoleDtoSet = new HashSet<UserRoleDto>();
        UserRoleDto userRoleDto = null;
        for (UserRole userRole : userRoles) {
            userRoleDto = new UserRoleDto();
            userRoleDto.setId(userRole.getId());
            userRoleDto.setName(userRole.getName());
            userRoleDto.setUserDto(prepareUserDto(userRole.getUser()));
            userRoleDtoSet.add(userRoleDto);
        }
    }
    return userRoleDtoSet;
}

but this method again invoke to method:

private UserDto prepareUserDto(User user) {
    UserDto userDto = new UserDto();
    userDto.setId(user.getId());
    userDto.setUsername(user.getUsername());
    userDto.setPassword(user.getPassword());
    userDto.setFirstName(user.getFirstName());
    userDto.setLastName(user.getLastName());
    userDto.setEmail(user.getEmail());
    userDto.setUserRoleSetDto(prepareUserRoleDtoSet(user.getUserRoleSet()));
    return userDto;
}

In conclusion when i try show the admin list I have recursive call and i get this error:

java.lang.StackOverflowError org.hibernate.collection.internal.PersistentSet.isEmpty(PersistentSet.java:166) pl.piotr.ibank.controller.AdminController.prepareUserRoleDtoSet(AdminController.java:170) pl.piotr.ibank.controller.AdminController.prepareUserDto(AdminController.java:139) pl.piotr.ibank.controller.AdminController.prepareUserRoleDtoSet(AdminController.java:177) pl.piotr.ibank.controller.AdminController.prepareUserDto(AdminController.java:139) pl.piotr.ibank.controller.AdminController.prepareUserRoleDtoSet(AdminController.java:177) pl.piotr.ibank.controller.AdminController.prepareUserDto(AdminController.java:139) pl.piotr.ibank.controller.AdminController.prepareUserRoleDtoSet(AdminController.java:177) pl.piotr.ibank.controller.AdminController.prepareUserDto(AdminController.java:139) pl.piotr.ibank.controller.AdminController.prepareUserRoleDtoSet(AdminController.java:177) pl.piotr.ibank.controller.AdminController.prepareUserDto(AdminController.java:139)

Line 139 is userDto.setUserRoleSetDto(prepareUserRoleDtoSet(user.getUserRoleSet())); in private UserDto prepareUserDto(User user) method and Line 177 is private UserDto prepareUserDto(User user) in private Set<UserRoleDto> prepareUserRoleDtoSet(Set<UserRole> userRoles) method.

How to write dto for this entity? If it would not have a constraint this is easy, but my entity have a constraint between user and user_role and I don't know how to good solution.

Upvotes: 1

Views: 12291

Answers (3)

Serge Ballesta
Serge Ballesta

Reputation: 148890

You declare the attribute fetch = FetchType.EAGER on the collection of roles. So when hibernate gives you a User the collection of its UserRole is already initialized. And even if it was not, it would be initialized on first call to getUserRoleSet(). You can simply write :

private List<UserDto> prepareUserListDto(List<User> users) {
    List<UserDto> userDtoList = null;
    Set<UserRoleDto> roleDtoSet = null;
    if (users != null && !users.isEmpty()) {
        userDtoList = new ArrayList<UserDto>();
        UserDto userDto = null;
        for (User user : users) {
            userDto = new UserDto();
            userDto.setId(user.getId());
            userDto.setUsername(user.getUsername());
            userDto.setPassword(user.getPassword());
            userDto.setFirstName(user.getFirstName());
            userDto.setLastName(user.getLastName());
            userDto.setEmail(user.getEmail());
            roleDtoSet = new HashSet<UserRoleDto>();
            for (UserRole role: user.getUserRoleSet) {
                UserRoleDto roleDto = new UserRoleDto();
                roleDto.setId(role.getId());
                roleDto.setUserDto(userDto);
                roleDto.setName(user.getName);
                roleDtoSet.add(roleDto);
            }
            userDto.setUserRoleSetDto(roleDtoSet);
            userDtoList.add(userDto);
        }
    }
    return userDtoList;
}

If you prefere small methods (easier to write and test), you can split it, almost the same you wrote but passing userDto that you already have :

private List<UserDto> prepareUserListDto(List<User> users) {
    List<UserDto> userDtoList = null;
    if (users != null && !users.isEmpty()) {
        userDtoList = new ArrayList<UserDto>();
        UserDto userDto = null;
        for (User user : users) {
            userDto = new UserDto();
            userDto.setId(user.getId());
            userDto.setUsername(user.getUsername());
            userDto.setPassword(user.getPassword());
            userDto.setFirstName(user.getFirstName());
            userDto.setLastName(user.getLastName());
            userDto.setEmail(user.getEmail());
            userDto.setUserRoleSetDto(prepareUserRoleDtoSet(userDto, user
                    .getUserRoleSet()));
            userDtoList.add(userDto);
        }
    }
    return userDtoList;
}

private Set<UserRoleDto> prepareUserRoleDtoSet(UserDto userDto,
        Set<UserRole> userRoles) {
    Set<UserRoleDto> userRoleDtoSet = null;
    if (userRoles != null && !userRoles.isEmpty()) {
        userRoleDtoSet = new HashSet<UserRoleDto>();
        UserRoleDto userRoleDto = null;
        for (UserRole userRole : userRoles) {
            userRoleDto = new UserRoleDto();
            userRoleDto.setId(userRole.getId());
            userRoleDto.setName(userRole.getName());
            userRoleDto.setUserDto(userDto);
            userRoleDtoSet.add(userRoleDto);
        }
    }
    return userRoleDtoSet;
}

This avoids all cyclic dependancy.

Upvotes: 2

jchampemont
jchampemont

Reputation: 2773

You have a bi-directionnal relation between UserDto and UserRoleDto.

If you really need to keep the bi-directional relation, I would modify your code as follow :

private UserDto prepareUserDto(User user) {
    UserDto userDto = new UserDto();
    userDto.setId(user.getId());
    userDto.setUsername(user.getUsername());
    userDto.setPassword(user.getPassword());
    userDto.setFirstName(user.getFirstName());
    userDto.setLastName(user.getLastName());
    userDto.setEmail(user.getEmail());
    userDto.setUserRoleSetDto(prepareUserRoleDtoSet(user.getUserRoleSet(), userDto));
    return userDto;
}

This pass the userDto being created to prepareUserRoleDtoSet.

private Set<UserRoleDto> prepareUserRoleDtoSet(Set<UserRole> userRoles, UserDto userDto) {
    Set<UserRoleDto> userRoleDtoSet = null;
    if (userRoles != null && !userRoles.isEmpty()) {
        userRoleDtoSet = new HashSet<UserRoleDto>();
        UserRoleDto userRoleDto = null;
        for (UserRole userRole : userRoles) {
            userRoleDto = new UserRoleDto();
            userRoleDto.setId(userRole.getId());
            userRoleDto.setName(userRole.getName());
            userRoleDto.setUserDto(userDto);
            userRoleDtoSet.add(userRoleDto);
        }
    }
    return userRoleDtoSet;
}

Upvotes: 3

Marios
Marios

Reputation: 1957

A very simple solution would be to remove completely the userDto property from the UserRoleDto class. In contrast to the entity classes where you might need to go from one entity to another and include bi-directional relations, the client will usually only need to get the roles of a user, and never users from a role. Therefore you do not need bi-directional relations between those two dtos

Upvotes: 1

Related Questions