【问题标题】:Best way to specify fields returned by a Service指定服务返回的字段的最佳方式
【发布时间】:2016-03-13 23:12:23
【问题描述】:

我们使用 Java EE 7 和 WildFly 9 来开发移动/网络应用程序的自定义后端。后端是一个经典的 3 层系统,具有通信逻辑 (JAX-RS)、业务逻辑 (Session EJB) 和持久层 (Hibernate)。

业务逻辑层由一组服务组成,每个服务由一个接口和一个 EJB 实现定义。假设

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

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

拥有

public class PostDTO {

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

    // getters and setters
}

假设有时客户只对idtitle 的帖子感兴趣。 API 端点将收到一个查询参数,其中包含要获取的字段列表。因此,JSON 序列化的 DTO 应该只包含帖子 idtitle。目标是避免不必要的处理,以便在不需要时加载非常大的 UserDTO 对象。

一个简单的解决方案是将自定义List&lt;String&gt; desiredFields 参数添加到getAllPosts()。这并不能说服我,因为我们需要将此参数添加到几乎每个服务方法。

这样做的最佳做法是什么?是否有用于此目的的 Java EE 对象?

【问题讨论】:

标签: java jakarta-ee jboss wildfly wildfly-9


【解决方案1】:

我的回答做了一些额外的假设:

  • 使用 JPA 2.1,您可以使用实体图有条件地获取实体的一部分,无论是惰性还是急切。
  • 将 JAX-RS 与 Jackson JSON 提供程序一起使用,您可以使用 @JsonView 执行相同的操作来将您的对象呈现为 JSON。

考虑以下示例:用户具有一组角色。默认情况下,角色是惰性获取的,不需要在 JSON 中呈现。但在某些情况下,您希望它们被急切地获取,因为您希望它们以 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 {
    /* ... */
}

以下是加载用户的代码,无论有无角色:

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();
}

现在是 REST 资源:

@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();
    }

}

因此,在持久性方面(JPA、Hibernate),您可以使用延迟获取来防止加载实体的某些部分,而在 Web 层(JAX-RS、JSON)上,您可以使用@JsonView 来决定应该使用哪些部分在实体的(反)序列化过程中进行处理。

【讨论】:

  • 非常详细的回答,谢谢!我仍然有几个疑问。 API 端点将收到一个查询参数,其中包含要获取的字段列表。如何在@JsonView 中使用这个列表?我是否需要为获取实体时可以包含/排除的每个字段指定一个布尔参数?
【解决方案2】:

通过直接返回一个唯一的模型类实例,比如说 Post 而不是 PostDTO,结合 JPA 和 JAXB 注释,您可以从 @XmlTransient 和默认延迟加载中受益,以避免查询用户实体不需要时的持久层,并在 RESTful Web 服务层隐藏此属性。

我个人经常这样做,因为它通过减少层和映射(实体 / dto)来简化应用程序代码。有关详细信息,请参阅these slides

我认为它非常适合 CRUD 操作,它是一个纯 Java EE 7 解决方案。无需使用额外的库。

Post.java 看起来像这样:

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 ...

}

优点:

  • 没有用于测试、编码和维护的 DTO/实体映射
  • 只有一个模型层可以放置持久性/业务/验证规则
  • Java EE 7 解决方案,无需使用额外的库

缺点:

  • 您的模型与 JPA 相关联。因此,如果您更改持久性解决方案并且它不允许管理多个不同的持久性层,则需要对其进行修改

编辑

由于有时您想获取作者,假设您在 REST 操作中将 QueryParam fetchAuthor 设置为 true/false,您需要在该模型对象中添加额外的 JPA @Transient 属性,例如 author 并在需要时对其进行初始化。所以它是 Post java 类中的一个额外映射,但是你保留了上述优点的好处。要初始化作者属性,您只需使用 getPersistedAuthor() 返回值设置它(这将触发查询以通过延迟加载获取持久作者)。

【讨论】:

  • 如果我弄错了请纠正我,但这样我永远不会将author返回给客户,即使他们请求所有字段。
  • 你是对的。我也更新了我的答案来处理这一点。请参阅末尾的 EDIT 部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-03
  • 2015-06-19
  • 1970-01-01
  • 1970-01-01
  • 2017-12-26
相关资源
最近更新 更多