You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:42:53