指定服务返回的字段的最佳方式
我们使用Java EE 7和WildFly 9为移动/ Web应用程序开发自定义后端。后端是一个经典的3层系统,具有通信逻辑(JAX-RS),业务逻辑(会话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
}
让我们假设,有时,客户只关心后id
和title
。 API端点将接收带有要获取的字段列表的查询参数。所以,JSON序列化的DTO应该只包含后id
和title
。目标是避免不必要的处理,以便在不需要时加载非常大的对象UserDTO
。
一个天真的解决方案是将一个自定义List<String> desiredFields
参数添加到getAllPosts()
。这并不完全说服我,因为我们需要将此参数添加到几乎每服务方法的。
这样做的最佳做法是什么? Java EE对象是否有此目的?
通过直接返回一个独特的模型类的实例,让我们说后代替PostDTO,结合JPA和JAXB注解,你可以从@XmlTransient和默认延迟加载受益,以避免在不必要的查询用户实体持久层,并从RESTful Web服务层隐藏此属性。
我personnaly使用这么做,因为它通过减少层和映射(实体/ 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属性,例如作者,并在需要时对其进行初始化。所以这是Post java类中的额外映射,但是您仍然保留上述优点的好处。要初始化作者属性,只需使用getPersistedAuthor()返回值(它将触发查询以延迟加载获取持久作者)来设置它。
纠正我,如果我错了,但以这种方式,我即使他们请求所有字段,也不会将'author'返回给客户端。 –
你说得对。我已经更新了我的答案来处理这一点。最后请参阅*编辑*部分。 –
我的回答使一些额外的假设:
- 使用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中使用这个列表?我是否需要为每个字段指定一个布尔参数,在提取实体时可以包含/排除? –
也许创建一个简单的NameId值对象,让Hibernate为你构建VO实例列表?假设你使用JPA:http://*.com/questions/2355728/jpql-create-new-object-in-select-statement-avoid-or-embrace – Gimby