前言
在当今的软件开发领域,前端开发人员需要不断拓展自己的技能,从纯粹的前端知识逐步进阶到后端技术和数据库领域。特别是对于想要系统性学习和记录Node.js后端知识的开发者来说,理解SQL数据库是其中重要的一部分。接下来我会分几个章节系列系统性地学习分享相关 NodeJs、数据库相关的内容。
作为前端开发者,我们已经熟悉了客户端的技术栈,但随着互联网应用的复杂性不断增加,了解后端技术和数据库已成为必然趋势。Node.js作为一个基于JavaScript的后端技术,为我们提供了构建服务器端应用程序的便利环境,使我们可以更深入地参与整个软件开发过程。
同时,学习SQL数据库也是不可或缺的一部分。掌握SQL能够帮助我们有效地存储、查询和操作数据,为我们的应用程序提供稳定可靠的数据支持。这种全面的技能拓展将使我们能够更好地理解整个应用的架构,并为我们的职业发展打下坚实的基础。
github 相关项目
项目地址 First Sql 放在开头,你可以直接 clone github.com/guanwanxiao... 开始测试用例。
测试驱动开发 TDD
当学习SQL时,使用测试驱动开发(TDD)方法是一种高效的方式。通过结合使用Better-Sqlite3、Jest和Node.js,我们可以以TDD的方式学习SQL。Better-Sqlite3是一个轻量级的SQLite客户端,适用于Node.js环境。Jest是一个流行的JavaScript测试框架,我们可以使用它来编写和运行测试用例。
在本教程中,我们将介绍如何使用Better-Sqlite3、Jest和Node.js进行TDD学习SQL。我们将从简单的查询开始,逐步扩展到插入、更新和删除数据等更复杂的操作。通过编写测试用例,我们将逐步构建一个完整的SQL数据库应用程序,并学习如何处理事务、处理错误以及进行性能优化。 让我们开始使用Better-Sqlite3、Jest和Node.js进行TDD学习SQL的旅程吧!
Better-Sqlite3
当涉及到SQL学习和开发数据库应用程序时,我们可以选择不同的数据库客户端来满足不同的需求。Better-Sqlite3和MySQL是两种常见的数据库客户端,它们在一些方面有着相似之处,但也存在一些关键的差异。
首先,让我们来看一下Better-Sqlite3。它是一个轻量级的SQLite客户端,专为Node.js环境设计。与MySQL相比,SQLite是一种嵌入式数据库,它将整个数据库存储在单个文件中,不需要独立的服务器进程。这使得SQLite在处理小型到中型数据库应用时非常高效,而且使用起来非常简单。
Better-Sqlite3提供了简洁易用的API,可以通过直接与SQLite数据库文件交互来执行各种SQL查询和操作。它的性能非常出色,并且支持参数绑定、预编译语句和批量插入等功能,可以提高查询效率和减少资源消耗。
之所以选择 Better-Sqlite3,是与 MySQL相比,MySQL的配置和管理可能更加复杂。MySQL需要独立安装和配置服务器进程,并且需要考虑网络连接、权限控制和数据库维护等方面的问题。相比于此,Better-Sqlite3 就简单许多了,可以帮我们省去很多配置上的烦恼。
最后无论您选择哪个数据库客户端,对于学习和实践SQL影响并不大,选择你自己喜欢的数据库。
Better-Sqlite3 使用
1. 连接和创建数据库
JS
const Database = require('better-sqlite3');
/**
* 打开数据库文件,如果数据库文件不存在则创建
* 同步行为
*/
const db = new Database('foobar.db', { verbose: console.log });
/**
* 或者你可以通过传递:memory:作为第一个参数来创建一个内存数据库
* 内存版的数据库不需要创建或连接本地数据库文件,所以不需要对应的文件名
*/
const db = new Database(':memory:')
console.log(db.open) // 查看数据库是否打开
db.close() // 关闭数据库,清理资源并确保测试的独立性和一致性。
2. 创建表插入查询数据
db.exec()
方法可以执行任何合法的 SQL 代码。它接受一个 SQL 字符串作为参数,并在 SQLite 数据库中执行该 SQL 语句。
db.prepare()
方法用于预编译 SQL 查询语句,并返回一个查询对象(statement object)。 预编译是指将 SQL 查询语句转换成一个查询模板,并生成一个执行计划。这样可以在后续的执行阶段中多次使用相同的查询模板,从而提高查询效率。
JS
const Database = require('better-sqlite3');
const db = new Database(':memory:')
// 创建 students 表
db.exec(`
CREATE TABLE students(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(30),
grade INT
)
`)
// 表示单个 SQL 语句的对象。
const addStmt = db.prepare(`INSERT INTO students (name,grade) VALUES ('jack', 99)`)
const queryStmt = db.prepare(`SELECT * FROM students`)
addStmt.run() // 执行插入
queryStmt.all() // 查询所有结果
两者的区别 db.exec() 相当于直接执行 SQL 字符串,所以你可以同时执行多条,而 db.prepare() SQL 语句会被预编译成一个 SQL 语句模板,而不是被直接执行通常是单条。
所以 db.prepare() 会有以下优点:
- 预编译:
prepare
方法将 SQL 语句进行预编译,将其转换为可执行的二进制代码。这样可以提高 SQL 查询的执行效率,并且可以重复使用已编译的查询计划,减少重复编译的开销。 - 参数绑定及复用:
prepare
方法还允许将参数绑定到 SQL 语句中。通过使用占位符(例如?
或:name
)来代替具体的参数值,然后通过bind
方法将参数值与占位符关联起来。这样可以避免 SQL 注入攻击,并且可以轻松地在多次执行之间更改参数值。 - 性能优化:通过使用
prepare
方法编译 SQL 语句,可以利用 SQLite 数据库的内置编译器和优化器来提高查询的性能。这些优化器可以对查询计划进行优化,选择最佳的索引和执行策略,从而提高查询速度。
3.事务处理
SQLite 会开启一个新的事务,并将所有插入操作放到这个事务中执行。如果在执行过程中发生了错误,整个事务会被回滚,保证数据的一致性。否则,当插入操作全部执行完毕后,SQLite 会自动提交这个事务。
js
const insert = db.prepare('INSERT INTO cats (name, age) VALUES (@name, @age)');
const insertMany = db.transaction((cats) => {
// 执行一组 sql 语句,如果其中有一个发生错误,整个事务会被回滚
insert({ name: 'Joey', age: 2 });
insert({ name: 'Sally', age: 4 })
});
insertMany()
4. 更多Api
更多 api 语法会在后续 TDD 测试中使用和介绍。比如
statement 对象存在的方法
run()
方法用于执行 INSERT、UPDATE 或 DELETE 操作,并返回一个代表执行结果的 Statement 对象。all()
方法用于执行 SELECT 操作,并返回一个包含所有匹配记录的数组。get()
方法用于执行 SELECT 操作,并返回第一条匹配记录。
Database api
- Database#aggregate()
- Database#table()
- Database#loadExtension()
- Database#exec()
- Database#close()
- Properties
jest
当您需要编写单元测试时,Jest是一个非常流行和强大的JavaScript测试框架。它被广泛用于测试各种前端和后端应用程序,包括Node.js项目。
Jest提供了易于使用的API和丰富的功能,使得编写和运行测试变得简单而高效。它支持自动化和并行执行测试,并提供了丰富的断言库、模拟和模拟函数、覆盖率报告等功能。
jest 钩子函数
beforeAll(fn, timeout)
: 在所有测试用例执行之前执行的钩子函数。afterAll(fn, timeout)
: 在所有测试用例执行之后执行的钩子函数。beforeEach(fn, timeout)
: 在每个测试用例执行之前执行的钩子函数。afterEach(fn, timeout)
: 在每个测试用例执行之后执行的钩子函数。
在所有测试用例之前执行的钩子函数 beforeAll()
只会执行一次,而在每个测试用例之前执行的钩子函数 beforeEach()
会在每个测试用例执行前都会执行一次。它们的差别主要体现在执行次数和执行时机上。
举个例子,假设我们有两个测试用例 test1
和 test2
:
js
beforeAll(() => {
console.log('在所有测试用例之前执行');
});
beforeEach(() => {
console.log('在每个测试用例之前执行');
});
test('test1', () => {
console.log('执行 test1');
});
test('test2', () => {
console.log('执行 test2');
});
当我们运行这段代码时,输出结果如下:
在所有测试用例之前执行
在每个测试用例之前执行
执行 test1
在每个测试用例之前执行
执行 test2
组织测试代码
describe(name, fn)
: 用于创建一个测试套件,可以包含多个相关的测试用例。name
参数是套件的描述信息,fn
参数是一个回调函数,用于编写套件中的测试用例。test(name, fn, timeout)
: 用于定义一个测试用例。name
参数是测试用例的描述信息,fn
参数是一个回调函数,用于编写测试用例的具体逻辑。可选的timeout
参数用于设置测试用例的超时时间。expect(value)
: 用于断言测试结果是否符合预期。它提供了一系列的匹配器(matchers)来进行断言,例如toBe()
、toEqual()
、toContain()
等。可以通过链式调用来组合多个断言。
开始单元测试
现在,让我们开始第一个单元测试。我们可以创建一个测试用例来验证Better-Sqlite3是否能够成功连接到数据库并执行查询。
js
// 引入所需的库和模块
const sqlite = require('better-sqlite3');
const db = new sqlite(':memory:'); // 在内存中创建一个临时数据库
// 定义要测试的函数或类
function getUserById(id) {
const query = db.prepare('SELECT * FROM users WHERE id = ?');
return query.get(id);
}
// 编写测试用例
test('getUserById should return the correct user', () => {
// 插入测试数据
db.exec(`
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
);
INSERT INTO users (id, name, email) VALUES (1, 'John', 'john@example.com');
`);
// 调用被测试的函数
const user = getUserById(1);
// 断言结果是否符合预期
expect(user).toEqual({
id: 1,
name: 'John',
email: 'john@example.com'
});
});
在上述例子中,我们使用Jest编写了一个名为getUserById
的函数,它会从数据库中获取指定ID的用户信息。然后,我们编写了一个测试用例来验证该函数是否返回了正确的用户对象。我们首先在临时数据库中创建了一个用户表,并插入了一条测试数据。接下来,我们调用getUserById
函数并使用expect
断言来验证返回结果是否与预期相符。
结尾
在下一篇文章中,我们将正式开始介绍 SQL 单元测试的内容。SQL 单元测试是确保数据库相关代码质量和正确性的重要手段。我们将探讨如何使用 Jest 和 better-sqlite3 来编写和运行 SQL 单元测试。我们将介绍 SQL 单元测试的基本概念,包括如何构造测试数据、如何编写测试用例以及如何验证查询结果的准确性。