Fala pessoal ..
Vendo o modelo agora ficou mais claro.
Cuidado com a tag <form:errors>
dentro da <form:select>
ela não deve estar ali, e sim fora do select.
Bem observado pelo Mário a necessidade do itemValue. Uma outra coisa no método form, não é necessário passar o objeto de Transaction por modelAndView.addObject("transaction", transaction);
. Só de colocar o parâmetro no método public ModelAndView form(Transaction transaction) {...}
o Spring, por padrão, já disponibiliza este objeto no contexto dessa requisição, ou seja, ele está disponível na JSP através de ${transaction}
.
Mas isso por si só não resolve o problema. Ele é mais grave aqui. Escrevi um código de teste e quando vamos enviar os valores vamos dar uma olhada:
exemplo de modelos:
public class Transaction {
private Long id;
private String name;
private Set<Category> categories = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Category> getCategories() {
return categories;
}
public void setCategories(Set<Category> categories) {
this.categories = categories;
}
@Override
public String toString() {
return "Transaction{id:" + id + ", name:" + name + ", categories: " + categories + "}";
}
}
public class Category {
private Long id;
private String name;
/**
* @deprecated
*/
public Category() {
}
public Category(Long id, String name) {
this.id = id;
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Category {id:" + id + ", name:" + name + "}";
}
}
exemplo no form jsp:
<div>
<form:select path="categories" items="${listCategory}" itemLabel="name" itemValue="id"/>
<form:errors path="categories" />
</div>
HTML gerado:
<div>
<select id="categories" name="categories" multiple="multiple">
<option value="1">cat1</option>
<option value="2">cat2</option>
<option value="3">cat3</option>
</select>
</div>
E controller:
@RequestMapping("/transactions")
public class TransactionController {
@RequestMapping("/form")
public ModelAndView form(Transaction transaction) {
List<Category> listCategory = Arrays.asList(
new Category(1L, "cat1"),
new Category(2L, "cat2"),
new Category(3L, "cat3"));
ModelAndView modelAndView = new ModelAndView("transactions/form");
modelAndView.addObject("listCategory", listCategory);
return modelAndView;
}
@RequestMapping(method=RequestMethod.POST)
public String save(@Valid Transaction transaction, BindingResult result, RedirectAttributes redirectAttributes) {
System.out.println(transaction);
return "redirect:/transactions");
}
...
}
Dessa forma quando enviamos os dados veja a saída: Transaction{id:null, name:Teste, categories: []}
. Fica vazio o conjunto de categorias, mesmo quando enviamos. Isso acontece por uma coisa até simples. Se repararmos no html gerado para o form veremos que o name do select é categories
se referindo ao Set de Category, mas os valores enviados são valores numéricos enviados como sempre em String - no caso de múltipla seleção, String[]. Acontece que, como o Spring vai saber onde vai cair esse número ? Uma categoria possui inúmeras propriedades, ele não vai saber que tem que chamar o setId dela.
Poderíamos pensar então em path="categories.id"
, mas aí temos erro, o Spring reclama. Afinal, um set não tem um getId ou setId internamente.
Poderíamos ir mais longe e pensar em path="categories[0].id
. Aqui sim, "funciona", mas não pra um Set, que é uma estrutura não indexada, e sim pra uma List ou um array. Mas teríamos outro problema, o dado sempre recairia sobre a posição zero, anulando a multiseleção.
Aqui não temos jeito. Ou implementamos um converter na mão, ou trabalhamos com um conjunto de algo mais simples, como um String, ou mesmo Long, pra guardar os ids.
private Set<Long> categories = new HashSet<>();
...
public Set<Long> getCategories() {
return categories;
}
public void setCategories(Set<Long> categories) {
this.categories = categories;
}
Aqui já teríamos a seguinte saída: Transaction{id:null, name:Teste, categories: [2, 3]}
. Melhorou!
Mas com certeza você não quer trabalhar com Set<Long>
no seu objeto de domínio, afinal voce precisa de Category na Transaction. Daí a ideia de ter um objeto especialista que representa exatamente o que vem através do formulário. O formulário HTML nos manda array de String ou possivelmente Long ? Então recebemos array/list/set de Long nesse objeto:
Vejamos:
@Controller
@RequestMapping("/transactions")
public class TransactionController {
@RequestMapping("/form")
public ModelAndView form(TransactionForm transactionForm) {
List<Category> listCategory = Arrays.asList(
new Category(1L, "cat1"),
new Category(2L, "cat2"),
new Category(3L, "cat3"));
ModelAndView modelAndView = new ModelAndView("transactions/form");
modelAndView.addObject("listCategory", listCategory);
return modelAndView;
}
@RequestMapping(method=RequestMethod.POST)
public String save(@Valid TransactionForm transactionForm, BindingResult result, RedirectAttributes redirectAttributes) {
Transaction transaction = transactionForm.build();
System.out.println(transaction);
return "redirect:/transactions";
}
...
}
<div>
<form:select path="categoryIds" items="${listCategory}" itemLabel="name" itemValue="id" style="width: 100px;"/>
<form:errors path="categoryIds" />
</div>
E a classe que representa o form ficaria:
public class TransactionForm {
private String name;
private Set<Long> categoryIds = new HashSet<>();
// getName() e setName()
public Set<Long> getCategoryIds() {
return categoryIds;
}
public void setCategoryIds(Set<Long> categoryIds) {
this.categoryIds = categoryIds;
}
public Transaction build() {
Set<Category> categories = categoryIds.stream()
.map(id -> new Category(id, ""))
.collect(Collectors.toSet());
Transaction transaction = new Transaction();
transaction.setName(this.name);
transaction.setCategories(categories);
return transaction;
}
}
E a saída agora: Transaction{id:null, name:Novo Teste, categories: [Category {id:1, name:}, Category {id:2, name:}]}
=)
Essa abordagem é legal pois te ajuda a desacoplar seu formulário do seu objeto de domínio. Quando nossa entidade é complexa, é muito comum ter um form que não corresponda exatamente às suas definições. Sua entidade por exemplo tem mais informações que o form. A transaction que você recebia no controller tinha estado inconsistente em algumas informações. O que aconteceria se eu invocasse getTraBalances()
ou getCompany()
e tentasse pegar o nome da company ao usar a referência no controller ? =/
Como agora temos um objeto totalmente fiel ao que o form enviou, podemos utilizar sua referência com tranquilidade e nele planejar uma transição - build()
- para nossos objetos de domínio, podendo ainda ter uma lógica mais complexa pra isso.
Espero ter ajudado no pensamento. Abraços!