- 原文地址:How to Use MySQL With Deno and Oak
- 原文作者:Adeel Imran
- 原文发布时间:2020-06-07
- 译者:hylerrix
- 备注:本文遵循 freeCodeCamp 翻译规范,同时本文会收录在《Deno 钻研之术》的翻译篇中。
- 备注:《Deno 钻研之术》电子书官网上线啦!deno-tutorial.js.org
- 备注:最近着手准备原创 JavaScript 模块化从零到 Deno 模块机制系列 & 构思 Oak 开源应用中...
我最近写了一篇关于 Deno + Oak 构建酷炫 Todo API 的文章 ,其中并没有使用数据库相关的知识点。您可以在我的 Github 仓库 adeelibr/deno-playground 的 chapter_1:oak 中查看当时的整套代码。
译者注:翻译版的《Deno + Oak 构建酷炫的 Todo API》在这里,相关 Demo 也可以在电子书仓库中找到。
本文将进一步讲起,一起来学习如何将 MySQL 集成到我们的 Deno + Oak 项目中。
如果你想随时了解本文的完整代码,可以在 chapter_2:mysql 中找到,欢迎给仓库点个 Star。
我将假设你已经阅读了上一篇文章,如果没有,可以先在此处阅读完后再回到本文来。
在我们开始前,请确保你已经安装了一个 MySQL 客户端并你能成功运行:
同时我也为 MacOS 用户写了一个简短的,关于如何安装 MySQL 的教程。
如果你是在 Windows 环境下,你可以使用和上面相同的工具,或者直接使用 XAMPP 来快速运行 MySQL 实例到你的机器上。
当你将 MySQL 成功跑起来后,我们就可以开始本文的探索了。
假设你已经阅读了上一篇文章,我们将编写如下功能:
开始前的最后一件事:我将上一篇的代码添加 MySQL 版本后的具体 Git 变动可以在这里查阅。
在你的项目根目录中(我的叫做 chapter_2:mysql
,你的可以随便起),创建一个 db 文件夹,并在其中创建一个 config.ts 并添加如下内容:
export const DATABASE: string = "deno"; export const TABLE = { TODO: "todo", }; 复制代码
这里没什么新知识点,仅仅导出了我们定义的数据库的名称以及一个 TABLE 对象。通过这个导出,我们的项目中将会有一个名为 “deno” 的数据库,其中又会有一个名为 “todo” 的数据表。
接下来,在 db 文件夹中再创建一个名为 client.ts 的文件并填充如下内容:
import { Client } from "https://deno.land/x/mysql/mod.ts"; // config import { DATABASE, TABLE } from "./config.ts"; const client = await new Client(); client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "", }); 复制代码
这段代码包含了若干条功能。
我们从 Deno 的一个第三方 mysql
模块中解构出了 Client
变量,这个变量可以用来连接数据库并执行指定的增删改查工作。
client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "", }); 复制代码
Client
内置一个 connect
方法,用来供我们设置 hostname
、username
、password
和 db
等字段的值,以设置与 MySQL 的连接配置。
请确保你的 username
用户没有设置 password
,因为目前的 Deno MySQL 模块无法连接有密码的用户。如果你不知道如何清空用户密码,可以阅读这里。
我在此处将 database
字段留空,因为我想稍后在脚本中手动选择它。
让我们添加一个用来初始化名为“deno”的数据库并为其创建一个名为“todo”表的脚本。
在 db/client.ts
文件中我们添加以下内容:
import { Client } from "https://deno.land/x/mysql/mod.ts"; // 加载配置文件里的配置 import { DATABASE, TABLE } from "./config.ts"; const client = await new Client(); client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "", }); const run = async () => { // 创建一个数据库 (前提是之前没有创建过) await client.execute(CREATE DATABASE IF NOT EXISTS ${DATABASE}); // 选择我们的数据库 await client.execute(USE ${DATABASE}); // 如果已经创建过名为 Todo 的数据表,将其删除 await client.execute(DROP TABLE IF EXISTS ${TABLE.TODO}); // 创建 Todo 数据表 await client.execute(CREATE TABLE ${TABLE.TODO} ( id int(11) NOT NULL AUTO_INCREMENT, todo varchar(100) NOT NULL, isCompleted boolean NOT NULL default false, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;); }; run(); export default client; 复制代码
这里我们从我们的最早的配置文件中导入了 DATABASE
和 TABLE
,并通过 run()
方法创建相关的数据库和表。
让我们截取 run()
方法相关的代码片段。我在代码中已经编写了帮助你理解的注释。
const run = async () => { // 创建一个数据库 (前提是之前没有创建过) await client.execute(CREATE DATABASE IF NOT EXISTS ${DATABASE}); // 选择我们的数据库 await client.execute(USE ${DATABASE}); // 如果已经创建过名为 Todo 的数据表,将其删除 await client.execute(DROP TABLE IF EXISTS ${TABLE.TODO}); // 创建 Todo 数据表 await client.execute(CREATE TABLE ${TABLE.TODO} ( id int(11) NOT NULL AUTO_INCREMENT, todo varchar(100) NOT NULL, isCompleted boolean NOT NULL default false, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;); }; run(); 复制代码
deno
的数据库,如果之前已经有这个数据库则跳过此步骤。deno
的这个数据库。deno
数据库中,如果名为 todo
的表存在,则将其删除。deno
数据库中创建一个新的 todo
表,并且定义其表结构:表结构将包含一个唯一的、自增长的、数值型的 id
字段;也将包含一个名为 todo
的字符串型字段;同时还包含一个名为 isCompleted
的布尔型字段;最后将 id
字段定义为主键。我写这段代码的原因是因为我不想在 MySQL 实例中有代码上无法直观看出来的信息。有了这段代码后,每次重启服务器时,它都会重新初始化所有内容。
你可以不编写这段代码。但如果不编写的话,则必须手动创建数据库和表。
同时,你也可以查阅 Deno MySQL 模块的 db creation 和 table creation 文档。
回到文章的主旨,我们已经完成了上面提到的四个目标的两个目标:
这意味着本文 50% 的知识点已经介绍完毕。但不幸运的是,现在还测试不了任何数据操作功能。一起来快速添加几个 CRUD 功能来看看具体是怎样执行的。
我们需要先编写 Todo 接口。创建 interfaces/Todo.ts
文件并添加如下内容:
export default interface Todo { id?: number, todo?: string, isCompleted?: boolean, } 复制代码
代码中的 ?
符号代表这个键是可选的。之所以这样做是因为接下来我们有的地方仅需要其中的若干个键。
如果你想了解更多 TypeScript 中可选的属性,可以查阅这里。
接下来,在根目录创建一个名为 models 的文件夹并在其中创建一个名为 todo.ts 的文件,添加如下内容:
import client from "../db/client.ts"; // 加载配置文件 import { TABLE } from "../db/config.ts"; // 加载接口文件 import Todo from "../interfaces/Todo.ts"; export default { /** * 通过解构的 id 参数值,来检查相应的 todo 元素是否存在于数据表中 * @param id * @returns 返回布尔值来代表是否存在 */ doesExistById: async ({ id }: Todo) => {}, /** * 将会返回 todo 表中的所有内容 * @returns 返回全部都是 todo 元素的数组 */ getAll: async () => {}, /** * 过解构的 id 参数值,来返回相应的 todo 元素 * @param id * @returns 返回一个 todo 元素 */ getById: async ({ id }: Todo) => {}, /** * 在 todo 表中增加一个新的 todo 元素 * @param todo * @param isCompleted */ add: async ( { todo, isCompleted }: Todo, ) => {}, /** * 修改某个 todo 元素的内容 * @param id * @param todo * @param isCompleted * @returns 返回一个数字 (代表影响的行数) */ updateById: async ({ id, todo, isCompleted }: Todo) => {}, /** * 通过 ID 来删除指定的元素 * @param id * @returns integer (count of effect rows) */ deleteById: async ({ id }: Todo) => {}, }; 复制代码
此时每个函数都是空的,不用担心,我们接下来会一一填充。
接下来创建 controllers/todo.ts
文件并保证填充如下内容:
// 加载接口文件 import Todo from "../interfaces/Todo.ts"; // 加载模型操作文件 import TodoModel from "../models/todo.ts"; export default { /** * @description 获取所有 todo 元素 * @route GET /todos */ getAllTodos: async ({ response }: { response: any }) => {}, /** * @description 新增一个 todo 元素 * @route POST /todos */ createTodo: async ( { request, response }: { request: any; response: any }, ) => {}, /** * @description 通过 id 获取指定的 todo 元素 * @route GET todos/:id */ getTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => {}, /** * @description 通过 id 更新指定的 todo 元素 * @route PUT todos/:id */ updateTodoById: async ( { params, request, response }: { params: { id: string }; request: any; response: any; }, ) => {}, /** * @description 通过 id 删除指定的 todo 元素 * @route DELETE todos/:id */ deleteTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => {}, }; 复制代码
这个文件目前同样是空的内容,现在开始一一将其填充吧。
在 models/todo.ts
文件中为 getAll
方法添加具体逻辑:
import client from "../db/client.ts"; // config import { TABLE } from "../db/config.ts"; // Interface import Todo from "../interfaces/Todo.ts"; export default { /** * 将会返回所有 todo 表中的数据 * @returns array of todos */ getAll: async () => { return await client.query(`SELECT * FROM ${TABLE.TODO}`); }, } 复制代码
我们这里直接用 SQL 原生语法来获取表中的所有内容。
除了 connect
(使用于 db/client.ts 文件中)方法外, Client
还公开了另一种方法,即 query
。通过 client.query
方法,我们可以直接从 Deno 代码上运行 MySQL 查询。
接下来打开 controllers/todo.ts
文件并为 getAllTodos
填充内容:
// interfaces import Todo from "../interfaces/Todo.ts"; // models import TodoModel from "../models/todo.ts"; export default { /** * @description 获取所有 todo * @route GET /todos */ getAllTodos: async ({ response }: { response: any }) => { try { const data = await TodoModel.getAll(); response.status = 200; response.body = { success: true, data, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } }, } 复制代码
我们这里所做的就是导入 TodoModel
对象并使用其中我们刚定义不久的 getAll
方法。因为需要该函数需要处理 Promise 类型的异步过程,所以我们将整个函数定义为 async/await 类型。
TodoModel.getAll()
方法返回一个数组后,我们将这个数组包装起来并将 response.body
的响应状态 status
设置为 200
。
如果执行过程中有任何异常比如 Promise 报错,程序将通过进入 catch 块,向用户返回状态码为 400 的响应体(此时 success
为 false,message
为错误原因。
就这么简单地搞定了,现在来在终端上运行。
请保证你的 MySQL 实例运行中,然后在终端输入:
$ deno run --allow-net server.ts 复制代码
不出意外的话,此时你的终端会有这样类似的结果:
这也是当我通过命令行运行服务器时终端的样子。
终端告诉了我们两件事:
127.0.0.1:3306
(http://localhost:3306
)上。让我们测试下我们的 API,我使用的是 Postman,但是你可以用任何你喜欢的 API 测试工具。
执行 [GET] localhost:8080/todos 后,我们得到了所有 todos 列表。
虽然现在的 todos 列表返回的是空数组,但当我们能成功给 todo
数据表增加数据后就会获得更多数据。
棒极了,一个 API 搞定只剩下四个要搞。
在 models/todo.ts
文件中,为 add()
函数添加如下内容:
export default { /** * 为 todo 数据表新增一行数据 * @param todo * @param isCompleted */ add: async ( { todo, isCompleted }: Todo, ) => { return await client.query( `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`, [ todo, isCompleted, ], ); }, } 复制代码
add 函数在参数列表中将解构 todo
和 isCompleted
两个变量。
同时,上面代码的 add: async ({ todo, isCompleted }: Todo) => {}
片段和 ({todo, isCompleted}: {todo:string, isCompleted:boolean})
语句是等价的。但我们已经在 interfaces/Todo.ts
中定义过 Todo 接口:
export default interface Todo { id?: number, todo?: string, isCompleted?: boolean, } 复制代码
此时我们将可以简单地写成 add: async ({ todo, isCompleted }: Todo) => {}
。这条语句告诉 TypeScript 当前函数有两个参数:字符串类型的 todo
,以及布尔类型的 isCompleted
。
如果你想了解更多关于接口的知识,TypeScript 官方文档上有一个绝佳的介绍,可以查看这里。
在 add 函数中还有如下代码:
return await client.query( `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`, [ todo, isCompleted, ], ); 复制代码
这是一条 MySQL 查询语句,可以被拆分为如下两个部分:
INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)
。其中的两个问号意味着这里需要使用到变量的值。[todo, isCompleted]
是上一部分需要使用的变量,其值将会替代 (?, ?)
。Table.Todo
是一个从 db/config.ts
读取来的字符串,其值为"todo
"。接下来在我们的控制器 controllers/todo.ts
文件中,编写 createTodo()
函数:
export default { /** * @description 新增一个 todo * @route POST /todos */ createTodo: async ( { request, response }: { request: any; response: any }, ) => { const body = await request.body(); if (!request.hasBody) { response.status = 400; response.body = { success: false, message: "No data provided", }; return; } try { await TodoModel.add( { todo: body.value.todo, isCompleted: false }, ); response.body = { success: true, message: "The record was added successfully", }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } }, } 复制代码
我们继续将其拆分为两个部分来介绍:
第一部分
const body = await request.body(); if (!request.hasBody) { response.status = 400; response.body = { success: false, message: "No data provided", }; return; } 复制代码
我们在这里所做的是检查用户请求当前接口时是否在 body 中传递了请求数据。如果没有的话将返回一个有 400
状态码且包括 success: false
和 message: <erromessage-string>
的响应体。
第二部分
try { await TodoModel.add( { todo: body.value.todo, isCompleted: false }, ); response.body = { success: true, message: "The record was added successfully", }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } 复制代码
接下来如果没有任何意外错误,则调用 TodoModel.add()
函数并返回一个状态为 200
且给用户提示执行函数成功的响应体。
否则将进入 catch 代码段,来返回函数执行错误及其原因,正如前文介绍过的 API 一样。
现在我们搞定了。打开你的终端并且确保你的 MySQL 正在运行。在终端输入:
$ deno run --allow-net server.ts 复制代码
打开 Postman 并且测试当前 API 能否正常运行:
执行 [POST] localhost:8080/todos => 将会为 todo 列表新增一个新的数据。
再次执行 [GET] localhost:8080/todos => 将会返回所有 todos,可以看到刚刚新增 todo 已经被加入到数据库中。
很棒,已经搞定了两个 API,只剩三个要做。
在你的 models/todo.ts
文件中,为 doesExistById()
和 getById()
两个函数填充其内容:
export default { /** * Takes in the id params & checks if the todo item exists * in the database * @param id * @returns boolean to tell if an entry of todo exits in table */ doesExistById: async ({ id }: Todo) => { const [result] = await client.query( `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`, [id], ); return result.count > 0; }, /** * 解构 id 变量 & 返回找到的相关 todo * against it. * @param id * @returns object of todo item */ getById: async ({ id }: Todo) => { return await client.query( `SELECT * FROM ${TABLE.TODO} WHERE id = ?`, [id], ); }, } 复制代码
让我们逐个介绍这两个函数:
doesExistById
函数从参数列表中解构出 id
变量,并返回一个 boolean
布尔值,来表明想要检测的这个独特 todo 是否存在于数据库中。const [result] = await client.query( `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`, [id], ); return result.count > 0; 复制代码
我们通过 count 值来检查指定 todo 是否存在。如果其值大于 0 返回 true
,否则返回 false
。
getById
函数通过指定的 id 返回相应的数据:return await client.query( `SELECT * FROM ${TABLE.TODO} WHERE id = ?`, [id], ); 复制代码
上面一行代码直接执行了 MySQL 语句,来通过 id 查询数据并返回结果。
接下来,打开 controllers/todo.ts
文件并为 getTodoById
控制器填充内容:
export default { /** * @description 通过 id 获取相关 tod * @route GET todos/:id */ getTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => { try { const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } const todo = await TodoModel.getById({ id: Number(params.id) }); response.status = 200; response.body = { success: true, data: todo, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } }, } 复制代码
这段代码也可以拆分为两段更小的部分来看:
const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } 复制代码
首先我们通过以下代码来检查想要查找的 todo 是否存在于数据库中:
const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); 复制代码
我们在其中需要转换 params.id
为 Number
数值类,因为接口中声明了我们的 id
键值必须是一个数字。接下来我们将转换为数值后的 params.id
传递给 doesExistById
方法,这个方法会返回布尔值。
接着检查这个布尔值,如果是 false 则返回像前文一样的包含 404
状态码的响应体:
if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } 复制代码
第二部分是:
try { const todo: Todo = await TodoModel.getById({ id: Number(params.id) }); response.status = 200; response.body = { success: true, data: todo, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } 复制代码
这段代码和前文的很像。我们从数据库中获取到指定数据给 todo
变量,然后返回响应体。如果执行过程中有任何错误则响应体将包含错误信息返回给用户。
现在打开你的终端并且确保你的 MySQL 正在运行。在终端输入:
$ deno run --allow-net server.ts 复制代码
打开 Postman 来测试当前接口能否正常运行。
请记住我们每次重启服务器时都会重置数据库。如果你不想要这个功能,你可以注释掉 db/client.ts
文件中的整个 run
方法。
执行 [POST] localhost:8080/todos => 将会新增一个新的 todo。
执行 [POST] localhost:8080/todos => 将会返回所有的 todo。
执行 [GET] localhost:8080/todos/:id => 将会当查找指定 todo,并返回其内容。
执行 [GET] localhost:8080/todos/ => 将会返回包含 404 状态码及其错误信息的响应体。
目前我们搞定了如下 API:
仅剩下的 API:
让我们先为这个 API 创建模型(models)代码。进入 models/todo.ts
文件并为 updateById
方法填充其内容:
** * 更新某个指定 todo * @param id * @param todo * @param isCompleted * @returns integer (count of effect rows) */ updateById: async ({ id, todo, isCompleted }: Todo) => { const result = await client.query( `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`, [ todo, isCompleted, id, ], ); // return count of rows updated return result.affectedRows; }, 复制代码
updateById
方法将会从参数列表解构三个变量:id
、todo
以及 isCompleted
。
我们直接编写了 MySQL 语句来执行查询:
const result = await client.query( `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`, [ todo, isCompleted, id, ], ); 复制代码
这段代码将通过 id
来更新指定的 todo
其 isCompleted
的值。
接下来我们返回此条 MySQL 语句执行后影响到的数据表行数:
// return count of rows updated return result.affectedRows; 复制代码
这个行数值只会是 0 或者 1,且绝不会超过 1。因为我们数据表中的 ID 是唯一的——不同 todo 共用同一 ID的情况是不存在的。
接下来打开 controllers/todo.ts
文件并为 updateTodoById
方法填充其内容:
updateTodoById: async ( { params, request, response }: { params: { id: string }; request: any; response: any; }, ) => { try { const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } // 如果 todo 被找到了则更新它 const body = await request.body(); const updatedRows = await TodoModel.updateById({ id: Number(params.id), ...body.value, }); response.status = 200; response.body = { success: true, message: `Successfully updated ${updatedRows} row(s)`, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } }, 复制代码
这段代码和前文的几个 API 中的代码几乎一样。与众不同的地方在这里:
// if todo found then update todo const body = await request.body(); const updatedRows = await TodoModel.updateById({ id: Number(params.id), ...body.value, }); 复制代码
我们将用户传递来的 JSON 格式的 body 数据直接传给 TodoModel.updateById
函数。
记得需要转换 id
的变量类型为数值型以遵循接口的类型约束。
这行代码执行后将返回受到影响的行数。我们直接将其包装在响应体里返回。执行过程中如果有任何错误,将会被 catch 到并返回通用的报错信息。
让我们来重启服务器来检查是否能成功运行。请确保你的 MySQL 正在运行并在终端输入:
$ deno run --allow-net server.ts 复制代码
打开 Postman 来测试当前接口能否正常运行:
执行 [PUT] localhost:8080/todos/:id => 将会通过指定的 id 来更新相应的 todo 内容
执行 [GET] localhost:8080/todos/ => 将会返回所有 todo 列表,来验证是否更新成功。
在你的 models/todo.ts
文件中创建一个 deleteById
函数并填充如下内容:
/** * 通过指定 ID 来删除相应 todo * @param id * @returns integer (count of effect rows) */ deleteById: async ({ id }: Todo) => { const result = await client.query( `DELETE FROM ${TABLE.TODO} WHERE id = ?`, [id], ); // return count of rows updated return result.affectedRows; }, 复制代码
这里我们根据解构出的 id
值来通过 MySQL 删除指定的元素,并返回受到影响的行数。影响的行数的值依然只能是 0 或者 1,因为这个 ID 最多只会对应一个元素。
接下来,打开 controllers/todo.ts
文件并填充 deleteByTodoId
方法:
/** * @description 通过指定 ID 来删除相应 todo * @route DELETE todos/:id */ deleteTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => { try { const updatedRows = await TodoModel.deleteById({ id: Number(params.id), }); response.status = 200; response.body = { success: true, message: `Successfully updated ${updatedRows} row(s)`, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } }, 复制代码
这里很“爽快”地将解构出的 params.id
交给 TodoModel.deleteById
发方法,并返回此次执行过程中在数据库中的影响行数。
如果执行过程中有任何错误都会返回标准错误响应体。
让我们来检查这个 API 能否成功运行。
请确保你的 MySQL 正在运行,并在终端输入:
$ deno run --allow-net server.ts 复制代码
打开 Postman 来测试:
执行 [GET] localhost:8080/todos/ => 将会得到所有 todo。
执行 [DELETE] localhost:8080/todos/:id => 将会通过指定的 id 删除相应元素。
执行 [GET] localhost:8080/todos/ => 将会返回所有 todo 列表,来看看之前想要删除的 todo 是否还在。
到了这里我们就结束了 Deno + Oak + MySQL 的实战教程。
整篇文章的代码可以在这里看到:github.com/adeelibr/de…。如果你有任何问题都可以在上面交流。或者提交你的 PR 到仓库中。
如果你感觉本系列很有帮助,可以分享它到社交网络中。同时我的 Twitter 账号是 @adeelibr。我会很期待听到你的任何想法。
译者注:欢迎关注我的公号:@ningowood