Dealing with Domain Objects in Spring MVC
I was recently surprised by how one code base had public default constructors (i.e. zero-arguments constructors) in all their domain entities, and had getters and setters for all the fields. As I dug deeper, I found out that the reason why the domain entities are the way they are is largely because the team thinks it was required by the web/MVC framework. And I thought it would be a good opportunity to clear up some misconceptions.
Specifically, we’ll look at the following cases:
- No setter for generated ID field (i.e. the generated ID field has a getter but no setter)
- No default constructor (e.g. no public zero-arguments constructor)
- Domain entity with child entities (e.g. child entities are not exposed as a modifiable list)
Binding Web Request Parameters
First, some specifics and some background. Let’s base this on a specific web/MVC framework – Spring MVC. When using Spring MVC, its data binding binds request parameters by name. Let’s use an example.
@Controller @RequestMapping("/accounts") ... class ... { ... @PostMapping public ... save(@ModelAttribute Account account, ...) {...} ... }
Given the above controller mapped to “/accounts”, where can an Account
instance come from?
Based on documentation, Spring MVC will get an instance using the following options:
- From the model if already added via
Model
(like via@ModelAttribute
method in the same controller). - From the HTTP session via
@SessionAttributes
. - From a URI path variable passed through a
Converter
. - From the invocation of a default constructor.
- (For Kotlin only) From the invocation of a “primary constructor” with arguments matching to Servlet request parameters; argument names are determined via JavaBeans
@ConstructorProperties
or via runtime-retained parameter names in the bytecode.
Assuming an Account
object is not added in the session, and that there is no @ModelAttribute
method, Spring MVC will end up instantiating one using its default constructor, and binding web request parameters by name. For example, the request contains “id” and “name” parameters. Spring MVC will try to bind them to “id” and “name” bean properties by invoking “setId” and “setName” methods, respectively. This follows JavaBean conventions.
No Setter Method for Generated ID Field
Let’s start with something simple. Let’s say that we have an Account
domain entity. It has an ID field that is generated by the persistent store, and only provides a getter method (but no setter method).
@Entity ... class Account { @Id @GeneratedValue(...) private Long id; ... public Account() { ... } public Long getId() { return id; } // but no setId() method }
So, how can we have Spring MVC bind request parameters to an Account
domain entity? Are we forced to have a public setter method for a field that is generated and read-only?
In our HTML form, we will not place the “id” as a request parameter. We will place it as a path variable instead.
We use a @ModelAttribute
method. It is called prior to the request handling method. And it supports pretty much the same parameters as a regular request handling method. In our case, we use it to retrieve an Account
domain entity with the given unique identifier, and use it for further binding. Our controller would look something like this.
@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 ...; } ... }
When updating an existing account, the request would be a PUT
to “/accounts/{id}” URI. In this case, our controller needs to retrieve the domain entity with the given unique identifier, and provide the same domain object to Spring MVC for further binding, if any. The “id” field will not need a setter method.
When adding or saving a new account, the request would be a POST
to “/accounts”. In this case, our controller needs to create a new domain entity with some request parameters, and provide the same domain object to Spring MVC for further binding, if any. For new domain entities, the “id” field is left null
. The underlying persistence infrastructure will generate a value upon storing. Still, the “id” field will not need a setter method.
In both cases, the @ModelAttribute
method populateModel
is called prior to the mapped request handling method. Because of this, we needed to use parameters in populateModel
to determine which case it is being used in.
No Default Constructor in Domain Object
Let’s say that our Account
domain entity does not provide a default constructor (i.e. no zero-arguments constructor).
... class Account { public Account(String name) {...} ... // no public default constructor // (i.e. no public zero-arguments constructor) }
So, how can we have Spring MVC bind request parameters to an Account
domain entity? It does not provide a default constructor.
We can use a @ModelAttribute
method. In this case, we want to create an Account
domain entity with request parameters, and use it for further binding. Our controller would look something like this.
@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 ...; } ... }
Domain Entity with Child Entities
Now, let’s look at a domain entity that has child entities. Something like this.
... 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 ... }
Note that the items in an order are not exposed as a modifiable list. Spring MVC supports indexed properties and binds them to an array, list, or other naturally ordered collection. But, in this case, the getItems
method returns an unmodifiable collection. This means that an exception would be thrown when an object attempts to add/remove items to/from it. So, how can we have Spring MVC bind request parameters to an Order
domain entity? Are we forced to expose the order items as a mutable list?
Not really. We must refrain from diluting the domain model with presentation-layer concerns (like Spring MVC). Instead, we make the presentation-layer a client of the domain model. To handle this case, we create another type that complies with Spring MVC, and keep our domain entities agnostic of the presentation layer.
... 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 }
Note that it is perfectly all right to create a presentation-layer type that knows about the domain entity. But it is not all right to make the domain entity aware of presentation-layer objects. More specifically, presentation-layer OrderForm
knows about the Order
domain entity. But Order
does not know about presentation-layer OrderForm
.
Here’s how our controller will look like.
@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 ...; } ... }
Closing Thoughts
As I’ve mentioned in previous posts, it is all right to have your domain objects look like a JavaBean with public default zero-arguments constructors, getters, and setters. But if the domain logic starts to get complicated, and requires that some domain objects lose its JavaBean-ness (e.g. no more public zero-arguments constructor, no more setters), do not worry. Define new JavaBean types to satisfy presentation-related concerns. Do not dilute the domain logic.
That’s all for now. I hope this helps.
Thanks again to Juno for helping me out with the samples. The relevant pieces of code can be found on GitHub.
Published on Java Code Geeks with permission by Lorenzo Dee, partner at our JCG program. See the original article here: Dealing with Domain Objects in Spring MVC Opinions expressed by Java Code Geeks contributors are their own. |
You never define What is a domain entity.