Flask 实战:35、在视图函数里操作数据库

本贴最后更新于 1677 天前,其中的信息可能已经事过境迁

在视图函数里操作数据库的方式和我们在 Python Shell 中的练习大致相同,只不过需要一些额外的工作。比如把查询结果作为参数传入模板渲染出来,或是获取表单的字段值作为提交到数据库的数据。在这一节,我们将把上一节学习的所有数据库操作知识运用到一个简单的笔记程序中。这个程序可以让你创建、编辑和删除笔记,并在主页列出所有保存后的笔记。

1.Create

为了支持输入笔记内容,我们先创建一个用于填写新笔记的表单,如下所示:


from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired

class NewNoteForm(FlaskForm):
    body = TextAreaField('Body', validators=[DataRequired()])
    submit = SubmitField('Save')

我们创建一个 new_note 视图,这个视图负责渲染创建笔记的模板,并处理表单的提交:


@app.route('/new', methods=['GET', 'POST'])
def new_note():
    form = NewNoteForm()
    if form.validate_on_submit():
        body = form.body.data
        note = Note(body=body)
        db.session.add(note)
        db.session.commit()
        flash('Your note is saved.')
        return redirect(url_for('index'))
    return render_template('new_note.html', form=form)

我们先来看看 form.validate_on_submit()返回 True 时的处理代码。当表单被提交且通过验证时,我们获取表单 body 字段的数据,然后创建新的 Note 实例,将表单中 body 字段的值作为 body 参数传入,最后添加到数据库会话中并提交会话。这个过程接收用户通过表单提交的数据并保存到数据库中,最后我们使用 flash()函数发送提示消息并重定向到 index 视图。

表单在 new_note.html 模板中渲染,这里使用我们的 form_field 宏渲染表单字段,传入 rows 和 cols 参数来定制 <textarea> 输入框的大小:


{% block content %}
<h2>New Note</h2>
<form method="post">
    {{ form.csrf_token }}
    {{ form_field(form.body, rows=5, cols=50) }}
    {{ form.submit }}
</form>
{% endblock %}

index 视图用来显示主页,目前它的所有作用就是渲染主页对应的模板:


@app.route('/')
def index():
    return render_template('index.html')

在对应的 index.html 模板中,我们添加一个指向创建新笔记页面的链接:

<h1>Notebook</h1>
<a href="{{ url_for('new_note') }}">New Note</a>

2.Read

在上一节我们为程序实现了添加新笔记的功能,当你在创建笔记的页面单击保存后,程序会重定向到主页,提示的消息告诉你刚刚提交的笔记已经成功保存了,可是你却无法看到创建后的笔记。为了在主页列出所有保存的笔记,我们需要修改 index 视图,修改后的 index 视图如代码清所示。

@app.route('/')
def index():
    notes = Note.query.all()
    return render_template('index.html', notes=notes, form=form)

在新的 index 视图里,我们像在 Python Shell 中一样使用 Note.query.all()查询所有 note 记录,然后把这个包含所有记录的列表作为 notes 变量传入模板。你已经猜到下一步了,没错,我们将在模板中将笔记们显示出来,如代码清单所示。


<h1>Notebook</h1>
<a href="{{ url_for('new_note') }}">New Note</a>
<h4>{{ notes|length }} notes:</h4>
{% for note in notes %}
    <div class="note">
        <p>{{ note.body }}</p>
    </div>
{% endfor %}

在模板中,我们迭代这个 notes 列表,调用 Note 对象的 body 属性(note.body)获取 body 字段的值。另外,我们还通过 length 过滤器获取笔记的数量。渲染后的示例如图 5-1 所示。
image.png

图 5-1 显示笔记列表

3.Update

更新一条笔记和创建一条新笔记的实现代码几乎完全相同,首先是编辑笔记的表单:


class EditNoteForm(FlaskForm):
    body = TextAreaField('Body', validators=[DataRequired()])
    submit = SubmitField('Update')

你会发现这和创建新笔记 NewNoteForm 唯一的不同就是提交字段的标签参数(作为的 value 属性),因此这个表单的定义也可以通过继承来简化:

class EditNoteForm(NewNoteForm):
    submit = SubmitField('Update')

用来渲染更新笔记页面和处理更新表单提交的 edit_note 视图如代码清单所示。


@app.route('/edit/<int:note_id>', methods=['GET', 'POST'])
def edit_note(note_id):
    form = EditNoteForm()
    note = Note.query.get(note_id)
    if form.validate_on_submit():
        note.body = form.body.data
        db.session.commit()
        flash('Your note is updated.')
        return redirect(url_for('index'))
    form.body.data = note.body
    return render_template('edit_note.html', form=form)

这个视图通过 URL 变量 note_id 获取要被修改的笔记的主键值(id 字段),然后我们就可以使用 get()方法获取对应的 Note 实例。当表单被提交且通过验证时,我们将表单中 body 字段的值赋给 note 对象的 body 属性,然后提交数据库会话,这样就完成了更新操作。和创建笔记相同,我们接着发送提示消息并重定向到 index 视图。

唯一需要注意的是,在 GET 请求的执行流程中,我们添加了下面这行代码:

form.body.data = note.body

因为要添加修改笔记内容的功能,那么当我们打开修改某个笔记的页面时,这个页面的表单中必然要包含笔记原有的内容。

如果手动创建 HTML 表单,那么你可以通过将 note 记录传入模板,然后手动为对应字段中填入笔记的原有内容,比如:


<textarea name="body">{{ note.body }}</textarea>

其他 input 元素则通过 value 属性来设置输入框中的值,比如:


<input name="foo" type="text" value="{{ note.title }}">

使用 WTForms 可以省略这些步骤,当我们渲染表单字段时,如果表单字段的 data 属性不为空,WTForms 会自动把 data 属性的值添加到表单字段的 value 属性中,作为表单的值填充进去,我们不用手动为 value 属性赋值。因此,将存储笔记原有内容的 note.body 属性赋值给表单 body 字段的 data 属性即可在页面上的表单中填入原有的内容。

模板的内容基本相同,这里不再赘述。最后的工作是在主页笔记列表中的每个笔记内容下添加一个编辑按钮,用来访问编辑页面:


{% for note in notes %}
<div class="note">
    <p>{{ note.body }}</p>
    <a class="btn"  href="{{ url_for('edit_note', note_id=note.id) }}">Edit</a>
</div>
{% endfor %}

生成 edit_note 视图的 URL 时,我们传入当前 note 对象的 id(note.id)作为 URL 变量 note_id 的值。

4.Delete

在程序中,删除的实现也非常简单,不过这里经常会有一个误区。大多数人通常会考虑在笔记内容下添加一个删除链接:

<a href="{{ url_for('delete_note', note_id=note.id) }}">Delete</a>

这个链接指向用来删除笔记的 delete_note 视图:


@app.route('/delete/<int:note_id>')
def delete_note(note_id):
    note = Note.query.get(note_id)
    db.session.delete(note)
    db.session.commit()
    flash('Your note is deleted.')
    return redirect(url_for('index'))

虽然这一切看起来都很合理,但这种处理方式实际上会使程序处于 CSRF 攻击的风险之中。我们在第 2 章曾强调过,防范 CSRF 攻击的基本原则就是正确使用 GET 和 POST 方法。像删除这类修改数据的操作绝对不能通过 GET 请求实现,正确的做法是为删除操作创建一个表单,如下所示:

class DeleteNoteForm(FlaskForm):
    submit = SubmitField('Delete')

这个表单类只有一个提交字段,因为我们只需要在页面上显示一个删除按钮来提交表单。删除表单的提交请求由 delete_note 视图处理,如代码清单所示。

@app.route('/delete/<int:note_id>', methods=['POST'])
def delete_note(note_id):
    form = DeleteForm()
    if form.validate_on_submit():
        note = Note.query.get(note_id)  # 获取对应记录
        db.session.delete(note)  # 删除记录
        db.session.commit()  # 提交修改
        flash('Your note is deleted.')
 else:
        abort(400)
    return redirect(url_for('index'))

在 delete_note 视图的 app.route()中,methods 列表仅填入了 POST,这会确保该视图仅监听 POST 请求。

和编辑笔记的视图类似,这个视图接收 note_id(主键值)作为参数。如果提交表单且通过验证(唯一需要被验证的是 CSRF 令牌),就使用 get()方法查询对应的记录,然后调用 db.session.delete()方法删除并提交数据库会话。如果验证出错则使用 abort()函数返回 400 错误响应。

因为删除按钮要在主页的笔记内容下添加,我们需要在 index 视图中实例化 DeleteNote-Form 类,然后传入模板。在 index.html 模板中,我们渲染这个表单:


{% for note in notes %}
<div class="note">
    <p>{{ note.body }}</p>
    <a class='btn' href="{{ url_for('edit_note', note_id=note.id) }}">Edit</a>
    <form method="post" action="{{ url_for('delete_note', note_id=note.id) }}">
        {{ form.csrf_token }}
        {{ form.submit(class='btn') }}
    </form>
</div>
{% endfor %}

我们将表单的 action 属性设置为删除当前笔记的 URL。构建 URL 时,URL 变量 note_id 的值通过 note.id 属性获取,当单击提交按钮时,会将请求发送到 action 属性中的 URL。添加删除表单的主要目的就是防止 CSRF 攻击,所以不要忘记渲染 CSRF 令牌字段 form.csrf_token。

在 HTML 中,

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...