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

如何在Flask表单中提交同一类多对象并存储到SQLAlchemy数据库?

Flask 动态购物清单实现方案

1. 数据库模型定义

先创建SQLAlchemy模型,对应购物清单条目:

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class ShoppingItem(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    cost = db.Column(db.Float, nullable=False)
    amount = db.Column(db.Integer, nullable=False)
    sell_by = db.Column(db.Date, nullable=False)

2. WTForms 表单设计

用FieldList+FormField实现多条目表单,完全适配你的场景:

from flask_wtf import FlaskForm
from wtforms import StringField, FloatField, IntegerField, DateField, FormField, FieldList, SubmitField
from wtforms.validators import DataRequired, NumberRange

# 单个购物条目表单
class ItemForm(FlaskForm):
    name = StringField(validators=[DataRequired()])
    cost = FloatField(validators=[DataRequired(), NumberRange(min=0)])
    amount = IntegerField(validators=[DataRequired(), NumberRange(min=1)])
    sell_by = DateField(format='%Y-%m-%d', validators=[DataRequired()])

# 包含多个条目清单的表单
class ShoppingListForm(FlaskForm):
    items = FieldList(FormField(ItemForm), min_entries=1)  # 初始显示1行
    submit = SubmitField('Save Shopping List')

3. Flask 后端视图

处理表单提交、数据保存和页面渲染:

from flask import Flask, render_template, redirect, url_for
from datetime import datetime

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///shopping_list.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db.init_app(app)

# 初始化数据库
with app.app_context():
    db.create_all()

@app.route('/', methods=['GET', 'POST'])
def index():
    form = ShoppingListForm()
    if form.validate_on_submit():
        # 遍历表单提交的条目,转换为SQLAlchemy对象并保存
        for item_data in form.items.data:
            sell_by_date = datetime.strptime(item_data['sell_by'], '%Y-%m-%d').date()
            new_item = ShoppingItem(
                name=item_data['name'],
                cost=item_data['cost'],
                amount=item_data['amount'],
                sell_by=sell_by_date
            )
            db.session.add(new_item)
        db.session.commit()
        return redirect(url_for('index'))
    return render_template('index.html', form=form)

# 查看已保存的购物清单
@app.route('/view')
def view_list():
    items = ShoppingItem.query.all()
    return render_template('view.html', items=items)

4. 前端模板实现

表单页面(templates/index.html)

包含动态添加/移除条目的功能,严格遵循WTForms命名规则:

<!DOCTYPE html>
<html>
<head>
    <title>Shopping List</title>
    <style>
        .s-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        .s-table th, .s-table td { border: 1px solid #ddd; padding: 8px; text-align: center; }
        .s-table th { background-color: #f2f2f2; }
        .btn { padding: 8px 16px; border: none; cursor: pointer; color: white; }
        .add-btn { background-color: #4CAF50; }
        .submit-btn { background-color: #008CBA; margin-top: 10px; }
        .remove-btn { background-color: #f44336; }
        .form-control { width: 90%; padding: 4px; }
    </style>
</head>
<body>
    <h1>Create Shopping List</h1>
    <form method="POST">
        {{ form.hidden_tag() }}
        <table class="s-table">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Cost</th>
                    <th>Amount</th>
                    <th>Sell-By</th>
                    <th>Action</th>
                </tr>
            </thead>
            <tbody id="items-container">
                {% for item_form in form.items %}
                <tr class="item-row">
                    <td>{{ item_form.name(class="form-control") }}</td>
                    <td>{{ item_form.cost(class="form-control", step="0.01") }}</td>
                    <td>{{ item_form.amount(class="form-control") }}</td>
                    <td>{{ item_form.sell_by(class="form-control") }}</td>
                    <td><button type="button" class="btn remove-btn" onclick="removeRow(this)">Remove</button></td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
        <button type="button" class="btn add-btn" onclick="addItemRow()">Add Item</button>
        <br>
        {{ form.submit(class="btn submit-btn") }}
    </form>
    <a href="{{ url_for('view_list') }}">View Saved List</a>

    <script>
        function addItemRow() {
            const container = document.getElementById('items-container');
            const rowIndex = container.querySelectorAll('.item-row').length;
            const newRow = document.createElement('tr');
            newRow.className = 'item-row';
            // 严格按照WTForms命名规则生成name属性:items-[index]-字段名
            newRow.innerHTML = `
                <td><input type="text" name="items-${rowIndex}-name" class="form-control" required></td>
                <td><input type="number" step="0.01" name="items-${rowIndex}-cost" class="form-control" min="0" required></td>
                <td><input type="number" name="items-${rowIndex}-amount" class="form-control" min="1" required></td>
                <td><input type="date" name="items-${rowIndex}-sell_by" class="form-control" required></td>
                <td><button type="button" class="btn remove-btn" onclick="removeRow(this)">Remove</button></td>
            `;
            container.appendChild(newRow);
        }

        function removeRow(button) {
            const row = button.closest('.item-row');
            row.remove();
            // 移除后重新索引所有行的name属性,避免表单解析失败
            document.querySelectorAll('.item-row').forEach((row, idx) => {
                const inputs = row.querySelectorAll('input');
                inputs[0].name = `items-${idx}-name`;
                inputs[1].name = `items-${idx}-cost`;
                inputs[2].name = `items-${idx}-amount`;
                inputs[3].name = `items-${idx}-sell_by`;
            });
        }
    </script>
</body>
</html>

清单查看页面(templates/view.html)

展示已保存的购物条目:

<!DOCTYPE html>
<html>
<head>
    <title>Saved Shopping List</title>
    <style>
        .s-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
        .s-table th, .s-table td { border: 1px solid #ddd; padding: 8px; text-align: center; }
        .s-table th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h1>Saved Shopping List</h1>
    <table class="s-table">
        <thead>
            <tr>
                <th>Name</th>
                <th>Cost</th>
                <th>Amount</th>
                <th>Sell-By</th>
            </tr>
        </thead>
        <tbody>
            {% for item in items %}
            <tr>
                <td>{{ item.name }}</td>
                <td>{{ item.cost }}</td>
                <td>{{ item.amount }}</td>
                <td>{{ item.sell_by.strftime('%Y-%m-%d') }}</td>
            </tr>
            {% endfor %}
        </tbody>
    </table>
    <a href="{{ url_for('index') }}">Back to Create List</a>
</body>
</html>

关键说明

  • FieldList+FormField是处理多条目表单的标准方案,min_entries=1确保页面初始显示一行输入框。
  • 动态添加行时,必须严格遵循WTForms的命名格式:字段列表名-索引-字段名,否则表单无法正确解析提交的数据。
  • 移除行后重新索引所有输入框的name属性,避免出现索引断层导致表单验证失败。
  • 提交表单后,遍历form.items.data即可获取所有条目数据,转换为SQLAlchemy对象后批量保存到数据库。

内容的提问来源于stack exchange,提问作者Jacob Hancock

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 02:54:22