最近,我惊讶于一个代码库在其所有域实体中都具有公共默认构造函数(即零参数构造函数),并且所有字段都具有getter和setter。 当我深入研究时,我发现域实体之所以如此,主要是因为该团队认为Web / MVC框架需要它。 我认为这是消除一些误解的好机会。
具体来说,我们将研究以下情况:
- 生成的ID字段没有设置器(即,生成的ID字段具有吸气剂但没有设置器)
- 没有默认的构造函数(例如,没有公共的零参数构造函数)
- 具有子实体的域实体(例如,子实体未显示为可修改列表)
绑定Web请求参数
首先,一些细节和背景。 让我们基于特定的Web / MVC框架-Spring MVC。 使用Spring MVC时,其数据绑定按名称绑定请求参数。 让我们举个例子。
@Controller
@RequestMapping("/accounts")
... class ... {
...
@PostMapping
public ... save(@ModelAttribute Account account, ...) {...}
...
}
给定上面的控制器映射到“ / accounts”,一个Account
实例可以从哪里来?
根据文档 ,Spring MVC将使用以下选项获取实例:
- 从模型(如果已通过
Model
添加(例如通过同一控制器中的@ModelAttribute
方法 )。 - 通过
@SessionAttributes
在HTTP会话中。 - 来自通过
Converter
的URI路径变量。 - 从默认构造函数的调用开始。
- (仅适用于Kotlin)通过调用具有与Servlet请求参数匹配的参数的“主要构造函数”; 参数名称是通过JavaBeans
@ConstructorProperties
或字节码中运行时保留的参数名称确定的。
假设没有在会话中添加Account
对象,并且没有@ModelAttribute
方法 ,Spring MVC最终将使用其默认构造函数实例化一个实例,并按name绑定Web请求参数。 例如,请求包含“ id”和“ name”参数。 Spring MVC将尝试通过分别调用“ setId”和“ setName”方法将它们绑定到“ id”和“ name” bean属性。 这遵循JavaBean约定。
生成ID字段的无设置方法
让我们从简单的事情开始。 假设我们有一个Account
域实体。 它具有由持久性存储生成的ID字段,并且仅提供getter方法(但不提供setter方法)。
@Entity
... class Account {
@Id @GeneratedValue(...) private Long id;
...
public Account() { ... }
public Long getId() { return id; }
// but no setId() method
}
那么,我们如何让Spring MVC将请求参数绑定到Account
域实体? 我们是否必须为生成的字段和只读字段提供公共设置方法?
在我们HTML表单中,我们不会将“ id”作为请求参数。 我们将其放置为路径变量。
我们使用@ModelAttribute
方法。 在请求处理方法之前调用它。 它支持与常规请求处理方法几乎相同的参数。 在我们的例子中,我们使用它来检索具有给定唯一标识符的Account
域实体,并将其用于进一步的绑定。 我们的控制器看起来像这样。
@Controller
@RequestMapping("/accounts")
... class ... {
...
@ModelAttribute
public Account populateModel(
HttpMethod httpMethod,
@PathVariable(required=false) Long id) {
if (id != null) {
return accountRepository.findById(id).orElseThrow(...);
}
if (httpMethod == HttpMethod.POST) {
return new Account();
}
return null;
}
@PutMapping("/{id}")
public ... update(...,
@ModelAttribute @Valid Account account, ...) {
...
accountRepository.save(account);
return ...;
}
@PostMapping
public ... save(@ModelAttribute @Valid Account account, ...) {
...
accountRepository.save(account);
return ...;
}
...
}
更新现有帐户时,请求将是对“ / accounts / {id}” URI的PUT
。 在这种情况下,我们的控制器需要检索具有给定唯一标识符的域实体,并向Spring MVC提供相同的域对象以进行进一步绑定(如果有)。 “ id”字段将不需要设置方法。
添加或保存新帐户时,请求将是“ / accounts”的POST
。 在这种情况下,我们的控制器需要使用一些请求参数创建一个新的域实体,并向Spring MVC提供相同的域对象以进行进一步绑定(如果有)。 对于新的域实体,“ id”字段保留为null
。 基础的持久性基础结构将在存储时生成一个值。 尽管如此,“ id”字段仍不需要设置方法。
在这两种情况下,@ @ModelAttribute
方法populateModel
均在映射的请求处理方法之前被调用。 因此,我们需要在populateModel
使用参数来确定在哪种情况下使用它。
域对象中没有默认构造函数
假设我们的Account
域实体没有提供默认构造函数(即,没有零参数构造函数)。
... class Account {
public Account(String name) {...}
...
// no public default constructor
// (i.e. no public zero-arguments constructor)
}
那么,我们如何让Spring MVC将请求参数绑定到Account
域实体? 它不提供默认的构造函数。
我们可以使用@ModelAttribute
方法。 在这种情况下,我们要创建一个带有请求参数的Account
域实体,并将其用于进一步的绑定。 我们的控制器看起来像这样。
@Controller
@RequestMapping("/accounts")
... class ... {
...
@ModelAttribute
public Account populateModel(
HttpMethod httpMethod,
@PathVariable(required=false) Long id,
@RequestParam(required=false) String name) {
if (id != null) {
return accountRepository.findById(id).orElseThrow(...);
}
if (httpMethod == HttpMethod.POST) {
return new Account(name);
}
return null;
}
@PutMapping("/{id}")
public ... update(...,
@ModelAttribute @Valid Account account, ...) {
...
accountRepository.save(account);
return ...;
}
@PostMapping
public ... save(@ModelAttribute @Valid Account account, ...) {
...
accountRepository.save(account);
return ...;
}
...
}
具有子实体的域实体
现在,让我们看一下具有子实体的域实体。 这样的东西。
... class Order {
private Map<..., OrderItem> items;
public Order() {...}
public void addItem(int quantity, ...) {...}
...
public Collection<CartItem> getItems() {
return Collections.unmodifiableCollection(items.values());
}
}
... class OrderItem {
private int quantity;
// no public default constructor
...
}
请注意,订单中的项目不会显示为可修改列表。 Spring MVC支持索引属性,并将它们绑定到数组,列表或其他自然排序的集合。 但是,在这种情况下, getItems
方法将返回无法修改的集合。 这意味着当对象尝试向其添加/删除项目时,将引发异常。 那么,如何让Spring MVC将请求参数绑定到Order
域实体? 我们是否被迫将订单项公开为可变列表?
并不是的。 我们必须避免用表示层关注点来稀释域模型(例如Spring MVC)。 相反,我们使表示层成为域模型的客户端。 为了处理这种情况,我们创建了另一个符合Spring MVC的类型,并使我们的域实体与表示层无关。
... class OrderForm {
public static OrderForm fromDomainEntity(Order order) {...}
...
// public default constructor
// (i.e. public zero-arguments constructor)
private List<OrderFormItem> items;
public List<OrderFormItem> getItems() { return items; }
public void setItems(List<OrderFormItem> items) { this.items = items; }
public Order toDomainEntity() {...}
}
... class OrderFormItem {
...
private int quantity;
// public default constructor
// (i.e. public zero-arguments constructor)
// public getters and setters
}
请注意,完全可以创建一个了解域实体的表示层类型。 但是让域实体知道表示层对象并不是全部。 更具体地说,表示层OrderForm
知道Order
域实体。 但是Order
不了解表示层OrderForm
。
这是我们的控制器的外观。
@Controller
@RequestMapping("/orders")
... class ... {
...
@ModelAttribute
public OrderForm populateModel(
HttpMethod httpMethod,
@PathVariable(required=false) Long id,
@RequestParam(required=false) String name) {
if (id != null) {
return OrderForm.fromDomainEntity(
orderRepository.findById(id).orElseThrow(...));
}
if (httpMethod == HttpMethod.POST) {
return new OrderForm(); // new Order()
}
return null;
}
@PutMapping("/{id}")
public ... update(...,
@ModelAttribute @Valid OrderForm orderForm, ...) {
...
orderRepository.save(orderForm.toDomainEntity());
return ...;
}
@PostMapping
public ... save(@ModelAttribute @Valid OrderForm orderForm, ...) {
...
orderRepository.save(orderForm.toDomainEntity());
return ...;
}
...
}
总结思想
正如我在之前的文章中提到的,可以让您的域对象看起来像具有公共默认零参数构造函数,getter和setter的JavaBean。 但是,如果域逻辑开始变得复杂,并且要求某些域对象失去其JavaBean风格(例如,不再有公共的零参数构造函数,没有更多的setter),则不必担心。 定义新的JavaBean类型以满足与表示相关的问题。 不要稀释域逻辑。
目前为止就这样了。 我希望这有帮助。
再次感谢Juno帮助我提供样品。 相关代码段可以在GitHub上找到 。
翻译自: https://www.javacodegeeks.com/2018/06/domain-objects-spring-mvc.html