请注意:此帖子是a series on creating a REST API with Node.js on Oracle Database。关于项目的详细信息和其他部分的链接,请参阅该帖子。获取代码here。
在本系列的这一点上,REST应用编程接口支持员工端点上的基本CRUD功能。但是,客户端通常需要对如何从数据库中提取多个记录进行一些控制。在这篇文章中,您将通过添加分页、排序和过滤功能使应用编程接口更加灵活。
目前,当在雇员的路由上发出一个HTTP GET请求时,表中的所有行都会被返回。HR . EMPLOYEES表中只有107行可能没什么大不了的,但是想象一下如果该表包含数千或数百万行会发生什么。移动和网络应用等客户端通常只消耗和显示数据库中可用行的一小部分,然后在需要时获取更多行——可能是当用户向下滚动或单击用户界面中某个分页控件上的“下一步”按钮时。
为此,REST APIs需要支持对返回的结果进行分页的方法。一旦支持分页,排序功能就变得很重要,因为数据通常需要在应用分页之前进行排序。此外,过滤数据的方法对性能非常重要。如果不需要,为什么要从数据库发送数据,通过中间层,一直发送到客户端?
我将使用网址查询字符串参数来允许客户端指定结果应该如何分页、排序和过滤。正如编程中经常出现的情况,实现可能会因您的需求、性能目标等而异。在这篇文章中,我将引导您通过手动方式将这些特性添加到一个应用编程接口中。这种方法提供了非常精细的控制,但是它可能是费力和重复的,所以我将在以后的文章中向您展示如何使用一个模块来简化这些操作。
我将用于分页的查询字符串参数有skip
和limit
。的skip
参数将用于跳过指定的行数limit
将限制返回的行数。我将使用默认值30limit
如果客户端没有提供值。
首先更新控制器逻辑,从查询字符串中提取值,并将它们传递给数据库应用编程接口。打开控制器/雇员. js文件,并在get
函数,位于解析出req.params.id
参数。
// *** line that parses req.params.id is here ***
context.skip = parseInt(req.query.skip, 10);
context.limit = parseInt(req.query.limit, 10);
现在,需要更新数据库逻辑,以将这些值考虑在内,并相应地更新SQL查询。在SQL中offset
子句用于跳过行,而fetch
子句用于限制查询返回的行数。通常,这些值不会被直接追加到查询中——出于性能和安全原因,它们将作为绑定变量添加。打开db _ API/employees . js并在if
在街区find
追加where
子句添加到查询中。
// *** if block that appends where clause ends here ***
if (context.skip) {
binds.row_offset = context.skip;
query += '\noffset :row_offset rows';
}
const limit = (context.limit > 0) ? context.limit : 30;
binds.row_limit = limit;
query += '\nfetch next :row_limit rows only';
这就是你需要做的分页!启动应用编程接口,然后在另一个终端运行一些cURL命令来测试它。这里有几个你可以使用的例子:
# use default limit (30)
curl "http://localhost:3000/api/employees"
# set limit to 5
curl "http://localhost:3000/api/employees?limit=5"
# use default limit and set skip to 5
curl "http://localhost:3000/api/employees?skip=5"
# set both skip and limit to 5
curl "http://localhost:3000/api/employees?skip=5&limit=5"
现在分页工作了,您可能已经看到了在应用分页之前能够对数据进行排序的重要性。您将在下一节中添加排序。
至少,客户端应该能够指定排序依据的列和顺序(升序或降序)。最简单的方法是定义一个查询参数(我将使用sort
)允许像“last_name:asc”或“salary:desc”这样的字符串传入。当然,您可以更进一步,也许允许客户端按多个列排序,控制如何处理空值,等等。我会保持简单,只允许客户指定一个列和方向如上。
在SQL中order by
子句用于对数据进行排序。遗憾的是,无法在order by
子句,因为它被视为标识符而不是值。这意味着在将列名和方向附加到查询时需要非常小心,以防止SQL注入。您可以清理传入的值,或者将它们与值的白名单进行比较。我将使用白名单方法,因为它提供了比一般清理更多的控制。
在我们得到代码之前还有最后一件事...保证从SQL查询返回的结果集的顺序的唯一方法是包含一个order by
子句。因此,违约是个好主意order by
子句,以确保在客户端没有指定时的一致性。
返回到controller/employees . js文件,并在get函数中,在解析出req.query.limit
参数。
// *** line that parses req.query.limit is here ***
context.sort = req.query.sort;
接下来,打开db _ API/employees . js,并在声明和初始化的行下面添加以下行baseQuery
。
// *** lines that initalize baseQuery end here ***
const sortableColumns = ['id', 'last_name', 'email', 'hire_date', 'salary'];
sortableColumns
是客户端能够用于排序的列的白名单。接下来,在find
函数,添加以下内容if
块,该块将order by
子句。这需要在where
子句,但是在offset
和fetch
条款。
第一部分if
block检查客户端是否传入了排序值。如果不是,默认order by
按姓氏升序排序的子句被附加到SQL查询中。如果指定了排序值,则首先将其分解为列值和顺序值,并且每个值在order by
子句被追加到查询中。
现在,您可以重新启动应用编程接口,并运行一些cURL命令来测试它。下面是一些可以尝试的例子:
# use default sort (last_name asc)
curl "http://localhost:3000/api/employees"
# sort by id and use default direction (asc)
curl "http://localhost:3000/api/employees?sort=id"
# sort by hire_date desc
curl "http://localhost:3000/api/employees?sort=hire_date:desc"
# use sort with limit and skip together
curl "http://localhost:3000/api/employees?limit=5&skip=5&sort=salary:desc"
# should throw an error because first_name is not whitelisted
curl "http://localhost:3000/api/employees?sort=first_name:desc"
# should throw an error because 'other' is not a valid order
curl "http://localhost:3000/api/employees?sort=last_name:other"
最后两个示例应该抛出异常,因为它们包含未列入白名单的值。目前,快速的默认错误处理程序正在使用,这就是为什么错误作为一个网页返回。我将在以后的文章中向您展示如何实现自定义错误处理。
过滤数据的能力是所有REST APIs都应该提供的一个重要特性。就像排序一样,实现可以简单也可以复杂,这取决于您想要支持什么。最简单的方法是添加对相等过滤器的支持(例如,姓氏=Doe)。更复杂的实现可以增加对基本操作符(例如,instr等)的支持。)和复杂的布尔运算符(例如and & or),它们可以将多个过滤器组合在一起。
在这篇文章中,我将保持简单,只在两列中添加对equals过滤器的支持:department_id和manager_id。对于每一列,我将在查询字符串中考虑一个相应的参数。附加一个where
子句,当在单个雇员端点上发出GET请求时,将需要更新以允许这些新的筛选器。
打开controller/employees . js,并在解析值的行下面添加以下行req.query.sort
在get
功能。
// *** line that parses req.query.sort is here ***
context.department_id = parseInt(req.query.department_id, 10);
context.manager_id = parseInt(req.query.manager_id, 10);
接下来,通过添加一个1 = 1
where子句添加到baseQuery
如下所示。
const baseQuery =
`select employee_id "id",
first_name "first_name",
last_name "last_name",
email "email",
phone_number "phone_number",
hire_date "hire_date",
job_id "job_id",
salary "salary",
commission_pct "commission_pct",
manager_id "manager_id",
department_id "department_id"
from employees
where 1 = 1`;
当然,1 = 1
将总是解析为true,因此优化器将忽略它。然而,这种技术将简化以后添加额外的谓词。
在find
函数,替换if
块,该块将where
当acontext.id
通过以下行传递。
// *** line that declares 'binds' is here ***
if (context.id) {
binds.employee_id = context.id;
query += '\nand employee_id = :employee_id';
}
if (context.department_id) {
binds.department_id = context.department_id;
query += '\nand department_id = :department_id';
}
if (context.manager_id) {
binds.manager_id = context.manager_id;
query += '\nand manager_id = :manager_id';
}
如你所见,每一个if
块只是将传递给binds
对象,然后将相应的谓词追加到where子句中。
保存您的更改,然后重新启动应用编程接口。然后使用这些cURL命令来测试它:
# filter where department_id = 90 (returns 3 employees)
curl "http://localhost:3000/api/employees?department_id=90"
# filter where manager_id = 100 (returns 14 employees)
curl "http://localhost:3000/api/employees?manager_id=100"
# filter where department_id = 90 and manager_id = 100 (returns 2 employees)
curl "http://localhost:3000/api/employees?department_id=90&manager_id=100"
这就是了——该应用编程接口现在支持分页、排序和过滤!手动方法提供了很多控制,但需要大量代码。这find
函数现在有58行长,它只支持有限的排序和过滤功能。当然,有办法让这变得更容易,尽管你可能不得不牺牲一些控制。
这篇文章是系列文章中最后一篇“核心”文章,内容是关于用Node.js和Oracle数据库构建一个REST API。但是,我将通过一些后续文章来扩展这个系列,这些文章涵盖了各种REST API和Oracle数据库特性。第一篇这样的后续文章将向您展示如何使用一个模块来简化和标准化分页、排序和过滤。敬请期待!