Spring MVC+Thymeleaf实现Vendor与Address一对多的动态表单绑定
嘿,这个场景我熟!咱们一步步来实现Vendor和Address的动态表单绑定,确保Spring MVC能正确接收你动态添加的地址数据。
第一步:完善JPA实体类的一对多映射
首先得把你的实体类补上JPA注解,维护好双向关联关系,不然持久化的时候会出问题:
Vendor类:
import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity public class Vendor { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; // 配置一对多关联,cascade确保地址随供应商一起保存/删除 @OneToMany(mappedBy = "vendor", cascade = CascadeType.ALL, orphanRemoval = true) // 初始化集合,避免页面渲染时空指针 private List<Address> addresses = new ArrayList<>(); // getter和setter // 新增方法维护双向关联,添加地址时自动设置供应商 public void addAddress(Address address) { addresses.add(address); address.setVendor(this); } }
Address类:
import javax.persistence.*; @Entity public class Address { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String addressline1; private String addressline2; // 配置多对一关联,关联到Vendor的主键 @ManyToOne @JoinColumn(name = "vendor_id") private Vendor vendor; // getter和setter }
第二步:配置Spring MVC控制器
控制器要做两件事:初始化表单对象(带一个空地址,让页面有初始输入框),以及处理表单提交:
import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @Controller public class VendorController { // 展示供应商表单页面 @GetMapping("/vendor/add") public String showVendorForm(Model model) { Vendor vendor = new Vendor(); // 初始化一个空地址,确保页面加载时至少有一个地址输入框 vendor.addAddress(new Address()); model.addAttribute("vendor", vendor); return "vendor-form"; } // 处理表单提交,Spring会自动绑定Vendor和关联的Addresses @PostMapping("/vendor/save") public String saveVendor(Vendor vendor) { // 这里调用你的Service层保存供应商,JPA会自动保存关联的地址 // vendorService.save(vendor); return "redirect:/vendor/list"; // 保存后跳转到供应商列表页 } }
第三步:实现Thymeleaf动态表单页面
页面需要渲染初始地址,支持点击按钮添加新地址,还要能删除地址,并且所有字段的命名要符合Spring MVC的集合绑定规则:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Add Vendor with Addresses</title> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> </head> <body> <h1>Add New Vendor</h1> <form th:action="@{/vendor/save}" th:object="${vendor}" method="post"> <!-- 供应商基本信息 --> <div style="margin: 10px 0;"> <label>Vendor Name:</label> <input type="text" th:field="*{name}" required/> </div> <!-- 地址容器,用来存放所有地址表单 --> <div id="addresses-container" style="margin: 20px 0;"> <!-- 遍历已有的地址(包括初始化的空地址) --> <div th:each="address, stat : *{addresses}" class="address-form" style="border: 1px solid #ccc; padding: 10px; margin: 10px 0;"> <h3>Address [[${stat.index + 1}]]</h3> <!-- 隐藏的ID字段,更新时会用到 --> <input type="hidden" th:field="*{addresses[__${stat.index}__].id}"/> <div style="margin: 5px 0;"> <label>Address Line 1:</label> <input type="text" th:field="*{addresses[__${stat.index}__].addressline1}"/> </div> <div style="margin: 5px 0;"> <label>Address Line 2:</label> <input type="text" th:field="*{addresses[__${stat.index}__].addressline2}"/> </div> <!-- 第一个地址不显示删除按钮 --> <button type="button" th:if="${stat.index > 0}" class="remove-address-btn" style="margin-top: 10px;">Remove</button> </div> </div> <!-- 添加新地址按钮 --> <button type="button" id="add-address-btn" style="margin: 10px 0;">Add New Address</button> <!-- 提交按钮 --> <button type="submit">Save Vendor</button> </form> <script> $(document).ready(function() { // 点击按钮添加新地址 $("#add-address-btn").click(function() { var addressCount = $(".address-form").length; // 构建新的地址表单HTML,注意name属性格式必须是addresses[索引].字段名 var newAddressHtml = ` <div class="address-form" style="border: 1px solid #ccc; padding: 10px; margin: 10px 0;"> <h3>Address ${addressCount + 1}</h3> <input type="hidden" name="addresses[${addressCount}].id"/> <div style="margin: 5px 0;"> <label>Address Line 1:</label> <input type="text" name="addresses[${addressCount}].addressline1"/> </div> <div style="margin: 5px 0;"> <label>Address Line 2:</label> <input type="text" name="addresses[${addressCount}].addressline2"/> </div> <button type="button" class="remove-address-btn" style="margin-top: 10px;">Remove</button> </div> `; $("#addresses-container").append(newAddressHtml); }); // 删除地址并重新调整索引,确保Spring能正确绑定 $(document).on("click", ".remove-address-btn", function() { $(this).closest(".address-form").remove(); // 重新设置所有地址表单的索引和标题 $(".address-form").each(function(index) { $(this).find("input[name^='addresses']").each(function() { var oldName = $(this).attr("name"); var newName = oldName.replace(/addresses\[\d+\]/, `addresses[${index}]`); $(this).attr("name", newName); }); $(this).find("h3").text(`Address ${index + 1}`); }); }); }); </script> </body> </html>
关键注意点:
- 实体类里一定要初始化
List<Address>,避免页面渲染时抛出空指针异常 - Thymeleaf中用
__${stat.index}__来处理集合索引,这是Thymeleaf的特殊语法,用来正确解析表达式 - 动态生成的表单字段name属性必须严格遵循
addresses[索引].字段名,Spring MVC才能自动把数据绑定到Vendor的addresses集合中 - 删除地址后要重新调整所有地址字段的索引,否则会出现索引断层,导致Spring无法正确绑定所有地址数据
内容的提问来源于stack exchange,提问作者Vishal Parashar
相关产品推荐
相关产品推荐

