Node.js 连接金仓数据库踩坑记(上篇):环境搭建与基础操作
开篇:一个让我熬夜到凌晨两点的坑
去年做一个内部管理系统,后端用的是Node.js。客户要求数据库必须用金仓,我当时心里还挺有底的——Node.js生态这么丰富,连个数据库驱动还不简单?
结果一搜,发现金仓的Node.js驱动不是我熟悉的那种npm install就能用的方式。下载、拷贝、版本匹配、回调嵌套......折腾了一整晚才跑通。
这篇文章分上下两篇。上篇讲环境搭建和基础CRUD,下篇讲连接池、事务、预处理语句这些稍微高级点的用法。今天先说说怎么把环境跑起来。
一、驱动配置
1.1 下载驱动包
金仓的Node.js驱动不是通过npm发布的,需要去官网下载。下载下来是一个压缩包,解压后里面有个node_modules文件夹,还有一个测试用例。
我一开始还纳闷:怎么不是.tgz文件?后来才知道,这个驱动需要手动放置,不能直接npm install。
1.2 放置驱动
把你项目里的node_modules文件夹和驱动里的node_modules合并一下就行。
项目的目录结构看起来像这样:
my-kingbase-project/
├── node_modules/
│ └── kb/ # 从驱动包拷贝过来的
│ ├── index.js
│ ├── lib/
│ └── ...
├── app.js
└── package.json
1.3 版本兼容性
这个驱动是基于Node.js 10.19.0开发的。官方文档说支持Node.js 8、10、12。我一开始用的是Node.js 14,结果require('kb')的时候报了一堆莫名其妙的错,换成Node.js 12就好了。
如果你用高版本Node跑不起来,降级试试。这个坑我花了两个小时才爬出来。
建议先用nvm切到Node.js 12,跑通之后再考虑要不要升。
二、连接数据库
2.1 基本连接方式
驱动提供了一个Client类,通过它来连接数据库。这个设计跟pg模块很像,用过的人应该不陌生。
const { Client } = require('kb');
const client = new Client({
user: 'SYSTEM',
host: '127.0.0.1',
database: 'TEST',
password: '123456',
port: 54321,
ssl: false
});
client.connect(err => {
if (err) {
console.error('连接失败', err.stack);
} else {
console.log('连接成功');
}
});
参数说明:
| 参数名 | 说明 |
|---|---|
| user | 数据库用户名,默认SYSTEM |
| host | 数据库IP地址 |
| database | 要连接的数据库名 |
| password | 密码 |
| port | 端口号,默认54321 |
| ssl | 是否启用SSL,默认false |
2.2 异步/等待模式
回调写起来有点繁琐,可以包装成Promise。我用util.promisify处理了一下:
const { Client } = require('kb');
const { promisify } = require('util');
async function connectDB() {
const client = new Client({
user: 'SYSTEM',
host: '127.0.0.1',
database: 'TEST',
password: '123456',
port: 54321
});
const connect = promisify(client.connect).bind(client);
await connect();
return client;
}
// 使用
const client = await connectDB();
三、执行SQL语句
3.1 不带参数的查询
连接成功后,可以用query方法执行SQL。这个方法是异步的,需要传入回调函数。
client.query('SELECT * FROM users', (err, res) => {
if (err) {
console.error('查询失败', err.stack);
return;
}
console.log(res.rows);
});
返回的res对象里,rows是结果集数组,每行数据就是一个对象,字段名对应键名。
3.2 带参数的查询
参数占位符用$1, $2,不需要手动拼接字符串,能防止SQL注入。
方式一:分开传参
const text = 'INSERT INTO users(name, email) VALUES($1, $2) RETURNING *';
const values = ['张三', 'zhangsan@test.com'];
client.query(text, values, (err, res) => {
if (err) {
console.log(err.stack);
} else {
console.log('插入成功', res.rows[0]);
}
});
方式二:用对象传参
const query = {
text: 'INSERT INTO users(name, email) VALUES($1, $2)',
values: ['李四', 'lisi@test.com']
};
client.query(query, (err, res) => {
if (err) {
console.log(err.stack);
} else {
console.log('插入成功');
}
});
RETURNING *很实用,能直接拿到刚插入的数据,省得再查一次。
3.3 预处理语句
如果要多次执行同一条SQL,可以用预处理语句(Prepared Statement)。它会提前解析SQL模板,多次执行时只需要传参数,效率更高。
const query = {
name: 'insert-user', // 语句名称
text: 'INSERT INTO users(name, email) VALUES($1, $2)',
values: ['王五', 'wangwu@test.com']
};
client.query(query, (err, res) => {
if (err) {
console.log(err.stack);
} else {
console.log('预处理语句执行成功');
}
});
给了name参数后,驱动会把这个查询存起来,下次同样name的查询可以直接复用。适合高频执行、结构固定的SQL。
3.4 参数传递的安全性
所有参数都要通过values数组传递,不要拼字符串。
// 正确写法
client.query('SELECT * FROM users WHERE name = $1', [userInput]);
// 错误写法
client.query(`SELECT * FROM users WHERE name = '${userInput}'`);
下面那种写法有SQL注入风险。userInput如果传'; DROP TABLE users; --,后果你懂的。
3.5 处理查询结果
res.rows是结果集数组,数组长度就是行数。
client.query('SELECT * FROM users WHERE age > $1', [18], (err, res) => {
if (err) {
console.error(err);
return;
}
// 结果集为空的情况
if (res.rows.length === 0) {
console.log('没有找到符合条件的数据');
return;
}
// 遍历结果
res.rows.forEach(row => {
console.log(`用户名: ${row.name}, 邮箱: ${row.email}`);
});
});
四、关闭连接
用完数据库要关连接,不然连接会一直占着。
client.end(err => {
if (err) {
console.log('关闭连接时出错', err.stack);
} else {
console.log('连接已关闭');
}
});
一个常见错误:在还没执行完查询的地方调用client.end(),会导致异步操作被中断。一定要等所有query的回调都执行完了再关。
五、完整示例
下面是一个完整的小程序,建表、插入、更新、查询、删除全流程都走一遍。
const { Client } = require('kb');
const client = new Client({
user: 'SYSTEM',
host: '127.0.0.1',
database: 'TEST',
password: '123456',
port: 54321,
});
client.connect();
// 建表
client.query('CREATE TABLE test_node (id int, name char(100), address varchar(200), salary real, created_at DATE, description TEXT)', (err, res) => {
if (err) {
console.error('建表失败', err.stack);
return;
}
console.log('建表成功');
// 插入数据
const now = new Date();
const text = 'INSERT INTO test_node VALUES($1, $2, $3, $4, $5, $6) RETURNING *';
const values = [1, '张三', '西湖区', 8000, now, '测试数据'];
client.query(text, values, (err, res) => {
if (err) {
console.error('插入失败', err.stack);
return;
}
console.log('插入成功', res.rows[0]);
// 更新数据
const updateQuery = {
text: 'UPDATE test_node SET name = $1, address = $2 WHERE id = $3 RETURNING *',
values: ['李四', '滨江区', 1]
};
client.query(updateQuery, (err, res) => {
if (err) {
console.error('更新失败', err.stack);
return;
}
console.log('更新成功', res.rows[0]);
// 查询数据
client.query('SELECT * FROM test_node', (err, res) => {
if (err) {
console.error('查询失败', err.stack);
return;
}
console.log('查询结果:', res.rows);
// 删除数据
client.query('DELETE FROM test_node WHERE id = 1', (err, res) => {
if (err) {
console.error('删除失败', err.stack);
return;
}
console.log('删除成功,影响行数:', res.rowCount);
// 删表
client.query('DROP TABLE test_node', (err, res) => {
if (err) {
console.error('删表失败', err.stack);
return;
}
console.log('删表成功');
client.end();
});
});
});
});
});
});
六、常见问题
6.1 模块找不到
报Cannot find module 'kb'。
检查node_modules目录里有没有kb文件夹,确认拷贝到了项目根目录。
6.2 连接被拒绝
报connect ECONNREFUSED 127.0.0.1:54321。
用ksql命令行测试一下能不能连。如果命令行也连不上,检查IP、端口、用户名、密码。金仓的默认端口是54321,别写成5432。
6.3 Node.js版本不兼容
报一些奇怪的错,比如Class extends value undefined。
降Node.js版本到12试试。驱动基于10.19.0开发,12比较稳,再高可能有问题。
6.4 中文乱码
数据库连接参数里没指定编码。建库时用UTF8,或者在连接后执行SET client_encoding TO 'UTF8'。
client.query("SET client_encoding TO 'UTF8'", (err) => {
// 继续后续操作
});
七、小结
上篇主要讲了Node.js连接金仓数据库的基础配置和CRUD操作:
- 驱动安装:下载驱动包,把
kb文件夹放到项目的node_modules里 - 版本要求:Node.js 8-12能跑,高了可能报错
- 基本操作:
Client类建连接,query方法执行SQL - 参数传递:用
$1, $2占位符,不要拼接字符串 - 连接管理:用完要
end(),不然连接会一直占着
下篇会讲一些高级用法:连接池、事务、批量插入、流式查询等。希望对你有帮助。