cutelyst教程_04 _基本CRUD

Tutorial_04_BasicCRUD

教程_04 _基本CRUD

Daniel Nicoletti edited this page on Oct 30, 2018 · 15 revisions

​Daniel Nicoletti于2018年10月30日编辑了本页

OVERVIEW

总览

  1. Introduction
  2. Cutelyst Basics
  3. More Cutelyst Basics
  4. Basic CRUD
  5. Authentication

INTRODUCTION

说明

This chapter of the tutorial builds on the fairly primitive application created in Chapter 3 to add basic support for Create, Read, Update, and Delete (CRUD) of Book objects. Note that the 'list' function in Chapter 3 already implements the Read portion of CRUD (although Read normally refers to reading a single object; you could implement full Read functionality using the techniques introduced below). This section will focus on the Create and Delete aspects of CRUD. More advanced capabilities, including full Update functionality, will be addressed in Chapter 9.

本教程的这一章以第3章中创建的相当原始的应用程序为基础,添加了对图书对象的创建、读取、更新和删除(CRUD)的基本支持。请注意,第3章中的“list”函数已经实现了CRUD的读取部分(尽管读取通常指读取单个对象;您可以使用下面介绍的技术实现完整的读取功能)。本节将重点介绍CRUD的创建和删除方面。更多高级功能,包括完整的更新功能,将在第9章中介绍。

FORMLESS SUBMISSION

无格式提交

Our initial attempt at object creation will utilize the "URL arguments" feature of Cutelyst (we will employ the more common form-based submission in the sections that follow).

我们最初创建对象的尝试将利用Cutelyst的“URL参数”功能(我们将在接下来的部分中使用更常见的基于表单的提交)。

Include a Create Action in the Books Controller

在Books控制器中包括创建操作

Edit src/books.h and enter the following method:

编辑src/books.h并输入以下方法:

public:
    ...
    /**
     * Create a book with the supplied title, rating, and author
     */
    C_ATTR(url_create, :Local :Args(3))
    void url_create(Context *c, const QString &title, const QString &rating, const QString &authorId);

And in src/books.cpp:

在src/books.cpp中:

void Books::url_create(Context *c, const QString &title, const QString &rating, const QString &authorId)
{
    // In addition to context, get the title, rating, &
    // author_id args from the URL. Note that Cutelyst automatically
    // puts extra information after the "/<controller_name>/<action_name/"
    // as QStrings.  The args are separated  by the '/' char on the URL.

    // Insert the book into it's table
    QSqlQuery query = CPreparedSqlQueryThreadForDB("INSERT INTO book (title, rating) VALUES (:title, :rating)", "MyDB");
    query.bindValue(":title", title);
    query.bindValue(":rating", rating);
    int bookId = 0;
    bool error = true;
    if (query.exec()) {
        bookId = query.lastInsertId().toInt();

        query = CPreparedSqlQueryThreadForDB("INSERT INTO book_author (book_id, author_id) VALUES (:book_id, :author_id)", "MyDB");
        query.bindValue(":book_id", bookId);
        query.bindValue(":author_id", authorId);
        if (query.exec()) {
            error = false;
        }
    }

    // On error show the last one
    if (error) {
        c->stash()["error_msg"] = query.lastError().text();
    }

    QVariantHash book;
    book["title"] = title;
    book["rating"] = rating;

    // Assign the Book object to the stash for display and set template
    c->stash({
                   {"book", book},
                   {"template", "books/create_done.html"}
               });

    // Disable caching for this page
    c->response()->setHeader("Cache-Control", "no-cache");
}

Notice that Cutelyst takes "extra slash-separated information" from the URL and passes it as arguments in (as long as the number of arguments is not "fixed" using an attribute like :Args(0)). The url_create action then makes a SQL insert to add the requested information to the database (with a separate call to update the join table). As do virtually all controller methods (at least the ones that directly handle user input), it then sets the template that should handle this request.

请注意,Cutelyst从URL中获取“额外的斜杠分隔信息”,并将其作为参数传入(只要参数的数量不是使用:Args(0)这样的属性“固定的”)。然后,url_create操作进行SQL插入,将请求的信息添加到数据库中(通过一个单独的调用来更新联接表)。就像几乎所有的控制器方法(至少是那些直接处理用户输入的方法)一样,它会设置应该处理这个请求的模板。

Also note that we are explicitly setting a no-cache "Cache-Control" header to force browsers using the page to get a fresh copy every time. You could even move this to a Auto method in src/Root.cpp and it would automatically get applied to every page in the whole application via a single line of code (remember from Chapter 3, that every auto method gets run in the Controller hierarchy).

还请注意,我们显式地设置了一个无缓存“Cache-Control”头,以强制使用该页面的浏览器每次都获取一个新副本。甚至可以在src/Root.cpp中将其移动到自动方法。它将通过一行代码自动应用于整个应用程序中的每个页面(请记住,从第3章开始,每个自动方法都会在控制器层次结构中运行)。

Include a Template for the 'url_create' Action:

包括“url_create”操作的模板:

Edit root/src/books/create_done.html and then enter:

编辑root/src/books/create_done.html,然后输入:

{% comment %} Output information about the record that was added.  First title.{% endcomment %}
<p>Added book '{{ book.title }}'.</p>

<p><a href="/books/list">Return to list</a></p>

Try the 'url_create' Feature

试试“url_create”功能

Make sure the development server is running with the "-r" restart option and that you rebuild the application with make.

确保开发服务器正在使用“-r”重启选项运行,并且使用Make重新构建应用程序。

Note that new path for /books/url_create appears in the startup debug output.

请注意,/books/url_create的新路径出现在启动调试输出中。

Next, use your browser to enter the following URL:

接下来,使用浏览器输入以下URL:

http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4

Your browser should display "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5.".

你的浏览器应该会显示“Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5.”。

If you then click the "Return to list" link, you should find that there are now six books shown (if necessary, Shift+Reload or Ctrl+Reload your browser at the /books/list page).

如果单击“返回列表”链接,您会发现现在显示了六本书(如有必要,请在/books/list页面按住Shift键并重新加载或按住Ctrl键并重新加载浏览器)。

CONVERT TO A CHAINED ACTION

转化为链式动作

Although the example above uses the same Local action type for the method that we saw in the previous chapter of the tutorial, there is an alternate approach that allows us to be more specific while also paving the way for more advanced capabilities. Change the method declaration for url_create in src/books.h you entered above to match the following:

尽管上面的示例使用了与我们在本教程前一章中看到的方法相同的本地操作类型,但有一种替代方法允许我们更加具体,同时也为更高级的功能铺平了道路。更改src/books.h中url_create的方法声明。您在上面输入以匹配以下内容:

-    C_ATTR(url_create, :Local :Args(3))
+    C_ATTR(url_create, :Chained("/") :PathPart("books/url_create") :Args(3))

This converts the method to take advantage of the Chained action/dispatch type. Chaining lets you have a single URL automatically dispatch to several controller methods, each of which can have precise control over the number of arguments that it will receive. A chain can essentially be thought of having three parts -- a beginning, a middle, and an end. The bullets below summarize the key points behind each of these parts of a chain:

这将转换方法以利用链式操作/分派类型。通过链接,可以将一个URL自动分派给多个控制器方法,每个方法都可以精确控制它将接收的参数数量。链基本上可以被认为有三个部分——开始、中间和结束。下面的项目符号总结了链中每个部分背后的关键点:

  • Beginning
  • 开始
  • Use :Chained("/") to start a chain
  • 使用:Chained(“/”)启动链
  • Get arguments through CaptureArgs() or AutoCaptureArgs
  • 通过CaptureArgs()或AutoCaptureArgs获取参数
  • Specify the path to match with PathPart()
  • 指定与PathPart()匹配的路径
  • Middle
  • 中间
  • Link to previous part of the chain with :Chained("name")
  • 链接到链的前一部分:Chained(“名称”)
  • Get arguments through CaptureArgs() or AutoCaptureArgs
  • 通过CaptureArgs()或AutoCaptureArgs获取参数
  • Specify the path to match with PathPart()
  • 指定与PathPart()匹配的路径
  • End
  • 结束
  • Link to previous part of the chain with :Chained("name")
  • 链接到链的前一部分:Chained(“名称”)
  • Do NOT get arguments through "CaptureArgs()," use "Args()" or AutoArgs instead to end a chain
  • 不要通过“CaptureArgs()”获取参数,而是使用“Args()”或AutoArgs来结束链
  • Specify the path to match with PathPart()
  • 指定与PathPart()匹配的路径

In our url_create method above, we have combined all three parts into a single method: :Chained("/") to start the chain, :PathPart("books/url_create") to specify the base URL to match, and :Args(3) to capture exactly three arguments and to end the chain.

在上面的url_create方法中,我们将所有三个部分组合成一个方法::Chained("/") 来启动链,:PathPart("books/url_create") 来指定要匹配的基本url,以及:Args(3)来准确捕获三个参数并结束链。

As we will see shortly, a chain can consist of as many "links" as you wish, with each part capturing some arguments and doing some work along the way. We will continue to use the Chained action type in this chapter of the tutorial and explore slightly more advanced capabilities with the base method and delete feature below. But Chained dispatch is capable of far more.

正如我们将很快看到的,一个链可以由您希望的任意多个“链接”组成,每个部分捕获一些参数,并在此过程中做一些工作。在本教程的这一章中,我们将继续使用链式动作类型,并使用下面的基本方法和删除功能探索更高级的功能。但链式调度的能力远不止这些。

Try the Chained Action

试试链式动作

If you look back at the application server startup logs from your initial version of the url_create method (the one using the :Local attribute), you will notice that it produced output similar to the following:

如果从url_create方法(使用:Local属性的方法)的初始版本中查看应用程序服务器启动日志,您会注意到它生成的输出类似于以下内容:

Loaded Path actions:
.-----------------------+-------------------.
| Path                  | Private           |
.-----------------------+-------------------.
| /...                  | /defaultPage      |
| /                     | /index            |
| /books                | /books/index      |
| /books/list/...       | /books/list       |
| /books/url_create/... | /books/url_create |
.-----------------------+-------------------.

When the application server restarts after our conversion to Chained dispatch, the debug output should change to something along the lines of the following:

当应用程序服务器在转换为链式分派后重新启动时,调试输出应更改为以下内容:

Loaded Path actions:
.-----------------+--------------.
| Path            | Private      |
.-----------------+--------------.
| /...            | /defaultPage |
| /               | /index       |
| /books          | /books/index |
| /books/list/... | /books/list  |
.-----------------+--------------.

Loaded Chained actions:
.-------------------------+-------------------.
| Path Spec               | Private           |
.-------------------------+-------------------.
| /books/url_create/*/*/* | /books/url_create |
.-------------------------+-------------------.

url_create has disappeared from the "Loaded Path actions" section but it now shows up under the newly created "Loaded Chained actions" section. And the "///*" portion clearly shows our requirement for three arguments.

url_create已从“加载的路径操作”部分消失,但它现在显示在新创建的“加载的链接操作”部分下。“//*”部分清楚地显示了我们对三个参数的要求。

As with our non-chained version of url_create, use your browser to enter the following URL:

与我们的url_create的非链接版本一样,使用浏览器输入以下url:

http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4

You should see the same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5.". Click the "Return to list" link, and you should find that there are now seven books shown (two copies of TCPIP_Illustrated_Vol-2).

你应该会看到同样的“Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5.”。点击“返回列表”链接,你会发现现在显示了七本书(两本TCPIP_Illustrated_Vol-2)。

Refactor to Use a 'base' Method to Start the Chains

重构以使用“base”方法启动链

Let's make a quick update to our initial Chained action to show a little more of the power of chaining. First, open src/books.h in your editor and add the following method:

让我们快速更新一下我们最初的链接操作,以展示更多的链接功能。首先,打开src/books.h,并添加以下方法:

class Books
{
public:
    /**
     * Can place common logic to start chained dispatch here
     */
    C_ATTR(base, :Chained("/") :PathPart("books") :CaptureArgs(0))
    void base(Context *c);
}

And on src/books.cpp:

在src/books.cpp上:

void Books::base(Context *c)
{
    // Print a message to the debug log
    qDebug("*** INSIDE BASE METHOD ***");
}

Here we print a log message. If your controller always needs a book ID as its first argument, you could have the base method capture that argument (with :CaptureArgs(1)) and use it to pull the book object and leave it in the stash for later parts of your chains to then act upon. Because we have several actions that don't need to retrieve a book (such as the url_create we are working with now), we will instead add that functionality to a common object action shortly.

这里我们打印一条日志消息。如果控制器总是需要一个book ID作为其第一个参数,那么可以让base方法捕获该参数(使用:CaptureArgs(1)),并使用它来拉取book对象,并将其保留在存储库中,以便链的后续部分采取行动。因为我们有几个不需要检索书籍的操作(比如我们现在正在使用的url_create),我们将很快将该功能添加到一个公共对象操作中。

As for url_create, let's modify it to first dispatch to base. Open up src/books.h and edit the declaration for url_create to match the following:

至于url_create,我们将其修改为首先分派到base。打开src/books.h并编辑url_create的声明,以匹配以下内容:

-    C_ATTR(url_create, :Chained("/") :PathPart("books/url_create") :Args(3))
+    C_ATTR(url_create, :Chained("base") :PathPart("url_create") :Args(3))

Once you rebuild your application, notice that the development server will restart and our "Loaded Chained actions" section will changed slightly:

重新构建应用程序后,请注意,开发服务器将重新启动,我们的“已加载的链接操作”部分将略有更改:

Loaded Chained actions:
.-------------------------+-----------------------.
| Path Spec               | Private               |
.-------------------------+-----------------------.
| /books/url_create/*/*/* | /books/base (0)       |
|                         | => /books/url_create  |
.-------------------------+-----------------------.

The "Path Spec" is the same, but now it maps to two Private actions as we would expect. The base method is being triggered by the /books part of the URL. However, the processing then continues to the url_create method because this method "chained" off base and specified :PathPart("url_create") (note that we could have omitted the "PathPart" here because it matches the name of the method, but we will include it to make the logic as explicit as possible).

“Path Spec”是相同的,但现在它映射到两个我们预期的私有操作。base方法由URL的/books部分触发。然而,处理将继续进行到url_create方法,因为该方法“链接”在base之外,并指定:PathPart(“url_create”)(请注意,我们可以在这里省略“PathPart”,因为它与方法的名称匹配,但我们将包含它以使逻辑尽可能明确)。

Once again, enter the following URL into your browser:

再次在浏览器中输入以下URL:

http://localhost:3000/books/url_create/TCPIP_Illustrated_Vol-2/5/4

The same "Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5." message and a dump of the new book object should appear. Also notice the extra "INSIDE BASE METHOD" debug message in the development server output from the base method. Click the "Return to list" link, and you should find that there are now eight books shown. (You may have a larger number of books if you repeated any of the "create" actions more than once. Don't worry about it as long as the number of books is appropriate for the number of times you added new books... there should be the original five books added via myapp01.sql plus one additional book for each time you ran one of the url_create variations above.)

同一本书“Added book 'TCPIP_Illustrated_Vol-2' by 'Stevens' with a rating of 5.”消息和新书对象的转储应该出现。还要注意base方法的开发服务器输出中额外的“INSIDE BASE METHOD”调试消息。点击“返回列表”链接,你会发现现在显示了八本书。(如果你多次重复任何“创建”操作,你可能会有更多的书。只要书的数量与你添加新书的次数相匹配,就不用担心。应该通过myapp01.sql添加原来的五本书,并且每次运行上面的url_create变体时都会添加一本书。)

MANUALLY BUILDING A CREATE FORM

手动构建创建表单

Although the url_create action in the previous step does begin to reveal the power and flexibility of Cutelyst, it's obviously not a very realistic example of how users should be expected to enter data. This section begins to address that concern (but just barely, see Chapter 9 for better options for handling web-based forms).

虽然上一步中的url_create操作确实开始揭示Cutelyst的强大和灵活性,但对于用户应该如何输入数据,这显然不是一个非常现实的例子。本节开始讨论这个问题(但几乎没有,请参阅第9章,了解处理基于web的表单的更好选项)。

Add Method to Display The Form

添加方法以显示表单

Edit src/books.h and add the following method:

编辑src/books.h并添加以下方法:

class Books : public Controller
{
public:
    ...
    /**
     * Display form to collect information for book to create
     */
    C_ATTR(form_create, :Chained("base") :PathPart("form_create") :Args(0))
    void form_create(Context *c);
};
void Books::form_create(Context *c)
{
    // Set the Grantlee Template to use
    c->setStash("template", "books/form_create.html");
}

This action simply invokes a view containing a form to create a book.

这个动作只是调用一个包含表单的视图来创建一本书。

Add a Template for the Form

为表单添加模板

Open root/src/books/form_create.html in your editor and enter:

打开root/src/books/form_create.html,在编辑器中输入:

<form method="post" action="form_create_do">
<table>
  <tr><td>Title:</td><td><input type="text" name="title"></td></tr>
  <tr><td>Rating:</td><td><input type="text" name="rating"></td></tr>
  <tr><td>Author ID:</td><td><input type="text" name="author_id"></td></tr>
</table>
<input type="submit" name="Submit" value="Submit">
</form>

Note that we have specified the target of the form data as form_create_do, the method created in the section that follows.

注意,我们已经将表单数据的目标指定为form_create_do,该方法在下面的部分中创建。

Add a Method to Process Form Values and Update Database

添加一个方法来处理表单值和更新数据库

Edit src/books.h and add the following method to save the form information to the database:

编辑src/books.h并添加以下方法将表单信息保存到数据库中:

class Books : public Controller
{
public:
    ...
    /**
     * Take information from form and add to database
     */
    C_ATTR(form_create_do, :Chained("base") :PathPart("form_create_do") :Args(0))
    void form_create_do(Context *c);
};
void Books::form_create_do(Context *c)
{
    // Retrieve the values from the form
    QString title = c->request()->bodyParam("title", "N/A");
    QString rating = c->request()->bodyParam("rating", "N/A");
    QString authorId = c->request()->bodyParam("author_id", "1");

    // Insert the book into it's table
    QSqlQuery query = CPreparedSqlQueryThreadForDB("INSERT INTO book (title, rating) VALUES (:title, :rating)", "MyDB");
    query.bindValue(":title", title);
    query.bindValue(":rating", rating);
    int bookId = 0;
    bool error = true;
    if (query.exec()) {
        bookId = query.lastInsertId().toInt();

        query = CPreparedSqlQueryThreadForDB("INSERT INTO book_author (book_id, author_id) VALUES (:book_id, :author_id)", "MyDB");
        query.bindValue(":book_id", bookId);
        query.bindValue(":author_id", authorId);
        if (query.exec()) {
            error = false;
        }
    }

    // On error show the last one
    if (error) {
        c->stash()["error_msg"] = query.lastError().text();
    }

    // Assign the Book object to the stash for display and set template
    c->stash({
                   {"book", QVariant::fromValue(c->request()->bodyParams())},
                   {"template", "books/create_done.html"}
               });
}

Test Out The Form

测试一下表单

Notice that the server startup log reflects the two new chained methods that we added:

请注意,服务器启动日志反映了我们添加的两个新链接方法:

Loaded Chained actions:
.-------------------------+--------------------------.
| Path Spec               | Private                  |
.-------------------------+--------------------------.
| /books/form_create      | /books/base (0)          |
|                         | => /books/form_create    |
| /books/form_create_do   | /books/base (0)          |
|                         | => /books/form_create_do |
| /books/url_create/*/*/* | /books/base (0)          |
|                         | => /books/url_create     |
.-------------------------+--------------------------.

Point your browser to http://localhost:3000/books/form_create and enter "TCP/IP Illustrated, Vol 3" for the title, a rating of 5, and an author ID of 4. You should then see the output of the same create_done.html template seen in earlier examples. Finally, click "Return to list" to view the full list of books.

​将浏览器指向http://localhost:3000/books/form_create然后输入“TCP/IP Illustrated, Vol 3”作为标题,评分为5,作者ID为4。然后,您应该会看到输出。和前面的示例中相同create_done.html模板。最后,单击“返回列表”查看完整的图书列表。

Note: Having the user enter the primary key ID for the author is obviously crude; we will address this concern with a drop-down list and add validation to our forms in Chapter 9.

注意:让用户为作者输入主键ID显然是粗鲁的;我们将通过下拉列表解决这个问题,并在第9章的表单中添加验证。

A SIMPLE DELETE FEATURE

一个简单的删除功能

Turning our attention to the Delete portion of CRUD, this section illustrates some basic techniques that can be used to remove information from the database.

将我们的注意力转向CRUD的删除部分,本节说明了可用于从数据库中删除信息的一些基本技术。

Include a Delete Link in the List

在列表中包括删除链接

Edit root/src/books/list.html and update it to match the following (two sections have changed):

编辑root/src/books/list.html并将其更新以匹配以下内容(两个部分已更改):

  1. the additional <th>Links</th> table header, and 2) the four lines <td>...</td> for the Delete link near the bottom):

附加的<th>链接<th>表格标题,以及2)四行<td></td>对于底部附近的删除链接): 

{% comment %}This is a Grantlee comment.{% endcomment %}

{% comment %}Some basic HTML with a loop to display books{% endcomment %}
<table>
<tr><th>Title</th><th>Rating</th><th>Author(s)</th><th>Links</th></tr>
{% comment %}Display each book in a table row{% endcomment %}
{% for book in books %}
  <tr>
    <td>{{ book.title }}</td>
    <td>{{ book.rating }}</td>
    <td></td>
    <td>
      {% comment %} Add a link to delete a book {% endcomment %}
      <a href="/books/id/{{ book.id }}/delete">Delete</a>
    </td>
  </tr>
{% endfor %}
</table>

The additional code is obviously designed to add a new column to the right side of the table with a Delete "button" (for simplicity, links will be used instead of full HTML buttons; but, in practice, anything that modifies data should be handled with a form sending a POST request).

额外的代码显然是为了在表的右侧添加一个新列,并带有一个“删除”按钮(为了简单起见,将使用链接而不是完整的HTML按钮;但是,在实践中,任何修改数据的操作都应该通过发送POST请求的表单来处理)。

Note: In practice you should never use a GET request to delete a record -- always use POST for actions that will modify data. We are doing it here for illustrative and simplicity purposes only.

注意:在实践中,永远不要使用GET请求来删除记录——对于修改数据的操作,始终使用POST。我们在这里这样做只是为了说明和简单的目的。

Add a Common Method to Retrieve a Book for the Chain

添加一个常用方法来检索链的书籍

As mentioned earlier, since we have a mixture of actions that operate on a single book ID and others that do not, we should not have base capture the book ID, find the corresponding book in the database and save it in the stash for later links in the chain. However, just because that logic does not belong in base doesn't mean that we can't create another location to centralize the book lookup code. In our case, we will create a method called object that will store the specific book in the stash. Chains that always operate on a single existing book can chain off this method, but methods such as url_create that don't operate on an existing book can chain directly off base.

如前所述,由于我们有对单个图书ID进行操作的操作和其他操作的混合,因此我们不应该让base捕获图书ID,在数据库中找到相应的图书,并将其保存在存储库中,以供链中的后续链接使用。然而,仅仅因为该逻辑不属于base,并不意味着我们不能创建另一个位置来集中图书查找代码。在我们的例子中,我们将创建一个名为object的方法,该方法将特定的书籍存储在存储库中。总是在一本现有图书上运行的链可以链接出这种方法,但不在现有图书上运行的url_create等方法可以直接链接出base。

To add the object method, edit src/books.h and add the following code:

要添加object方法,请编辑src/books.h并添加以下代码:

class Books : public Controller
{
public:
    ...
    /**
     * Fetch the specified book object based on the book ID and store
     * it in the stash
     */
    C_ATTR(object, :Chained("base") :PathPart("id") :CaptureArgs(1))
    void object(Context *c, const QString &id);
};
void Books::object(Context *c, const QString &id)
{
    // Find the object on the database
    QSqlQuery query = CPreparedSqlQueryThreadForDB("SELECT * FROM book WHERE id = :id", "MyDB");
    query.bindValue(":id", id);
    if (query.exec()) {
        c->setStash("object", Sql::queryToHashObject(query));
    } else {
        // You would probably want to do something like this in a real app:
        // c->detach("/error_404");
    }
    qDebug() << "*** INSIDE OBJECT METHOD for obj id=" << id << " ***";
}

Now, any other method that chains off object will automatically have the appropriate book waiting for it in c->stash("object").

现在,任何其他链接对象的方法都会自动在c->stash("object")中等待相应的书籍。

Add a Delete Action to the Controller

向控制器添加删除操作

Open src/books.h in your editor and add the following method:

打开src/books.h,并添加以下方法:

class Books : public Controller
{
public:
    ...
    /**
     * Fetch the specified book object based on the book ID and store
     * it in the stash
     */
    C_ATTR(delete_obj, :Chained("object") :PathPart("delete") :Args(0))
    void delete_obj(Context *c);
};
void Books::delete_obj(Context *c)
{
    QVariantHash book = c->stash("object").toHash();

    // Delete the object on the database
    QSqlQuery query = CPreparedSqlQueryThreadForDB("DELETE FROM book WHERE id = :id", "MyDB");
    query.bindValue(":id", book.value("id"));
    if (query.exec()) {
        // Set a status message to be displayed at the top of the view
        c->setStash("status_msg", "Book deleted.");
    }

    // Forward to the list action/method in this controller
    c->forward("list");
}

This method first deletes the book object saved by the object method.

此方法首先删除由object方法保存的book对象。

Then, rather than forwarding to a "delete done" page as we did with the earlier create example, it simply sets the status_msg to display a notification to the user as the normal list view is rendered.

然后,它不再像前面的创建示例那样转发到“delete done”页面,而是简单地设置状态,以便在呈现正常列表视图时向用户显示通知。

The delete action uses the context forward method to return the user to the book list. The detach method could have also been used. Whereas forward returns to the original action once it is completed, detach does not return. Other than that, the two are equivalent.

删除操作使用上下文转发方法将用户返回到图书列表。也可以使用分离方法。虽然forward在完成后会返回到原始操作,但detach不会返回。除此之外,两者是等价的。

Try the Delete Feature

尝试删除功能

Once you save the Books controller, the server should automatically restart. The delete method should now appear in the "Loaded Chained actions" section of the startup debug output:

保存Books控制器后,服务器应自动重启。delete方法现在应该出现在startup debug输出的“Loaded Chained actions”部分:

Loaded Chained actions:
.-------------------------+--------------------------.
| Path Spec               | Private                  |
.-------------------------+--------------------------.
| /books/id/*/delete      | /books/base (0)          |
|                         | -> /books/object (1)     |
|                         | => /books/delete_obj     |
| /books/form_create      | /books/base (0)          |
|                         | => /books/form_create    |
| /books/form_create_do   | /books/base (0)          |
|                         | => /books/form_create_do |
| /books/url_create/*/*/* | /books/base (0)          |
|                         | => /books/url_create     |
.-------------------------+--------------------------.

Then point your browser to http://localhost:3000/books/list and click the "Delete" link next to the first "TCPIP_Illustrated_Vol-2". A green "Book deleted" status message should display at the top of the page, along with a list of the eight remaining books.

​然后将浏览器指向http://localhost:3000/books/list点击第一个“TCPIP_Illustrated_Vol-2”旁边的“删除”链接。页面顶部将显示一条绿色的“图书已删除”状态消息,以及剩余八本图书的列表。

Fixing a Dangerous URL

修复一个危险的URL

Note the URL in your browser once you have performed the deletion in the prior step -- it is still referencing the delete action:

在上一步中执行删除操作后,请注意浏览器中的URL——它仍在引用删除操作:

http://localhost:3000/books/id/6/delete

What if the user were to press reload with this URL still active? In this case the redundant delete is harmless (although it does generate an exception screen, it doesn't perform any undesirable actions on the application or database), but in other cases this could clearly lead to trouble.

如果用户在该URL仍处于活动状态时按“重新加载”会怎样?在这种情况下,冗余删除是无害的(虽然它确实会生成一个异常屏幕,但不会对应用程序或数据库执行任何不必要的操作),但在其他情况下,这显然会导致问题。

We can improve the logic by converting to a redirect. Unlike c->forward("list") or c->detach("list") that perform a server-side alteration in the flow of processing, a redirect is a client-side mechanism that causes the browser to issue an entirely new request. As a result, the URL in the browser is updated to match the destination of the redirection URL.

我们可以通过转换为重定向来改进逻辑。与在处理流程中执行服务器端更改的c->forward("list")或c->detach("list")不同,重定向是一种客户端机制,可导致浏览器发出全新的请求。因此,浏览器中的URL将被更新,以匹配重定向URL的目标。

To convert the forward used in the previous section to a redirect, open src/books.cpp and edit the existing delete_obj method to match:

要将上一节中使用的转发转换为重定向,请打开src/books.cpp并编辑现有的delete_obj方法以匹配:

void Books::delete_obj(Context *c)
{
    QVariantHash book = c->stash("object").toHash();

    // Delete the object on the database
    QSqlQuery query = CPreparedSqlQueryThreadForDB("DELETE FROM book WHERE id = :id", "MyDB");
    query.bindValue(":id", book.value("id"));
    if (query.exec()) {
        // Set a status message to be displayed at the top of the view
        c->setStash("status_msg", "Book deleted.");
    }

    // Redirect the user back to the list page. Note the use
    // of actionFor as earlier in this section (BasicCRUD)
    c->response()->redirect(c->uriFor(CActionFor("list")));
}

Try the Delete and Redirect Logic

尝试删除和重定向逻辑

Point your browser to http://localhost:3000/books/list (don't just hit "Refresh" in your browser since we left the URL in an invalid state in the previous section!) and delete the first copy of the remaining two "TCPIP_Illustrated_Vol-2" books. The URL in your browser should return to the http://localhost:3000/books/list URL, so that is an improvement, but notice that no green "Book deleted" status message is displayed. Because the stash is reset on every request (and a redirect involves a second request), the status_msg is cleared before it can be displayed.

​将浏览器指向http://localhost:3000/books/list(不要只点击浏览器中的“刷新”,因为我们在上一节中让URL处于无效状态!)并删除剩下的两本"TCPIP_Illustrated_Vol-2"。浏览器中的URL应返回到http://localhost:3000/books/listURL,所以这是一个改进,但请注意,不会显示绿色的“Book deleted”(图书已删除)状态消息。由于每次请求时都会重置隐藏(重定向涉及第二个请求),因此状态_msg在显示之前会被清除。

Using 'uriFor' to Pass Query Parameters

使用“uriFor”传递查询参数

There are several ways to pass information across a redirect. One option is to use the flash technique that we will see in Chapter 5 of this tutorial; however, here we will pass the information via query parameters on the redirect itself. Open src/books.cpp and update the existing sub delete method to match the following:

有几种方法可以通过重定向传递信息。一种选择是使用flash技术,我们将在本教程第5章中看到;然而,这里我们将通过重定向本身上的查询参数传递信息。打开src/books.cpp并更新现有的子删除方法以匹配以下内容:

void Books::delete_obj(Context *c)
{
    QVariantHash book = c->stash("object").toHash();
    QString statusMsg;

    // Delete the object on the database
    QSqlQuery query = CPreparedSqlQueryThreadForDB("DELETE FROM book WHERE id = :id", "MyDB");
    query.bindValue(":id", book.value("id"));
    if (query.exec()) {
        // Set a status message to be displayed at the top of the view
        statusMsg = "Book deleted.";
    } else {
        // Set an error message to be displayed at the top of the view
        statusMsg = query.lastError().text();
    }

    // Redirect the user back to the list page. Note the use
    // of actionFor as earlier in this section (BasicCRUD)
    c->response()->redirect(c->uriFor(CActionFor("list"), ParamsMultiMap{
                                {"status_msg", statusMsg }
                            }));
}

This modification simply leverages the ability of uriFor to include an arbitrary number of name/value pairs in a ParamsMultiMap. Next, we need to update root/src/wrapper.html to handle status_msg as a query parameter:

这种修改只是利用uriFor在ParamsMultiMap中包含任意数量的名称/值对的能力。接下来,我们需要更新root/src/wrapper.html,将状态消息作为查询参数处理:

...
<div id="content">
    {% comment %} Status and error messages {% endcomment %}
        <span class="message">{{ status_msg }}{{ c.request.queryParams.status_msg|escape }}</span>
        <span class="error">{{ error_msg }}</span>
    {% comment %} This is where Grantlee will stick all of your template's contents.{% endcomment %}
    {{ content }}
</div><!-- end content -->
...

Although the sample above only shows the content div, leave the rest of the file intact -- the only change we made to the wrapper.html was to add {{ c.request.queryParams.status_msg|escape }} to the <span class="message"> line. Note that we definitely want the |escape Grantlee filter here since it would be easy for users to modify the message on the URL and possibly inject harmful code into the application if we left that off, by default Grantlee escapes all strings but if in future you decide to change the default behavior this is in place.

尽管上面的示例只显示了content div,但请保持文件的其余部分不变——这是我们对wrapper.html所做的唯一更改。将{{ c.request.queryParams.status_msg|escape }}添加到<span class="message"> 行中。请注意,我们这里肯定需要| escape Grantlee过滤器,因为用户很容易修改URL上的消息,并且可能会向应用程序中注入有害代码。如果我们不这样做,默认情况下Grantlee会转义所有字符串,但如果将来您决定更改默认行为,则这一点已经到位。

Try the Delete and Redirect With Query Param Logic

使用查询参数逻辑尝试删除和重定向

Point your browser to http://localhost:3000/books/list (you should now be able to safely hit "refresh" in your browser). Then delete the remaining copy of "TCPIP_Illustrated_Vol-2". The green "Book deleted" status message should return. But notice that you can now hit the "Reload" button in your browser and it just redisplays the book list (and it correctly shows it without the "Book deleted" message on redisplay).

将浏览器指向http://localhost:3000/books/list(您现在应该可以在浏览器中安全地点击“刷新”)。然后删除“TCPIP_Illustrated_Vol-2”的剩余副本。绿色的“图书已删除”状态信息应返回。但请注意,现在你可以在浏览器中点击“重新加载”按钮,它只会重新显示图书列表(并且在重新显示时,它不会正确显示“图书已删除”消息)。

NOTE: Be sure to check out Authentication where we use an improved technique that is better suited to your real world applications.

注意:请务必查看身份验证,我们使用了一种更适合您的实际应用程序的改进技术。

You can jump to the next chapter of the tutorial here: Authentication

​你可以在这里跳到教程的下一章:身份验证

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值