在本文中,我将解释如何在您的Node.js应用程序中使用数据库钩子来解决在开发过程中可能出现的特定问题。
许多应用程序只需要在服务器、数据库之间建立连接池并执行查询。然而,根据您的应用程序和数据库部署情况,可能需要进行其他配置。
例如,多区域分布式SQL数据库可以根据应用程序用例的不同拓扑进行部署。某些拓扑需要在每个会话中在数据库上设置属性。
让我们探索一下Node.js生态系统中一些最受欢迎的数据库客户端和ORM提供的一些钩子。
在使用最流行的关系数据库时,Node.js社区有许多可供选择的驱动程序。在这里,我将重点关注与兼容PostgreSQL的数据库客户端,它们可以用于连接到YugabyteDB或其他PostgreSQL数据库。
Sequelize、Prisma、Knex和node-postgres是一些功能各异的受欢迎的客户端,具体取决于您的需求。我鼓励您阅读它们的文档,以确定哪个最适合您的需求。
这些客户端提供了不同用例的钩子。例如:
连接钩子:在连接和断开与数据库的连接之前或之后立即执行函数。
日志钩子:在不同的日志级别下将消息记录到stdout。
生命周期钩子:在对数据库进行调用之前或之后立即执行函数。
在本文中,我将介绍这些客户端提供的一些钩子,并说明您如何在分布式SQL应用程序中使用它们的好处。
我还将演示如何在创建用户之前使用钩子对用户的密码进行散列,并在连接到具有读副本的多区域数据库后如何设置运行时配置参数。
Sequelize ORM提供了许多钩子来管理数据库事务的整个生命周期。
beforeCreate生命周期钩子可用于在创建新用户之前对密码进行散列处理:
User.beforeCreate(async (user, options) => {
const hashedPassword = await hashPassword(user.password);
user.password = hashedPassword;
});
接下来,我使用afterConnect连接钩子来设置会话参数。
通过这个YugabyteDB部署,您可以从跟随者执行读取操作以减少延迟,并且无需从主集群节点读取:
const config = {
host: process.env.DB_HOST,
port: 5433,
dialect: "postgres",
dialectOptions: {
ssl: {
require: true,
rejectUnauthorized: true,
ca: [CERTIFICATE],
},
},
pool: {
max: 5,
min: 1,
acquire: 30000,
idle: 10000,
},
hooks: {
async afterConnect(connection) {
if (process.env.DB_DEPLOYMENT_TYPE === "multi_region_with_read_replicas") {
await connection.query("set yb_read_from_followers = true; set session characteristics as transaction read only;");
}
},
},
};
const connection = new Sequelize(
process.env.DATABASE_NAME,
process.env.DATABASE_USER,
process.env.DATABASE_PASSWORD,
config
);
通过使用这个钩子,在连接池中的每个数据库会话在建立新连接时都会设置这些参数:
set yb_read_from_followers = true;:此参数控制是否启用从跟随者读取。
set session characteristics as transaction read only;:此参数将只读设置应用于后续的所有语句和事务块。
尽管在Node.js社区中,Prisma是许多人首选的ORM,但在撰写本文时,Prisma并不包含Sequelize中的许多内置钩子。目前,该库包含用于处理查询生命周期、日志记录和断开连接的钩子,但在建立连接之前或之后提供的帮助很少。
以下是如何使用Prisma的生命周期中间件在创建用户之前对密码进行哈希处理的方法:
prisma.$use(async (params, next) => {
if (params.model == 'User' && params.action == 'create') {
params.args.data.password = await hashPassword(params.args.data.password);
}
return next(params)
})
const create = await prisma.user.create({
data: {
username: 'bhoyer',
password: 'abc123'
},
})
要设置会话参数以利用我们的读取副本,我们需要在查询数据库之前执行一条语句:
await prisma.$executeRaw(`set yb_read_from_followers = true; set session characteristics as transaction read only;`);
const users = await prisma.user.findMany();
如果您需要立即在连接池中建立连接以设置参数,您可以使用Prisma显式地进行连接,而不使用连接池的延迟连接。
Prisma具有查询(query)、错误(error)、信息(info)和警告(warn)等日志级别。可以使用基于事件的日志记录来处理查询事件:
const prisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'stdout',
level: 'error',
},
{
emit: 'stdout',
level: 'info',
},
{
emit: 'stdout',
level: 'warn',
},
],
});
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Params: ' + e.params);
console.log('Duration: ' + e.duration + 'ms');
});
这在开发过程中对于在分布式系统中进行查询调优非常有帮助。
下面是如何在退出之前使用beforeExit钩子来访问数据库的示例:
const prisma = new PrismaClient();
prisma.$on('beforeExit', async () => {
// PrismaClient still available
await prisma.issue.create({
data: {
message: 'Connection exiting.'
},
})
});
Knex是一个轻量级的查询构建器,但它没有更全功能的ORM中的查询中间件。
要对密码进行哈希处理,可以使用自定义函数来手动处理:
async function handlePassword(password) {
const hashedPassword = await hashPassword(password);
return hashedPassword;
}
const password = await handlePassword(params.password);
knex('users').insert({...params, password});
在Knex.js查询构建器中实现连接钩子所需的语法与Sequelize类似。以下是如何设置会话参数以从YugabyteDB的副本节点读取的示例代码:
const knex = require('knex')({
client: 'pg',
connection: {/*...*/},
pool: {
afterCreate: function (connection, done) {
connection.query('set yb_read_from_followers = true; set session characteristics as transaction read only;', function (err) {
if (err) {
//Query failed
done(err, conn);
} else {
console.log("Reading from replicas.");
done();
}
});
}
}
});
node-postgres库是所有讨论过的库中最低级的库。在底层,使用Node.js的EventEmitter来触发连接事件。
当在连接池中建立新连接时,会触发connect事件。我们可以使用它来设置我们的会话参数。我还添加了一个错误钩子来捕获和记录所有错误消息的示例代码:
const config = {
user: process.env.DB_USER,
host: process.env.DB_HOST,
password: process.env.DB_PASSWORD,
port: 5433,
database: process.env.DB_NAME,
min: 1,
max: 10,
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
ssl: {
rejectUnauthorized: true,
ca: [CERTIFICATE],
servername: process.env.DB_HOST,
}
};
const pool = new Pool(config);
pool.on("connect", (c) => {
c.query("set yb_read_from_followers = true; set session characteristics as transaction read only;");
});
pool.on("error", (e) => {
console.log("Connection error: ", e);
});
在node-postgres中,我们没有可用的生命周期钩子,所以像Prisma一样,我们必须手动进行密码哈希处理:
async function handlePassword(password) {
const hashedPassword = await hashPassword(password);
return hashedPassword;
}
const password = await handlePassword(params.password);
const user = await pool.query('INSERT INTO user(username, password) VALUES ($1, $2) RETURNING *', [params.username, password]);
正如你所看到的,钩子可以解决之前由复杂且容易出错的应用程序代码所引起的许多问题。每个应用程序都有不同的要求和面临新的挑战。在你的开发过程中,可能会经过很多年才需要使用特定的钩子,但现在,当那一天来临时,请准备就绪。
作者:Brett Hoyer
更多技术干货请关注公号“云原生数据库”
squids.cn,基于公有云基础资源,提供云上 RDS,云备份,云迁移,SQL 窗口门户企业功能,
帮助企业快速构建云上数据库融合生态。