Best way to specify fields returned by a Service

2019-06-28 07:22发布

问题:

We're using Java EE 7 with WildFly 9 to develop the custom backend for a mobile/web application. The backend is a classic 3-tier system, with communication logic (JAX-RS), business logic (Session EJBs) and persistence layer (Hibernate).

The business logic layer is composed by a set of services, each defined by an interface and an EJB implementation. Let's suppose

public interface IPostService {
    List<PostDTO> getAllPosts();
}

and

@Stateless
public class PostService implements IPostService {
    List<PostDTO> getAllPosts(){
    // retrieving my Posts through Hibernate
    }

having

public class PostDTO {

    private Long id;
    private String title;
    // UserDTO is a VEEERY big object
    private UserDTO author;

    // getters and setters
}

Let's suppose that, sometimes, the clients are interested only in post id and title. The API endpoint will receive a query parameter with the list of fields to be fetched. So, the JSON-serialized DTO should contains only post id and title. The goal is to avoid unnecessary processing in order to load the very big UserDTO object when it is not required.

A naive solution is to add a custom List<String> desiredFields parameter to getAllPosts(). This doesn't quite convince me, since we need to add this parameter to almost every service method.

What is the best practices to do that? Are there Java EE object intended to this purpose?

回答1:

My answer makes some additional assumptions:

  • using JPA 2.1, you could make use of an entity graph to conditionally fetch parts of your entity either lazy or eager.
  • using JAX-RS with Jackson JSON provider, you could use @JsonView to do the same for rendering your objects to JSON.

Consider the following example: A user has a set of roles. Roles are fetched lazy by default and don't need to be rendered in JSON. But in some situations you want them to be fetched eager, because you want them to be rendered in JSON.

@Entity
@NamedQueries({
    @NamedQuery(name = "User.byName", query = "SELECT u FROM User u WHERE u.name = :name"),
    @NamedQuery(name = "Users.all", query = "SELECT u FROM User u ORDER BY u.name ASC")
})
@NamedEntityGraph(name = "User.withRoles", attributeNodes = {
    @NamedAttributeNode("roles") // make them fetched eager
})
public class User implements Serializable {

    public static interface WithoutRoles {}
    public static interface WithRoles extends WithoutRoles {}

    @Id
    private Long id;

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

    @ManyToMany // fetched lazy by default
    @JoinTable(/* ... */)
    private Set<Role> roles;

    public String getName() {
        return name;
    }

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

    // include in JSON only when "@JsonView(WithRoles.class)" is used:
    @JsonView(WithRoles.class)
    public Set<Role> getRoles() {
        if (roles == null)
            roles = new HashSet<>();
        return roles;
    }

    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

}

@Entity
public class Role implements Serializable {
    /* ... */
}

Here's the code for loading a user, either with or without the roles:

public User getUser(String name, boolean withRoles) {
    TypedQuery<User> query = entityManager.createNamedQuery("User.byName", User.class)
        .setParameter("name", name);

    if (withRoles) {
        EntityGraph<User> graph = (EntityGraph<User>) entityManager.createEntityGraph("User.withRoles");
        query.setHint("javax.persistence.loadgraph", graph);
    }

    try {
        return query.getSingleResult();
    } catch (NoResultException e) {
        return null;
    }
}

public List<User> getAllUsers() {
    return entityManager.createNamedQuery("Users.all", User.class)
        .getResultList();
}

And now the REST resource:

@RequestScoped @Path("users")
public class UserResource {

    private @Inject UserService userService;

    // user list - without roles
    @GET @Produces(MediaType.APPLICATION_JSON)
    @JsonView(User.WithoutRoles.class)
    public Response getUserList() {
        List<User> users = userService.getAllUsers();
        return Response.ok(users).build();
    }

    // get one user - with roles
    @GET @Path("{name}") @Produces(MediaType.APPLICATION_JSON)
    @JsonView(User.WithRoles.class)
    public Response getUser(@PathParam("name") String name) {
        User user = userService.getUser(name, true);
        if (user == null)
            throw new NotFoundException();

        return Response.ok(user).build();
    }

}

So, on the persistence side (JPA, Hibernate) you can use lazy fetching to prevent loading parts of your entity, and on the web tier (JAX-RS, JSON) you can use @JsonView to decide what parts should be processed in the (de-)serialization of your entities.



回答2:

By returning directly a unique model class instance, let's say Post instead of PostDTO, combining JPA and JAXB annotations, you could benefit from @XmlTransient and default lazy loading to avoid to query User entity in persistence layer when not necessary, and to hide this property from RESTful web service layer.

I personnaly use to do that a lot as it simplifies application code by reducing layers and mappings (entity / dto). See these slides for details.

I think it's perfectly suited for CRUD operations and it's a pure Java EE 7 solution. No need to use extra libs.

Post.java would look like this:

import javax.persistence.*;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;

@Entity
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @XmlTransient
    @ManyToOne
    private User persistedAuthor;

    @Transient
    private User author;

    // + Getters and Setters ...

}

PROS:

  • No DTO / entity mapping to test, code and maintain
  • Only one model layer where you can put persistence / business / validation rules
  • Java EE 7 solution, no need to use extra libs

CONS:

  • Your model is tied to JPA. So you would need to modify this in case you change persistence solution and it does not permit to manage multiple different persistence layers

EDIT

As sometimes you want to get the author, assuming you have a QueryParam fetchAuthor set to true/false for sample in your REST operation, you would need to add an extra JPA @Transient property in that model objet such as author and initialize it when needed. So it's an extra mapping in Post java class, but you keep however the benefit of advantages mentioned above. To initialize author property, you just have to set it with the getPersistedAuthor() returned value (which will trigger a query to get the persisted author with lazy loading).